7

ドメインモデルにはいくつかの異なるエンティティ(たとえば、動物種)があり、それぞれにいくつかのプロパティがあります。エンティティは読み取り専用であり(アプリケーションの存続期間中は状態が変更されません)、動作は同じです(プロパティの値のみが異なります)。

そのようなエンティティをコードで実装する方法は?

失敗した試行:

列挙型

私はこのような列挙型を試しました:

enum Animals {
    Frog,
    Duck,
    Otter,
    Fish  
}

そして、他のコードは列挙型をオンにします。ただし、これにより、コードの切り替えが醜くなり、ロジックが分散し、コンボボックスに問題が発生します。すべての可能な動物をリストするためのきれいな方法はありません。ただし、シリアル化はうまく機能します。

サブクラス

また、各動物タイプが共通ベース抽象クラスのサブクラスである場所についても考えました。ただし、 Swim()の実装はすべての動物で同じであるため、ほとんど意味がなく、直列化可能性が大きな問題になっています。動物の種類(必要に応じて種)を表すため、アプリケーションごとにサブクラスのインスタンスが1つあるはずです。これは、シリアル化を使用するときに維持するのが難しく、奇妙です。

public abstract class AnimalBase {
    string Name { get; set; } // user-readable
    double Weight { get; set; }
    Habitat Habitat { get; set; }
    public void Swim(); { /* swim implementation; the same for all animals but depends                  uses the value of Weight */ }
}

public class Otter: AnimalBase{
    public Otter() {
        Name = "Otter";
        Weight = 10;
        Habitat = "North America";
    }
}

// ... and so on

ただひどい。

静的フィールド

このブログ投稿は、各オプションが次のように型内で静的に定義されたフィールドであるソリューションのアイデアを私に与えました。

public class Animal {
   public static readonly Animal Otter = 
       new Animal 
       { Name="Otter", Weight = 10, Habitat = "North America"}
   // the rest of the animals...

   public string Name { get; set; } // user-readable
   public double Weight { get; set; }
   public Habitat Habitat { get; set; }

   public void Swim();

}

それは素晴らしいことです:列挙型(AnimalType = Animal.Otter)のように使用でき、定義されたすべての動物の静的リストを簡単に追加でき、実装するための賢明な場所がありますSwim()。プロパティセッターを保護することで、不変性を実現できます。ただし、大きな問題があります。それは直列化可能性を壊します。シリアル化されたAnimalは、そのすべてのプロパティを保存する必要があり、逆シリアル化すると、Animalの新しいインスタンスが作成されます。これは、避けたいものです。

3回目の試行を機能させる簡単な方法はありますか?そのようなモデルを実装するためのその他の提案はありますか?

4

4 に答える 4

3

シリアライゼーションに問題がある場合は、いつでもアプリケーション コードをシリアライゼーション コードから分離できます。つまり、シリアル化された状態との間で変換する変換クラスを配置します。シリアル化されたインスタンスは、必要な空のコンストラクターとプロパティを公開している可能性があり、それらの唯一の仕事は状態をシリアル化することです。一方、アプリケーション ロジックは、シリアル化できない不変オブジェクトを操作します。このようにして、シリアライゼーションの懸念と論理的な懸念を混在させず、発見したときに多くの欠点をもたらします。

編集:ここにいくつかのサンプルコードがあります:

public class Animal 
{
    public string Name { get; private set; }
    public double Weight { get; private set; }
    public Habitat Habitat { get; private set; }

    internal Animal(string name, double weight, Habitat habitat)
    {
        this.Name = name;
        this.Weight = weight;
        this.Habitat = habitat;
    }

    public void Swim();
}

public class SerializableAnimal
{
    public string Name { get; set; }
    public double Weight { get; set; }
    public SerializableHabitat Habitat { get; set; } //assuming the "Habitat" class is also immutable
}

public static class AnimalSerializer
{
    public static SerializableAnimal CreateSerializable(Animal animal)
    {
        return new SerializableAnimal {Name=animal.Name, Weight=animal.Weight, Habitat=HabitatSerializer.CreateSerializable(animal.Habitat)};
    }

    public static Animal CreateFromSerialized(SerializableAnimal serialized)
    {
        return new Animal(serialized.Name, serialized.Weight, HabitatSerializer.CreateFromSerialized(serialized.Habitat));
    }

    //or if you're using your "Static fields" design, you can switch/case on the name
    public static Animal CreateFromSerialized(SerializableAnimal serialized)
    {
        switch (serialized.Name)
        {
            case "Otter" :
                return Animal.Otter
        }

        return null; //or throw exception
    }
}

次に、シリアル化のアプリケーション ロジックは次のようになります。

Animal myAnimal = new Animal("Otter", 10, "North America");
Animal myOtherAnimal = Animal.Duck; //static fields example

SerializableAnimal serializable = AnimalSerializer.CreateSerializable(myAnimal);
string xml = XmlSerialize(serializable);
SerializableAnimal deserialized = XmlDeserializer<SerializableAnimal>(xml);

Animal myAnimal = AnimalSerializer.CreateFromSerialized(deserialized);

繰り返しますが、SerializableAnimal クラスと使用法は、シリアル化/逆シリアル化が必要なアプリケーションの最終層で のみ使用されます。他のすべては、不変の Animal クラスに対して機能します。

EDITx2: この管理された分離のもう 1 つの大きな利点は、コード内の従来の変更に対処できることです。たとえば、Fishかなり広いタイプがあります。Sharkたぶん、それをとに分割し、Goldfishすべての古いFishタイプを考慮する必要があると判断しますGoldfish。このシリアライゼーションの分離により、Fish が存在しないため、直接シリアライゼーションを行うと例外が発生しますが、古い Fish をチェックしてそれらを Goldfish に変換できるようになりました。

