众所周知,良好的系统研发应该有延续性和一致性,所以很多公司非常注意代码版本控制,并逐渐慢慢的迭代自己的产品。
具体到自然资源行业来说,我司纯粹的产品销售较少,项目开发较多,不同的地区,不同的客户对于同一个功能的理解可以千差万别。即使是产品销售也同样面对相对强势的无可避免的增加一些个性化功能。
长期以来,公司事业部研发期望统一代码版本的愿望与具体各个项目之间个性化需求导致的代码变更(数据库结构变更)之间矛盾越演愈烈。最终形成了底层框架尽量不改,新增功能通过扩展来改,业务相关沉淀到前端或数据库函数(视图,存储过程)去改的微妙平衡。
但是这个平衡是很脆弱的,有较大的风险。
- 首先,前端页面,脚本,数据库结构没有通过代码版本管理器进行管理,很容易出现页面,脚本,数据库表结构冲突的问题。一旦冲突虽然影响范围局限在项目当地,但是对公司的产品质量口碑影响较大。
- 扩展的新功能,绝大多数控制权在具体的研发手中,很难把控代码质量,进行代码审核,员工因各种原因离职后,也不利于公司对代码的回收管理。
- 虽然采取了主框架,前端,扩展的分离,但是主框架更新时总部研发依然要小心翼翼,尽量避免任何与通用功能无关的逻辑写入,而事实证明,除非紧锁代码权限,只让极少数人修改相关代码,否则这一点基本不能做到。
如上所述,对于代码版本控制的讨论在我们公司已经举行了很多次,绝大多数讨论都是从管理层面出发,聚焦于代码权限控制,代码分支控制等等。今天我想从技术角度谈一谈代码版本控制。
从技术角度怎么做代码版本控制?如何解决通用版本与各个地区相同功能不同表现形式之间的代码冲突?其实C#的asp.net框架和Java的spring框架已经告诉了我们答案:基于接口开发。
就拿日志来说,无论是asp.net还是spring都为我们提供了官方默认的日志类,但是如果这些日志类无法满足我们的需求怎么办?我们完全可以基于官方接口自己实现一个特殊的,基于个性化业务需求的日志类,然后把这个独一无二的日志类通过依赖注入注册到框架中,替换和接管原来的通用日志类。核心框架内部因为全部是基于接口实现的操作,所以什么都不需要修改。而实现这一点最关键的一步就是核心框架中,不同的组件之间,不同的类之间通过LoggerFactory生成的ILogger去执行日志记录,而具体到底使用官方的通用日志类实现ILogger还是第三方的特殊日志框架如Log4都无所谓。
通过这种方式,asp.net和spring做到了核心框架和业务代码的剥离以及各自的独立发展。简略的架构描述如下:
回到我们的自然资源以及自身的产品(至少我目前了解到的政务,不动产登记以及权籍系统),为什么感觉代码版本控制很难,本质上是将通用业务模块与框架紧紧的耦合在一起了。简略的架构描述如下:
这种架构的优点是:框架即产品,开箱即用。它的缺点也非常明显:只要是和具体业务相关的改动都很不灵活(包括业务代码,甚至包括部分工作流代码),并且地区扩展中的代码只能扩展全新的方法,一旦设计到旧方法的改造,还是只能回来修改框架,而为了减少对框架的污染只能依赖关键核心员工修改,效率较低,响应也不快。
对于改造工作量,我认为无需担心,要想使用这种模式来控制版本控制,也不是一定要全面的重构代码,大动干戈的改造,我们完全可以一个模块一个模块的逐步的进行改造。
以不动产农房改造的户籍信息为例:
最初的农房批量生成附记因为逻辑相对复杂,没有按照管理在前端或数据库完成,而是写在了核心代码库BDC2的HjxxDomainService领域类中。
但是批量附记属于业务性非常强的内容,每一个地区的不动产可能都有自己的要求,如衡阳提出要求在家庭成员下添加预编不动产单元号,房屋来源等等内容。这些内容的数据库字段甚至是衡阳特有的,根本无法在通用版本里面直接添加 。
遇到这种情况,要求衡阳自己通过扩展另外重写一套批量生成附记进而导致有两套极其类似的大段代码,要么通过核心程序员在通用版本中增加非常麻烦的版本判断分支语句。不论采用哪一种方式都非常不利于代码的版本控制和维护。
如果我们采用先前描述的框架对应接口,附带一个通用实现,不同地区有自己的对应实现,那么我们就可以简单的通过增加类文件的形式完成衡阳修改。甚至在个性化类还在利用大部分通用类方法时可以引入一个通用方法代替实现。粗略架构图如下:
核心代码改造点如下:
-
将外部控制器要操作的核心领域类从通用的HjxxDomainService,修改为抽象的AbstractHjxxDomainService。
-
HengyangHjxxDoMainService通过通用的CommonHjxxDomainService实现无需特殊化处理的方法,减少重复代码。
-
CommonHjxxDomainService(原HjxxDomainService)继承AbstractHjxxDomainService和IplGenerateFj(视情况复杂度可以不要),其余不做修改。
通过以上5步改造,我们就不动产的农房业务的户籍信息领域模块单独拆分了一套通用版本和一套衡阳版本,其中衡阳版本除了批量生成附记,其他模块本质上还是用的通用版本方法,只不过已经具备了继续扩展独立的可能性。改动量小,安全,且切实可行的能够解决版本控制问题。因为既然能够拆分成单独的文件,那么一定可以继续拆分成单独的动态库,然后在工厂方法中通过反射的方式动态寻找,加载和绑定。
遇到全新的方法时,我们也可以从容的选择多种方式,如果是在现有工程中修改,则修改抽象类(接口),然后首先实现通用版本,对其他地区版本直接调用通用版本方法。当然这样对代码破坏性比较大,我们完全可以不要改动现有类库中的抽象类和接口,通过扩展的方式在原类上增加新的方法和接口(以C#语言为例)。