重构,是编写代码必须要面对的一项操作,同时也应该是程序员乐于实践的一项内容。不论是逻辑实现还是设计过程,乃至整个分层结构,我们都可能面临并且实施重构。这篇文章不会告诉您什么是重构,如何去优美的重构等等的理论,只想和大家分享一些感受,并且探讨一些问题。最近的两周,我一直对我们团队的一个子业务框架做重构的工作,很多地方让我感到很痛苦,于是便有了这篇文章。
牵一发而动全身的根源在哪里
当我打开解决方案,查看代码的时候,我们会发现很多问题,比如冗余的代码,性能低下的逻辑实现等等,但是当我着手去改造的时候,潜意识告诉我整个似乎不能动,牵扯的面太广了。更改一个小地方,上下一串都要做相应的调整,这当然不是我想要看到的。大范围的调整会直接影响系统的稳定性,带来潜在的危险,同时会增加测试团队的负担;在版本控制方面会造成线上和线下版本在同一内容的巨大差异,版本更新的时候拿什么来保证一套几乎全新的代码替换线上系统是正确的选择呢?因为很多问题只有在最真实的环境才能被暴露出来。
这样的修改,修改成本无疑是巨大的,因为我们期望修改的只是那一小块代码而已。大范围的代码调整,同时也伴随着单元测试代码的调整。测试团队如果因此来重新走测试用例,那么付出的辛苦可想而知。
我要做的是重构而不是重写,造成这种现象的原因在哪里呢?
整个解决方案具有相对完整的分层结构,DAO层、实体层、业务逻辑层。实体层也对数据实体和业务实体做了分别定义。但是进行业务实现的时候我们并没有进行有效的隔离和代码的职责划分。
很多代码在处理业务逻辑的时候直接调用DAO,然后使用返回的数据组织业务实体。当我们的业务实体需要按照领域划分为两个或者更多的层次的时候,结果会变得更为糟糕,因为我们需要以底层的业务实体为输入从而输出上层的实体。当你以一个顺序工作流的方式完成一整套操作的时候,也许感觉很有成就感,整个过程天衣无缝,完美无缺。但是当我们尝试改变其中的某些内容的话,噩梦就开始了,实体的改变势必会引起逻辑的改变,但是这种改变是有连锁反应的。
业务实现的过程很多时候就是不同层次间的实体的转化过程,那么实现过程中单单考虑解除依赖不能收到很好的效果,从业务逻辑的职责出发,划分出清晰的业务层次,再以实体转变的结合点来考虑分解才能达到良好的效果。
独立的领域层尤为重要
各种经典的MVC架构的实现,常常让人产生误解,认为那样做就已经完美了。事实上,一个业务的框架的重点不是增、删、改、查,我更倾向于将DAO从业务框架中分离出去(最后我也是这样做的),整个系统应该提供统一的DAO服务,子业务框架要专注于业务的实现。
当我们尝试将一整套业务实体独立出来的时候,我们认为已经做了很好的业务理解,但是这是只见树木不见森林的想法。在某些系统中,领域的实现只占代码总量的很少一个比例,但是其重要性往往却是相反的一个比例。当我们选择将领域代码和其他代码混合在一起的时候,意味着我们的分层结构也随之混乱。
“用标准的架构模式来完成与上层的松散关联。将所有与领域相关的代码都集中在一层,并且将它与用户界面层、应用层和基础结构层的代码分离。领域对象可以将重点放在表达领域模型上,不需要关系它们自己的显示、存储和管理应用任务等。这样使模型发展得足够丰富和清晰”。
在清楚了整个领域模型之后,再考虑选择合适的模式来解决分层问题,我觉得是合理的做法。
在对业务和领域没有充分理解的时候不要下手
在重构过程中,发现很多业务实体的定义不着边际,很多概念只是对数据实体的拓展,结果出来的东西和数据实体的逻辑关系截然不同。
对数据调取的逻辑没有充分理解,那么组织业务实体的时候很容易出现不恰当的数据访问方式,比如循环访问数据库。
如果整个领域模型的建立和划分都是错误的话,我们仍然能实现所需要的功能,但是如果对这样的代码进行重构无疑等于推倒重来。
现在看来,当我们为了实现功能而急匆匆的不择手段的时候,为将来的维护和升级埋下了隐患。
当我们想要将公用的数据调取和业务逻辑实现从各个子项目中抽象出来变成基类、接口、Helper或者Service的时候,我发现不同子项目的开发者对业务和领域的理解有着很大的差异,因而在实现方式和实体定义上都有很大的不同。这个时候我们又注意到组织实体的逻辑并没有单独的分离出来,抽取的工作遇到了难题。也许我们可以分离出代理,然后使用Adapter来适应原有的实体组织逻辑,又或许我们应该废除不合理的实体定义,也意味着废除了相应的实现逻辑。
如果我选择将不合理的实体替换为正确的实体定义将面临巨大的挑战,也许从数据调取到最终的逻辑都要调整。当然全面调整的原因是我们没有实现很好的隔离。
迷茫,面对一个几千行的Method
这是我无法容忍的情况,整个子业务的实现几个大方法全搞定了,每个方法里无数个“Region”和“End Region”。这样的情况就别谈什么分离和设计了。最起码的,当你在用“Region”和“End Region”的时候,就应该意识到这里可以分离出一个方法来。
大方法带来很多弊端。它的可维护性差,可阅读性差,和系统的分层结构不融合,不能进行有效的单元测试。
当然对大方法的重构并不像代码本身那么发杂,如果它的逻辑足够清晰的话。但是如果一个思维足够清晰的程序员又怎么会写出这样的代码,所以对这样的代码进行重构,面临一个很大的问题就是那些在不同逻辑里重用的局部变量。当然更重要的问题是理不清头绪。
光去抱怨是没什么意义的,这样的方法出现的原因是什么呢?一个是开发者没有很好的理解业务框架的结构和目的,二是对程序设计的基本思想理解的不够好,三是对业务逻辑本身理解的不够清晰。
对于这样的实现去重构,除了从业务角度出发,抽丝剥茧,慢慢的剥离,还有什么好办法呢?推倒重来吗?
重构,要随时进行
当有一份代码觉得不合适,而没有及时重构的话,那么整个解决方案就可能变成垃圾场。后续的开发人员会以存在即合理的想法来看待这些垃圾代码。尤其是新加入的成员,只能模仿别人在怎么做。
从测试驱动的开发理念看,程序开发是一个不断重构的迭代过程。很多人将重构看成是一件大事,一听到重构就害怕起来,尤其是测试团队。当然这里不能否认,不恰当的重构会给测试团队造成很大的麻烦。
集中重构是极其错误的思想。不要想着等某些开发任务结束了,有时间了再集中精力来重构代码。当系统相对稳定之后,重构要付出的代价可能是整个团队无法接受的。
对分层架构的重构应该是建立架构的最初一段时间,不断的调整达到最优。当项目进行一段之后,再来调整整体结构无疑是让人无法接受的。
重构要避免过度设计
最后要说的是,重构要围绕一个适度的目标来进行,要考虑代价,同时不代表模式应用的越多越好。相反的,在重构过程中,要时时考虑是否把简答的事情想复杂了。