域实体是否违反单一责任原则?


13

实体的单一责任(改变的原因)应该是唯一地标识自己,换句话说,是可以发现其责任的。

埃里克·埃文(Eric Evan)的DDD书,第16页。93:

实体的最基本责任是建立连续性,使行为清晰可预测。如果他们备用,他们会做到最好。与其着重于属性甚至行为,不如将实体对象的定义简化为最固有的特征,尤其是那些识别该特征或通常用于查找或匹配该特征的特征。仅添加对于该行为所需的概念和属性必不可少的行为。

除此之外,还希望将行为和属性删除到与核心Entity相关联的其他对象中。除了身份问题外,实体还倾向于通过协调其拥有的对象的操作来履行其职责。

1。

...将ENTITY对象的定义简化为最固有的特征,尤其是那些识别它或通常用于查找或匹配它的特征。仅添加对该概念必不可少的行为...

实体分配一个唯一的ID后,它的身份就会建立,因此我认为这样的实体不需要任何行为就能维持其身份帮助其识别自己。因此,我不明白作者用“ 对于概念必不可少的行为 ” 指的是什么行为(除了findmatch 操作之外)?

2。

...将ENTITY对象的定义简化为最固有的特征,尤其是那些识别它或通常用于查找或匹配它的特征。...除此之外,还希望将行为和属性删除到与核心ENTITY相关的其他对象中。

因此,任何无助于识别实体的行为,但我们仍将其表征为该实体的固有特征(即吠叫是狗固有的,飞行是飞机固有的,产卵是鸟类固有的.. ),应该放入与该实体关联的其他对象中(例如:我们应该将吠叫行为放入与dog实体关联的对象中)?

3。

除此之外,还希望将行为和属性删除到与核心ENTITY相关联的其他对象中。

一)MyEntity代表的职责A_respB_resp对象a,并b分别。

即使大多数的A_respB_resp工作是由做ab的情况下,客户端仍担任A_respB_resp通过MyEntity,这意味着,从客户的角度来看,这两个职责属于MyEntity。这样,难道这不是意味着MyEntity也有A_respB_resp责任,因此违反了SRP

b)即使我们假设A_resp并且B_resp不属于MyEntityMyEntity仍然有责任AB_resp协调对象a和的操作b。因此,不MyEntity违反SRP,因为它至少具有两项职责 –唯一标识自己,以及AB_resp

class MyEntity
{
    private A a = ...
    private B b = ...


    public A GetA()
    { ... }

    public B GetB()
    { ... }

    /* coordinates operations of objects a and b */
    public int AworkB()
    { ... }
}

/* A encapsulates a single responsibility resp_A*/
/* A is value object */
class A
{ ... }

/* B encapsulates a single responsibility resp_B*/
/* B is value object */
class B
{ ... }

更新:

1。

在这种情况下,行为是指语义行为。例如,用于唯一标识它的类的属性(即域对象的属性)具有行为。虽然这不能直接在代码中表示。预期的行为是该属性将没有任何重复的值。

因此,在代码中,我们几乎永远不需要真正实现某种方式(即操作)以某种方式保持实体的身份,因为正如您所解释的,这种行为仅作为概念存在于域模型中(以ID属性的形式存在)。实体),但是当我们将此ID属性转换为代码时,其语义的一部分会丢失(即,隐含地确保ID值唯一的那一部分)?

2。

此外,诸如Age之类的属性在Person实体之外没有上下文,因此移入其他对象没有任何意义。但是,信息可以很容易地存储在唯一标识符所在的单独位置,因此对行为的混淆。年龄可能是延迟加载的值。

a)如果Age属性是延迟加载的,那么即使在语义Age上仅仅是属性,我们也可以将其称为行为。

3。

您可以轻松进行特定于地址的操作,例如验证其是否为有效地址。您可能在设计时不知道这一点,但是整个概念是将对象分解为最小的部分

虽然我同意通过移动Age到其他对象来丢失上下文DateOfBirth,但是如果将属性移动到另一个对象中,上下文也不会丢失,但是通常不会移动它。

我们Address移入另一个对象而不是移入另一个对象的主要原因是DateOfBirth什么?因为DateOfBirthPerson实体固有的,或者是因为将来在某处发生的可能性较小,所以我们可能需要定义特定于DateOfBirth?的操作。

4.我必须说,我仍然不知道是否MyEntity也有A_respB_resp责任,为什么MyEntity也已经AB_resp不被认为是违反了SRP

