从“贫血”和“充血”说起

从“贫血”和“充血”说起

这两个词对我来说也是很新鲜的,看看我在Jdon的注册日期也就是从那时候开始才有所耳闻的。这两天看到有人在讨论于是整理了一下思维。

看到网络上很多的讨论中对于充血和贫血的看法往往是以绝对的方式进行描述,都是用类似“不是。。。就是”的方式进行修辞。我个人的理解在实际的应用环境中是很难出现这种“不是。。。就是”境地,特别是在进行设计的过程中。贫血和充血应并不只是两种思考的方式更多的应该把这两种思考的方式放在我们所需要的实际情况下进行权衡,这样才有比较的意义否则就好像在争论“土豆和白菜哪一个比较好吃”一样的没有意义了。正确的方式是应该设定一个应用场景,比如:在xxxx项目中当我们描述从图书馆借书的过程时,“书”这个对象是充血还是贫血更合理呢?脱离了实际应用场景任何讨论都是没有意义的因为大家都是站在各自不同的立场上,各自的解决方案所面对的问题域也是不尽相同的,同样是一个“书”类型在书店系统里面和印刷厂系统里面承担的职责显然有着本质的区别,此时不可能再用同样的角度进行讨论了。

在一个实际应用系统中,我们通常需要面对两个类型的对象“数据集”和“实体”,其中数据集承担的任务非常的简单就是作为一个传递数据的场所。比如一个A对象的创建过程中需要赋予一个ID,我们可以创建一个接口 A.setSessionId(Object instId),此时从业务的角度出发instId这个对象除了作为“id”的载体什么都不是只需要是一个“数据集”,显然我们对他也没有更多的期望,那么这个对象应该是贫血的了。那么换一个应用场景,假设A是一个游乐场的来客,instId是游乐场不记名、不挂失、同时提供支付功能的电子票,并且这个instId需要在多个系统独立且无法整合的游乐场间通用,那么这个instId需要同时承担了多种责任:唯一标示持票人(比如记录购票人的相片、年龄。。。等),与其它收支系统的协议适配,支付余额记录。。。。等等,此时这个instId就肯定不能是一个贫血的实现了。

无论是以何种方式实现都不能够脱离实际应用场景,脱离了应用环境任何讨论都是没有意义的了。

我还看到了一个发言:
我要查找一本书:
Book book = Book.Find(),“书去找书”,太别扭了。
Book book = BookManage.Find() , “图书管理员帮我找本书。”符合自然习惯。

发言者认为Book.Find()就是书的充血实现。其实这又是另外的一种错误了,本质上是没有理解00的含义或者没有切实的思考找书的业务过程。书怎么能去找书呢?显然索引书的职责应该是“图书管理员”或者“图书名录”承担的,而书本身只能提供章节和页数的索引功能。血是不可以乱充或者乱贫的,在中医里面有放血的疗法,在西医里面有输血的疗法,但这些都是有着严格的应用场景,如果输血的血型不匹配是会要人命的!!!

下面开始发散思维了。

大家都在热衷于讨论技术,这是好现象因为没有技术基础其他一切都是免谈的,但是更多的人似乎忘记了“为什么需要技术”这个关键性的问题。比如上面的例子中只是在考虑“贫血”和“充血”这些名词了,集中于考虑怎么去实现这样思想的表现形式,却恰恰忽略了思想的本质“为什么要充或者贫”。技术本身是没有任何价值的,一个技术再高的程序高手(暂且比如Banq吧,谁让他这的幌子呢)如果没有项目可作恐怕也要饿死了。现在的风气中绝大多数人都在热烈的讨论怎么做,用程序做出了各种各样天花乱坠的效果,可是如果忘记了自己要做什么所有的这些有没有什么根本价值呢?说句实话一丁点价值也没有。

很多人都在犯一个共同的毛病“空想”,没有时间、地点、人物、事件,什么都没有只是在思考一个结论。这样的思考是没有意义的,所有的思考都必须建立在一个特定的场景之上。举个例子,现在评价JiveJdon的时候首先要搞清楚一点,到目前为止这个实现都还不是一个完整的产品,更倾向于一个完成度比较高的Demo,因此我们不能过多的从配置的易用性和页面布局是否符合多数人的使用习惯这个角度去衡量,更多的讨论需要基于它的系统结构、实现方式等等,我们应该更多的讨论“他的数据和业务是否过于耦合”而不是“他的首页应该使用淡蓝色”。

