代码之家  ›  专栏  ›  技术社区  ›  Niall Connaughton

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

  •  58
  • Niall Connaughton  · 技术社区  · 16 年前

    我所从事的项目非常重视单一责任原则。我们有很多小班,事情很简单。然而,我们有一个贫血的领域模型-在我们的任何模型类中都没有行为,它们只是属性包。这并不是对我们的设计的抱怨-实际上它看起来工作得很好

    在设计评审期间,每当向系统中添加新的行为时,就会产生srp,因此新的行为通常会出现在一个新类中。这使得事情很容易进行单元测试,但我有时会感到困惑,因为这感觉像是把行为从相关的地方拉出来。

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

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

    这两种观念能共存吗?

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

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

    我不是那种只想找到一个预言家并遵循他们所说的福音的开发者。因此,我不提供这些链接来说明“这些是规则”,只是作为这两个概念的定义来源。

    7 回复  |  直到 12 年前
        1
  •  9
  •   CPerkins    16 年前

    我不得不说“是的”,但你必须做好你的srp。如果同一个操作只应用于一个类,那么它就属于那个类,不是吗?如果同一个操作应用于多个类呢?在这种情况下,如果你想遵循把数据和行为结合起来的oo模型,你应该把操作放到一个基类中,不是吗?

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

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

        2
  •  13
  •   Cornel Masson    16 年前

    富域模型(rdm)和单一责任原则(srp)并不一定矛盾。rdm与一个非常专业的srp子类(提倡“数据bean+控制器类中的所有业务逻辑”的模型(dbablicc))更不一致。

    如果你读马丁的 SRP chapter ,你会看到他的 调制解调 示例完全在域层中,但将数据通道和连接概念抽象为单独的类。他将调制解调器本身作为包装器保存,因为这对客户机代码是有用的抽象。更重要的是 (再)保理 比单纯 分层 . 衔接和耦合仍然是设计的基本原则。

    最后,有三个问题:

    • 正如马丁自己指出的那样,要看到不同的“变革原因”并不总是容易的。雅格尼、敏捷等概念本身就阻碍了人们对未来变革原因的预期,因此我们不应该在那些不太明显的地方发明它们。我认为“过早的,预期的改变原因”是 真实风险 在应用srp时,应该由开发人员管理。

    • 更进一步,甚至 对的 (但不必要 肛门 )应用srp可能导致不必要的复杂性。总是想一想下一个必须维护你的类的可怜虫:将琐碎的行为勤勉地抽象到它自己的接口、基类和单行实现中,真的有助于他理解什么应该只是一个类吗?

    • 软件设计通常是为了在相互竞争的力量之间获得最佳的折衷。例如,分层架构主要是srp的一个很好的应用,但是,例如,业务类的属性从 布尔 枚举 有连锁反应 全部的 从数据库到域、外观、web服务,再到gui?这是否意味着糟糕的设计?不一定:它指出了这样一个事实:你的设计倾向于从一个方面到另一个方面的改变。

        3
  •  6
  •   KeithS    15 年前

    SRP论文中的引用是非常正确的;SRP很难得到正确的答案。这一点和ocp是solid的两个元素,为了实际完成一个项目,这两个元素必须放松到至少某种程度。过分热心地应用两者都会很快产生馄饨代码。

    如果“改变的原因”过于具体,SRP确实可以被视为荒谬的长度。即使一个POCO / POJO“数据包”可以被认为是违反SRP,如果你认为一个字段的类型改变为“改变”。您可能认为常识会告诉您,字段的类型更改是“更改”所必需的允许,但我见过为内置值类型使用包装器的域层;一个让adm看起来像乌托邦的地狱。

    基于可读性或期望的内聚水平,用一些现实的目标为自己打下基础通常是好的。当你说“我想让这个班做一件事”的时候,它应该没有比做这件事所必需的更多或更少的东西。你至少可以用这个基本哲学来保持程序衔接。”我希望这个类维护发票的所有数据“通常允许一些业务逻辑,甚至汇总小计或计算销售税,基于对象的责任,知道如何为它包含的任何字段提供准确、内部一致的值。

    我个人对“轻量级”域没有大问题。只需扮演“数据专家”的角色,域对象就可以成为与类相关的每个字段/属性以及所有计算字段逻辑、任何显式/隐式数据类型转换以及更简单的验证规则(即必需字段、值限制,如果允许,会在内部中断实例的内容)。如果一个计算算法,可能是一个加权平均值或滚动平均值,可能会发生变化,那么将该算法封装起来,并在计算字段中引用它(这只是一个好的ocp/pv)。

    我不认为这样的领域对象是“贫血”。我对这个术语的理解是一个“数据包”,它是一个字段集合,除了包含这些字段之外,它对外部世界甚至其字段之间的关系都没有任何概念。我也看到过,跟踪对象状态中的不一致并不有趣,因为对象从来不知道这是一个问题。过分热心的srp会导致这种情况,因为它声明一个数据对象不负责任何业务逻辑,但常识通常会首先介入,并说该对象作为数据专家必须负责维护一致的内部状态。

    再说一次,个人观点,比起活动记录,我更喜欢存储库模式。一个对象,有一个职责,而且在这个层之上的系统中几乎没有任何东西需要知道它是如何工作的。活动记录要求域层至少知道有关持久化方法或框架的一些特定细节(无论是用于读/写每个类的存储过程的名称、特定于框架的对象引用,还是用orm信息修饰字段的属性)。因此在默认情况下,将第二个原因注入到每个域类中。

    我的0.02美元。

        4
  •  4
  •   Saem    16 年前

    我发现遵循这些坚实的原则确实让我远离了ddd的富域模型,最后,我发现我并不在乎。更重要的是,我发现域模型的逻辑概念和任何语言的类都不是1:1映射的,除非我们讨论的是某种门面。

    我不会说这完全是一种C风格的编程,你有结构和模块,但你可能会以更实用的方式结束,我意识到风格是相似的,但细节有很大的不同。我发现我的类实例最终的行为类似于高阶函数、部分函数应用程序、延迟计算的函数或以上的一些组合。这对我来说有点难以形容,但这就是我从遵循tdd+solid编写代码中得到的感觉,它最终表现为一种混合的oo/函数风格。

    至于继承是一个不好的词,我认为这更多的是因为继承在Java和C语言中没有足够的细粒度。在其他语言中,它不那么重要,而且更有用。

        5
  •  1
  •   JontyMC    16 年前

    我喜欢SRP的定义是:

    “一个类只有一个业务原因需要更改”

    因此,只要行为可以被归类为单一的“商业原因”,那么就没有理由让他们不在同一个阶级共存。当然,什么定义了“业务原因”值得商榷(所有利益相关者都应该商榷)。

        6
  •  1
  •   Merritt    14 年前

    在我开始咆哮之前,我的观点是:在某个地方,所有的事情都必须走到一起…然后一条河流过。

    我被编码困扰着。

    不受欢迎的=

    贫血数据模型和我…嗯,我们经常在一起。也许这只是中小型应用程序的本质,其中很少内置业务逻辑。也许我只是有点“油腻”。

    不过,这是我的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、数据包和一系列“服务”,每个服务都有一个单一的职责。他的工作就是确保所有这些东西都能正常互动。所以现在他的代码变成了一个事务脚本,该脚本本身将包含正确完成特定事务(或工作单元)所需的所有职责。

    此外,您可以说,“没有brah,他需要做的只是访问服务层。就像object1service.doActionX(object1)。小菜一碟。“那么,现在的逻辑在哪里呢?所有这些都是一种方法?你仍然只是在推送代码,不管怎样,你最终都会得到数据和逻辑的分离。

    所以在这个场景中,为什么不向客户机代码公开特定的object1服务,让它的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是什么意思。

        7
  •  0
  •   this. __curious_geek    16 年前

    将纯域对象转换为 ActiveRecord 对所有域对象使用公共基类的模式。将公共行为放在基类中,并在必要时重写派生类中的行为,或者在需要时定义新行为。

    推荐文章