使用与域实体一对一接口是好的还是坏的做法?为什么?

27

我在一些我工作的领域驱动设计企业应用程序中看到的一件事情是使用与领域实体相同的接口,其中属性和函数存在一对一的映射。实际上,领域对象总是通过其一对一的接口使用,并且所有领域实体都具有这种风格的一对一接口。

例如:

领域对象Account:

public class Account : IAccount
{
     public string Name {get;set;}
     //...some more fields that are also in IAccount
     public decimal Balance {get;set;}
}

并且它有一个匹配的界面

public interface IAccount
{
   string Name {get;set;}
   //... all the fields in Account
   decimal Balance {get;set;}
}
但最近我越来越相信这实际上是一个反模式。 我向一些开源社区的架构师咨询了一下,他们说这基于设计错误或缺陷,在设计链的某个地方。
所以我告诉我的同事不要为领域对象创建接口。 因为它们没有任何目的,并且每当您更新领域实体时都必须更新接口。
首先提出的声明是这些接口提供“解耦”,但我反驳说,因为这些接口与领域实体具有一对一的关系,它们实际上并没有提供任何解耦,接口的更改意味着领域实体的更改,反之亦然。
下一个声明是我们需要接口进行测试。 我的反驳是Rhino-mocks为具体类的模拟和存根提供支持。 但是他们声称Rhino-mocks在处理具体类时会出现问题。 我不知道我是否买单了,即使rhino-mocks与具体类有问题,也不一定意味着我们应该为领域实体使用接口。
所以我很好奇:
为什么您的领域实体需要一对一接口?
为什么不?
这是好的还是坏的惯例?
感谢您的阅读! 编辑:我应该指出,我一直在使用接口,如果需要,我相信我会随时使用接口。 但我特别指的是具有一对一接口的领域实体。

据我所知,Rhino Mocks无法模拟具体类方法,除非它们是可重写的。因此,您必须使所有领域对象方法都虚拟化。这不好。 - Klas Mellbourn
相关:https://dev59.com/q3E85IYBdhLWcg3wr1ke - jaco0646
6个回答

9

如所描述的那样,这是一种不好的做法,但是......

你的接口没有必要与你的领域实体不同;有时候确实是正确的映射方式。但是如果总是这样,就值得怀疑它们是否真正被设计过,或者只是因为缺乏时间/懒惰而随意放置。

以你的例子为例,你所描述的IAccount接口在Account对象上公开了getter和setter;这似乎有点奇怪和不太可能,因为使用Account的所有内容都需要设置账户余额,并且该隐含权限在接口层面上指定。在你的系统中没有任何地方需要仅仅检查而不设置账户余额吗?


是的,有很多领域整个界面都没有被使用。实际上,界面的一部分没有被使用的情况比被使用的情况更为普遍。 - Mark Rogers
1
是的,那些应用程序不是领域驱动设计,而是纯粹的数据驱动设计。听起来这是非常糟糕的设计。 - Paul Sonier
这些设计师是否特别反对复合接口?也就是说,将几个基础领域对象的功能组合成一个连贯的接口的接口? - Paul Sonier
你的意思是像一个门面模式之类的东西吗?不是的。这些接口总是与域对象完全相同,甚至在域内部也使用接口而不是域对象。具体的域对象只有在创建时才会被看到。 - Mark Rogers
3
我认为他们误解了“脱钩”的含义以及为什么需要这样做。 - Mark Rogers

7

将领域对象总是指定为接口而不是直接指定为类的最大原因是为了在实现上给您一定的自由度。在您的示例中,您只有一种IAccount,因此有点多余。

但是,如果您拥有例如:

public class Account : IAccount { ... }       // Usual account, persistent
public class MockAccount : IAccount { ... }   // Test mock object
public class TransAccount : IAccount { ... }  // Account, not persistent
public class SimAccount : IAccount { ... }    // Account in a performance sim

等等还有什么?

通过将域对象定义为接口,您可以替换实现而不影响您的域定义。