程序员需要不只是技术,更需要良好的“有益思考”能力,首先我们要习惯思考进而需要习惯从正确的方向进行思考。任何技术都是会过时的但是思考能力可以永远跟上时代。在技术领域发展是一个不变的法则,今天是C++明天是Java后天谁也不指导会出现什么。昨天可能是比尔大叔的思想先进,今天有可能是Banq的更好一点,但是明天呢?没有人能够预言。我们今天学到的技术必定会在明天被淘汰,但是如果我们学到了思考进而分辨技术高下的能力那么就无论如何不可能被淘汰了。

我多年前就指出,其实如果真正掌握Evans DDD,真正做好实体和值对象,抛弃数据库编程思路,这个问题自然就豁然开朗。

之所以有那么多人喜欢讨论这个问题,实际上还是他们的数据库编程思路导致,数据库编程将对象看成数据的集合体,实际是一个大的数据结构。但是,对象不只是数据啊,还有操作行为。

数据库编程思路的人将这些操作行为都放到service等其他地方去了,这当然是 失血模型了。

不懂OO的有的还故作聪明:以为将所有行为都塞入一个模型就是充血了,他又不明白OO细分原则。

到底哪些行为应该放入模型中?取决于你实体聚合的根以及他们的边界和生命周期,这些都是DDD的要旨,不明白这个道理就永远是围绕数据式对象编程。

在这一点上同意你的想法,很多时候看到对于血型的争论实质上都是在围绕数据归属的争论,他们的核心还是在数据而不是站在业务或者对象的起点上。

软件是一个集合体,在整个的过程中必须从整体的角度进行考量,数据、操作、过程,不能够在任何一个方面有所偏颇。在设计的过程中一项重要的工作就是在进行平衡,如果缺少了对整体中任何一个方面的考量都会得出一个似是而非的答案,也才会有了这么多的争论。

我以前就说了:失血和充血只是结果,什么结果?就是你是否OO思维的结果。

过于执着讨论是否充血,就总是在一个结果现象上纠缠不清,好好掌握DDD,然后可以用这个结果来检验你掌握DDD的程度。

SSH想实现充血有难度。哎。。

你认为
Book book = Book.Find()
不如
Book book = BookManage.Find()
这无可厚非,无可厚非的原因在于"书"在客观世界中比比皆是,任何人都能找到书,也很容易想象书自己不会找书
然而在系统中存在着大量的这样的对象:客观世界并不存在这样的东西,那么对于这样的类型,方法该归给谁,就不那么好想象了....

地确实这样的,现实的环境常常让我们陷入两难的境地。
如果所有的问题都有明显的答案那我们的价值为何?我们的价值正在此处,在一个困难的境地下做出一个更合适的选择,虽然这个选择常常是有缺陷的。
你所提到的问题需要更加抽象的思维,在足够高的抽象角度总能够找到一个相通的结合。只要足够抽象几乎所有软件的问题都可以在现实中找到一个合理的映射。

Book book = Book.Find()
我认为对上面的代码存在表象认识错误,以前我也认为这代码是错误的,现在我认为它是正确的。
原因:
把find看成被动语态,Book.beFinded()就好了,因为我们不关心它的调用者的细节,既然从来都不管理调用者的状态,也就没必要真实地把调用者实现出来,并且这种情况在逻辑简单的时候很有用处,在互联网程序方面更显出价值了,没那么多逻辑,RoR等看上去就比jee太简单了。

一旦逻辑变得有些复杂了,我需要缓存一个状态(A),这在企业应用里是再平常不过的事了。但A本身的定义并不隶属于Book(或其它)的定义域,不能通过Book的定义来推断A的定义,这时候A放在哪呢?A既然不属于任何一个实体定义,就要显示地把A实现出来并且存在那么一个管理者来管理它。

因为在数据上A是“虚”的,它可能是把一些“实”的对象碾平或立体化后得到的,此时的A就不能管理自己了,它不知道如何被动地取得自身,需要一个管理者来管理,并且这些管理动作是依赖于其它“实”对象的,这时候Book.find()和mgr.find()的差距就被缩小了,mgr显得不那么“简单”,以至于再也不能忽略掉mgr的实现,不能丢掉mgr把一切都放进Book里。(RoR也完全可以实现mgr来应用。)