于 2012-06-02T22:37:04.950 に答える
3

サブクラスで実装しますが、サブクラスのインスタンスには次のようにデータが保存されません。

public abstract class AnimalBase {
    public abstract string Name { get; } // user-readable
    public abstract double Weight { get; }
    public abstract Habitat Habitat { get; }
    public void Swim(); { /* swim implementation; the same for all animals but uses the value of Weight */ }

    // ensure that two instances of the same type are equal
    public override bool Equals(object o)
    {
        return o != null && o.GetType() == this.GetType();
    }
    public override int GetHashCode()
    {
        return this.GetType().GetHashCode();
    }
}

// subclasses store no data; they differ only in what their properties return
public class Otter : AnimalBase
{
    public override string Name { return "Otter"; }
    public override double Weight { return 10; }
    // here we use a private static member to hold an instance of a class
    // that we only want to create once
    private static readonly Habitat habitat = new Habitat("North America");
    public override Habitat Habitat { return habitat; }
}

各インスタンスにはそのタイプ情報のみが含まれているため (実際のデータは含まれていません)、複数の「インスタンス」があっても問題ありません。基本クラスのオーバーライドEqualsGetHashCodeは、同じクラスの異なるインスタンスが等しいと見なされることを意味します。

于 2012-06-02T23:01:47.863 に答える
1

私の見方では、あなたは自分のニーズに合った適切な創造パターンを探しています. 最初のオプションはfactory methodに似ています。2 つ目は、オプションの抽象ファクトリを持つ型階層のように見えます。3 つ目はsingletonです。

あなたの唯一の問題はシリアライゼーションのようです。私たちが話しているシリアライゼーションの種類は、バイナリですか、それとも XML ですか? バイナリの場合、カスタムのシリアライゼーションを見ましたか? XML の場合は、2 番目のオプションに固執するか、カスタムのシリアル化も使用するか、シリアル化ロジックをクラスの外部に委任する必要があります。

個人的には、後者が最もアーキテクチャ的に健全なソリューションだと思います。オブジェクトの作成とシリアル化を混在させることは、悪い考えです。

于 2012-06-02T22:49:43.573 に答える
0

私は 3 番目のオプション (オブジェクト!) を使用しますが、少しひねりがあります。

要点: 特定のスキーマを持つ一連のオブジェクトがあります...

public class Animal {

   public string Name { get; set; } // user-readable
   public double Weight { get; set; }
   public Habitat Habitat { get; set; }

   public void Swim();
}

しかし、あなたはそれらを事前定義したいと考えています。キャッチは次のとおりです。そのようなオブジェクトをシリアル化する場合、そのフィールドをシリアル化したくありません。フィールドの初期化はアプリケーションの責任であり、シリアル化されたバージョンで実際に持ちたいのは動物の「タイプ」だけです。これにより、「Otter」を「Sea Otter」に変更し、データの一貫性を保つことができます。

したがって、「動物の種類」の表現が必要になります。シリアル化する必要があるのはそれだけです。逆シリアル化では、型識別子を読み取り、それに基づいてすべてのフィールドを初期化する必要があります。

ああ、もう 1 つの落とし穴 - 逆シリアル化の際に、新しいオブジェクトを作成したくありません! ID (および ID のみ) を読み取り、(この ID に対応する) 定義済みオブジェクトの 1 つを取得します。


コードは次のようになります。

public class Animal {

   public static Animal Otter;
   public static Animal Narwhal;

   // returns one of the static objects
   public static Animal GetAnimalById(int id) {...}

   // this is here only for serialization,
   // also it's the only thing that needs to be serialized
   public int ID { get; set; } 
   public string Name { get; set; }
   public double Weight { get; set; }
   public Habitat Habitat { get; set; }

   public void Swim();
}

ここまでは順調ですね。インスタンスを静的にすることができない依存関係がある場合は、すべての Animal オブジェクトに対して遅延初期化を行うことができます。

Animal クラスは、「1 つの場所に複数のシングルトン」のように見え始めます。

次に、.NET のシリアル化メカニズム (BinarySerializer または DataContractSerializer) にフックする方法について説明します。デシリアライズ時にコンストラクターの代わりにシリアライザーを使用しGetAnimalById、シリアライズ時に ID のみを保存するようにします。

シリアル化 API に応じて、ISerializationSurrogate または IDataContractSurrogate を使用してこれを行うことができます。これは例です:

class Surrogate : IDataContractSurrogate {

    public Type GetDataContractType(Type type) {
        if (typeof(Animal).IsAssignableFrom(type)) return typeof(int);
        return type;
    }

    public object GetObjectToSerialize(object obj, Type targetType) {
        // map any animal to its ID
        if (obj is Animal) return ((Animal)obj).ID;
        return obj;
    }

    public object GetDeserializedObject(object obj, Type targetType) {
        // use the static accessor instead of a constructor!
        if (targetType == typeof(Animal)) return Animal.GetAnimalById((int)obj);
    }
}

ところで: DataContacts にはバグ (または機能ですか?) があり、代替タイプが基本タイプの場合に奇妙な動作をするようです。オブジェクトを文字列としてシリアル化するときにこのような問題が発生しました-オブジェクトを逆シリアル化するときに GetDeserializedObject メソッドが起動されることはありませんでした。この動作に遭遇した場合は、サロゲートの単一の int フィールドの周りにラッパー クラスまたは構造体を使用します。

于 2012-06-05T08:12:44.573 に答える