オブジェクト指向のGin Installer開発

最初の部分へのリンク

取引



トランザクションで保護されたブロック内でエラーが発生した場合、操作のブロックをロールバックできるトランザクションメカニズムを実装するつもりだったことを思い出させてください。 最初に、状態の維持とロールバック操作の責任の問題を解決する必要があります。 以下で説明するアーキテクチャはすぐに現れたのではなく、レイアウトの設計と実装を数回試みた後、起こったことに成功するまで、すぐに言います。

トランザクションアーキテクチャを簡単にスケーラブルにするために、以前と同様に継承を使用します。 この場合、状態を維持し、チーム自体に保存された状態にロールバックする責任を割り当てます。 すべてのチームが本質的にトランザクション対応ではないことに注意してください。 たとえば、レジ​​ストリからの読み取りは、システム内の何も変更しないため、トランザクションの一部にはできません。 ただし、レジストリエントリは既にトランザクションの一部です。 また、ファイルの作成はトランザクションの一部です。

そして、別の抽象クラスTransactionalCommandを宣言し、Commandクラスから継承します。

次のようになります。

public abstract class TransactionalCommand : Command { public abstract TransactionStep Do(ExecutionContext context, Transaction transaction); public abstract void Rollback(TransactionStep step); }
      
      





どうした トランザクション型の別の引数を割り当てることにより、Do()メソッドにオーバーロードを追加しました。 Transactionクラスについては少し後で説明しますが、本質的にはコマンドが実行されるトランザクションです。 新しいDoメソッド(ExecutionContext、トランザクション)の主な目的は次のとおりです。

  1. 新しいトランザクションステップTransactionStepを作成し、
  2. システムの状態を物理的に(ディスク上、データベース内などに)保存するか、コマンドの実行後に変化するシステムの側面を保存します。
  3. CommandクラスのDo(ExecutionContext)メソッドを呼び出します(つまり、元の非トランザクション操作を実行します)。
  4. 最初に作成されたTransactionStepを返します。


また、トランザクションステップをロールバックするRollback()メソッドも追加しました。

したがって、状態を維持し、チームをチーム自体にロールバックする責任を割り当てました。これにより、ユーザーはインストーラーによって実行されるトランザクションコマンドの配列を簡単に増やすことができます。

インストーラーインフラストラクチャは、トランザクションが完了するまでシステムの状態を維持するための構造化ストレージとそれにアクセスするインターフェイスをユーザーに提供する必要があります。 サードパーティのコンポーネント(データベースなど)のインストールにできるだけ依存しないように、システムの状態を、インストーラーの作業フォルダーの特定のサブフォルダーに保存されている任意のファイルセットとして提示します。 ファイル構造を次のようにします。

%GIN_DIRECTORY%\packages\[PackageId]\transactions\[TransactionName]\steps\[StepNumber]\





または、ツリーの形で:

画像

インフラストラクチャは、コマンドを実行する前にシステムの状態を保存するために、ユーザーに[StepNumber]フォルダーへのパスを提供します。 インフラストラクチャ自体は、トランザクション自体やそのステップに関するデータなど、ツリーの対応するフォルダーに他のすべてのデータを保存します。 もちろん、インフラストラクチャ自体がこのツリー全体を作成し、ユーザーを日常業務から節約します。 ユーザーとは、インストーラーの拡張機能の開発者を意味します。

TransactionStepクラスの説明は次のとおりです。

 public class TransactionStep { public const string STEPS_SUBFOLDER_NAME = @"steps"; public int StepNumber { get; set; } public string TransactionName { get; set; } public TransactionalCommand Command { get; set; } public string GetPath(ExecutionContext context) { string transactionsPath = context.ExecutedPackage.TransactionsPath; string transactionPath = Path.Combine(transactionsPath, TransactionName); string stepsPath = Path.Combine(transactionPath, STEPS_SUBFOLDER_NAME); string stepPath = Path.Combine(stepsPath, StepNumber.ToString()); if (!Directory.Exists(stepPath)) { Directory.CreateDirectory(stepPath); } return stepPath; } }
      
      





クラスの説明からわかるように、クラスには2つの主な目的があります。



このクラスから具象ステップのサブクラスを継承します。 例:

 public class SingleFileStep: TransactionStep { public string OldFilePath { get; set; } }
      
      