EULERFX

1)

作者所指的行为是与实体相关联的行为。这些是修改实体状态的行为

a)如果我理解正确的话,你说,实体应该只包含那些行为,修改其属性(即其状态)?

B)又是怎么回事的行为不一定修改实体的状态,但仍被认为是一种内在该特性的实体(例如:吠叫固有的一个特征Dog的实体,即使它没有修改狗的状态)?我们应该将这些行为包含在实体中还是应该将其转移到其他对象?

2)

至于将行为转移到其他对象上,作者专门指的是价值对象。

尽管我的报价未包括在内,但作者确实在同一段落中提到,在某些情况下,行为(和属性)也将转移到其他实体中(尽管我了解将行为转移到VO 的好处)

3)假设MyEntity(见问题3.在我原来的职位)不违反SRP,我们会说一个责任MyEntity是除其他事项外还组成:

一个。A_resp + B_resp + AB_respAB_resp协调对象ab

要么

b。 AB_resp +委托A_resp和关联B_resp到的对象(abMyEntity

4)埃里克·埃文(Eric Evan)的DDD书,第16页。94:

CustomerID是Customer ENTITY的唯一标识符(图5.5),但是电话号码和地址通常用于查找或匹配Customer。这个名字并没有定义一个人的身份,但是经常被用作确定身份的一部分。

在此示例中,电话和地址属性移入“客户”,但是在一个实际项目中,该选择将取决于通常如何匹配或区分域的客户。例如,如果客户有许多用于不同目的的联系电话号码,则该电话号码与身份无关,应与销售联系人保持联系。

一个)

CustomerID是Customer ENTITY的唯一标识符(图5.5),但是电话号码和地址通常用于查找或匹配Customer。这个名字并没有定义一个人的身份,但是经常被用作确定身份的一部分。

Quote指出只有与身份相关的属性才应保留在实体中。我假设笔者意味着实体应该只包含那些属性是经常被用来查找或匹配实体,而其他所有的属性应该被移到?

b)但是其他属性应该如何/在何处移动?例如(假设这里不使用address属性查找或匹配 Customer,因此我们想将address属性移出Customer):

如果不是创建Customer.Address类型string的属性,而是将地址属性移到关联的VO对象(类型),而不是创建Customer.Address类型Address属性AddressCustomer还是说仍然包含地址属性

C)

在此示例中,电话和地址属性移入“客户”,但是在一个实际项目中,该选择将取决于通常如何匹配或区分域的客户。例如,如果客户有许多用于不同目的的联系电话号码,则该电话号码与身份无关,应与销售联系人保持联系。

这里的作者不是错的,因为如果我们假设许多仅属于该特定联系人的联系电话号码中的每一个,那么我想说这些电话号码身份的关联就像只有一个电话号码时一样CustomerCustomerCustomer

5)

作者建议剥离实体的原因是,当一个实体最初创建一个Customer实体时,有一种趋向于将其认为可以与客户关联的任何属性填充到该实体中。这是一种以数据为中心的方法,它忽略了最终导致贫血领域模型的行为。

题外话,但我认为贫血的域模型从运动成绩行为出的实体,而你的例子是填充的实体有很多属性,这将导致Customer有太多的行为(因为我们很可能还包括在Customer行为其修改这些其他属性),从而违反SRP?

谢谢


2
我强烈建议罗伯特·马丁(Robert Martins)清洁代码视频系列,cleancoders.com。他详细介绍了不同的原理如何导致问题或彼此平衡。否则,我认为您的示例公式的一部分将查看Person对象所关注的时间跨度。如果它在短时间内像“购买”一样,则用于购买的帐单邮寄地址将是其中的一部分,并且无法更改。如果是图书馆帐户,则地址应该可以更改。
卡洛洛特

2
我认为这个问题可能违反了SRP ...;)
IntelliData,2017年

Answers:


6

在这种情况下,行为是指语义行为。例如,用于唯一标识它的类的属性(即域对象的属性)具有行为。虽然这不能直接在代码中表示。预期的行为是该属性将没有任何重复的值。诸如地址之类的东西 可能具有其自己的身份,但是在个人实体的上下文之外不存在,仍应移到其自己的对象中。因此,将实体提升为汇总根。

