レプリケーションフレームワーク•オブジェクトの接続されたグラフのディープコピーと一般化された比較

読者の皆さん、こんにちは!



.NETプラットフォーム用のレプリケーションフレームワークの若いが有望なライブラリを紹介したいと思います(おそらく、トピックに十分な関心があれば、 Javaバージョンも将来実装されるでしょう)。 このライブラリは移植性があり、 Microsoft .NETまたはMonoの任意のプロジェクトで使用できます。



ライブラリの目的は、オブジェクトと任意の複雑なグラフのディープコピー、それらの一般化された比較、歪みのないシリアル化とシリアル化解除、突然変異の追跡、状態の操作です。



画像





まず、用語と基本的なエンティティを決定します



スナップショットは、オブジェクトの状態の即時キャストであり、ソースから隔離され、プログラムの実行中はかなり静的であるため、偶発的な突然変異から保護されます。 これは、後で以前の状態で新しいオブジェクト[グラフ]を再作成したり、既存のオブジェクトに特定の状態を設定したりできる図面やスケッチのようなものです。



写真はさまざまな角度から撮影できます。つまり、オブジェクトの状態をさまざまな方法で解釈します。たとえば、インスタンスのすべてのプロパティとフィールドの値を収集するか、パブリックのみですが、多くの場合、特別なDataMember属性でマークされたメンバーのみを収集します 。 スナップショットの作成方法は、 ReplicationProfile [レプリケーションプロファイル]、特にMemberProviders [メンバープロバイダー]の内部リストによって異なります。



*デフォルトでは、クラスにDataContract属性またはCollectionDataContract属性がある場合、 DataMember属性を持つメンバーのみがスナップショットに変換されます。そうでない場合、クラスのすべてのフィールドとプロパティ(パブリックであるかどうかに関係なく)がスナップショットに含まれます。


