我参与的项目非常重视单一职责原则。我们有很多小班,事情很简单。然而,我们有一个贫乏的领域模型——我们的任何模型类中都没有行为,它们只是属性包。这并不是对我们的设计的抱怨——它实际上似乎工作得很好。
在设计评审期间,每当新行为添加到系统中时,SRP 就会被提出,因此新行为通常会出现在一个新类中。这使得事情非常容易进行单元测试,但有时我感到困惑,因为这感觉就像将行为从相关的地方拉出来。
我正在努力提高对如何正确应用 SRP 的理解。在我看来,SRP 反对向一个对象添加共享相同上下文的业务建模行为,因为该对象最终不可避免地要么做不止一件相关的事情,要么做一件事情但知道改变形状的多个业务规则其输出。
如果是这样,那么感觉最终结果是一个贫乏的领域模型,这在我们的项目中确实是这样。然而贫血领域模型是一种反模式。
这两种想法可以共存吗?
编辑:几个与上下文相关的链接:
SRP - http://www.objectmentor.com/resources/articles/srp.pdf
贫血域模型 - http://martinfowler.com/bliki/AnemicDomainModel.html
不是那种只喜欢寻找先知并遵循他们所说的福音的开发人员。因此,我不会提供这些链接作为陈述“这些是规则”的方式,而只是作为这两个概念的定义来源。
I'm in a project that takes the Single Responsibility Principle pretty seriously. We have a lot of small classes and things are quite simple. However, we have an anemic domain model - there is no behaviour in any of our model classes, they are just property bags. This isn't a complaint about our design - it actually seems to work quite well
During design reviews, SRP is brought out whenever new behaviour is added to the system, and so new behaviour typically ends up in a new class. This keeps things very easily unit testable, but I am perplexed sometimes because it feels like pulling behaviour out of the place where it's relevant.
I'm trying to improve my understanding of how to apply SRP properly. It seems to me that SRP is in opposition to adding business modelling behaviour that shares the same context to one object, because the object inevitably ends up either doing more than one related thing, or doing one thing but knowing multiple business rules that change the shape of its outputs.
If that is so, then it feels like the end result is an Anemic Domain Model, which is certainly the case in our project. Yet the Anemic Domain Model is an anti-pattern.
Can these two ideas coexist?
EDIT: A couple of context related links:
SRP - http://www.objectmentor.com/resources/articles/srp.pdf
Anemic Domain Model - http://martinfowler.com/bliki/AnemicDomainModel.html
I'm not the kind of developer who just likes to find a prophet and follow what they say as gospel. So I don't provide links to these as a way of stating "these are the rules", just as a source of definition of the two concepts.
发布评论
评论(7)
富域模型 (RDM) 和单一职责原则 (SRP) 不一定是矛盾的。 RDM 与 SRP 的一个非常专业的子类更不一致 - 该模型提倡“数据 bean + 控制器类中的所有业务逻辑”(DBABLICC)。
如果您阅读 Martin 的 SRP 章节,您将看到他的调制解调器示例完全位于域层中,但将 DataChannel 和 Connection 概念抽象为单独的类。他将调制解调器本身保留为包装器,因为这对于客户端代码来说是有用的抽象。这更多的是关于正确的(重新)分解,而不仅仅是分层。内聚和耦合仍然是设计的基本原则。
最后,三个问题:
正如马丁自己所说,看到不同的“变革原因”并不总是那么容易。 YAGNI、敏捷等概念本身就阻碍了对未来变革原因的预期,因此我们不应该发明那些不是立即显而易见的概念。我认为“过早的、预期的变更原因”是应用 SRP 时的一个真正风险,应该由开发人员进行管理。
除上述之外,即使是正确(但不必要彻底)的SRP 应用也可能会导致不必要的复杂性。永远想想下一个必须维护你的类的可怜虫:将琐碎行为勤奋地抽象成自己的接口、基类和单行实现真的有助于他理解什么应该只是一个类吗?
软件设计通常是为了在竞争力量之间取得最佳妥协。例如,分层架构主要是 SRP 的良好应用,但是,例如,将业务类的属性从 boolean 更改为 >enum 在所有层中产生连锁反应 - 从数据库到域、外观、Web 服务,再到 GUI?这是否表明设计不好?不一定:它指出这样一个事实,即您的设计有利于从一个方面更改为另一个方面。
Rich Domain Model (RDM) and Single Responsibility Principle (SRP) are not necessarily at odds. RDM is more at odds with a very specialised subclassof SRP - the model advocating "data beans + all business logic in controller classes" (DBABLICC).
If you read Martin's SRP chapter, you'll see his modem example is entirely in the domain layer, but abstracting the DataChannel and Connection concepts as separate classes. He keeps the Modem itself as a wrapper, since that is useful abstraction for client code. It's much more about proper (re)factoring than mere layering. Cohesion and coupling are still the base principles of design.
Finally, three issues:
As Martin notes himself, it's not always easy to see the different 'reasons for change'. The very concepts of YAGNI, Agile, etc. discourage the anticipation of future reasons for change, so we shouldn't invent ones where they aren't immediately obvious. I see 'premature, anticipated reasons for change' as a real risk in applying SRP and should be managed by the developer.
Further to the previous, even correct (but unnecessary anal) application of SRP may result in unwanted complexity. Always think about the next poor sod who has to maintain your class: will the diligent abstraction of trivial behaviour into its own interfaces, base classes and one-line implementations really aid his understanding of what should simply have been a single class?
Software design is often about getting the best compromise between competing forces. For example, a layered architecture is mostly a good application of SRP, but what about the fact that, for example, the change of a property of a business class from, say, a boolean to an enum has a ripple effect across all the layers - from db through domain, facades, web service, to GUI? Does this point to bad design? Not necessarily: it points to the fact that your design favours one aspect of change to another.
我不得不说“是”,但你必须正确制定建议零售价。如果相同的操作仅适用于一个类,那么它就属于该类,不是吗?如果同一个操作适用于多个类怎么办?在这种情况下,如果您想遵循结合数据和行为的 OO 模型,您可以将操作放入基类中,不是吗?
我怀疑从你的描述来看,你最终得到的类基本上都是操作包,所以你基本上重新创建了 C 风格的编码:结构和模块。
来自链接的 SRP 论文:
“SRP 是最简单的原则之一,也是最难正确执行的原则之一。”
I'd have to say "yes", but you have to do your SRP properly. If the same operation applies to only one class, it belongs in that class, wouldn't you say? How about if the same operation applies to multiple classes? In that case, if you want to follow the OO model of combining data and behavior, you'd put the operation into a base class, no?
I suspect that from your description, you're ending up with classes which are basically bags of operations, so you've essentially recreated the C-style of coding: structs and modules.
From the linked SRP paper:
"The SRP is one of the simplest of the principle, and one of the hardest to get right."
SRP论文中的引用是非常正确的; SRP 很难确定。这个和 OCP 是 SOLID 的两个要素,必须至少在某种程度上放松才能真正完成项目。过度热心地应用其中任何一个都会很快产生馄饨代码。
如果“变更原因”过于具体,SRP 确实可能会达到荒谬的程度。如果您将字段类型更改视为“更改”,则即使 POCO/POJO“数据包”也可能被视为违反 SRP。您可能认为常识会告诉您字段的类型更改是“更改”的必要条件,但我见过带有内置值类型包装器的域层;一个让 ADM 看起来像乌托邦的地狱。
基于可读性或所需的凝聚力水平,为自己设定一些现实的目标通常是件好事。当你说“我希望这个类做一件事”时,它的内容不应多于或少于完成这件事所需的内容。您至少可以通过这一基本理念保持程序上的凝聚力。 “我希望此类维护发票的所有数据”通常会允许某些业务逻辑,甚至根据对象的责任求和小计或计算销售税,以了解如何为任何字段提供准确的、内部一致的值它包含。
我个人对“轻量级”域名没有什么大问题。仅具有“数据专家”这一角色就可以使域对象成为与该类相关的每个字段/属性的守护者,以及所有计算的字段逻辑、任何显式/隐式数据类型转换以及可能更简单的验证规则(即必填字段、值限制、如果允许的话会在内部破坏实例的东西)。如果计算算法(可能是加权平均值或滚动平均值)可能会发生变化,请封装该算法并在计算字段中引用它(这就是很好的 OCP/PV)。
我不认为这样的领域对象是“贫乏的”。我对这个术语的理解是一个“数据包”,一个字段的集合,除了包含它们之外,对外部世界甚至其字段之间的关系没有任何概念。我也看到过这种情况,追踪对象状态中的不一致之处并不有趣,而对象从来不知道这是一个问题。过分热心的 SRP 会导致这种情况,因为它声称数据对象不负责任何业务逻辑,但常识通常会首先介入并说该对象作为数据专家必须负责维护一致的内部状态。
再次强调,个人观点,相比 Active Record,我更喜欢 Repository 模式。一个对象具有一个职责,而该层之上的系统中几乎没有任何其他对象需要了解其工作原理。 Active Record 要求域层至少了解有关持久性方法或框架的一些具体细节(无论是用于读/写每个类的存储过程的名称、特定于框架的对象引用,还是使用 ORM 信息装饰字段的属性) ),从而默认情况下将第二个更改原因注入到每个域类中。
我的 0.02 美元。
The quote from the SRP paper is very correct; SRP is hard to get right. This one and OCP are the two elements of SOLID that simply must be relaxed to at least some degree in order to actually get a project done. Overzealous application of either will very quickly produce ravioli code.
SRP can indeed be taken to ridiculous lengths, if the "reasons for change" are too specific. Even a POCO/POJO "data bag" can be thought of as violating SRP, if you consider the type of a field changing as a "change". You'd think common sense would tell you that a field's type changing is a necessary allowance for "change", but I've seen domain layers with wrappers for built-in value types; a hell that makes ADM look like Utopia.
It's often good to ground yourself with some realistic goal, based on readability or a desired cohesion level. When you say, "I want this class to do one thing", it should have no more or less than what is necessary to do it. You can maintain at least procedural cohesion with this basic philosophy. "I want this class to maintain all the data for an invoice" will generally allow SOME business logic, even summing subtotals or calculating sales tax, based on the object's responsibility to know how to give you an accurate, internally-consistent value for any field it contains.
I personally do not have a big problem with a "lightweight" domain. Just having the one role of being the "data expert" makes the domain object the keeper of every field/property pertinent to the class, as well as all calculated field logic, any explicit/implicit data type conversions, and possibly the simpler validation rules (i.e. required fields, value limits, things that would break the instance internally if allowed). If a calculation algorithm, perhaps for a weighted or rolling average, is likely to change, encapsulate the algorithm and refer to it in the calculated field (that's just good OCP/PV).
I don't consider such a domain object to be "anemic". My perception of that term is a "data bag", a collection of fields that has no concept whatsoever of the outside world or even the relation between its fields other than that it contains them. I've seen that too, and it's not fun tracking down inconsistencies in object state that the object never knew was a problem. Overzealous SRP will lead to this by stating that a data object is not responsible for any business logic, but common sense would generally intervene first and say that the object, as the data expert, must be responsible for maintaining a consistent internal state.
Again, personal opinion, I prefer the Repository pattern to Active Record. One object, with one responsibility, and very little if anything else in the system above that layer has to know anything about how it works. Active Record requires the domain layer to know at least some specific details about the persistence method or framework (whether that be the names of stored procedures used to read/write each class, framework-specific object references, or attributes decorating the fields with ORM information), and thus injects a second reason to change into every domain class by default.
My $0.02.
我发现遵循坚实的原则实际上让我远离了 DDD 的丰富领域模型,最后我发现我不在乎。更重要的是,我发现域模型的逻辑概念和任何语言的类都不是 1:1 映射的,除非我们谈论某种外观。
我不会说这完全是一种 C 风格的编程,其中有结构和模块,但你可能最终会得到更实用的东西,我意识到风格是相似的,但细节有很大不同。我发现我的类实例最终表现得像高阶函数、部分函数应用程序、延迟计算函数或上述函数的某种组合。这对我来说有点难以形容,但这就是我按照 TDD + SOLID 编写代码时得到的感觉,它最终表现得像混合 OO/函数式风格。
至于继承是一个坏词,我认为这更多是因为继承在 Java/C# 等语言中不够细粒度。在其他语言中,这不是什么问题,而且更有用。
I've found following the solid principles did in fact lead me away from DDD's rich domain model, in the end, I found I didn't care. More to the point, I found that the logical concept of a domain model, and a class in whatever language weren't mapped 1:1, unless we were talking about a facade of some sort.
I wouldn't say this is exactly a c-style of programming where you have structs and modules, but rather you'll probably end up with something more functional, I realise the styles are similar, but the details make a big difference. I found my class instances end up behaving like higher order functions, partial functions application, lazily evaluated functions, or some combination of the above. It's somewhat ineffable for me, but that's the feeling I get from writing code following TDD + SOLID, it ended up behaving like a hybrid OO/Functional style.
As for inheritance being a bad word, i think that's more due to the fact that the inheritance isn't sufficiently fine grained enough in languages like Java/C#. In other languages, it's less of an issue, and more useful.
我喜欢 SRP 的定义:
“一个类只有一个需要更改的业务原因”
因此,只要行为可以分组为单个“业务原因”,那么它们就没有理由不共存于同一个类中。当然,“商业原因”的定义是有争议的(并且应该由所有利益相关者进行辩论)。
I like the definition of SRP as:
"A class has only one business reason to change"
So, as long as behaviours can be grouped into single "business reasons" then there is no reason for them not to co-exist in the same class. Of course, what defines a "business reason" is open to debate (and should be debated by all stakeholders).
在我开始咆哮之前,简而言之,这是我的观点:在某个地方,一切都必须聚集在一起……然后一条河流流过它。
我被编码困扰了。
=======
贫乏的数据模型和我……好吧,我们经常交往。也许这只是中小型应用程序的本质,其中内置的业务逻辑很少。也许我只是有点“迟到了”。
然而,这是我的 2 美分:
难道你不能只提取实体中的代码并将其绑定到接口吗?
这是否违反了 SRP 原则?
此外,让一堆类闲置在一起,除了使用代码之外没有任何其他东西相互联系,实际上不是对 SRP 的更大违反,而是推高了一层吗?
想象一下编写客户端代码的人坐在那里试图弄清楚如何执行与 Object1 相关的操作。如果他必须使用您的模型,他将使用 Object1、数据包和一堆“服务”,每个服务都有一个职责。他的工作就是确保所有这些事情都能正常互动。因此,现在他的代码变成了一个事务脚本,并且该脚本本身将包含正确完成该特定事务(或工作单元)所需的所有职责。
此外,您还可以说,“不,他所需要做的就是访问服务层。这就像 Object1Service.DoActionX(Object1)。小菜一碟。”那么,现在的逻辑在哪里?全部用一种方法?您仍然只是推送代码,无论如何,您最终都会得到数据和逻辑分离的结果。
因此,在这种情况下,为什么不向客户端代码公开特定的 Object1Service 并让它的 DoActionX() 基本上只是域模型的另一个钩子呢?我的意思是:
您仍然从 Object1 中提取出 Action1 的实际代码,但出于所有密集目的,拥有一个非贫乏的 Object1。
假设您需要 Action1 来表示 2 个(或更多)不同的操作,您希望这些操作成为原子操作并分成各自的类。只需为每个原子操作创建一个接口并将其连接到 DoAction1 内部即可。
这就是我处理这种情况的方式。但话又说回来,我真的不知道SRP到底是什么。
Before I get into my rant, here's my opinion in a nutshell: somewhere everything has got to come together... and then a river runs through it.
I am haunted by coding.
=======
Anemic data model and me... well, we pal around a lot. Maybe it's just the nature of small to medium sized applications with very little business logic built into them. Maybe I am just a bit 'tarded.
However, here's my 2 cents:
Couldn't you just factor out the code in the entities and tie it up to an interface?
Does this somehow violate the principles of SRP?
Furthermore, isn't having a bunch of classes sitting around not tied to each other by anything but the consuming code actually a larger violation of SRP, but pushed up a layer?
Imagine the guy writing the client code sitting there trying to figure out how to do something related to Object1. If he has to work with your model he will be working with Object1, the data bag, and a bunch of 'services' each with a single responsibility. It'll be his job to make sure all those things interact properly. So now his code becomes a transaction script, and that script will itself contain every responsibility necessary to properly complete that particular transaction (or unit of work).
Furthermore, you could say, "no brah, all he needs to do is access the service layer. It's like Object1Service.DoActionX(Object1). Piece of cake." Well then, where's the logic now? All in that one method? Your still just pushing code around, and no matter what, you'll end up with data and the logic being separated.
So in this scenario, why not expose to the client code that particular Object1Service and have it's DoActionX() basically just be another hook for your domain model? By this I mean:
You still have factored out the actual code for Action1 from Object1 but for all intensive purposes, have a non-anemic Object1.
Say you need Action1 to represent 2 (or more) different operations that you would like to make atomic and separated into their own classes. Just create an interface for each atomic operation and hook it up inside of DoAction1.
That's how I might approach this situation. But then again, I don't really know what SRP is all about.
将普通域对象转换为 ActiveRecord 模式,并为所有域对象提供通用基类。将公共行为放在基类中,并在必要时重写派生类中的行为,或者在需要时定义新行为。
Convert your plain domain objects to ActiveRecord pattern with a common base class to all domain objects. Put common behaviour in the base class and override the behaviour in derived classes wherever necessary or define the new behaviour wherever required.