因此,以“在这个例子中...”开头的句子为例。但是,您要实现接口的方式肯定是变化的焦点,而面向对象设计的一个理想属性是在可能发生变化的关键点上拥有“铰链点”。顺便说一下,我曾经以你现在所持的观点辩论过这个问题,并且当我想开始模拟一些测试内容时,我被转化了。 - Charlie Martin
+1 这似乎很傻,直到你在维护一个单体应用程序两年后需要实现一项支持新变化的更改而不会破坏依赖层。此外,TDD是一个不言自明的选择。 - Rex M
如果你已经知道答案,为什么还要问这个问题呢? - Charlie Martin
2
你看起来很有把握。但你仍然没有读懂我写的内容。如果你只有一种IAccount实现者,那就是冗余的。但在现实世界中,你从来不会只有一种;一旦你开始拥有多个——包括想要一个测试模拟——它就很有用了。在DDD中,使用工厂和存储库,可以使用模板方法和工厂模式来获得相同的效果,但仍然需要处理单元测试等问题。然而,最终还是归结于同一点:如果你有多个相同的实现,那么你需要一个接口。 - Charlie Martin
1
我想给这个答案点赞不止一次。在其他地方使用正确编写的代码后,“两年后”我不得不加入一个项目。我的生产力急剧下降了。 - jeremyjjbrown

5

通常情况下,如果我的课程不会成为类似策略模式或访问者模式的设计模式的一部分,我就不会添加接口。

添加接口对于像策略模式和访问者模式这样的设计模式非常有用,但在这些情况下,我不会复制域类的getter和setter。相反,我会创建专门针对设计模式接口的接口。

interface SomeStrategy {
   void doSomething(StrategyData data);
}

interface StrategyData {
   String getProperty1();

   String getProperty2();
} 

这使我可以让域类实现这些接口,或者使用适配器模式。我认为这是一种更加清晰的方法,而不是仅仅为了创建接口而创建接口。

设计应该始终减少不确定性。仅仅为了创建接口而创建接口并不能减少不确定性,事实上,它可能会增加混乱,因为它没有任何意义。


4

实体上的一对一接口是一种反模式

詹姆斯·格雷戈里在这里表述得更好


当然,贫血领域确实是一个真正的问题。但是当你已经有了一个时,你至少可以在依赖项和实现之间创建一个绝缘体,这样你就可以更轻松地进行更改。 - jeremyjjbrown

1
我同意你的观点。接口应该像合同一样运作,因此与域实体一一对应的接口没有任何价值。如果您想要抽象某种行为,这可能是有用的。但是,在某些情况下才能起作用。

0

为什么这个项目会失去动力?

正如Charlie Martin所说,我发现为域对象提供接口可以让我选择实现方式。

一个基本的例子是对象的标识符(object.Id),这将取决于您存储该对象的位置以及创建它的责任是否在数据实现中。在SQL Server中,您可能会选择自动编号,但在Azure表存储中,您可能会选择Guid,但您不希望因更改数据存储位置而更改应用程序的业务逻辑。

我可能会选择将我的域对象持久化,甚至在表示层中使用它-这取决于我的应用程序范围哪个最好。但是,在公共层中添加一组常见的域接口使我能够针对它们编写服务并反复重用它们。

如果没有IAddress,我们将就同一个地址进行争论;如果没有ICreditCard,新程序员将为信用卡重新编写代码。

反模式标签是语言使用不当,它过于简单化了描述复杂和多样化任务解决方案的价值。

大多数模式都有其应用场景,即使是备受诟病的 Singleton,这意味着它并不是一个“反模式”,至少从术语上来说是这样。


当需要抽象对象ID类型的差异时,您肯定需要使用泛型而不是接口,除非键本身是基于最弱类型(例如字符串)建模的。 - StuartLC
现在我每次都使用字符串,如果使用泛型会变得非常复杂,你必须在传递的模型中告诉你的服务关于泛型,如果它有一个子属性是泛型实体并且服务是泛型的 - 你就会遇到我的问题。 - Anthony Johnston

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