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

时间:2009-09-09 11:10:50

标签: oop solid-principles

我正处于一个非常认真对待单一责任原则的项目中。我们有很多小班,事情很简单。但是,我们有一个贫血的领域模型 - 我们的任何模型类都没有行为,它们只是属性包。这不是对我们设计的抱怨 - 它实际上看起来效果很好

在设计审核期间,每当向系统添加新行为时都会引出SRP,因此新行为通常会在新类中结束。这使得事情很容易被单元测试,但我有时会感到困惑,因为它感觉就像将行为拉出相关的地方。

我正在努力提高我对如何正确应用SRP的理解。在我看来,SRP反对添加与一个对象共享相同上下文的业务建模行为,因为该对象不可避免地最终要么做多个相关的事情,要么做一件事,但要知道改变形状的多个业务规则其产出。

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

这两个想法能共存吗?

编辑:一些与上下文相关的链接:

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

我不是那种喜欢找先知并遵循他们所说的福音的开发者。所以我不提供这些链接,作为说明“这些是规则”的方式,只是作为两个概念定义的来源。

7 个答案:

答案 0 :(得分:13)

富域模型(RDM)和单一责任原则(SRP)并不一定是不一致的。 RDM与SRP的一个非常专业的子类更加不一致 - 该模型主张“数据bean +控制器类中的所有业务逻辑”(DBABLICC)。

如果您阅读了Martin的SRP chapter,您会看到他的调制解调器示例完全在域层中,但将DataChannel和Connection概念抽象为单独的类。他将调制解调器本身保留为包装器,因为这对客户端代码是有用的抽象。它更多地是关于正确的(重新)分解而不仅仅是分层。内聚和耦合仍然是设计的基本原则。

最后,有三个问题:

  • 正如马丁所说,要看到不同的“变革原因”并不容易。 YAGNI,Agile等的概念阻碍了对未来变化原因的预期,因此我们不应该发明那些不会立即明显的原因。在应用SRP时,我将“过早的,预期的变更原因”视为真正的风险,并应由开发人员管理。

  • 除此之外,甚至正确的(但不必要的肛门)SRP应用程序可能会导致不必要的复杂性。总是想想下一个必须维护你的课程的可怜的草皮:将琐碎的行为勤奋地抽象到自己的界面,基类和单行实现中是否真的有助于他理解应该只是一个单独的类?

  • 软件设计通常是在竞争力量之间达成最佳平衡。例如,分层体系结构主要是SRP的一个很好的应用程序,但事实上,例如,业务类的属性从例如 boolean 更改为 enum 所有层之间产生连锁反应 - 从db到域,外观,Web服务,再到GUI?这是否意味着糟糕的设计?不一定:它表明你的设计倾向于改变另一方面的一个方面。

答案 1 :(得分:9)

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

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

来自链接的SRP文件: “ SRP是最简单的原则之一,也是最难做到的。

答案 2 :(得分:6)

SRP文件的引用非常正确; SRP很难做对。这个和OCP是SOLID的两个元素,它们必须至少在一定程度上放松才能真正完成项目。过度使用任何一种都会很快产生馄饨代码。

如果“改变的原因”过于具体,SRP确实可以被视为荒谬的长度。如果您认为字段的类型更改为“更改”,即使POCO / POJO“数据包”也可以被视为违反SRP。您认为常识会告诉您字段的类型更改是“更改”的必要条件,但我已经看到了包含内置值类型包装的域图层;一个让ADM看起来像乌托邦的地狱。

基于可读性或期望的凝聚力水平,基于一些现实的目标为自己奠定基础通常是好的。当你说“我希望这个班级做一件事”时,它应该没有或多于做必要的事情。你可以用这个基本哲学保持至少程序上的凝聚力。 “我希望这个类能够维护发票的所有数据”通常会允许一些业务逻辑,甚至是汇总小计或计算销售税,这取决于对象有责任知道如何为任何字段提供准确的,内部一致的值它包含。

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

我不认为这样的域对象是“贫血的”。我对这个术语的看法是一个“数据包”,这是一个对外界没有任何概念的领域集合,甚至是除了包含它们之外的其他领域之间的关系。我也看到了这一点,追踪对象永远不会知道的对象状态不一致并不是一件有趣的事情。过度热心的SRP将通过声明数据对象不对任何业务逻辑负责来导致这一点,但常识通常会首先介入,并说该对象作为数据专家必须负责维护一致的内部状态。

再次,个人意见,我更喜欢Repository模式到Active Record。一个对象,只有一个责任,而且在该层之上的系统中的其他任何东西都必须知道它是如何工作的。 Active Record要求域层至少知道有关持久性方法或框架的一些特定细节(无论是用于读/写每个类的存储过程的名称,特定于框架的对象引用,还是用ORM信息装饰字段的属性) ),因此默认情况下会注入第二个更改为每个域类的原因。

我的0.02美元。

答案 3 :(得分:4)

我发现遵循坚实的原则确实让我远离了DDD的丰富域名模型,最后,我发现我并不在意。更重要的是,我发现域模型的逻辑概念和任何语言的类都没有按1:1映射,除非我们讨论的是某种外观。

我不会说这是一个c风格的编程,你有结构和模块,但你最终可能会得到更多功能的东西,我意识到风格是相似的,但细节很大区别。我发现我的类实例最终表现得像高阶函数,部分函数应用程序,懒惰评估函数或上面的某些组合。这对我来说有点不可言喻,但这就是我在TDD + SOLID之后编写代码的感觉,它最终表现得像混合OO /功能风格。

至于继承是一个坏词,我认为更多是由于继承在Java / C#等语言中不够细致。在其他语言中,它不是一个问题,而是更有用。

答案 4 :(得分:1)

我喜欢SRP的定义:

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

因此,只要行为可以归为单个“业务原因”,那么它们就没有理由不在同一个类中共存。当然,定义“商业理由”的内容可以辩论(并且应该由所有利益相关者进行辩论)。

答案 5 :(得分:1)

在我进入咆哮之前,简而言之,这是我的意见:在某个地方,一切都必须走到一起......然后一条河流贯穿其中。

我被编码所困扰。

=======

贫血数据模型和我......好吧,我们经常玩耍。也许这只是中小型应用程序的本质,它们内置的业务逻辑非常少。也许我只是有点儿'。

然而,这是我的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)。一块蛋糕。”那么,现在的逻辑在哪里?所有这一种方法?你仍然只是推动代码,无论如何,你最终会得到数据和逻辑分离。

所以在这种情况下,为什么不向客户端代码公开特定的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个(或更多个)不同的操作。只需为每个原子操作创建一个接口,并将其挂钩在DoAction1内。

这就是我如何处理这种情况。但话说回来,我真的不知道SRP究竟是什么。

答案 6 :(得分:0)

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