单一责任原则与贫困领域模式反模式

我在一个认真对待单一职责原则的项目中。 我们有很多小class,事情很简单。 然而,我们有一个贫血的领域模型 – 在我们的任何模型类中都没有任何行为,它们只是属性包。 这不是对我们的devise抱怨 – 它似乎工作得很好

在devise评审过程中,无论何时将新行为添加到系统中,都会引发SRP,因此新行为通常会以新类别结束。 这使得unit testing非常容易,但是有时我感到困惑,因为感觉就像是在相关的地方拉动行为。

我试图提高我对如何正确应用SRP的理解。 在我看来,SRP反对增加业务build模行为,它们共享一个对象的相同上下文,因为对象不可避免地要做多个相关的事情,或者做一件事,但知道改变形状的多个业务规则的产出。

如果是这样,那么感觉最终的结果就是一个贫血的领域模型,这在我们的项目当然是这样的。 然而,贫血领域模型是一种反模式。

这两个想法可以共存吗?

编辑:几个上下文相关的链接:

SRP – http://www.objectmentor.com/resources/articles/srp.pdf
贫血领域模型 – http://martinfowler.com/bliki/AnemicDomainModel.html

我不是那种喜欢寻找先知的开发者,而是追随他们所说的福音。 所以我不提供这些链接作为“这些就是规则”的一种说法,只是作为两个概念定义的来源。

我不得不说“是”,但是你必须正确地做你的SRP。 如果同一个操作只适用于一个class级,那它就属于那个class级,你不说吗? 如果同一个操作适用于多个类,那么怎么样? 在这种情况下,如果你想遵循组合数据和行为的OO模型,你会把这个操作放到一个基类中,不是吗?

我怀疑从你的描述中,你最终得到的是基本上类似于操作的类,所以你基本上重新创build了C风格的编码:结构和模块。

从相关的SRP文件中:“ SRP是原理中最简单的一个,也是最难做到的一个。

富域模型(RDM)和单一责任原则(SRP)不一定矛盾。 RDM与SRP的一个非常特殊的子类更为矛盾 – 这个模型提倡“数据bean +控制器类中的所有业务逻辑”(DBABLICC)。

如果您阅读Martin的SRP章节 ,您会看到他的调制解调器示例完全位于领域层,但将DataChannel和Connection概念抽象为单独的类。 他保持调制解调器本身作为包装,因为这是客户端代码有用的抽象。 正确(重新)分解比单纯分层更重要 。 内聚和耦合仍然是devise的基本原则。

最后有三个问题:

  • 正如马丁所指出的那样,看到不同的“变化原因”并不总是容易的。 YAGNI,敏捷等的概念阻碍了对未来变化原因的预期,所以我们不应该发明那些并不明显的概念。 我认为“过早的,预期的变化原因”是应用SRP的真正风险 ,应该由开发者来pipe理。

  • 除此之外,即使SRP 正确 (但不必要的肛门 )应用也可能导致不必要的复杂性。 总是想想要维护你的class级的下一个可怜的草率人:勤勉地将微不足道的行为抽象成自己的界面,基类和单行实现,是否真的有助于他理解单纯的class级?

  • 软件devise通常是在竞争力量之间取得最好的折衷。 例如,一个分层的体系结构大多是SRP的一个很好的应用,但是,例如,一个业务类的属性从一个布尔值到一个枚举 的变化在所有的层次上都会产生连锁反应 – 从数据库通过域,外墙,Web服务,到GUI? 这是否指向糟糕的devise? 不一定:它指出了一个事实,即你的devise倾向于改变另一个方面。

SRP文件的引用非常正确, SRP很难得到正确的。 这个和OCP是SOLID的两个要素,为了实际完成一个项目,必须至less在一定程度上放宽。 任何一方的过度使用都会很快产生馄饨代码。

如果“变革的原因”太具体,SRP的确可以被认为是荒谬的。 即使是一个POCO / POJO“数据包”可以被认为是违反SRP,如果你认为字段的types改变为“改变”。 你会认为常识会告诉你,一个字段的types改变是“改变”的一个必要的限制,但是我已经看到了带有内置值types的包装的域图层; 让ADM看起来像乌托邦的地狱。

基于可读性或期望的凝聚力水平,以一些现实的目标为基础往往是好事。 当你说“我要这个class做一件事”的时候,应该没有什么比做这件事更有必要。 这个基本的哲学至less可以保持程序的凝聚力。 “我希望这个class级保留发票的所有数据”通常会允许某些商业逻辑,即使总结小计或计算销售税,根据对象的责任知道如何为您提供任何领域的准确,内部一致的价值它包含。

我个人没有一个“轻量级”域的大问题。 仅仅担任“数据专家”这一angular色就会使域对象与类相关的每个字段/属性的守护者,以及所有计算的字段逻辑,任何显式/隐式数据types转换以及可能的更简单的validation规则(即必需的字段,数值限制,如果允许的话会在内部破坏实例)。 如果计算algorithm(可能是加权平均或滚动平均值)很可能发生变化,则封装algorithm并在计算字段中引用它(这只是很好的OCP / PV)。

