记一次百万行WPF项目代码的重构记录
此前带领小组成员主导过一个百万行代码上位机项目的重构工作,分析项目中存在的问题做了些针对性的优化,整个重构工作持续了一年半之久。
主要针对以下问题:
1.产品型号太多导致代码工程的分支太多,维护时会产生非常多的重复性的工作。
这是一个历史遗留问题,公司成立之初的开发人员在开发时没有考虑到后期其他机型的合并而留有余地,后面增加其他机型而导致的代码差异,是直接通过创建工程的分支来进行维护。两个不同机型之间可能大部分的业务逻辑都相同而只有少部分的界面和业务存在差异性,当修改一个共有的bug时,需要在所有分支上面都修改一遍。
同时有可能因为分支代码的改动,而无法直接进行代码合并,进而导致重复工作量的增加。如果分支不多的时候倒还好,而当机型越来越多,分支越来越多,这个重复性的工作会消耗相当大的时间和精力。所以合并所有机型的代码,实现上位机代码在不同机型上的通用性势在必行。
一个良好的软件架构应当是 面向接口编程,而不是面向实现编程。对于不同机型中的业务和流程在主体上是一样的,只是其中某些细节存在差异性的分支,所以我们需要将业务代码提炼出主体流程和对应的接口,通过这些业务接口来实现机型差异所带来的的业务流程上的差异和分支,而非工程上的分支。
在软件运行时,可以通过相应的配置来决定业务接口的具体实现是哪一个,同时由于不同机型的接口实现是分离的,修改一个机型不会影响到另一个机型的代码。当然,在修改主流程代码时候就要小心了,需要考虑这一个改动对所有机型的影响。除非是非常有把握的情况下直接改动,否则还是先抽象出接口保证原主流程代码不变,只修改你需要修改的实现。
2.配置零散,没有统一的管理机制,不利于打包。
软件中的配置数据保存的地方太零散,有保存在数据库的,有保存在txt文件的,有保存在注册表的,有保存在app.config的。经过不同开发人员的不断迭代,积累了很多无用的配置数据,并且没有人敢删除。而售后有时候为了查找修改某个配置需要在各个地方查找,非常繁琐。
同时也因为公司机型太多导致很多配置数据在不同机型上存在差异性,这些差异性有可能是来自硬件差异,也有可能是来自软件功能上的差异。而每发布一个版本,就有可能需要同时打包多个机型的软件包,每一个软件包至少有1个G 的大小。而随着机型的越来越多,一个版本不同软件包也会越来越多,这对于打包人员来说是个不小的负担,同时也要求打包人员需要明确的知道每一个机型配置上的差异性来保证软件包的正确性。
为了解决这个问题,我们花了数个月的时间,对所有机型保存在不同位置的配置做了一个整理。我们整理出了所有机型通用的配置,统一保存在数据库表中,同时为每一个机型建立数据库表用来保存存在差异的配置数据。为了方便打包人员和售后管理查看这些数据库,我们开发了一个配置管理工具用来专门查看和修改配置。
另外,我们为了解决多个软件包的问题,把所有硬件相关的配置整合进一个文件中,并开发出一个版本升级软件。在这个版本升级软件中,售后可以选择机型对应的硬件,升级程序可以通过所选硬件对应的配置写入到数据库中来实现同一个软件包不同机型的升级工作。
同时我们开发了通用的http接口给上层C#程序和下层C++程序使用,用于读写数据库的配置数据。
由于我们的设备有多个PC,并且在医院内部无法连接外网,此前软件升级时每个PC都需要售后人员拷贝软件包并手动调用升级脚本来完成升级。而现在,我们的升级程序可以通过远程调用的方式来同时完成多个PC的升级工作,做到了一键升级功能。
基于第一点的软件代码合并,在本次配置优化之后,打包人员每次打包仅需要一个软件包,即可实现不同机型的一键升级,省时又省力。
3.UI和业务逻辑混杂
项目以WPF为主,整体使用MVVM框架。项目中没有使用开源的控件库,其中含有非常多的高度自定义控件的开发,这些控件的UI表现代码和业务逻辑代码夹杂在一起,耦合性太高非常不利于理解业务代码和后期维护。
WPF的MVVM框架本身最大的优势就是为了分离业务和UI,降低耦合性,提高可重用性,所以这个项目并没有发挥出MVVM 框架的优势。针对这个问题,我们分离UI和业务,提炼出一个单纯的UI库,这个UI库不包含任何的业务代码,除了.Net Framework的依赖库之外,不依赖项目中的任何其他库。
每一个UI控件暴露自定义的依赖属性,在业务逻辑调用时,通过绑定这些依赖属性来改变UI的表现逻辑。这样做的好处是完全分离了UI表现代码和业务逻辑代码,并且这个UI库具有高重用性,可以交给其他项目使用,甚至可以直接开源出来。
而且在后期维护时,UI代码的改动与业务代码的改动互不干扰,也有利于Bug的排查。
4.由于前期开发人员的层次不一,并且没有CodeReview机制来保证代码质量,导致代码存在很多低级错误。
项目中存在大量重复代码,明明可以提炼出一个简单方便的方法,偏偏要在各个地方不断的Copy相同的代码。
明明只要增加一个参数就可以合并成一个方法,偏偏要写上几十个方法,诸如xxxx1,xxxx2....xxxx30,三十几个方法执行同样的功能,只有一个参数上的差异。我们的业绩考核又不是看代码量,这种代码看了让人啼笑皆非。
没有统一的命名规则,有些属性首字母小写,C#和C++风格混用,有些命名直接使用缩写,类似“ggr”这样缩写,除了作者谁能看懂这是什么意思?
一个类,一个方法代码过多,几千行一个类的文件不在少数,一个类承担的功能也过多也过余复杂。类和方法都应该遵循职责单一原则,不过在实际开发过程中单一原则不太好把控,但应该合理的控制代码行数。
我们在项目重构工作之前,制定了统一的命名风格,并严格限制了每个类每个方法的代码行数。一个文件,一个类,最多不超过一千行,一个方法最多不超过六十行。
六十行代码一个屏幕很难放下,这个要求其实比较低了,最合理的应该是三四十行,一个屏幕正好可以看完一个方法的所有代码。
在项目重构的过程中,我们规定所有人提交的代码都必须提交给高级工程师CodeReview,高级工程师之间互相CodeReview。每个人的知识面都是有限的,但可以通过合作来达到无限的广度和深度,我们应该尽量去避免犯一些低级的、显而易见的错误。
5.软件需要显示处理大量的图片,导致程序内存和CPU占用过大。
由于我们的软件需要处理显示大量的Dicom文件,并且对这些展示的图片都有较为复杂的操作,诸如旋转,放大、像素提取、锐化等,这些功能都集成在一个ImageControl中。然而由于我们的ImageControl写的不合理,读取一张500K的Dicom并显示会至少占用2M的内存。如果同时读取上百个Dicom文件,程序内存可以轻轻松松突破1个G,同时由于我们系统中不止一个程序需要处理这些图片,所以对系统的内存要求非常高。
在前期,我们只能不断的累加硬件,把内存扩展到了32G,甚至是64G,然而这只是饮鸩止渴的错误方式,应该从根本上解决ImageControl控件占用内存过大的问题,同时应该优化程序显示排列图片的逻辑。
在考虑到项目人员的分配情况和项目计划,我们决定优化ImageControl这个思路暂缓,因为这个工作量会更大。我们决定先着手优化程序展示图片的逻辑。程序只加载用户能够看到的Dicom,当用户下拉进度条时,再陆续加载可见的图片,并且将其余不可见的ImageControl销毁,优化之后程序占用的内存锐减60%-70%,可以接受。