我想知道使用Anemic域模型的利弊(请参见下面的链接)。
Answers:
优点:
缺点:
当“ Anemic Domain Model”成为反模式时,为什么有这么多系统实现此功能?
我认为有几个原因
1.系统的复杂性
如果要实现一个简单的系统(几乎是您在Internet上找到的所有示例和示例代码),请执行以下操作:
将产品添加到订单
我把这个功能放在订单上
public void Order.AddOrderLine(Product product)
{
OrderLines.Add(new OrderLine(product));
}
尼斯和超级面向对象。
现在让我们说,我需要确保需要验证产品是否存在于库存中,如果不存在则抛出异常。
我真的不能再将其放入“订单”了,因为我不希望我的订单依赖于“库存”,因此现在需要继续使用该服务
public void OrderService.AddOrderLine(Order order, Product product)
{
if (!InventoryService.Has(product)
throw new AddProductException
order.AddOrderLine(product);
}
我也可以将IInventoryService传递给Order.AddOrderLine,这是另一个选择,但是仍然使Order依赖于InventoryService。
Order.AddOrderLine仍有一些功能,但通常仅限于Order范围,而根据我的经验,Order范围之外还有很多业务逻辑。
当系统不仅仅是基本的CRUD时,您最终将在OrderService中使用大部分逻辑,而在Order中则很少。
2.开发人员对OOP的看法
互联网上有很多关于实体应采用哪种逻辑的激烈讨论。
就像是
订单保存
Order应该知道如何自我保存吗?假设我们有相应的存储库。
现在,订单可以添加订单行吗?如果我尝试使用简单的英语来理解它,那也没有任何意义。用户将产品添加到订单,所以我们应该做User.AddOrderLineToOrder()吗?这似乎有点过分了。
怎么样OrderService.AddOrderLine()。现在有点道理了!
我对OOP的理解是,对于封装,您将函数放在需要访问类的内部状态的类上。如果需要访问Order.OrderLines集合,请将Order.AddOrderLine()放在Order上。这样,类的内部状态就不会暴露出来。
3. IoC容器
使用IoC容器的系统通常是完全贫血的。
这是因为您可以测试具有接口的服务/存储库,但是不能(轻松地)测试域对象,除非将接口放在所有接口上。
由于“ IoC”目前被誉为解决所有编程问题的解决方案,因此许多人盲目地遵循它,最终以“贫血领域模型”告终。
4. OOP很难,程序容易
我对此有一点“知识的诅咒”,但是我发现对于拥有DTO和服务的新开发人员而言,它比Rich Domain容易得多。
可能是因为使用Rich Domain,更难知道将逻辑放在哪个类上。什么时候创建新类?使用哪种模式?等等..
使用无状态服务,您只需将其以最接近的名称添加到服务中即可。
之后,很长一段时间以来我一直在想一个想法。我认为,“ OOP”一词具有的含义并非真正针对它。众所周知,字谜的意思是“面向对象的程序设计”。当然,重点是“面向”一词。它不是“ OMP”,意思是“对象授权编程”。ADM和RDM都是OOP的示例。它们利用对象,属性,方法接口等。但是,在我们选择封装事物的方式上,ADM和RDM之间存在差异。他们是两个不同的东西。说ADM不好是OOP,这不是准确的说法。也许我们需要各种不同级别的封装术语。另外,我从不喜欢反模式一词。通常由对立小组的成员分配给某物。ADM和RDM都是有效的模式,它们简单地具有不同的目标,并且旨在解决不同的业务需求。我们中那些实践DDD的人至少应该对此表示赞赏,并且不要因为抨击选择实施ADM的人而落到其他人的水平。只是我的想法。
“这是一种反模式,因此其他开发人员会询问您是否了解面向对象设计的概念。”
“贫血领域模型是一种反模式。反模式没有优点。”
贫血域模型是否是反模式是一个意见问题。马丁·福勒(Martin Fowler)表示确实如此,许多从内而外了解OO的开发人员表示并非如此。将观点陈述为事实很少有帮助。
一个,即使它被普遍认为是一种反模式,它仍有可能(尽管相对较少)有上升空间。
the chances are it would still have some (though relatively little) upside.
然后,请命名!至少一个,而不是假设!
在我看来,福勒的主要异议是从以下意义上说ADM不是OO。如果人们围绕被动数据结构“从头开始”设计一个由其他代码段操纵的系统,那么这肯定会比面向对象设计更像过程设计。
我建议至少有两种力量可以产生这种设计:
仍然在程序上认为仍需要在面向对象的环境中工作(或假设他们可以...)以产生新系统的设计者/程序员,以及
开发人员致力于在非OO方式(无论语言如何)设计的遗留系统上放置类似服务的“面孔” 。
例如,如果正在构建一组服务以公开现有COBOL大型机应用程序的功能,则可以使用不反映内部COBOL数据结构的概念模型来定义服务和接口。但是,如果服务将新模型映射到旧数据以使用现有的但隐藏的实现,那么就福勒的文章而言,新模型可能很“贫瘠”-例如,一组TransferObject样式的定义和没有真实行为的关系。
这种妥协对于理想的纯OO系统必须与现有的非OO环境进行交互的边界非常普遍。
如果您的团队无法或不愿构建富域模型(RDM)并随时间进行维护,那么贫血域模型(ADM)可能是一个不错的选择。要赢得RDM,需要仔细注意系统中使用的主要抽象。可以看出,在任何开发组中,只有不超过一半的成员,也许只有十分之一的成员具有抽象能力。除非该干部(也许只有一个开发人员)能够对整个团队的活动保持影响,否则RDM将屈服于熵。
熵RDM尤其会造成伤害。它的开发人员将学习严酷的教训。起初,他们将能够满足利益相关者的期望,因为他们将没有任何历史可言。但是随着他们的系统变得更加复杂(而不是复杂),它将变得脆弱。开发人员将尝试重用代码,但倾向于在开发中引入新的错误或后退(从而超出了他们的估计)。
相反,ADM开发人员对自己的期望较低,因为他们不会期望为新功能重用太多代码。随着时间的流逝,他们将拥有许多不一致的系统,但它可能不会意外中断。与成功的RDM相比,他们的上市时间将更长,但是他们的利益相关者不太可能意识到这种可能性。
“开发人员致力于在以非OO方式(无论语言如何)设计的遗留系统上放置类似服务的“面孔”。
如果您想到许多LOB应用程序,那么这些旧系统通常将不会使用与您相同的域模型。Anemic域模型通过在服务类中使用业务逻辑来解决此问题。您可以将所有这些接口代码放入模型中(按照传统的OO含义)-但通常最终会失去模块化。
当我第一次遇到有关Anemic Domain Model的文章时,我以为“这是我的工作。恐怖!” 我坚持并遵循了对埃里克·埃文(Eric Evan)的书的引用,认为这是一个很好的例子,并下载了源代码。事实证明,“不使用贫血域模型”并不意味着“不使用服务类,不使用中介器,不使用策略”,甚至“将逻辑放在要操纵的类上”。
DDD示例具有服务类,XyzUpdaters,单例和IoC。
我仍然对确切的Anemic域模型感到困惑。我希望“我一看到它就会知道”。目前,我对良好设计的积极实例感到满意。
在使用带有ADM的“成熟”系统之后,我感到至少可以提供一些有关该问题的轶事反馈。
1)缺乏封装
在带有ADM的实时系统中,可以写例如'obj.x = 100; obj.save”,即使这违反了业务逻辑。如果不变量是在对象上建模的,这会导致许多错误。我认为这些错误的严重性和普遍性对ADM来说是最严重的负面影响。
我觉得在这里指出这一点很重要,这是功能解决方案与ADM的过程解决方案之间显着不同的地方,而其他人可能在ADM和功能解决方案之间的表面相似性所引起的任何相似之处都是偶然的。
2)代码膨胀
我估计ADM中产生的代码量是OOP / RDM解决方案将产生的代码量的5-10倍。这可能是由于50%是代码重复,30%是样板代码以及20%是由于缺少RDM而引起的问题的解决或解决。
3)对领域问题了解不足
ADM和对域问题的理解不一而足。幼稚的解决方案出现了,由于很难用现有的DM支持需求,因此对需求的考虑不充分;鉴于更长的开发时间和缺乏灵活性,ADM成为业务创新的重要障碍。
4)维修困难
考虑到域概念可能不仅仅是复制和粘贴的重新实现,因此需要一定程度的严格性,以确保在域表示的所有位置都对其进行更改。这通常会导致对相同的错误进行多次调查和修复。
5)增加入职难度
我认为RDM的好处之一是概念的凝聚力,可以更快地了解领域。使用ADM时,概念可能分散且缺乏清晰性,因此新开发人员更难获得。
我也很想将ADM的运营支持成本包括在比RDM更高的成本中,但这将取决于许多因素。
正如其他人指出的那样,请查看DDD(Greg Evans,Vince Vaughn和Scott Millett)以了解RDM的好处。
与大多数反模式相同,它的优点是:它可以让很多人长时间忙碌。由于管理者管理更多的人时往往会得到更高的报酬,因此有强烈的动机去不进行改进。
与Eric P的回答以及上面其他一些人的回答一致,似乎ADM的主要缺点是OOD的丢失,特别是将域概念的逻辑和数据保持在一起,从而隐藏实现细节的同时, API可以很丰富。
埃里克(Eric)继续指出,领域类之外通常还存在一些信息,这些信息对于对该类起作用的逻辑是必需的,例如在将商品添加到订单之前检查库存。但是,我想知道答案是服务层是否拥有这种总体逻辑,还是作为对象设计的一部分更好地进行处理。 有人知道库存对象,产品对象和订单对象。也许它只是一个OrderSystem对象,它具有一个库存成员,一个订单列表等。这看起来与Service并没有太大区别,但是我认为它在概念上更加一致。
或这样看:您可以拥有一个具有内部信用余额的User,并且每次调用User.addItemToOrder(item)时,它都会获取商品的价格并在添加信用之前检查其信用,等等。这看起来很合理设计。我不确定用Service.addItemToUserOrder(user,item)替换掉它到底丢失了什么,但是我也不知道获得了什么。我猜想这将是多余的代码层,再加上笨拙的书写风格和底层域模型的强制性无知。
应当注意,随着系统的复杂性和变化粒度的增长,精心设计的消息传递对象模型提供的接口点的封装和合并使无需广泛重构即可更安全地更改和维护关键代码。
由ADM创建的服务层虽然确实易于实施(因为它们需要的思考相对较少,并且具有许多分散的接口点),但是当需要修改实时且不断发展的系统时,可能会给您带来麻烦。
我还可以补充一点,并不是所有情况下都根本不需要领域模型(更不用说ADM了)。有时最好使用更具过程性/功能性的任务,该任务是数据驱动的,并且不依赖于应用程序范围的逻辑/业务规则。
如果您要决定整个应用程序的优缺点,我认为在开始编写一行代码之前,首先设计给定应用程序的外观是很重要的。在对两种样式的应用程序进行CRC编码或线框化后,请退后一步,确定哪种方法更有意义并更适合该应用程序。
还要预先考虑哪一个将更容易维护...
在我第一次阅读Eric Evans的关于域驱动设计的书之后,我并没有真正理解,这不仅仅是用于设计好的域模型类的一堆战术模式。
在学习了有关该主题的更多信息并使用了战略模式之后,我终于开始了解到,起初这是对业务问题的深入了解要解决。
并且只有在此之后,您才能决定系统的哪些部分适合于应用战术模式(例如聚合,实体,存储库等)以及所谓的丰富域模型(与贫乏模型相对)。但是为了从这些模式中受益,必须在业务逻辑方面有足够的复杂性针对该系统的那部分,在。
因此,在实施眼前问题的解决方案时,应首先确定是否可以通过使用基于CRUD的方法或投资于富域模型来更好地解决此特定问题与上述战术模式一起。
如果CRUD更有意义,例如,如果没有复杂的业务逻辑,并且大多数逻辑都与转换,实现域模型的数据的传输和持久化有关,那将是不必要的过大杀伤力。这并不意味着不会有很多工作要做,而仅仅是产生最大工作量的不是业务规则。但是在这种情况下,根本就没有贫血领域模型,仅仅是因为根本没有领域模型。您宁愿看到的是诸如DTO(数据传输对象)或DAO之(数据访问对象)和将对数据进行操作的服务类。而且,相应的操作在很大程度上与将数据从一种表示形式转换为另一种表示形式以及在几乎没有或几乎没有业务逻辑的情况下移动数据有关。
如果您确定有很多复杂的业务逻辑,这些逻辑也会随着时间的推移而发生变化,那么根据我的经验,投资领域模型是一个好主意。原因是更容易通过代码表示业务透视图,并且更容易理解反映业务域及其规则的相应操作。这并不意味着在每个用例中都必须有域模型类。例如,如果没有要被突变和持久化的状态,那么也只能有包含域逻辑的域服务被实现得更像纯函数。
但是,如果在业务领域中也存在具有目的和意义的要变异和持久的状态,则该状态和更改该状态的行为应被封装。这样一来,没有人能绕过容易的业务规则,从而导致无效状态以及严重的故障。所谓的贫血症领域模型通常是此类问题的根源,。如果您看到代码,其中不同的组件在同一个“贫乏”域模型类上运行,则在不关心或不了解该业务实体的整体不变性的情况下,检查其状态的某些部分并更改其状态的某些部分,通常是这种情况。不必将其称为反模式,但重要的是要了解您会失去丰富领域模型的许多好处在基于DDD的方法中,以及上述问题。当使用将行为及其数据放在同一个类中的域模型时,也可以有很多不同的“客户端”调用该类的操作,但是他们不必关心遵守业务实体的业务不变量,因为域模型类将始终处理该问题,并且还可以将无效操作告知“客户端”,甚至将异常作为安全网抛出。
因此,最重要的是,我认为重要的是不要将类(如DTO或DAO)之类的数据结构与贫血的领域模型类混为一谈。在精心选择的基于CRUD的方法中,尝试使用域模型没有任何优势,因为业务逻辑太复杂了。
通过贫血领域模型,我将参考代码,从中我可以看到有很多复杂的业务逻辑和业务规则分布在不同的组件上,而这些组件应该更接近这些逻辑所更改的数据。
我在此过程中还吸取了另一课:如果您尝试在代码中使用与牛排店主在日常工作中使用的相同的业务语言(也称为无处不在的语言),则您已经获得了许多有关理解业务领域和提高代码的可读性,无论您使用基于CRUD的方法还是基于域模型的方法,都将对您有很大帮助。
为了扩展Michael的答案,我认为(相当)清楚该代码应该去哪里:放入专门的调解员,该调解员处理订单和库存之间的交互。
从我的观点来看,关于域的关键是它必须保持简单的测试行为, isInThisState()
方法等。以我的经验,这些也散布在大多数公司的服务眼泪中(sic :)),并且要么被复制,要么被无休止地重写。所有这些都违反了标准的融合规则。
在我看来,这种方法应该是旨在用于保存许多BIZ beahaviour作为为实用一个DM,把休息,在明确指定的区域(即不在服务)
一个贫血域模型是一个反模式。反模式没有优点。