我不认为这样的领域对象是“贫血”的。 我对这个术语的理解是一个“数据包”,是一个对外部世界没有任何概念的领域集合,甚至包含它们之外的领域之间的关系。 我也看到了这一点,追踪对象状态不一致的不一致是不好的, 过度热情的SRP会导致这种情况,指出一个数据对象不负责任何业务逻辑,但常识一般会首先介入,并说作为数据专家的对象必须负责维护一致的内部状态。

同样,个人观点,我更喜欢Repository模式到Active Record。 一个对象,有一个责任,而且在这个层之上的系统中,如果有什么其他的东西,就不得不知道它是如何工作的。 Active Record要求领域层至less知道持久性方法或框架的某些具体细节(无论是用于读/写每个类的存储过程的名称,特定于框架的对象引用,还是使用ORM信息装饰字段的属性),因此默认注入第二个变为每个域类的原因。

我的$ 0.02。

我发现下面这些坚实的原则实际上导致我离开了DDD丰富的领域模型,最后,我发现我并不在乎。 更重要的是,我发现领域模型的逻辑概念和任何语言的类都不是1:1映射的,除非我们正在谈论某种types的外观。

我不会说这完全是一个C风格的程序devise,你可能会有更多的function,我意识到风格是相似的,但是细节却有很大的不同。 我发现我的类实例最终performance得像高阶函数,部分函数应用程序,懒散评估的函数或者上面的一些组合。 这对我来说有点不可言喻,但是这是我从TDD + SOLID编写代码后得到的感觉,它最终performance得像一个混合的面向对象/function风格。

至于inheritance是一个坏词,我认为更多的是因为Java / C#等语言中的inheritance不够细致。 在其他语言中,这不是一个问题,更有用。

我喜欢SRP的定义如下:

“一个class级只有一个商业理由要改变”

所以,只要行为可以归结为单一的“商业原因”,那么他们没有理由不在同一个class级共存。 当然,定义“商业理性”的东西是有争议的(所有利益相关者都应该讨论)。

在我进入我的咆哮之前,我的观点简而言之就是:在某个地方,所有事情都要聚集在一起……然后一条河stream穿过它。

我被编码困扰。

=======

贫血的数据模型和我…呃,我们联系了很多。 也许这只是中小型应用程序的本质,它们内置的业务逻辑很less。 也许我只是一点点“tarded”。

不过,这是我的2美分:

难道你不能把实体中的代码分解出来并把它连接到一个接口上吗?

public class Object1 { public string Property1 { get; set; } public string Property2 { get; set; } private IAction1 action1; public Object1(IAction1 action1) { this.action1 = action1; } public void DoAction1() { action1.Do(Property1); } } public interface IAction1 { void Do(string input1); } 

这是否违反了SRP的原则?

而且,没有一堆类别之间没有任何关系,只是消费代码实际上是对SRP的一个更大的违反,而是推了一层呢?

想象一下,编写客户端代码的人试图弄清楚如何做一些与Object1相关的事情。 如果他必须与你的模型一起工作,他将与Object1,数据包,以及一系列“服务”一起工作。 确保所有这些事情正确地交互是他的工作。 所以现在他的代码变成了交易脚本,而脚本本身就包含了正确完成特定交易(或工作单元)所必需的每一个责任。

而且,你可以说:“不要啰嗦,他只需要访问服务层,就像Object1Service.DoActionX(Object1),一块蛋糕。 那么,现在的逻辑呢? 所有在这一个方法? 你仍然只是推动代码,不pipe怎样,你最终会得到数据和逻辑分离。

所以在这种情况下,为什么不暴露给客户端的代码,特定的Object1Service,并且它的DoActionX()基本上只是你的域模型的另一个钩子? 我的意思是:

 public class Object1Service { private Object1Repository repository; public Object1Service(Object1Repository repository) { this.repository = repository; } // Tie in your Unit of Work Aspect'ing stuff or whatever if need be public void DoAction1(Object1DTO object1DTO) { Object1 object1 = repository.GetById(object1DTO.Id); object1.DoAction1(); repository.Save(object1); } } 

您仍然已经从Object1中分解了Action1的实际代码,但是对于所有密集的目的,都有一个非贫血的Object1。

假设你需要Action1来代表2个(或更多)不同的操作,你可以将它们分解成自己的类。 只需为每个primefaces操作创build一个接口并将其连接到DoAction1中。

这就是我可能会遇到这种情况。 但是再一次,我真的不知道SRP是什么。

将普通域对象转换为具有公共基类的ActiveRecord模式,以将其转换为所有域对象。 将常见行为放在基类中,并在必要时重写派生类中的行为,或者在需要的地方定义新行为。

Interesting Posts