编写可维护代码的总结
前些日子写了个小项目,它即简单又有一定复杂度,正好可以用它来总结一下面向对象设计的一些原则和方法。
由于是公司的商业代码,不便透露,我这里就简单总结描述一下核心需求
简化版代码下载(VS2010,C#,无Web无Windows Service)
业务需求:
1. 有三个第三方的Web Service:SourceReaderService、OASystemService和OrderService
2. 监控从SourceReaderService获取的ReaderResults集合。筛选出符合条件的Result列表,根据这些符合条件的Result,通过OASystemService创建OATask
3. 等待人工确认新建的OATask是否需要创建Order
4. 可以手动在OA System中创建需要创建Order的OATask
5. 监控需要创建Order的OATask(包含手工创建的OATask),一旦发现存在需要创建Order的OATask,通过OrderService创建一条Order,并将Order ID写回相应的OATask;若该OATask为手动创建的,则需要将它添加到程序的日志记录中
6. 监控OrderService中OrderStatus有变化的Order,将OrderStatus更新回OATask
7. 若ReaderResult(从SourceReaderService获取回来的)在未完成的OATask中存在,不得创建新的OATask
8. 程序需要记录操作日志(Reader ID,Task ID,Task Create Date,Order ID,Order Create Date,Task Completed Date)
9. 使用Windows Service实现监控功能,此外还需要Web界面来查看日志,设置由ReaderResult创建OATask的触发条件,OATask的Subject、Assigned To等
三个Web Service我自己大体Mock了一下
下面是我写这个程序时的思路
业务较简单时的架构设计:透过表象看本质
当业务逻辑较简单时(比如本例),可以抛开一切细节来看程序的行为,而面向行为编程就等于面向接口编程,因为接口描述的就是行为!
本例中,表面上是三个Web Service之间的互操作,但是实际上该程序的核心业务应该至少包含三个行为:创建Task,创建Order和更新Task。照这个思路,创建三个相应的接口:ITaskCreator、IOrderCreator和ITaskUpdater。同时,它们的行为当然也自然而然地出来了:CreateTask()、CreateOrder()和UpdateTask()。
public interface ITaskCreator { void CreateTask(); } public interface IOrderCreator { void CreateOrder(); } public interface ITaskUpdater { void UpdateTask(); }
写出这三个接口之后,在我脑中就有了一个大概的构想:在一个Windows Service程序中使用三个Timer持有以上接口,然后在Timer的Elapsed事件中调用相应的接口方法就可以了
要写易于测试的代码
接口写出来后,该写具体实现了。
似乎ConcreteTaskCreator与SourceReaderService和OASystemService有直接关系,等等,如果ConcreteTaskCreator直接持有了SourceReaderService和OASystemService对象,那么对它进行单元测试异常地困难。本身第三方的Web Service就对测试不友好,将这些Web Service直接耦合到ConcreteTaskCreator中,使得ConcreteTaskCreator也变得不易测试了。
这个时候就需要将第三方Web Service封装起来了,具体思路如下图:
MockSourceReader专门为单元测试设计,而WebServiceSourceReader中则实际持有第三方API类(Web Service),它们实现了ISourceReader接口。
同理,其它两个Web Service也要使用接口封装:名称分别为ITaskSystem和IOrderSystem。
上面的做法实际上是Facade模式的应用。
ConcreteTaskCreator中持有的则是ISourceReader和ITaskSystem接口的对象,这样,可以通过传入不同的具体实现来对ConcreteTaskCreator进行测试或者实际运行产品代码。
同理,ConcreteOrderCreator和ConcreteTaskUpdater均持有ITaskSystem、IOrderSystem接口对象;对于手动创建的OATask,为了获取ReaderResult的信息以创建Order,ConcreteOrderCreator还需要持有ISourceReader接口对象。
为了记录日志,ConcreteTaskCreator、ConcreteOrderCreator和ConcreteTaskUpdater三个类都要持有数据持久层的对象。这个持久层对象也是使用和封装第三方Web Service相同的思路,通过接口来封装它,使得在运行单元测试时不使用真实的数据库(除非单元测试的目的就是要测试操作真实的数据库,否则在单元测试时使用真实的数据库会带来一系列问题,如测试运行速度慢、旧数据影响新测试等)。
本例是个简单的例子,无法体现当业务逻辑比较复杂无法立刻得到高层抽象时,写易于测试的代码有时会逼出更好的设计这个特点。
不要让第三方的类库过度污染你的程序
SourceReaderService返回ReaderResult,OASystemService中包含OATask和OATaskStatus,OrderService中包含Order和OrderStatus。以上这些都是第三方类库中我们必须用到的类。
如果在ISourceReader、ITaskSystem和IOrderSystem中直接使用这些类当参数或返回类型,那么,我们的高层抽象接口就会被第三方类库“绑架”。目前来看ITaskSystem使用的是OASystemService这个Web Service,如果变成另外一个第三方API而它需要的参数是XXTask类的实例的时候,被“绑架”的高层接口实际上已经无法与OASystemService分离了。所以,要限制第三方类库的“污染”范围,尤其是在第三方类库不稳定的情况下(不稳定的类库远比你想像的普遍,连微软的.Net Framework都有好多过时的方法、成员)。
按照上面的想法,我们要创建自己的Task、Task Status、Order和ReaderData类(即使目前它们的属性和三个Web Service提供的类中属性可能完全一样),把主动权掌握在自己的手里!
数据库是实现细节!
它太重要了,以至于我要再重复一遍:数据库是实现细节!在项目设计时请不要先考虑数据库Schema!
太多的程序因为在业务逻辑层掺进了数据库细节而使得业务层十分笨重!太多的程序因为在设计时是按照Database First的设计思想而使得业务层过多地关注持久层的细节从而导致业务逻辑混乱无法维护!这样的程序我自己也写过,也被坑过。
实际上,如果一个程序在持久层无论用哪一种方法(数据库、XML,甚至是内存对象)都不影响其它部分的运行,那么它应该是一个好的项目,至少在隔离持久层方面,它是好的。
本例的代码我不准备使用任何复杂的持久方法,只使用一个内存对象(IDbAccessor)就能让程序(或者是单元测试)运行起来。
博弈:代码量、代码复杂度与可维护性之间的平衡
单元测试无疑会增加代码量;
写出可维护的单元测试是一门大学问,这也会增加工作量;
可测试的程序无疑比不可测试的程序复杂度更高;
设计模式会增加复杂度,在滥用设计模式的情况下更甚;
好的抽象、封装和单元测试会大大增加代码的可维护性;
如何取舍?
我个人是这样认为的:
1. 如果一个项目要维护较长时间(个人认为一年半或两年以上就足够长了),好的单元测试必不可少
2. 好的抽象和封装水平因人而异,总之努力吧(这方面我水平也不高)
3. 使用设计模式要慎重。每当要应用一个设计模式时先问一下为什么要用?如果你明确地知道应用DP的代码在今后会产生变化(大多数DP的目的是封装变化点)或者为了让代码可测试,那么是应该应用DP的;如果是为了DP而DP(It’s cool,it’s 霸气酷狂屌!),或者是你根本不确定这一块代码今后会不会变化,那么,先做简单的封装也许会更好。
4. 如果Deadline很紧,要做的事情太多,根本没时间写单元测试或考虑抽象,《代码之殇》里有一节“向死亡进军”,好好看一下,免费的迷你书里面就有这一节。如果你改变不了这样的项目管理制度,也许你应该考虑换个老板吧