这是一个从组件和分层角度的系统架构迭代过程的例子,选择这个角度因为这个角度最直观,也最具表现力,这个例子并不完整,不过没有包含进来的部分并不影响这个例子就是了,原本是有说明各个组件之间的调用关系,通讯方式什么的,不过感觉那个图看着乱,而且我觉得只要能说明问题就可以了;另外一点就是公司今年初就开始重组,这个系统也一直再跟着调整,没有大动作的情况下,我偶尔可能会改下最后的图。
例子开始之前,先简单提三点:一是随机应变;二是没有银弹;三是KISS。这三点其实是一回事,很多书将理论讲设计讲的其实就是一个随什么样的机应该怎么变,现在很多开发人员都有一种想法,就是一次性什么都做好,以后可能会怎么变都想好,这种想法就是所谓的过度设计,没有银弹就是说没有做好这种事的,没有能适应一切情况的设计,只有最合适,没有最好,复杂的结构就代表着要付出理解、维护及性能上的代价,只要将真实的需求理解好了,未来的需求就一定能适应,对于确定的未来一定会出现的需求,可以思考,但不到真正出现的那一天,最好不要去做(除非实在是闲着没事,而且做了客户也发现不了,一般是很少见有项目时间是充足的)。
先看一眼现在变化成什么样了:
正式开始:背景是这样的(这部分是我听说的,因为那时候我还没入职,真的假的反正关系不大),原本公司用的别人的OA系统,但是由于公司是又至少5,6家公司合并的,类似联邦似的管理,所以业务不是很规范(国内公司业务非常规范的应该不多吧),总之,这套OA用起来有些不通畅,同时由于是成品的产品,修改维护非常困难,所以领导决定自己做一套,然后就开始下手了,而且是没有规划直接就下手了,听说用了一年的时间做了两个审批流程出来,工作流驱动开始用的一个网上完全没有资料的产品,然后大部分功能没用上,后来我自己封装了一个,关于审批流程部分我这里不明确说了,有时间会单独细写的(现在有两篇随便写的),由于工作流驱动被我用依赖注入注入到应用逻辑层了,在分层中就忽略不计了,总之原本的情况大概出了个如下图的架子出来:
<图1>
一、原本状态,这个 <图1> 可能不完全,因为我只是大概浏览了下原本的代码,这个结构很简单,问题也很简单:
1.大部分数据层使用的是EF,EF现在是个挺普遍的东西,这里只有一点问题,EF用的是3.5版本的,这个版本熟悉EF的人都知道什么情况,各种别扭,这一点倒是怨不得原来选型的人,没什么性能压力的情况下用ORM框架辅助很正常,选择熟悉的技术更没问题,至于这个版本。。。是由于技术以外的不可抗力造成的,要求没有足够经验的人在一个不太明确的限制条件下(你懂得,不可说)对版本的可行性做评估确实很难;
2.还有小部分数据访问在 <图1> 中的Service中,直接使用的ADO.NET,由于这部分的数据访问和业务逻辑以及服务本身混杂在一起了,也就不细化了,提供的服务非常少,用的asp.net web service 没有分布式部署;
3.DTO的话,本身作用应该是为了将上下层解耦,可以使上层不用关心下层的数据类型和结构,我猜原本做的时候应该是有好好这么做的,但是由于后来需求不断的变化,最终导致的结果是,DTO和EF生成的实体混合着BLL的业务逻辑被应用层直接调用,这里用图不太好表达,所以<图1>中就不费这神了;
4.由WebForm充当应用层和UI,图上也不细分了,受原OA系统,业务人员及技术选型的限制,界面上充斥了大量的隐式包含业务逻辑的js脚本,基本没有使用局部刷新,说基本是因为系统中有两处位置使用了ajax加载数据,但是诡异的是,调用的是和web部署在一起,且和普通调用BLL没有区别的asp.net web service,自然返回的数据是xml的格式,ajax本质上是读取异步请求的页面输出的字符串,大量的xml标记会造成网络传输的无谓的损失。
这几点是主要的问题,也是调整开始的切入点,细节就不细说了,结果是领导不满意开发效率,开发人员自己也对开发出来的系统感觉不舒服,代码的可读性和扩展性都不怎么好。
二、慢慢调整整体上的问题,首先说“慢慢”,由于我刚刚入职对公司业务不了解,了解的渠道也很薄弱,这不是托词,因为公司的财务和行政部门甚至都不了解有的部门为什么会有某个职位的情况下,我能了解到的业务基本也都是来至于推测;再说“整体”,细节虽然也调整了,但是由于各种各样的原因,没有太多精力也不愿意深入细节,原因在于上部分说的进度慢,开发效率低,不是说做的东西少,而是做出了大量的妥协于技术和需求的额外逻辑,另外,毕竟是公司内部系统,没有压力,所以程序员们想要提高的动力也不足(这里说句体外话,总有朋友抱怨公司技术实力弱啊,学不到东西什么的,虽然有牛人带进步确实快,但不代表没有人带就不能学习,内事有baidu,外事有google,大量的开源代码,有msdn和各种开源社区,自己摸索着进步才是真实的能力提高),为了尽量不影响已经开发的系统,就先进行些局部调整:
<图2>
1.ORM被我换成了NHbernate,它比较成熟,而且用的比较熟,然后在nh上提供了两个接口以供上层调用,一个是DAO,一个是存储过程的调用;将这两个接口的实现封装起来,拒绝继承,使程序员无法直接使用nhibernate,以备未来可以升级到4.0以上时,还换回EF,至于为什么要换回来,原因主要有两点,一是跟着微软混当然是用微软出的东西最牢靠,二是NH的社区越来越没有活力了,没有微软的专职团队这么有生机;此外,封装中实现了NH提供的日志接口EmptyInterceptor,可以记录所有使用NH对数据库的操作,当然一般都配置关闭了的,开启的时候可以借助消息队列来保存日志;还有一点好处是NH自带缓存,当对映射表单表访问时命中率很高,对效率有很大好处。
2.调整后的图里的Service不是指Web Service,因为当时没有使用的必要,这里是指当领域中的某个重要过程或转换不属于实体或值对象的自然职责(这句话出自DDD),这里包含了业务逻辑的调用,或者业务逻辑接口的简单实现,原本的web service包含的业务逻辑绝大多数移入BLL,只保留了来至于外部的服务;
3.DTO移除掉了,使用DTO的绝大多数都是DAO的简单操作,DTO的存在价值无法体现出来,直接使用NH映射的贫血实体,简单明了维护方便,系统并不复杂,构建DTO去解耦没有意义,软件系统的分层主要是为了将变动限制在更小的范围内,也使开发不同部分的开发人员并行开发对相互之间的影响更少,但是系统一人开发足矣的情况下虽然为了代码整洁而分层,但是扩展的代码(比如层之间传递数据的DTO)是应当在不扩展会对系统有不良影响时才写的,所以这种规模的系统使用DTO会有过度设计的嫌疑。
4.将界面的脚本进行了整理,统一脚本的写法,将一些脚本进行了封装,对webform后台代码写法进行了规范,简化界面开发。
之前说的工作流这里不得不再提一下了,之前虽然使用了某种工作流引擎,但不得不说,用了和没用差不多,仍然有大量的应该由引擎处理的逻辑被放到了webform中,所以匆忙中,用了些时间简单的封装了个工作流引擎,然后为了不影响某些已有的东西,用了依赖注入,可以将新引擎和原有引擎通过配置分别使用。
三、经过一段时间调整,开发效率提高很大,相应增加的需求也越来越多,我也参与了一点应用开发的工作(比如写个DEMO什么的),对程序员灌输了些面向接口,面向抽象的概念(我很奇怪他们获取听说过但完全不想去用),以及作为一个程序员,写重复的代码是一种耻辱(他们很喜欢复制粘贴。。。),大致整理了一下业务接口,发现业务虽然很多,但大多类似,这段时间将业务的基本接口大致梳理了些,交给程序员参造整理并对部分的业务代码进行重构(另一部分没重构是因为些上不得台面儿的原因,就不细说了),将映射的实体分成了三类,分别继承了源自同一基类的三个略有不同子类,这主要是应对各事业部拍脑袋改需求所做(比如,某天突然想再所有单据上加上个什么属性,但是过十天半月又突然不要了);
在这段时间内,对界面层做了一些变动,由于在参与开发的过程中,发现很多页面结构及逻辑相似,完全没必要总是做个新页面出来,总是写些微妙的虽然不同但是很像的代码,基于此就将WebForm的前后台分离,引入了angular,使用双向绑定在页面上只保留页面元素,将脚本逻辑移入angular的controller,后台逻辑移入service和web api中,封装了些angular的provider使用ajax的方式进行前后台交互,原本webform直接使用的实体由angular中的$scope来维护了。
Service部分正在着手做封装,这一Service的含义又有些不同,一方面内部的实现与上部分所说的含义相同,另一方面表现形式是分布式的服务接口,这里准备使用WCF和Web API,还会包含一些直接调用的其他系统的Web Service,WCF应该算是个微软的集大成之作了吧,所有的微软的分布式消息通讯方面的技术大融合,又支持业界标准,至于Web API处理一些简单的请求更方便,不需要通过依靠很多约定等。
这里可能会有人奇怪为什么不用MVC,其实我考虑过asp.net MVC,暂时(注意:只是暂时)没用的原因也很狗血,我们所有的东西都是部署在sharepoint 2010上的,但是没有专职的相关人员(应该说基本就没人懂),也没深入了解过这东西,搞不清楚究竟怎么和MVC整合,举个例子,一般情况下,sharepoint会有个名称以sp开头的组件对应普通的asp.net,比如SPContext对应httpcontext,但是sphttpapplicant居然就没实现ihttpapplacation接口,sharepoint的global又是轻易动不得的东西,使用了httpmodule到是拦截到了,但是结果不尽人意,github上倒是有个老外做的解决问题的例子,可惜使用的是sharepoint 2007的,而且语焉不详,测试了下,感觉不踏实,去微软社区问也没有结果,现在也还在研究当中,好在领导终于松口打算不用sharepoint了,人精力有限我也是实在不愿意花费太多精力在它上面,所以,MVC暂时还没用上,不过早晚能用上的吧,只要机会来的时候我还没离职。
<图3>
四、在我将注意力放在对简陋封装的工作流引擎不满而进行一系列修改,还有诸如邮件审批服务,信息部门一些日常工作等一些琐碎的事上面时,由于开发效率的提高,造成了需求大量增多而引起业务对象数量上的快速膨胀,而由于业务对象被分为贫血的映射实体和BLL的业务逻辑操作类两部分(当初选择这么做主要是因为开发人员这么处理习惯,而且看起来清晰,在系统规模不大时便于阅读和维护),造成了一个业务对象至少会增加两个类,有碍观瞻,十分影响心情,于是要解决它:
将贫血实体改为充血实体,整个Service借用DDD中领域服务的概念了,由它实现非当前业务对象本身的职责(包含大多数原本的业务逻辑接口),比如单据的提交应该是由提交人而非单据本身,至于实现方法,在另一篇随笔中写了个演示例子,这里就不细说了(由继承演变为角色扮演)。顺便说一下贫血和充血,网上有好多分析这个的,在我看来不用理解那么复杂(也有可能是我没理解透彻,等这俩概念对我真正有帮助的时候再细致了解吧),就是一个是将对象的动作分离了出去,方便一般程序员理解和小型项目开发,另外一个就是即包含对象属性又包含对象动作,这种情况下处理需要注意一下,对象是不能直接提供给上层调用的,否则封装就会被破坏,业务会在一定程度上被泄露出去。
经过上面一步,BLL中的业务逻辑大体上出去了,剩下的就是具体的业务执行了,也就是应用层,在图中称为应用逻辑。
至于这中间的代码工作写个例子就交由开发人员去做了,这时候需要开始考虑DTO的问题了,因为充血的实体是不能直接提供给上层用的,得封起来,那交给应用层(这时候angular和原来的webform方式同时存在,所以就叫应用层了)的只能是DTO,DTO的另外一个好处是,拼装好的DTO可以将一部分原本暴露给应用层的业务逻辑或者说拼装逻辑封装起来。当然,这事咱也是能偷懒就偷点懒,交给开发人员做,索性来例子都不写了,本来他们就用过DTO的,区别仅仅是那时候他们的DTO构建是没有依据的,用到什么算什么,这次统一按照界面来,拼装中公用的东西再缓个存(已经申请到手了linux还没时间用,准备以后缓存都交给redis了,不过毕竟还没上所以缓存这部分先不画图了)。,
这里将服务统一成WCF,主要是因为之后公司各种系统,像ERP什么的都会通过ESB衔接起来,对其他系统的服务的调用都会通过ESB,分布式且统一标准的接口是必须做的,用以提供给ESB供其他系统调用,至于本系统内部当然适当的Web API还是会方便些。
然后,还有一样体力活交给了开发人员去做,就是读写分离,最开始做数据层对外接口的时候,就将存储过程调用与普通的SQL执行都预备好了,所以只需要将复杂查询剥离出来就好了,区别是以前返回实体或DTO,现在统一返回ILIST,这一部分主要是为了将要进行的报表查询统计,一些BI的工作,另外,NH本身只支持单数据库操作,但是可以通过些 手段多数据库也不是问题,这个最早就做到了,因为这里虽然只说了一个系统,但是系统其实有几个,所以数据库也是多个。
<图4>
上图中还有一处值得一提的是Provider,这个Provider在之前一直是BLL中的一个类,作用是将所有数据层的接口全部封装起来统一提供给业务使用,这样做看起来是有点大杂烩的感觉,不过当时有两个原因需要它存在(其实我就是那个闲的无聊且干了什么别人也发现不了的人):
一是数据层提供的接口从概念上完全是对数据如何使用的描述,将接口重新包装后可以让它在业务层面上有一定的表现力(比如:插入历史表在业务中描述为归档),容易融入到业务当中,便于程序员在开发中专注于业务,也有利于之后应用DDD的设计理念后转变为Repository的重要组成部分。
二是充当从<图3>到<图4>转变的缓冲,读写分离是很早就做了准备的,将数据层接口的封装到这里,读和写可以在上层不知不觉中完成分离,当分离成熟后,可以很轻松的将其中查询部分移出,只需要借助工具就可以轻松将查询的引用指向新的查询类,成为<图4>中的状态。
目前,这个分层架构尚尚未调整完成。
五、我预想中,下一步在这个分层基础上的进一步调整,就不画图了:
1.接入ESB,目前在怎么接上,我的想法和领导不太一样,不过我相信终究会按我想的来的,大概。。。;
2.由于很多Entity在业务上是要保证数据一致性的,可以作为一个整体在系统中执行,这里可以使用DDD的思想,可以参考CQRS的架构,当然现在系统还没有那种规模的复杂度;
3.使用MVC深入研究下,我觉得在sharepoint2010上应该是可以的,而且,据说sharepoint2013上是已经支持了MVC,另外可以考虑一定程度上结合angular;
4.目前尚不清楚是否有必要使用缓存,不过既然redis环境我都准备好了,不用下实在对不起自己,大不了暂时让他们发现不了就是了;
5.异常处理和日志准备重新统一规划下,不过目前似乎领导不大愿意做这件事,我也先偷偷做着。