为什么要让我们的“领域模型”裸奔?
做不完的应用软件
我爸是个乡村小学教师,对我所从事的软件行业一无所知,但是他对我的工作稳定性表示怀疑:“你这做软件的,要是有一天软件做完了,你岂不是要失业了?”也许他想起了他作为老师的情况,教完一批学生,下一批又上来了,一茬一茬的。于是又问我:“你们是不是一个软件接着一个软件做?”我回答他:“不是,就一个软件,好几十个人得做好几年呢。”解释了很多次仍旧没有消除他的疑问:“你们做软件怎么会一直做下去?怎么没有个做完的时候呢?”。
如果他在通往张江的地铁上,知道有那么多我伤不起的IT同类们,他也许会更加迷惑。为什么如此庞大的程序员大军,日复一日年复一年地敲着代码,生产出无数的软件,可是他们不用担心失业?为什么需要那么多看上去类似的软件?为什么这些软件永远没有做完的那天?
应用软件独一无二的地方
答案其实很简单,我们做的每一套软件,都是为了解决某个领域的业务需求。而业务需求永远没有停止变化的一天,这就是为什么应用软件永远也做不完的原因。我刚开始工作时,在一个小团队里为一家公司定制开发ERP,其实就是一个中小型的管理系统,在知道国内有用友金蝶在做ERP的时候,觉得很奇怪:有那么好的公司在做ERP,我们还有做的必要吗?客户怎么不去买用友金蝶呢?现在想来这想法很幼稚,因为每家公司的业务都是独一无二的,因此每个应用软件,即使都叫ERP,它也是独一无二的。
把独一无二的东西分离出去
想想我们为了构建一个应用软件起来,要做哪些事?最基本的三件是:Domain Business Logic、Presentation(UI展现)、Persistence(存数据库)。还有就是Authentication,Authorization,Cache等等。复杂的系统还包括:与其它系统的集成,提供Service(API)等等。
就说最基本的3件吧,稍微思考一下就会发现,只有Business Logic是独一无二的。
Presentation层是个重复重复再重复的事情,再怎么不同的应用,我们都可以用同一套工具来实现它:用Grid来展现多条记录;用Combobox来提供选择;用MVC模式来分离数据与展现……框架应运而生。
Persistence也是个重复重复又重复的事情,关系型数据库,ORM框架,NoSql……同样约定俗成。
所有的东西都可以找到框架,这就是为什么写应用软件,跟写游戏,或者写操作系统等比起来,是最没有技术含量的。
然而唯独“业务逻辑”没有框架。正是这“业务逻辑”,让每个应用软件区别于其它应用软件。因此我们决定要做一个应用软件,我们要做的就是实现客户的业务逻辑,这是唯一真正的目的。持久化,UI,Cache……,都是手段。
如何实现业务逻辑?
现在知道了,我们做的每一个系统,都在为客户交付独一无二的业务价值。这就解决了程序员们“存在的意义”的哲学问题。那么如何实现业务逻辑呢?把客户的业务需求转化为解决方案的过程,就是设计的过程。因此,这个问题可以这样问:通过何种的设计,让客户的业务需求得到满足呢?这个问题很傻,因为显而易见是通过写代码实现啊,这不是常识吗?很可惜这不是常识,因为很多团队把设计写在设计文档里了。他们把写文档说成设计,把写代码说成实现。大错特错,代码是唯一的设计,MsBuild把代码build成exe才是实现!用Unit Test确保自己的设计(即代码)正确反应了自己脑中的设计意图,用Integration Test来确保自己的设计(即代码)正确地满足客户的需求,有着这种认识和追求的程序员,是我想要共事的程序员。
想起来,台湾习惯把程序员说成“设计师”,是更贴切的。把代码当做设计的程序员是幸福的,比较一下大楼的设计人员,当他们设计好的方案(设计图纸)一旦被建筑工人们开始“实现”的时候,他们的设计几乎就不能再改了,因为“实现”的成本太昂贵了。而作为软件设计师,我们的“建筑工人”MsBuild包工头以及它的团队(CPU,RAM等小兵)是多么的廉价和高效,几秒钟就把我们的设计给实现了。这就是我们能够利用“重构”技术的理由。(想象一下如果大楼设计人员也这么说:“你们先按照这方案盖起来,我看效果,然后再调整(重构)”……)
与传统行业的设计师相比,我们软件设计师能得到的反馈更快更多(因为我们面对的是电脑),这就是我们幸福的地方,也是我们应该利用的地方。准备写个“软件开发中的反馈系统”系列文章阐述此问题。
在哪里实现业务逻辑?
用代码实现业务逻辑,那么,在代码的哪个地方呢?有很多个地方:
1,前些年很常见如今被人很鄙视的一种是,存储过程。这种曾经非常流行的技术,自然有它产生的原因。存储过程是什么?是数据库里的东西,而且是关系型数据库里的东西。很多人的思维是这样的:当他试图理解一个业务逻辑时,他心里想的是表以及表与表之间的关系,这就是Database-Driven逻辑,在这种逻辑下,把业务逻辑写在存储过程里,是很自然的事情。如果把思维切换到Domain-Driven的模式中:业务逻辑是我的核心,持久化只是一个辅助的手段,我可以用关系型数据库,也可以用NoSql,而NoSql根本没有存储过程,如此,你把业务逻辑写在存储过程中让人情何以堪啊?
2,写在UI里,这就要提到当年的RAD之王Delphi了。并不是说在Delphi里只能这么做,而是说Delphi里很多人就这么做,UI直接绑定DataSet,用户点击了某按钮,直接在IDE里双击该按钮,生成Btn1_Click方法,把业务逻辑通通写在那。这么做有一万种缺点,但有一个优点,就是RAD中的R(Rapid)。哥用这种方式写过好几年的Delphi,不堪回首。
3,写在MVC的Controller里,其实等同于2。现在Asp.net MVC框架很热,可是很多人不知道MVC本质上是啥东西。虽然它有个“M”,可是他在分层的架构体系里,只是非核心的Presentation层的一个pattern而已。跟Domain层毫无关系。
4,最好的方式,当然是写在一个独立的Domain Layer里。别忘了业务逻辑是一个应用系统唯一独一无二的地方。
如何实现Domain Layer(Business Logic Layer)?
我做过几次技术面试,一般都会有个问题:“你能说说你对架构的理解吗?”得到的回答,第一句往往是:“关于架构,一般是分成3层,Presentation,Business Logic,Persistence……”。这句话即使是很Junior的人也能说得上来,可是再往下问就能问出有意思的东西了:3层之间的依赖关系是怎样的?
一般的回答是:Presentation依赖于Business Logic, Business Logic依赖于Persistence。
可是既然每个应用系统的“业务逻辑”才是应用系统存在的理由,才是开发它的目的所在。而UI展现、数据库存储、Cache等都是为了实现“业务逻辑”这个目的所提供的手段,都有成熟的框架、模式可用,都可以是雷同的。
那么为什么“业务逻辑”要依赖于“存储技术”?为什么“目的”要依赖于“手段”?
--------------------------------------------
在此对上篇做下补充说明:
1,因本人毕业以来从事的项目全是业务逻辑复杂的企业应用软件,ERP,SCM,HRP,CRM……,这种系统,如Martin Fowler在PEAA一书中所说,是适合使用Domain Model的,上文和本篇讨论的都是基于这样的场景和前提。
2,正如一哥们回复中说的,天下没有绝对的东西,我们都在写随笔,不是写论文。这两篇文章只是提供一种看待问题的视角,看问题的视角多了,到了具体的项目,就会有更多的选择。
3,写上篇时没想到要分上下篇,导致整个上篇没有说明啥叫“裸奔”,不过从评论看,大部分人都读懂了:就是让“领域模型”不依赖于其它任何东西(如数据访问层)。
天气热了,实在不想下了班还鼓捣技术,不过想想还是一鼓作气写完拉倒。
逻辑依赖与物理依赖
上篇留下的问题是:为什么“业务逻辑”要依赖于“存储技术”?为什么“目的”要依赖于“手段”?
其实“目的”依赖于“手段”并没有什么问题,但更准确的说法应该是“目的”受约束于“手段”,具体说就是“业务逻辑层”受约束于“数据存储层”,举个例子,如果使用NHibernate作为ORM框架,设计的“领域模型”一定是把所有属性都设置为virtual,为了迁就于NHibernate的LazyLoad实现技术。这种迁就或者依赖是无法消除的,然而这里说的是概念上或逻辑上的依赖。
如果到了具体实现上,仍然存在这种依赖,就成了物理上的依赖,简单地说就是BLL这个assembly会对DAL这个assembly有个引用。物理依赖有什么问题?
反馈延迟带来的伤害
先离题一下说说反馈。举个例子,我们拿着杯子去饮水机接水,随着水位的上升,我们知道何时应该停止,这就是眼睛看到水位后,大脑给出的反馈。如果反馈延迟(哪怕只延迟2秒)甚至根本没有反馈,会有什么后果?水溢出来了,大脑才反应过来,后果一定是手被烫到。
简单的例子可以说明反馈被延迟带来的危害。然而在软件开发中,很多团队不断地被延迟的反馈所反复蹂躏伤害。此话怎讲呢?
举个例子吧,“代码即设计”,如果代码就是我们的设计,那么如何保证我们的设计正确?很多团队最常见的办法是人肉测试。把代码打包成软件,然后丢给测试人员甚至客户。在我经历过的一个瀑布式软件过程里,今天写好的代码,也许要一个月后才会到测试人员手中,半年后到客户手中,也就是说,外界对我们设计(代码)的验证和反馈周期,需要几个月之久。这是多么大的延迟,2秒延迟就会烫伤我们的手,几个月,我们伤的起吗?
如何加速反馈
这就是“迭代开发”被引入的一个理由:缩小反馈周期。一个迭代(常见的是2周)内必须把反馈圈给结束掉,也就是2周内完成一个Feature的需求分析、设计、代码、测试等所有环节。从这个角度出发,如果一个迭代里不能getting things done,那不叫迭代,那就叫“两周”。
对于一个Feature来说,两周的反馈周期是可以接受的,毕竟每两周有个功能点给客户看看,确保我们do the right thing,很不错了。
然而如何保证我们do things right(比如,设计和可维护性等等足够好)呢?还有,这两周做的正确的东西,如何保证随着功能的不断增加而不会在将来被破坏呢(答案:回归测试)?如果每两周都人肉回归以前做过的所有功能,那就需要太多QA了。
答案就是自动化测试。Unit Test保证do things right;验收测试/集成测试来保证do right things。
自动化测试金字塔
如图,意思是什么呢?如果一个项目的所有自动化测试用例是100,那么最下面的Unit Test应该占80个左右,中间的集成测试占15个左右,上面的UI驱动的验收测试占5个左右。(还有个最上面的人肉测试,那是浮云:))为啥呢?因为Unit Test的ROI(投资回报率)最高,它上手容易、运行快,UI驱动的验收测试的ROI最低,运行慢、维护成本高(因为UI是很易变的,UI一变,UI测试脚本就得改。)
所以一个团队如果要开始自动化测试,最好从Unit Test开始。而最应该写Unit Test的地方是哪个地方呢?毫无疑问,是我们的“目的层”——“领域模型层”。
Persistence Ignorance
回到我们的问题,“领域模型层”对“数据存储层”有物理上的依赖,导致的不好的结果就是,很难写Unit Test。想象一下,有个Customer类,它的AddOrder()方法里面调用了DAL层的东西,也就是连接了数据库,那我跑我的UT时也一定要连数据库。连数据库的UT那不叫UT。
怎么办呢?“依赖反转”,Inversion Of Control,IOC。具体做法是:本来BLL依赖于DAL,现在抽一个接口IDAL,让BLL依赖于IDAL,DAL从IDAL继承。从Assembly上来说,BLL和IDAL放到一个Assembly里,DAL放到另一个Assembly,那么DAL这个Assembly现在对BLL那个Assembly有个依赖了。——这样,就把依赖给反转了。然后通过Dependency Injection,在运行时把DAL作为IDAL的运行时实例,注入到BLL中。这就是IOC和DI的关系,他们其实不是一个东西,只不过很相关,有时就用IOC或DI泛指这项技术了。
BLL对DAL的依赖,从编译期延迟到了运行期,编译期对DAL没有依赖,只对IDAL有依赖,这就是Persistence Ignorance(不知的请google之)。
这有多重要?
BLL(领域模型)开始裸奔了,它对其它层没有依赖,我们可以为他写丰富的Unit Test,这有多重要?
每个unit test都用其方法名说明了我们的设计意图,甚至小片业务逻辑,比如有个测试用例,方法名叫“should_promote_to_VIP_when_customer_buying_platinum_card”,如果让你接手一个别人留下的代码,你不是很清楚里面的业务逻辑,你是愿意去看文档?还是愿意去看他留下的存储过程、或者100行又臭又长的方法?还是愿意看这样的一句话:“当客户买了白金卡后,应该把它提升为VIP”?
unit test的覆盖率足够高时,我们读完所有的unit test方法名(只是名字),我们已经了解了大部分的业务逻辑。
事实上,一个项目的维护成本往往是开发成本的四五倍甚至几十倍(越差的代码,这个比例越高)。另外大家也深有体会:读代码比写代码难。那么为了降低读代码或者维护别人/自己代码的痛苦(当老板的,降低维护成本意味着白花花的银子啊),有啥理由不让我们的“领域模型”裸奔呢?
丰满的领域模型裸奔着向我们呼啸而来
下图是敏捷宣言签署者之一的Alistair Cockburn的Hexagonal Architecture,很精彩的图,留作参考资料,大家意会,不解释了。