如何表示具有相同行为的不同实体?

时间:2012-06-02 22:02:19

标签: c#

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

如何在代码中实现此类实体?

尝试失败:

枚举

我尝试了这样的枚举:

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的新实例,这是我想要避免的。

有没有简单的方法让第三次尝试有效?有关实施此类模型的更多建议吗?

4 个答案:

答案 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类型,这是非常广泛的。也许您稍后将其拆分为SharkGoldfish,并将所有旧的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; }
}

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

答案 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字段周围使用包装类或结构。