继承与枚举属性在领域模型中的应用

47
我在工作中讨论过“领域模型中的继承会使开发人员的生活变得复杂”的问题。作为一名面向对象的程序员,我开始寻找证据,证明在领域模型中使用继承实际上会让开发者的生活更轻松,而不是到处使用switch语句。
我希望看到的是这个:
class Animal {

}

class Cat : Animal {

}

class Dog : Animal {

}

其他同事的意思是:

public enum AnimalType {
    Unknown,
    Cat,
    Dog
}

public class Animal {

    public AnimalType Type { get; set; }

}

我该如何说服他(链接受欢迎)在这种情况下使用类层次结构比使用枚举属性更好?

谢谢!


3
这是为内存对象模型还是ORM?我认为动物的例子不太有用,因为它没有涵盖实际面向对象设计中遇到的许多常见问题。 - Marcelo Cantos
@Marcelo:同意。具体来说,行为角度是什么? - sfinnie
狗会叫,猫会喵喵叫。如果使用同事的实现,在C#中实现这些行为将会很困难。但在函数式语言F#中,它仍然非常有用,因为辨别联合是提供多态性的非常强大的手段。 - vc 74
@Marcelo 和 @sfinnie:这是一个针对 ORM 和业务层的贫血领域模型。从我的角度来看,域模型类可以拥有对其他类没有意义的属性。 - Daniel Severin
6个回答

29

以下是我的理解:

只有当角色/类型永远不会改变时才使用继承。 例如:

对于像这样使用继承:

消防员 <- 雇员 <- 人 是错误的。

一旦消防员弗雷迪(Freddy)更换工作或失业,您必须摧毁它并重新创建一个新类型的对象,并将所有旧关系附加到它上面。

因此,上述问题的简单解决方案是给Person类添加JobTitle枚举属性。 在某些情况下,这可能足够简单,例如如果您不需要与角色/类型相关联的非常复杂的行为。

更正确的方法是给Person类一个角色列表。 每个角色代表例如具有时间跨度的雇佣。

例如:

freddy.Roles.Add(new Employement( employmentDate, jobTitle ));

或者如果那太过于复杂:

freddy.CurrentEmployment = new Employement( employmentDate, jobTitle );

这样,弗雷迪可以成为开发者而不必我们先杀了他。

然而,我的胡言乱语还没有回答是否应该使用枚举或类型继承来处理职位标题。

在完全的内存对象导向编程中,我会说在这里使用继承更为正确。

但是,如果你正在进行O/R映射,则可能会在后台得到一个过于复杂的数据模型,如果映射器试图将每个子类型映射到新表中。 因此,在这种情况下,如果没有与类型相关的真正/复杂的行为,我通常会选择枚举方法。如果使用有限并使事情更轻松或不那么复杂,我可以接受“if type == JobTitles.Fireman …”。

例如,.NET的Entity Framework 4设计器只能将每个子类型映射到新表。在查询数据库时,您可能会得到一个丑陋的模型或许多连接,而没有任何实际的好处。

但是,如果类型/角色是静态的,我确实使用继承。 例如,对于产品。

您可能拥有CD <- Product 和 Book <- Product。 在这种情况下,继承获胜,因为您很可能会将不同状态与类型关联起来。 CD可能具有多个曲目属性,而书可能具有页数属性。

因此,总之,这取决于情况;-)

另外,在一天结束时,无论如何,您最终都可能会得到许多switch语句。 例如,如果您要编辑“产品”,即使使用继承,您也可能会有如下代码:

if (product is Book) Response.Redicted("~/EditBook.aspx?id" + product.id);

因为在实体类中编码编辑图书的网址会很丑陋,它会强制您的业务实体了解您的站点结构等。