此外,诸如Age之类的属性在Person Entity之外没有上下文,因此移入其他对象没有任何意义。上下文将丢失,因此您可以安全地确定它是对“个人实体”必不可少的值。否则,您将找不到该值。但是,该信息很容易存储在唯一标识符所在的单独位置,因此对行为的引用令人困惑。年龄可能是延迟加载的值。

因此,回答您的问题。不,它不违反单一责任原则。仅仅说明一个人应该只有人的东西,而没有像地址这样的更复杂并且与人有关的东西应该作为自己的实体存在。

您可以轻松进行特定于地址的操作,例如验证其是否为有效地址。您可能在设计时就不知道这一点,但是整个概念是将对象分解为最小的部分,这样事后做起来相对比较简单。

更新:1)在大多数情况下,此身份验证是在将对象保存到数据存储中后完成的。这意味着代表实体验证的代码存在,但在其他地方也存在。它通常与负责颁发标识值的代码一起存在。这就是为什么我声明不直接在实体的代码中表示唯一性的原因。

2)正确的陈述将是Age具有行为的属性。您将需要记录Age是延迟加载的事实,以便使用该属性的开发人员可以对如何使用该属性做出准确的决定。

3)DateOfBirth通常是一个不同的对象;日期对象已经对其进行了预定义的操作。在某些语言中,日期对象已经具有定义的整个域模型。例如,在c#中,您可以指定时区,如果日期为UTC,则添加和减去日期以获取时间跨度。因此,您关于移动的假设DateOfBirth是正确的。

4)如果唯一要做的MyEntity是委派和协调,则不违反SRP。它的唯一职责是委托和协调,被称为立面模式


你能看一下我所做的更新吗?
EdvRusj

更新了我的答案
Charles Lambert

4

很好的问题。SRP不应该从字面上理解。对于SRP,标识/查找不是实体的责任。其他人负责为其提供一个ID(即商店)并负责查找它(即Repository)。

实体的主要目的是代表模型所揭示的概念。实体和值对象之间的唯一区别是,实体的含义超出了其非标识属性。例如,如果某个人更改了他的名字,那么他仍然是同一个人,只是名字不同。


1

一旦为实体分配了唯一的ID,便会建立其身份,因此我认为该实体不需要任何行为即可维持其身份或帮助其标识自己。因此,我不明白作者用“对于概念必不可少的行为”指的是什么行为(除了查找和匹配操作)?

如果建立了身份,则是,该实体不需要识别其他任何东西。作者所指的行为是与实体相关联的行为。这些是修改实体状态的行为。例如,一个Customer实体可能有MakePreferred行为。作者建议将实体剥离的原因是,当一个Customer实体最初创建一个实体时,有一种趋向于将其认为可以与客户关联的任何属性填充到该实体中。这是一种以数据为中心的方法,它忽略了最终导致贫血领域模型的行为。

至于将行为转移到其他对象上,作者专门指的是价值对象。将行为转移到VOs是个好主意的原因是因为VOs通常比实体“小”,因此更加集中。而且,诸如不变性和操作闭包之类的方面简化了代码的推理,同时也使代码更加牢固

实体与VO一起用作一种锚,它协调实现其行为的各种VO。

关于SRP,您的困惑并非没有根据。实体的陈规定型OOP实现的一个问题是身份和状态的混合。确实,从行为的角度来看,身份与行为无关。换句话说,实体的身份不需要其任何行为。在某些实现中消除了这种合并,例如AggregateSource或我在此介绍的功能性方法。

另一个问题是,在一定程度上,SRP可以作为定性指标。任何人都可以提出某个类违反的单一责任的定义。可以说,实体的责任是实现该实体所需的行为。从这个意义上讲,它负有单一责任。此外,当实体将行为委托给组成的VO时,它并不违反SRP。SRP不禁止此类组合。确实要注意将对象之间的耦合减少到最小,使接口尽可能裸露,等等。

更新

a)如果我正确理解您的意思,是说实体应该只包含那些修改其属性(即状态)的行为?

是的,尽管有例外...

b)那些不一定修改实体状态但仍然被认为是该实体的固有特征的行为(例如:吠叫将是Dog实体的固有特征,即使它没有修改Dog的状态)?我们应该将这些行为包含在实体中还是应该将其转移到其他对象?

实体包含工厂方法是可以接受的,用于创建实体实例的工厂方法实际上是子实体,但是其中不使用对象引用进行遍历。在这种情况下,子实体需要由应用程序服务保留。应用程序服务使用父实体来构造子实体。