レプリケーションプロファイルを使用する小さな例
var snapshot0 = instance0.CreateSnapshot(); /* use default ReplicationProfile */ var customReplicationProfile = new ReplicationProfile { MemberProviders = new List<MemberProvider> { //new MyCustomMemberProvider(), /* you may override and customize MemberProvider class! */ new CoreMemberProviderForKeyValuePair(), //new CoreMemberProvider(BindingFlags.Public | BindingFlags.Instance, Member.CanReadWrite), new ContractMemberProvider(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance, Member.CanReadWrite) } }; var snapshot1 = instance1.CreateSnapshot(customReplicationProfile ); Snapshot.DefaultReplicationProfile = customReplicationProfile;
      
      







一般的に、スナップショットはjsonに似たデータ構造であり、複雑な複合オブジェクトはプリミティブに分解され、辞書に変換されます。キーはメンバーの名前(プロパティまたはフィールド)で、値は対応するプリミティブ( stringintDateTimeなど)です。 ) 配列を含むすべてのコレクションは特別な種類のオブジェクトであり、通常のプロパティに加えて、enum操作( foreach )に対してもう1つの暗黙の値を持ち、その値はjson配列に相当します。



再構築 —スナップショットと既存のキャッシュされたオブジェクトインスタンスに基づいて、オブジェクトグラフを元の状態に転送する操作。 通常、プログラムの実行中に、オブジェクトとそれらで構成されるグラフが変化します。つまり、それらは変化しますが、グラフとそれに含まれるオブジェクトを以前に修正された特定の状態に戻すことができると便利な場合があります。



再構成は次のとおりです
 var cache = new Dictionary<object, int>(); var snapshot0 = graph0.CreateSnapshot(cache); /* modify 'graph0' by any way */ var graphX = snapshot0.ReconstructGraph(cache); /* graphX is the same reference that graph0, all items of the graph reverted to the previous state */
      
      







*キャッシュされたオブジェクトはガベージコレクションから保持され、再構築中はすべて元の状態に戻ることに注意してください。


複製( 複製 -イメージに基づいてオブジェクトのグラフをディープコピーする操作。元のグラフから分離されたグラフの新しいコピーを作成します。



複製は次のとおりです
 var snapshot0 = graph0.CreateSnapshot(cache); /* modify 'graph0' by any way */ var graph1 = snapshot0.ReplicateGraph(cache); /* graph1 is a deep copy of the source graph0 */
      
      







*表面と深さのコピーの違い
コピーには、表層と深層の2つのタイプがあります。 オブジェクトAとBが与えられ、AにはBへのリンクが含まれているとします(列A => B)。 オブジェクトAの表面コピーを行うと、オブジェクトAが作成され、これもBを参照します。つまり、2つの列A => BとA '=> Bが得られます。 それらには共通の部分Bがあるため、最初の列でオブジェクトBを変更すると、その状態は2番目の列で自動的に変化します。 オブジェクトAとAは独立したままです。 しかし、最も興味深いのは、閉じた(循環)リンクを持つグラフです。 AがBを参照し、BがAを参照するようにします(A <=> B)。オブジェクトAをAに表面的にコピーすると、非常に珍しいグラフA '=> B <=> A、つまり、元のオブジェクトクローンされました。 ディープコピーには、グラフに含まれるすべてのオブジェクトのクローンが含まれます。 この場合、A <=> BはA '<=> B'に変換されます。その結果、両方のグラフは互いに完全に分離されます。 場合によっては、表面的なコピーで十分ですが、常にではありません。




並置 -オブジェクトの参照画像を現在のサンプルのスナップショットと比較するための再帰的操作。



2つのスナップショットを一致させる例
 var snapshot0 = instance0.CreateSnapshot(); /* etalon */ var snapshot1 = instance1.CreateSnapshot(); /* sample */ var juxtapositions = snapshot0.Juxtapose(snapshot1).ToList(); var differences = juxtapositions.Where(j=>j.State == Etalon.State.Different);
      
      







オブジェクトの比較はプログラミングの広範なトピックであり、比較はそれを問題のクラス全体に一般化する試みです。 試行がどれほど成功したかは、自分で評価できます。



任意のオブジェクトのインスタンスが与えられます。 最初の瞬間に、彼の状態のスナップショットを取得し、しばらくしてからスナップショットを繰り返し、両方のスナップショットを比較して、このインスタンスで発生した、つまり状態の追跡を実装する、私たちにとって重要なすべての突然変異を明らかにします。 実際には、オブジェクトのコピーは変更を監視するために常に可能または単純ではないが、その写真を撮るのは常に簡単であることは注目に値します。



オブジェクトのインスタンスが2つ以上ある場合、たとえば、参照と作業の場合、それらは同じタイプでも異なるタイプでもかまいません。 それらの写真は、任意の時点で撮影し、制限なしに任意の組み合わせで比較できます。 マッピングは、メンバー名(プロパティとフィールド)によって行われます。



*重要なことに、マッチング操作の結果はIEnumerable<Juxtaposition>



であり、特定の条件に到達するといつでも再帰的なマッチングプロセスを中断し、完全に生成することはできません。これはパフォーマンスにとって重要です。


練習に移り、キーポイントに注意を払いましょう



オブジェクトの診断グラフを生成するためのコード
 using System; using System.Collections.Generic; using System.Runtime.Serialization; namespace Art.Replication.Diagnostics { [DataContract] public class Role { [DataMember] public string Name; public string CodePhrase; [DataMember] public DateTime LastOnline = DateTime.Now; [DataMember] public Person Person; } public class Person { public string FirstName; public string LastName; public DateTime Birthday; public List<Role> Roles = new List<Role>(); } public static class DiagnosticsGraph { public static Person Create() { var person0 = new Person { FirstName = "Keanu", LastName = "Reeves", Birthday = new DateTime(1964, 9 ,2) }; var roleA0 = new Role { Name = "Neo", CodePhrase = "The Matrix has you...", LastOnline = DateTime.Now, Person = person0 }; var roleB0 = new Role { Name = "Thomas Anderson", CodePhrase = "Follow the White Rabbit.", LastOnline = DateTime.Now, Person = person0 }; person0.Roles.Add(roleA0); person0.Roles.Add(roleB0); return person0; } } }
      
      







便利になるネームスペース
 using Art; using Art.Replication; using Art.Replication.Replicators; using Art.Replication.MemberProviders; using Art.Serialization; using Art.Serialization.Converters;
      
      







スナップショットを作成し、デフォルト設定で歪みのないストリングにシリアル化する
  public static void CreateAndSerializeSnapshot() { var person0 = DiagnosticsGraph.Create(); var snapshot0 = person0.CreateSnapshot(); string rawSnapsot0 = snapshot0.ToString(); Console.WriteLine(rawSnapsot0); Console.ReadKey(); }
      
      







作業の結果(画像の完全な構造がはっきりと見える)
 { #Id: 0, #Type: "Art.Replication.Diagnostics.Person, Art.Replication.Diagnostics, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", FirstName: "Keanu", LastName: "Reeves", Birthday: "1964-09-02T00:00:00.0000000+03:00"<DateTime>, Roles: { #Id: 1, #Type: "System.Collections.Generic.List`1[[Art.Replication.Diagnostics.Role, Art.Replication.Diagnostics, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null]], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", #Set: [ { #Id: 2, #Type: "Art.Replication.Diagnostics.Role, Art.Replication.Diagnostics, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", Name: "Neo", LastOnline: "2017-06-14T14:42:44.0000575+03:00"<DateTime>, Person: { #Id: 0 } }, { #Id: 3, #Type: "Art.Replication.Diagnostics.Role, Art.Replication.Diagnostics, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", Name: "Thomas Anderson", LastOnline: "2017-06-14T14:42:44.0000575+03:00"<DateTime>, Person: { #Id: 0 } } ] } }
      
      







•PersonクラスにはDataContract属性があるため、 CodePhraseを除くDataMember属性を持つすべてのフィールドはスナップショットにあります。



•各オブジェクトには独自の識別子#Id:0が割り当てられます。オブジェクトへのリンクがオブジェクトグラフで複数回発生する場合、次の構造が繰り返し複製に置き換えられます。



  Person: { #Id: 0 }
      
      





これにより、オブジェクトの同じインスタンスの複数の複製から保護され、循環参照の場合、無限再帰およびスタックオーバーフロー例外の発生を防ぎます :すべてのシリアライザーがこのような状況を処理するわけではありません)。



•完全なタイプ情報は、 #Typeキーを使用して各オブジェクトに追加されます。

•一部のプリミティブには、 Birthday: "1964-09-02T00:00:00.0000000+03:00"<DateTime>



タイプに関する情報も含まれていますBirthday: "1964-09-02T00:00:00.0000000+03:00"<DateTime>



。 画像を歪みなく復元(デシリアライズ)する必要があります。

•List <Role>コレクションはオブジェクトとしてシリアル化されますが、ネストされたオブジェクトを列挙するために使用される#Setプロパティがあります。



ただし、ライブラリはそのような完全な形式でのシリアル化と逆シリアル化のみをサポートしていると考えてはいけません。複製を設定してプロファイルを保存することにより、より古典的なjsonを使用することも可能です(通常のシリアライザーに固有の小さな歪みが発生する可能性があります)。



オブジェクトをクラシックJSONにシリアル化し、正常にデシリアライズする
  public static void UseClassicalJsonSettings() { Snapshot.DefaultReplicationProfile.AttachId = false; Snapshot.DefaultReplicationProfile.AttachType = false; Snapshot.DefaultReplicationProfile.SimplifySets = true; Snapshot.DefaultReplicationProfile.SimplifyMaps = true; Snapshot.DefaultKeepProfile.SimplexConverter.AppendTypeInfo = false; Snapshot.DefaultKeepProfile.SimplexConverter.Converters .OfType<NumberConverter>().First().AppendSyffixes = false; } public static void CreateAndSerializeSnapshotToClassicJsonStyle() { UseClassicalJsonSettings(); var person0 = DiagnosticsGraph.Create(); var snapshot0 = person0.CreateSnapshot(); string rawSnapsot0 = snapshot0.ToString(); Console.WriteLine(rawSnapsot0); var person0A = rawSnapsot0.ParseSnapshot().ReplicateGraph<Person>(); Console.WriteLine(person0A.FirstName); Console.ReadKey(); }
      
      







クラシックjson
 { FirstName: "Keanu", LastName: "Reeves", Birthday: "1964-09-02T00:00:00.0000000+03:00", Roles: [ { Name: "Neo", LastOnline: "2017-06-14T18:31:20.0000205+03:00", Person: { #Id: 0 } }, { Name: "Thomas Anderson", LastOnline: "2017-06-14T18:31:20.0000205+03:00", Person: { #Id: 0 } } ] }
      
      







歪みのない状態の保存と復元について



もちろん、オブジェクト(グラフ)の状態を文字列またはバイトの配列に保存して、後で復元できると便利です。 通常、これにはシリアル化メカニズムが使用されます。 それらは非常に機能的ですが、よく調べてみると、オブジェクトに特定のシリアライザーによって課せられている多くの厳しい制限が明らかになります。たとえば、属性や注釈、特別なメソッド、グラフに閉じたリンクがない、パラメーターのないコンストラクターなどが必要な場合があります。



しかし、多くのシリアライザーに共通する2つの暗黙的なマイナスもあります。 まず、少し前に述べたように、列内のオブジェクトの同じインスタンスへの複数の参照がある場合、いくつかのシリアライザーはそれを再度保存します。そのため、直列化を解除するときに同じオブジェクトのいくつかのコピーが既に取得されています(グラフが大幅に変更されます)。 第二に、場合によっては、オブジェクトの型に関する情報が失われる可能性があり、逆シリアル化中にオブジェクト型の復元が歪む場合があります。たとえば、 長い変換がintにガイドが文字列に、またはその逆です。



  public class Distorsion { public object[] AnyObjects = { Guid.NewGuid(), Guid.NewGuid().ToString(), DateTime.Now, DateTime.Now.ToString("O"), 123, 123L, }; }
      
      





レプリケーションフレームワークは、オブジェクトの種類に関するメタデータを格納する独自のjsonシリアライザーを使用し、グラフ内の複数の循環リンクをサポートするため、歪みのない完全な逆シリアル化が可能です。



基本的な使用例



複製:



  public static void Replicate() { var person0 = DiagnosticsGraph.Create(); var snapshot0 = person0.CreateSnapshot(); var person1 = snapshot0.ReplicateGraph<Person>(); person1.Roles[1].Name = "Agent Smith"; Console.WriteLine(person0.Roles[1].Name); // old graph value: Thomas Anderson Console.WriteLine(person1.Roles[1].Name); // new graph value: Agent Smith Console.ReadKey(); }
      
      





再構成:



  public static void Reconstract() { var person0 = DiagnosticsGraph.Create(); var cache = new Dictionary<object, int>(); var s = person0.CreateSnapshot(cache); Console.WriteLine(person0.Roles[1].Name); // old graph value: Thomas Anderson Console.WriteLine(person0.FirstName); // old graph value: Keanu person0.Roles[1].Name = "Agent Smith"; person0.FirstName = "Zion"; person0.Roles.RemoveAt(0); var person1 = (Person)s.ReconstructGraph(cache); Console.WriteLine(person0.Roles[1].Name); // old graph value: Thomas Anderson Console.WriteLine(person1.Roles[1].Name); // old graph value: Thomas Anderson Console.WriteLine(person0.FirstName); // old graph value: Keanu Console.WriteLine(person1.FirstName); // old graph value: Keanu Console.ReadKey(); // result: person0 & person1 is the same one reconstructed graph }
      
      





マッチング:



  public static void Justapose() { // set this settings for less details into output Snapshot.DefaultReplicationProfile.AttachId = false; Snapshot.DefaultReplicationProfile.AttachType = false; Snapshot.DefaultReplicationProfile.SimplifySets = true; Snapshot.DefaultReplicationProfile.SimplifyMaps = true; var person0 = DiagnosticsGraph.Create(); var person1 = DiagnosticsGraph.Create(); person0.Roles[1].Name = "Agent Smith"; person0.FirstName = "Zion"; var snapshot0 = person0.CreateSnapshot(); var snapshot1 = person1.CreateSnapshot(); var results = snapshot0.Juxtapose(snapshot1); foreach (var result in results) { Console.WriteLine(result); } Console.ReadKey(); } <Different> [this.FirstName] {Zion} {Keanu} <Identical> [this.LastName] {Reeves} {Reeves} <Identical> [this.Birthday] {9/2/1964 12:00:00 AM} {9/2/1964 12:00:00 AM} <Identical> [this.Roles[0].Name] {Neo} {Neo} <Identical> [this.Roles[0].LastOnline] {6/14/2017 9:34:33 PM} {6/14/2017 9:34:33 PM} <Identical> [this.Roles[0].Person.#Id] {0} {0} <Different> [this.Roles[1].Name] {Agent Smith} {Thomas Anderson} <Identical> [this.Roles[1].LastOnline] {6/14/2017 9:34:33 PM} {6/14/2017 9:34:33 PM} <Identical> [this.Roles[1].Person.#Id] {0} {0}
      
      





パフォーマンスについて



現時点では、ライブラリのパフォーマンスは非常に優れていますが、一般的な中間スナップショットメカニズムを使用すると、メモリと一部のタスクの実行速度の両方で追加コストが発生することを理解する価値があります。 ただし、実際には、画像のメカニズムが多くのシナリオでメリットをもたらすため、すべてがそれほど明確ではありません。



失う:



-オブジェクトのシリアル化時のメモリ消費量が増加

-シリアル化とそれに続くデシリアル化の速度が約2〜2.5倍遅くなります(シリアル化設定とテストの種類によって異なります)



受賞:



-シリアル化と逆シリアル化を使用せずにスナップショットを介してグラフをコピーします(プリミティブを文字列またはバイト配列に変換する必要はありません。これにより、高速化が実現します)

-完全なコピーではなく、スナップショットに大きなオブジェクトを部分的に保存する場合のメモリ使用量の改善

* BinaryFormatterNewtonsoft.Json 、およびDataContractJsonSerializerを使用してパフォーマンスを比較しました。


レプリケーションフレームワークについてのいくつかの結論



小さなスタジオのクリエイティブプログラミング「Mayloft」 [ Makeloft ]でソリューションを開発しました。 現在、プロジェクトは暫定版の段階にありますが、基本的な機能のみが実装されていますが、その機能は印象的です。 開発には多大な労力と時間が費やされたため、このフレームワークは教育的および非営利的なプロジェクトに対してのみ無料です。



現在、別のプロジェクトで使用する商用ライセンスの価格は15ドルです( ライセンスの購入時、ソースコードへのアクセスが提供され 、必要に応じて、パラメーター化されたコンストラクターでオブジェクトを複製する方法など、技術的な詳細に関するより詳細な相談が提供されます)。 おそらく、将来的には、ソリューションの開発に伴い、価格が上昇するでしょう。 多くのプロジェクトで継続的にフレームワークを使用する予定がある場合は、そのようなライセンスの費用を直接交渉することができます。



Nugetから試用版をダウンロードできます2017年9月まで機能します。 この記事のコード例のプロジェクトは、ここからダウンロードできます 。 ライブラリが良い印象を残し、あなたが決定のいずれかでそれを使用することに決めた場合、無料または有料のライセンスのリクエストをmakeman@tut.byに送信してください リクエストで、ライブラリを使用する予定のプロジェクトの名前とタイプを指定します。



ご清聴ありがとうございました! 気軽に質問して、願い事を書いてください!



All Articles