但是没有逻辑(血)的mgr就没有意义,一旦存在这样没血的mgr,就不必区分CRUD是由谁来调用的了,主动发现与被动自荐都说得通,既然都说得通,也就回到上面的问题:没血我还要mgr干什么?干脆扔掉。

而有些本属于Book自身的逻辑(像Book能感知目录结构),像RoR那样也不必担心像jee里的Entity不能注入(其实是可以注入)目录的Repository,RoR里的“目录”对象承担了自己的仓储,尽管是被动的从仓储里被发现,但在Book对象里实现自身拥有的逻辑却很有效。而这在jee里更好的做法就是都拿进Rep里作业,在取得Book的时候为它装配好目录结构,保证从Rep里出来的都是“良品”。

最后从大意上引用别人的一句话:“别把DDD的原理和它的实现方法(AOP、DI)弄混了。”

在设计过程中什么样的结构都可以接受,唯一不能接受的是不知道为什么会有这样的结构。

find()放到那一个接口里面其实不是重点,重点在于为什么。在很多很多情况下都会如如楼上所说的出于简化的目的产生一个简单的结构,但是错误的在于很多人会认为这样的结构就是本来应该具有的样子,本末倒置了。

软件环境里对或者错都是有具体环境的,需要特定的进行分析。

Book book = Book.Find() 并不是充血模型,这是典型的设计错误。用来举例不合适。

我承认Book book = Book.Find()不是充血模型,因为这没有血,只是简易的CRUD。
但我不承认这是设计错误,为什么就不能把Find()看成beFinded()呢?书没有查找自己的能力,也没有被查找到的能力吗?再看更典型的:
Book book=new Book();
book.create();
书没有创造自己的能力,但是这里面创建是留给了构造,而create完成的实际上不是创造而是持久,现在我们不去证实书到底有没有持久功能,但就算mgr.create(book);也证实了书有被持久的功能,如果在简易应用中从不关心mgr的状态,一定要设计出mgr这个对象就多余了,而book的一系列被动语态完全解释得通。
而且不能因为RoR使用了Book.Find()就说RoR不带有扩展性,这不是说RoR不能创建mgr吗?但实际上完全可以,复杂应用中的CRUD用主动和被动差别就小了,业务代码行数远大于一个C/R/U/D语句,但不是说被动用法就不对了。

在java设计的思维里,设计一个实体类往往要附带考虑这个实体类的集合类或者说仓库类,这往往是成对出现的,Book和BookManager这是JAVA设计的惯用思维,但是其他语言不知道,也可能其他语言中,集合和实体本身就是在一起的,也不能太思维定势,这个没必要深究.

>>>>为什么就不能把Find()看成beFinded()呢?

如果出于简化设计的考虑在某些状况下是可以是最佳的实践方式,但是如果这么强调就有问题了。beFinded()那主角是谁呢?显然是另一个操作者。假设A.beFinded() by B,那么A的实例需要主动地去寻找B,这样的限定本身就是错误的,B是谁在哪里这种问题和A一丁点关系都没有,而且更多的时候B是A不可知觉的存在。比如内存对于数据,在一个特定的OS下数据本身永远也不可能知道自己所在的内存区间,甚至于不知道自己是不是在内存里。所有的B都被赋予A那显然系统会变成一个混乱不堪的样子,为了找到自己的下一个字节码,需要遍历和运行空间所有相关联的存储设备了。

软件界经过了无数的努力才出现了mgr.find()这样的方式,book.beFinded()一下子就回去了。

并非所有的B行为都加入了A,但既然CRUD不关心B,A就没必要寻找一个显示的B,显示地实现出B来也没什么必要,不如让它隐式地存在,我没有仔细读过RoR的实现,但我认为RoR系统中的a对象和领域系统中的a实体是有区别的。
我认为这两种形式都能很好地描述系统,而RoR应用了剃刀原理选择了隐藏调用者的被动形式。

我来说说我的观点,关于mgr.find()和book.find():

首先要确定的是book.find()并一定只返回一条记录,有可能返回一个列表,这个列表可能是由find方法从数据库中查找.而book本身封装了一些书的信息,比如书号\书名\作者等等,book本身不会封装一个装满book的列表,所以find()方法与book本身的属性关系不大,这个方法与book不够亲密,按照重构的思路,应该将这个方法提取到book类外面.