3)假设MyEntity(请参阅我的原始帖子中的问题3)没有违反SRP,我们是否可以说MyEntity的责任还包括:

您正在从实现的角度看待责任。相反,应将实体视为一种承担责任的黑匣子。从外部看,它对您的处理方式并不感兴趣。VO甚至其他实体之间的责任划分是实施方面的关注点。

Quote指出只有与身份相关联的属性才应保留在实体中。我假设作者的意思是实体应该只包含那些经常用于查找或匹配该实体的属性,而所有其他属性都应该移动?

更具体地说,行为或查找不需要的属性不应成为实体的一部分。何苦?而且,对于类似read-model模式的东西,实体除了行为所需的属性外,不需要任何其他东西。

如果我们不是创建一个类型为Address的Customer.Address属性,而是将其属性设置为一个关联的VO对象(类型为Address),而不是创建一个类型为Address的Customer.Address属性,还是说Customer仍然包含address,属性?

是的,实际上,字符串地址或地址VO地址之间没有区别。

这里的作者不是错的,因为如果我们假设客户仅属于该特定客户的许多联系电话中的每一个,那么我想说这些电话号码与身份相关联的程度与客户仅拥有该联系电话时一样一个电话号码?

我并不是100%出于作者的意图,但我认为他只是在描述实体查找要求如何改变实体及其相应VO的结构。

不在主题之列,但是我认为贫乏领域模型是由于将行为移出实体而产生的,而您的示例正在为实体填充大量属性,这将导致Customer行为过多(因为我们可能还会在Customer中包含修改这些附加属性的行为),从而违反了SRP?

很多属性并不意味着很多行为。实际上,它通常暗示相反的意思。具有getter和setter的许多属性,但没有封装行为。


我做了更新
EdvRusj 2013年

1

TL; DR:您正在考虑这个问题。但是,我很高兴与您一起思考它。系好安全带...

实体的单一责任(改变的原因)应该是唯一地标识自己,换句话说,是可以发现其责任的。

不,那是不对的。实体的唯一责任是连续性。

身份是连续性的必然结果。将身份建模为可分离的想法是性能优化。

这是一个例子:一位顾客提供的餐厅是代客停车的汽车。一个小时后,一家餐厅的顾客要这辆车。代客应该给吗?

很容易地说,如果顾客是“相同”的,代客应该给汽车。但这到底是什么意思?正确的确定方法是从“现在”的顾客开始,然后向后搜索该顾客的历史,以了解将汽车交给代客的服务是否属于该历史。

当然,我们实际上不能这样做。我们很难精确地跟踪自己的历史,不要管一直以来都不存在的历史。因此,我们不使用赞助者的历史,而是捷径。顾客是否拥有与当前绑在钥匙上的标签号码相同的票根?顾客钱包中的驾驶执照是否与DMV标题上的名称相匹配,驾驶执照上的图片是否类似于顾客的脸。等等。

简而言之:我们不检查顾客的历史,而是检查顾客的当前状态,以查看当前状态是否与跨越汽车到达和返回请求之间的历史记录一致。

在为实体建模时,我们使用类似的优化方法。我们将所有实体的共同责任赋予

  1. 确保历史记录的开头包括将不变标识符分配给对象的状态
  2. 确保下一个状态始终包含前一个状态的标识符的真实副本。

我在这里没有描述实体的第二个责任;实体仍然对连续性负责-确保历史是一致的叙述。标识符责任只是所有实体共有的子集。

我们还没有任何强制性的强制措施。在单个实体中这是不可能的,因为唯一性要求访问所有实体的状态。单个实体只能访问自己的实体。

再一次,每次都检查所有标识符是不切实际的,因此,我们可以通过简单的方法来满足唯一性:生成下一个标识符的代码绝不能重复。

最后,这意味着我们可以通过测试内存中两个不同状态的等价性来验证连续性,从而节省了查询非循环图的麻烦。

您似乎也将单一责任原则(这是一个好主意)与原子责任原则相混淆。将职责分解为更小,更易于管理的部分与SRP兼容。


-3

好吧,这取决于您如何看待它。

另一种方法是:“单一责任原则是否违反域实体?”

两者都是准则。在软件设计中,任何地方都没有“原则”。但是,有好的设计和坏的设计。这两种概念可以以不同的方式使用,以实现良好的设计。


无法解释的降票== SRP迷们
h bob
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.