我们在EF 4中成功实现了“层次结构表继承”(http://msdn.microsoft.com/en-us/library/bb738443.aspx),因此查询不再那么丑陋。 - Daniel Severin
是的,表格层次结构使数据库更加流畅,但是为了使其正常工作,您需要编写多少XML呢?可能比使用switch语句还要多,只是这样说而已。 - Roger Johansson
我们避免了先使用数据库编写 XML,然后通过手工创建其余的映射实体并将它们映射到正确的表。 - Daniel Severin
我会不遗余力地使用任何“用多态替换条件表达式”类型的重构,以避免完全基于对象类型或枚举值进行切换。 - user74754

13

拥有一个枚举类型就像是为那些声称“开闭原则只是傻瓜才会遵守”的人举办聚会。

它让你可以检查动物是否属于某个类型,然后针对每种类型应用自定义逻辑。这可能产生可怕的代码,使得在系统上继续构建变得困难。

为什么?

使用“如果是这个类型,做这个事情,否则做那个事情”的方式会阻碍良好的编码。

每当引入新的类型时,如果该新类型没有被处理,则所有这些if语句都会失效。在更大的系统中,很难找到所有这些if语句,这最终会导致错误。

一种更好的方法是使用小型、定义明确的特性接口(接口隔离原则)。

然后您将只有一个if语句,但没有'else',因为所有具体类都可以实现特定的特性。

比较

if (animal is ICanFly flyer)
  flyer.Sail();
// A bird and a fly are fundamentally different implementations
// but both can fly.
if (animal is Bird b)
   b.Sail();
else if (animal is Fly f)
   b.Sail();

看到了吗?前者只需要检查一次,而后者必须检查每一个能飞的动物。


如果 (animal.Type == AType.Dog) {应用逻辑} vs 如果(animal is Dog) {应用逻辑} 当涉及应用具体类型逻辑时,第二个例子比第一个例子更好吗? - mxmissile
@mxmissile:我把我的替换移到了答案。 - jgauffin
应该回复而不是替换。 - jgauffin

11

枚举适用于以下情况:

  1. 值的集合是固定的,并且几乎永远不会或很少更改。
  2. 您想要能够表示值的联合(例如,组合标志)。
  3. 您不需要将其他状态附加到每个值上。(Java没有此限制。)

如果您可以使用数字解决问题,则枚举很可能很适合并且更具类型安全性。 如果您需要比上述更多的灵活性,则枚举很可能不是正确的答案。使用多态类,您可以:

  1. 静态地确保处理所有类型特定的行为。例如,如果您需要所有动物都能够Bark(),则创建具有抽象Bark()方法的Animal类将使编译器检查每个子类是否实现该方法。如果您使用枚举和一个大的switch,它不会确保您已经处理了每种情况。

  2. 您可以添加新案例(例如您的示例中的动物类型)。这可以在源文件和甚至跨包边界完成。 对于面向对象编程来说,开放式扩展是其中的一项主要优点。

重要的是要注意,您的同事的示例并不直接与您的示例相矛盾。 如果他想让动物的类型成为公开属性(对于某些事情很有用),则可以使用类型对象模式而无需使用枚举:

public abstract class AnimalType {
    public static AnimalType Unknown { get; private set; }
    public static AnimalType Cat { get; private set; }
    public static AnimalType Dog { get; private set; }

    static AnimalType() {
        Unknown = new AnimalType("Unknown");
        Cat = new AnimalType("Cat");
        Dog = new AnimalType("Dog");
    }
}

public class Animal {
    public AnimalType Type { get; set; }
}
这样做给你提供了枚举的方便性:你可以使用 AnimalType.Cat 来获取动物的类型。但是它同时也提供了类的灵活性:你可以添加字段到 AnimalType 中以存储每个类型的额外数据,添加虚方法等。更重要的是,你可以通过创建 AnimalType 的新实例来定义新的动物类型。

8

我建议您重新考虑:在贫血领域模型中(如上面的评论所述),猫和狗的行为并没有不同,因此不存在多态性。动物的类型只是一个属性。很难看出继承在这里可以带来什么好处。


2
谢谢。"猫的行为与狗并没有不同,因此不存在多态性"似乎是一个非常好的理由。 - LCJ

1

两种解决方案都是正确的。 你应该看看哪种技术更适合你的问题。

如果你的程序使用了很少的不同对象,并且不添加新类,最好还是使用枚举。

但是,如果你的程序使用了很多不同的对象(不同的类),并且将来可能会添加新类,最好尝试继承的方式。


1
最重要的是OOPS意味着对现实进行建模。继承使你有机会说猫是一种动物。动物不应该知道它现在是一只猫,然后决定它应该喵喵叫而不是汪汪叫,这就打败了封装性。现在你不需要使用if-else语句,代码更少了。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接