【问题标题】:How to represent different entities that have identical behavior?如何表示具有相同行为的不同实体?
【发布时间】:2012-06-07 15:19:50
【问题描述】:

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

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

不成功的尝试:

枚举

我试过这样的枚举:

enum Animals {
    Frog,
    Duck,
    Otter,
    Fish  
}

其他代码段将打开枚举。然而,这会导致难看的切换代码,分散逻辑和problems with comboboxes。没有漂亮的方法可以列出所有可能的动物。不过,序列化效果很好。

子类

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

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

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

【问题讨论】:

  • 我会选择子类,我不明白序列化是一个大问题和单个实例。您可以使用依赖注入。
  • 如果有多个实例代表同一事物,则必须考虑相等条件。当两种动物类型相等时?如果名字一样呢?如果我将以前版本中的序列化名称与当前版本中的名称进行比较会怎样?
  • 所有解决方案都有相同的序列化问题。它应该由序列化逻辑处理。子类更适合您的应用程序的测试、封装和未来扩展。
  • @Dominik:你是说如果你把Otter的名字从Otter改成Sea Otter,那么你还是希望旧实例反序列化为Otter——即使他们有不同的名字?
  • @Gabe:如果我将 Otter 的名称更改为 Sea Otter,我希望将所有旧实例反序列化为 Sea Otter - 新名称。

标签: c#


【解决方案1】:

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

重点是:您有一组具有某些特定架构的对象...

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

【讨论】:

    【解决方案2】:

    在我看来,您正在寻找合适的creational pattern 来满足您的需求。 您的第一个选项类似于factory method。 第二个看起来像一个带有可选abstract factory 的类型层次结构。 第三个是singleton

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

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

    【讨论】:

      【解决方案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 意味着同一类的不同实例将被视为相等。

      【讨论】:

      • 嘿,这很不错。对象应该可以很好地序列化,并且它们需要存储的只是它们的类型名称而不是任何属性。
      • 这种方法很有趣——看起来很容易实现,容易理解,而且序列化工作不需要额外的努力。我也喜欢 Chris 的回答,但这个看起来更干净。
      【解决方案4】:

      如果您遇到序列化问题,您始终可以将应用程序代码与序列化代码分开。也就是说,放置转换到/从您的序列化状态的转换类。序列化的实例可以公开任何所需的空构造函数和属性,它们唯一的工作就是序列化状态。同时,您的应用程序逻辑适用于不可序列化的不可变对象。这样一来,您就不会将序列化问题与逻辑问题混为一谈,这会带来许多您发现的缺点。

      编辑:这是一些示例代码:

      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 类型,它非常广泛。也许您稍后将其拆分为SharkGoldfish,并决定您所有旧的Fish 类型都应视为Goldfish。通过这种序列化分离,您现在可以检查任何旧 Fish 并将它们转换为 Goldfish,而直接序列化会导致异常,因为 Fish 不再存在。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2014-01-19
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-03-03
        相关资源
        最近更新 更多