このサブクラスは、ファイルの作成、ファイルの削除、ファイルの変更など、単一のファイルを使用する操作に適しています。 このサブクラスは、ソースファイルを変更する前に保存されたコピーへのフルパスを提供します。

そして今、Transactionクラス:

 public class Transaction { public string TransactionName { get; set; } public TransactionState TransactionState { get; set; } public List<TransactionStep> Steps { get; set; } public Transaction() { Steps = new List<TransactionStep>(); } public T CreateStep<T>(TransactionalCommand command) where T: TransactionStep, new() { T step = new T(); int stepNumber = Steps.Count; step.StepNumber = stepNumber; step.Command = command; step.TransactionName = TransactionName; Steps.Add(step); return step; } public void Save(ExecutionContext context) { string transactionsPath = context.ExecutedPackage.TransactionsPath; string transactionPath = Path.Combine(transactionsPath, TransactionName + @"\"); Directory.CreateDirectory(transactionPath); string dataFilePath = Path.Combine(transactionPath, @"data.xml"); GinSerializer.Serialize(this, dataFilePath); } public void Rollback() { Steps.BackForEach(s => { s.Command.Rollback(s); }); } }
      
      





ご覧のとおり、クラスの主な機能は次のとおりです。



BackForEachメソッドは、他のクラスのコードが乱雑にならないように、別のクラスに移動された拡張メソッドです。 まあ、一般的に、私は拡張メソッドとラムダ式を使用したいのですが、この弱点を許してください。

ここで、TransactionalCommandの後継としてのクラスの1つを見て、トランザクションをサポートするコマンドをユーザーがどのように実装する必要があるかを正確に理解しましょう。

 public class CreateFile: TransactionalCommand { public string SourcePath { get; set; } public string DestPath { get; set; } public override void Do(ExecutionContext context) { File.Copy(SourcePath, DestPath, true); } public override TransactionStep Do(ExecutionContext context, Transaction transaction) { SingleFileStep step = null; if (transaction != null) { step = transaction.CreateStep<SingleFileStep>(this); } if (File.Exists(DestPath)) { string rollbackFileName = Guid.NewGuid().ToString("N") + ".rlb"; string dataPath = step.GetPath(context); string rollbackFilePath = Path.Combine(dataPath, rollbackFileName); step.OldFilePath = rollbackFilePath; File.Copy(DestPath, rollbackFilePath); } Do(context); return step; } public override void Rollback(TransactionStep step) { SingleFileStep currentStep = (SingleFileStep)step; if (File.Exists(currentStep.OldFilePath)) { File.Copy(currentStep.OldFilePath, DestPath, true); } else { File.Delete(DestPath); } } }
      
      





2つの引数を持つDo()メソッドは、渡されたトランザクションのフレームワーク内で、CreateStep <>メソッドを使用して新しいトランザクションステップを作成し、インストーラーが現在のステップで新しいファイルを作成するパスに沿ってファイルがすでに存在するかどうか、およびファイルが既に存在するかどうかを確認しますつまり、別の名前で(名前は変更できませんでしたが)インストーラーインフラストラクチャによって提供されたフォルダー(TransactionStep.GetPath)にコピーし、非トランザクションコマンドコマンドのクラスから継承されたDo()メソッドを呼び出して、結果として作成されたものを返しますステップの始めに ションのステップ。

Rollbackメソッドは、トランザクションステップを引数として受け取り、OldFilePathプロパティを確認すると、古いバージョンのファイルを宛先フォルダーにコピーするか、宛先フォルダーからファイルを削除します。

Transactionクラスのコードで、コンテキスト内で実行されるパッケージへの参照を表すExecutionContext.ExecutedPackageプロパティに気付くかもしれません。 このリンクは、Packageクラスのコンストラクター内で初期化されます。 Packageクラスには、パッケージトランザクションフォルダーへのパスを示すTransactionsPathプロパティがあることに注意してください。



そのため、トランザクションの1ステップを保存およびロールバックすることを検討しました。 ただし、トランザクション全体をロールバックするには、トランザクションの手順を逆の順序で実行する必要があります。 そして、以前にディスクに保存されたトランザクションをロールバックできるようにするため、ディスクにトランザクションをロードする必要があります。ディスクにトランザクションを保存する方法を既に知っているためです(Transaction.Save()メソッドを参照)

これを行うには、PackageクラスでLoadTransactions()メソッドを実装します。 クラスパッケージに含まれる理由 Packageクラスの主な目的はパッケージを実行することだからです。 また、トランザクションはパッケージの実行プロセスで正確に発生し、本質的にはすでに完了したパッケージの特性です。 したがって、トランザクションのリストをPackageクラスに配置することは論理的です。 トランザクションの読み込みコードは次のとおりです。

 private void LoadTransactions() { _transactions = new List<Transaction>(); if (String.IsNullOrEmpty(TransactionsPath) || !Directory.Exists(TransactionsPath)) { return; } string[] transactionNames = Directory.GetDirectories(TransactionsPath); foreach (string name in transactionNames) { string transactionPath = Path.Combine(TransactionsPath, name); string dataFilePath = Path.Combine(transactionPath, TRANSACTIONS_DATA_FILENAME); Transaction transaction = GinSerializers.TransactionSerializer.Deserialize(dataFilePath); _transactions.Add(transaction); } }
      
      





ここではすべてが簡単です:TransactionsPathフォルダー内のすべてのサブフォルダーのリストを取得し(このプロパティは少し前にPackageクラスに表示されたことを思い出してください)、サブフォルダー名のリストはトランザクション名のリストになり(パッケージファイルツリーの構造を記憶します)、各名前に対してフルパスを形成しますトランザクションのdata.xmlファイルに追加し、次のトランザクションにデシリアライズします。 この場合、シリアライザーは、ロールバック()メソッドとともに、ネストされたトランザクションステップのリストと、これらのステップにネストされたコマンドの両方を作成します。 LoadTransactions()メソッドは、Packageクラスのコンストラクターで呼び出されます。 Packageクラスのクライアントにトランザクションのリストの内部構造を明かさないために、Packageクラスのインターフェースにトランザクションを操作するためのいくつかのメソッドを追加します。

 public void Rollback(string transactionName) { Transaction transaction = GetTransactionByName(transactionName); transaction.Rollback(); } public void Rollback() { foreach (Transaction transaction in _transactions) { transaction.Rollback(); } } public void AddTransaction(Transaction transaction) { _transactions.Add(transaction); }
      
      







次に、パッケージ内でトランザクションを作成する方法を考えてみましょう。 私はこの擬似コードでこれをしたいと思います:

 PackageBody body = new PackageBody() { Command = new CommandSequence() { Commands = new Command[] { new TransactionContainer() { TransactionName = "myTransaction", Commands = new List<Command>() { new CreateFile() { SourcePath = @"D:\test\newfile.txt", DestPath = @"C:\test\folder1\file1.txt" }, new CreateFile() { SourcePath = @"D:\test\newfile.txt", DestPath = @"C:\test\folder1\file2.txt" }, new ThrowException() { Message = "Test exception" }, new CreateFile() { SourcePath = @"D:\test\newfile.txt", DestPath = @"C:\test\folder1\file3.txt" } } } } } }; PackageBuilder builder = new PackageBuilder(body); string fileName = builder.GetResult(); Package pkg = new Package(fileName); ExecutionContext ctx = pkg.Execute();
      
      





ご覧のとおり、クラスの構造は大幅に変更されています。

  1. PackageBodyクラスに1つのコマンドを渡すことができるようになりました。ほとんどの場合、コマンドはCommandSequenceコマンドのインスタンスになります。
  2. Commandから継承されたTransactionContainerクラスは、一連のコマンドをトランザクションに結合するために使用されます。 CommandSequenceに似ていますが、追加のTransactionNameパラメーターがあります。


TransactionContainer内で、いくつかのファイル作成コマンドをネストし、それらの間に例外をスローしました。 このパッケージの動作は次のとおりです。



  1. パッケージは、最初の2つのコマンドを実行してCreateFileファイルを作成します。
  2. 3番目のThrowExceptionコマンドで例外がスローされ、パッケージの実行が停止します。つまり、最後のCreateFileコマンドはまったく実行されません。
  3. パッケージは、含まれている2つのCreateFileコマンドをロールバックすることにより、myTransactionという名前のトランザクションロールバックプロシージャに進みます。


したがって、トランザクションのロールバックは、トランザクションに含まれるすべてのパッケージコマンドのロールバックではなく、完了したトランザクションコマンドのロールバックのみです。

また、Package.Execute()メソッドは、情報を提供する目的で、ユーザーにパッケージ実行のコンテキストを返すため、実行されたコマンドの結果、トランザクションとパスのリストを確認できます。

PackageクラスのExecute()メソッドは非常に簡単な仕事をします:

  1. パッケージが機能するために必要なすべてのフォルダー構造をディスク上に作成します(パッケージのファイルツリー構造を参照)
  2. PackageBodyクラスのCommandのDo()メソッドを呼び出します。


さらに、チーム自体がすでに実行を開始しています。 単一のコマンドである場合は単純に実行され、CommandSequenceである場合は、ネストされているすべてのコマンドが順次実行されます。 TransactionContainerの場合、最初に新しいトランザクションを作成し、それに含まれるコマンドを順次実行し、この作成したトランザクションを引数として渡します。 この場合、通常のコマンドとトランザクション実行をサポートするコマンドの両方をトランザクションに埋め込むことができるという事実を考慮する必要があります。 したがって、TransactionContainerは、次の実行可能コマンドの親タイプを監視し、対応するオーバーロードされたDo()メソッドを呼び出す必要があります。 TransactionContainerはtry catch finallyコンテナで一連のコマンドを実行し、ネストされたコマンドのいずれかで例外が発生した場合、現在のトランザクションのRollback()メソッドを呼び出してトランザクションのロールバックを続行する必要があります。 コマンドブロックの実行が終了すると、トランザクションをディスクに保存し、その後のトランザクションの読み込みと分析を可能にします。

TransactionContainerクラスのDo()メソッドのコードは、最初の近似に見えます。

 Transaction transaction = new Transaction() { TransactionName = TransactionName, TransactionState = TransactionState.Undefined }; try { context.Transactions.Add(transaction); foreach (Command command in Commands) { if (command is TransactionalCommand) { ((TransactionalCommand)command).Do(context, transaction); } else { command.Do(context); } } transaction.TransactionState = TransactionState.Active; } catch { transaction.Rollback(); } finally { transaction.Save(context); }
      
      





インストーラー設計のこの段階では、TransactionContainerは単一のトランザクションで次々に実行されるコマンドの順序付きリストです。 しかし、これらのコマンドの1つがコンテナコマンド、つまり1つ以上の他のコマンドが埋め込まれているコマンドである場合はどうでしょうか。 たとえば、ExecuteIfまたはCommandSequenceコマンドがトランザクションの一部として実行された場合はどうなりますか? ExecuteIfおよびCommandSequenceのみではトランザクションをサポートしません。つまり、トランザクションコマンドがトランザクション内にネストされている場合、トランザクションの範囲外で実行されます。ExecuteIfおよびCommandSequenceには1つの引数を持つDo(ExecutionContext)メソッドがあるためです。 これは、それらに組み込まれたチームがトランザクションの外部で動作することを意味し、これはあまり良い解決策ではありません。

したがって、すべてのコンテナーコマンド(ExecuteIf、ExecuteNotIf、およびCommandSequence、およびユーザーがインストーラーに追加するその他のコンテナーコマンド)はトランザクション対応にする必要があります。 ただし、コンテナコマンド自体は、トランザクションにステップを追加しません。親トランザクションに通知するものがないためです。コンテナコマンド自体は、システム内で何も変更しません。 彼女は、ネストされたトランザクションチームにステップの作成を委任します。 つまり、コンテナコマンドは次のようになります。



上記のCommandSecquenceクラスとExecuteIfクラスは、次のようになります。

 public class CommandSequence: TransactionalCommand { public List<Command> Commands; public override void Do(ExecutionContext context) { foreach (Command command in Commands) { command.Do(context); } } public override TransactionStep Do(ExecutionContext context, Transaction transaction) { foreach (Command command in Commands) { if (command is TransactionalCommand) { ((TransactionalCommand)command).Do(context, transaction); } else { command.Do(context); } } return null; } public override void Rollback(TransactionStep step) { } } public class ExecuteIf : TransactionalCommand { public string ArgumentName { get; set; } public Command Command { get; set; } public override void Do(ExecutionContext context) { if ((bool)context.GetResult(ArgumentName)) { Command.Do(context); } } public override TransactionStep Do(ExecutionContext context, Transactions.Transaction transaction) { if ((bool)context.GetResult(ArgumentName)) { if (Command is TransactionalCommand) { ((TransactionalCommand)Command).Do(context, transaction); } else { Command.Do(context); } } return null; } public override void Rollback(TransactionStep step) { } }
      
      





これで、あらゆるレベルのネストのコマンドを含むトランザクションを作成できます。



第三部へのリンク



All Articles