DDD 以及 Getters 和 Setters 的使用
我读过一些关于 Getters 和 Setters 的使用的文章/帖子,以及它们如何帮助破坏域模型对象中封装的目的。我理解不使用 setter 背后的逻辑 - 您允许客户端代码在对象业务规则和不变量的上下文之外操作该对象的属性。
现在这位校长仍然让我困惑。例如,如果我需要更改对象的成员变量的值,会发生什么情况?例如,如果一个人的名字发生变化,我如何在模型中反映这一点?起初我想,为什么不有一个名为“ChangeName”的函数,它让我传入新名称,然后它又可以更改内部“name”变量。嗯……那只是一个二传手不是吗!
我需要澄清的是 - 如果我要完全消除设置器,那么在上述情况下,我是否应该仅依赖构造函数参数?我是否应该通过构造函数传递新的属性值来代替旧的属性值,然后我可以通过将对象传递到我拥有的任何持久性基础设施来持久化更改?
这两篇文章在此讨论中很有用:
- http://kellabyte.com/tag/ddd/
- < a href="http://典型programmer.com/?p=23" rel="noreferrer">http://典型programmer.com/?p=23
I've read a few articles/posts regarding the use of Getters and Setters, and how they help to defeat the purpose of encapsulation in domain model objects. I understand the logic behind not using setters - you are allowing client code to manipulate attributes of that object, outside the context of the object business rules and invariants.
Now this principal does still confuse me. For example, what happens if I need to change the value of a member variable of an object? For example, if the name of a person changes how can I reflect this in the model? At first I thought, well why not have a function called 'ChangeName' which let's me pass in the new name and it in turn can change the internal 'name' variable. Well.... that's just a setter isn't it!
What I need to clarify - if I were to completely eliminate setters, then in situations such as the above, am I supposed to solely rely on constructor parameters? Should I pass the new attribute value in place of the old attribute value via a constructor, after which I can persist the changes by passing the object to whatever persistence infrastructure I have?
These two articles are useful in this discussion:
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
嗯,这是一个经典的讨论。 Stack Overflow 上还有其他几个与此相关的线程。
但。获取/设置(自动属性?)并不都是坏事。但它们往往会让您将实体构建为只有 prop 而没有方法的“死”数据容器。这种症状通常被称为贫血区域 - 并且几乎没有什么行为。我的建议是:
像前任一样在一起。名字、中间名和姓氏。另一个例子
是邮政编码、城市、街道。这些数据最好通过
方法。它可以最大限度地减少您的实体无效的可能性。
目的。
来自您的实体,它们是“动词”而不是通常的“名词”
实体。
行为,也许会减少你的“胖”服务(也许你不
有太多泄露业务逻辑的服务...)。
这里还有更多要说的......但一个简短的答案。
关于在构造函数中设置数据:只有当该实体在没有该数据的情况下无法“生存”/存在时,我才会这样做。对于实体 Person 我想说 Name 可能没那么重要。但社会安全号码可能是构造函数数据的候选者。或者实体 Employee 必须在构造函数中包含 Company,因为员工必须属于公司。
Well, this is a classic discussion. There are several other threads here in Stack Overflow about this.
But. Get/Set's (Auto Properties?) are not all bad. But they tend to make you construct your entities as "dead" data containers that only have prop and not methods. The signs of this is often called Anemic Domain - and have very little behavior. My recommendation is:
together like ex. Firstname Middlename and Lastname. Another example
is Zipcode, City, Street. These data is better to set through a
method. It minimizes the chances for your entity to be invalid.
object.
from your entity that are "Verbs" instead of your usually "Nouns"
entities.
behavior and maybe reducing your "Fat" services (maybe you don't
have services with too much leaked business logic...).
There are more to say here... but a short answer.
About setting data in constructor: I only do that if this entity cannot "live"/exist without that data. For entity Person I would say that Name maybe isn't that kind of important. But Social Security Number may be a candidate for constructor data. Or entity Employee must have Company in constructor, simply because an employee must belongs to a company.
我认为我们应该看看 DDD 的原理并从中得出正确的答案。
C# 中的公共自动属性 getter/setter在功能上只是公共属性。只要没有关于各个属性的正确值的业务规则,并且在这些属性更改时没有需要触发的域事件,使用自动属性 getter/setter 本质上并不是坏事。
此外,人们不应该构建仅公共自动属性的聚合或实体,因为这会导致贫乏的模型和贫乏的领域。这样的“聚合”不是实际的聚合,而更像是 DTO 或值对象。
就我个人而言,我认为如果我们将属性访问器(get/set)与主体一起使用来集成业务逻辑,我们可以使我们的代码更具可读性,并且可能不会那么冗长。
例如:
上述唯一的问题是,在内部,如果您直接在某些方法中设置
_name
,您可以绕过业务逻辑。但如果你足够自律,我认为这不是问题。不过,对于某些人来说,这可能看起来很可怕,我理解。从好的方面来说,如果您使用的是实体框架之类的东西,我认为您可以通过调用属性(而不是支持字段)将其配置为水合新实例,从而防止从数据库加载无效的聚合(比如说,如果您导入了一些可能包含一些垃圾的批量数据)。我还没有测试过这个。
第二个示例中使用表达式主体访问器只是为了表明您可以大量减少样板文件。
可以使用像上面这样的表达式主体 getter,因为字符串在 C# 中具有值语义,因此表达式返回
_name
的副本,从而不会公开对内部变量的引用。请注意,以 C# 9 记录为例,您只有基于值的相等语义。一条记录还是通过引用传递的!由于记录在理想情况下应该是不可变的(仅限 init),因此您可以返回对此类记录的引用(这更具性能)并跳过克隆(这对于浅克隆来说很简单,但对于深层克隆来说很困难)。
如果聚合中有这样一个对象,例如 DDD 值对象,它不是不可变记录或可以轻松克隆的记录,则需要确保不会返回对可以被变异,从而绕过业务逻辑并扰乱总体完整性。
以列表为例。您可以使用 IReadOnlyList 作为返回类型,但如果您仅转换私有内部属性,那么这还不够,因为该引用可以在外部“向上转换”回 List,然后使用来修改它。
在这种情况下,您还应该使用
List
的.AsReadOnly()
方法返回原始列表元素的新只读包装列表。请注意,尽管只有包装器列表受到保护,不被更改(它没有添加或删除方法),但元素本身却不受保护。他们有责任保护自己免受变化的影响。
编辑:
我刚刚意识到我的例子并不完全正确。这种具有逻辑的(私有?)访问器可用于简单的逻辑,例如确保设置结束日期时,它不在开始日期之前,但对于复杂的情况,例如,设置结束日期可能会由于多种原因而发生必须全部建模为动词,例如
terminateContract(DateTime FinalDay, string Reason)
或closeContract(DateTime closeEarlyDate)
,它们必须更明确地说明设置结束日期。无论如何,在这种情况下,应该始终应用的通用逻辑可以存在于 setter 访问器中(这提供了代码的重复数据删除),并且每个案例每个操作逻辑可以存在于特定的操作方法中。I think we should look at the principles of DDD and derive the correct answer from there.
Public auto-property getters/setters in C# are functionally just public properties. Using auto-property getters/setters is not inherently bad as long as there are no business rules regarding the correct values of the respective properties and no domain events that need to fire when those properties change.
Also, one should not build aggregates or entities that have only public auto-properties, as that leads to an anemic model and an anemic domain. Such an "aggregate" is not an actual aggregate, but more a DTO or Value Object.
Personally, I think that if we use property accessors (get/set) with bodies to integrate business logic, we can make our code a little bit more readable and probably a lot less verbose.
For example:
The only issue with the above is that, internally, you can bypass business logic if you set
_name
directly in some method. But if you're disciplined enough, I don't think it's an issue. It might seem scary for some though and I understand.On the upside, if you're using something like Entity Framework, I think you can configure it to hydrate new instances by calling properties (not backing fields), thus preventing loading an invalid aggregate from the database (say if you're imported some bulk data that might contain some garbage). I haven't tested this though.
Expression bodied accessors are used in the second example just to show that you can reduce boilerplate a lot.
It's possible to use an expression bodied getter like above because strings have value semantics in C# so that expression returns a copy of
_name
, thus not exposing a reference to an internal variable.Note that with C# 9 records for example, you have only value-based equality semantics. A record is still passed by reference! Since records should ideally be immutable (init only), you could return references to such records (which is more performant) and skip having to clone (which is simple for shallow clones but hard for deep clones).
If you have such an object inside an aggregate, for example a DDD Value Object that isn't an immutable record or a record that can be easily cloned, you need to make sure you're not returning a reference to an internal object that can be mutated, thus bypassing business logic and messing with aggregate integrity.
Take a List for example. You can use
IReadOnlyList
as the return type, but that is not enough if you're only casting a private internal property, because that reference can be "up-casted" outside back to a List and then used to modify it.In this case you should also use the
.AsReadOnly()
method ofList
to return a new read only wrapper list over the elements of the original list.Beware though that only the wrapper list is protected from change (it has no add or remove methods) but not the elements themselves. It's their responsibility to protected themselves from change.
EDIT:
I just realised that my example is not exactly correct. Such (private?) accessors with logic can be used for simple logic like making sure that when setting an end date, it is not before a start date, but for complex cases, setting an end date for example can happen for a number of reasons which must be all modelled as verbs, for example
terminateContract(DateTime finalDay, string reason)
orcloseContract(DateTime closedEarlyDate)
, they are must more explicit on the reason to set the end date. In this case anyway, generic logic that should always be applied can live in the setter accessor (this provides deduplication of code) and per-case per-action logic can live in the specific action method.