62

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

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

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

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

这两种想法可以共存吗?

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

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

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

4

7 回答 7

13

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

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

最后,三个问题:

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

  • 进一步说,即使是正确的(但不必要的) SRP应用也可能导致不必要的复杂性。总是想着下一个必须维护你的类的可怜的草皮:将琐碎的行为努力抽象到它自己的接口、基类和单行实现中真的有助于他理解什么应该只是一个单一的类吗?

  • 软件设计通常是为了在竞争力量之间取得最佳折衷。例如,分层架构主要是 SRP 的一个很好的应用程序,但是,例如,业务类的属性从布尔值枚举的更改会在所有层之间产生连锁反应,这又如何呢? - 从数据库到域、外墙、Web 服务,再到 GUI?这是否指向糟糕的设计?不一定:它表明您的设计偏向于从一个方面改变另一个方面。

于 2010-06-03T08:28:53.687 回答
9

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

我怀疑根据您的描述,您最终得到的类基本上是操作包,因此您基本上重新创建了 C 风格的编码:结构和模块。

来自链接的 SRP 论文:“ SRP 是最简单的原理之一,也是最难正确的原理之一。

于 2009-09-09T11:42:14.400 回答
7

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

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

基于可读性或所需的凝聚力水平,为自己设定一些现实的目标通常是件好事。当你说“我想让这门课做一件事”时,它不应该多或少地做这件事。您可以至少在程序上保持这种基本理念的凝聚力。“我希望这个类维护发票的所有数据”通常会允许一些业务逻辑,甚至是汇总小计或计算销售税,基于对象的责任知道如何为任何字段提供准确的、内部一致的值它包含。

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

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

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

我的 0.02 美元。

于 2010-08-31T15:44:11.110 回答
5

我发现遵循可靠的原则实际上确实让我远离了 DDD 的丰富领域模型,最后,我发现我不在乎。更重要的是,我发现域模型的逻辑概念和任何语言的类都不是 1:1 映射的,除非我们谈论的是某种外观。

我不会说这完全是一种 c 风格的编程,你有结构和模块,但你可能最终会得到更实用的东西,我意识到风格相似,但细节有很大的不同。我发现我的类实例最终表现得像高阶函数、部分函数应用程序、惰性求值函数或上述的某种组合。这对我来说有点难以言喻,但这是我从遵循 TDD + SOLID 编写代码中得到的感觉,它最终表现得像一种混合的 OO/Functional 风格。

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

于 2009-09-14T20:38:21.627 回答
1

我喜欢 SRP 的定义:

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

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

于 2009-10-29T20:12:47.827 回答
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 是什么。

于 2012-03-14T23:06:30.510 回答
0

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

于 2010-06-03T08:34:42.977 回答