我的域模型中有几个不同的实体(动物物种,比方说),每个实体都有一些属性。实体是只读的(它们在应用程序生命周期内不会改变状态)并且它们具有相同的行为(仅与属性值不同)。
如何在代码中实现此类实体?
我尝试了这样的枚举:
enum Animals {
Frog,
Duck,
Otter,
Fish
}
其他代码将打开枚举。然而,这导致了丑陋的切换代码,分散了逻辑和problems with comboboxes。列出所有可能的动物没有很好的方法。序列化虽然很好。
我还想过每个动物类型在哪里是公共基础抽象类的子类。但是,对于所有动物,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
简直太糟糕了。
This blog post给了我一个解决方案,其中每个选项都是类型中静态定义的字段,如下所示:
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的新实例,这是我想要避免的。
有没有简单的方法让第三次尝试有效?有关实施此类模型的更多建议吗?
答案 0 :(得分: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类和用法是 ONLY ,用于需要序列化/反序列化的应用程序的最后一层。 Everything else可以对付不可变的Animal类。
EDITx2:此托管分离的另一个主要好处是您可以处理代码中的旧版更改。例如,您有Fish
类型,这是非常广泛的。也许您稍后将其拆分为Shark
和Goldfish
,并将所有旧的Fish
类型视为Goldfish
。通过这种序列化分离,您现在可以检查任何旧的Fish并将它们转换为Goldfish,而直接序列化会导致异常,因为Fish不再存在。
答案 1 :(得分: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; }
}
现在你有多个“实例”并不重要,因为每个实例只包含它的类型信息(没有实际数据)。覆盖基类上的Equals
和GetHashCode
意味着同一类的不同实例将被视为相等。
答案 2 :(得分:1)
我看到它的方式,您正在寻找合适的creational pattern以满足您的需求。 您的第一个选项与factory method类似。 第二个看起来像一个带有可选abstract factory的类型层次结构。 第三个是singleton。
似乎你唯一的问题是序列化。我们在谈论什么样的序列化:二进制还是XML?如果它是二进制文件,你看过custom serialization吗?如果它是XML,您应该坚持使用第二个选项,也可以使用自定义序列化或在类之外委派序列化逻辑。
我个人认为后者是最具架构性的解决方案。混合对象创建和序列化是一个坏主意。
答案 3 :(得分: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对象引入一些延迟初始化。
动物类开始有点像“一个地方的情侣”。
现在如何将其挂钩到.NET的序列化机制(BinarySerializer或DataContractSerializer)。我们希望序列化程序在反序列化时使用GetAnimalById
而不是构造函数,并且在序列化时只存储ID。
您可以使用ISerializationSurrogate或IDataContractSurrogate执行此操作,具体取决于序列化API。这是一个例子:
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);
}
}
BTW:DataContacts似乎有一个错误(或者它是一个特征?),当替换类型是基本类型时,它会导致它们奇怪地行为。当将对象序列化为字符串时,我遇到了这样的问题 - 在反序列化时,从未触发过GetDeserializedObject方法。如果遇到此行为,请在代理项中的单个int字段周围使用包装类或结构。