7

我的域模型中有几个不同的实体(比如说动物物种),每个实体都有一些属性。实体是只读的(它们在应用程序生命周期内不会更改状态)并且它们具有相同的行为(仅在属性值上有所不同)。

如何在代码中实现这些实体?

不成功的尝试:

枚举

我尝试了这样的枚举:

enum Animals {
    Frog,
    Duck,
    Otter,
    Fish  
}

其他代码将打开枚举。然而,这会导致难看的切换代码,分散逻辑和组合框的问题。没有漂亮的方法可以列出所有可能的动物。序列化虽然效果很好。

子类

我还考虑了每种动物类型在哪里是公共基础抽象类的子类。但是,所有 Animals的 Swim() 的实现都是相同的,所以它没有什么意义,而且可序列化现在是一个大问题。因为我们代表一种动物类型(物种,如果你愿意的话),每个应用程序应该有一个子类的实例,当我们使用序列化时,维护起来既困难又奇怪。

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();

}

那太好了:你可以像 enums ( AnimalType = Animal.Otter) 一样使用它,你可以轻松地添加所有已定义动物的静态列表,你有一个明智的地方来实现Swim()。可以通过保护属性设置器来实现不变性。但是有一个主要问题:它破坏了可序列化性。一个序列化的 Animal 必须保存它的所有属性,并且在反序列化时它会创建一个 Animal 的新实例,这是我想避免的。

有没有一种简单的方法可以使第三次尝试成功?对于实施这样的模型还有什么建议吗?

4

4 回答 4

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; }
}

现在,您拥有多个“实例”并不重要,因为每个实例仅包含其类型信息(没有实际数据)。在基类上覆盖EqualsandGetHashCode意味着同一类的不同实例将被视为相等。

于 2012-06-02T23:01:47.863 回答
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:这种托管分离的另一个主要好处是您可以处理代码中的遗留更改。例如,您有一个Fish非常广泛的类型。也许您将其拆分为稍后Shark并决定应考虑Goldfish所有旧类型。通过这种序列化分离,您现在可以检查任何旧 Fish 并将它们转换为 Goldfish,而直接序列化会导致异常,因为 Fish 不再存在。FishGoldfish

于 2012-06-02T22:37:04.950 回答
1

在我看来,您正在寻找适合您需求的正确创建模式。您的第一个选项类似于factory method。第二个看起来像一个带有可选抽象工厂的类型层次结构。第三个是单例

看来您唯一的问题是序列化。我们在谈论什么样的序列化:二进制还是 XML?如果它是二进制的,你看过自定义序列化吗?如果是 XML,您应该坚持使用第二个选项,也使用自定义序列化或将序列化逻辑委托给您的类之外。

我个人认为后者是架构上最合理的解决方案。混合对象创建和序列化是一个坏主意。

于 2012-06-02T22:49:43.573 回答
0

我会选择第三个选项(对象!),但有点扭曲。

关键是:您有一组具有某些特定模式的对象...

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”并保持数据一致。

因此,您需要一些“动物类型”的表示 -这是您唯一想要序列化的东西。在反序列化时,您希望读取类型标识符并基于它初始化所有字段。

哦,还有一个问题——在反序列化时,你不想创建一个新对象!您想要读取 ID(和仅 ID)并检索预定义对象之一(对应于此 ID)。


代码可能如下所示:

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 类开始有点像“几个单身人士在一个地方”。

现在如何将它挂接到 .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 回答