如果你认为这是一个标题党,那么真诚的恳请你耐心的把文章的第一部分读完,然后再下结论。如果你认为能够戳中G点,那么请随手点个赞。
AD:
如果你认为这是一个标题党,那么真诚的恳请你耐心的把文章的第一部分读完,然后再下结论。如果你认为能够戳中G点,那么请随手点个赞。
把三千行代码重构为15行
那年刚毕业,进了现在这个公司。公司是搞数据中心环境监控的,里面充斥着嵌入式、精密空调、总线、RFID的概念,一个都不懂。还好,公司之前用Delphi写的老客户端因为太慢,然后就搞了个Webform的替代,恰好对Asp.Net还算了解,对业务的不了解并不妨碍称成为这个公司的一个程序员。小公司也有小公司的好,人少,进去很快负责代码开发。当然也就搞这个数据中心智能管理系统啦。
这个系统非常的庞大,尤其牛逼的是支持客户端组态,然后动态生成网页,数据还能通过Socket实时监控(那时还真就不懂网络编程)。这个对于当时的来说,真真是高、大、上呐!!当时跟着了解整个系统大半个月才算能够调试,写一些简单的页面。
在维护系统的过程中,时不时要扩展一些功能,也就接触了下面这个类:
看到没有,就是当年最最流行的三层架构的产物,对于刚出茅庐的毛头小子来说,这是多么专业的文件头注释,还有反射也就算了,这构造函数还能静态的,还能私有的?那时刚接触这么高大上的代码的,瞬间给跪了!
但是,类写多了,就感觉越来越别扭,就是下面这段代码:
每增加一个表,除了要改接口、要改DAL、要改BLL之外,还得在这个工厂类添加一个方法,真真是累到手抽筋,即使有当时公司了的G工给推荐的神器 动软代码生成器,这粘贴复制的几遍,也是让感觉到异常繁琐,有时候打键盘稍微累了点,还把复制出来代码改错了,你妹的,难道这就是程序员该干的事情,不,绝对不是!想起了一句至理名言:当你觉得代码重复出现在程序中的时候,就应该重构了。是的,在这句话的指导下,开始了折腾,决定挑战这个高大上的代码,事实证明,思想的力量是无穷的。
那么,怎么修改呢,仔细观察之后,发现其中className的生成跟返回的类型非常类似,只是一个是类名,一个是字符串,这两者之间应该能够关联起来。于是google了一下(当时GFW还没猖獗起来哈),隐隐约约就找到了 反射 这两个字,深入了解之后,确定可以完成。
接下来,就是返回的类型了,返回的类型并不固定,但是似乎很有规律 这个似乎好像在哪里见过,对了,模板,C++课程上有讲过的,于是再次google,了解到了C#中使用了泛型代替了C++中的模板。在学习完泛型和反射之后,并参考了网上的一些文章,捣鼓出了下面的代码:
没错,就是它了,三层架构年代最流行的工厂类
看着原来滚十几屏幕的代码,变成了十多行的代码,真是爽到了骨子里去了,太干净了!唯一让担忧的是,进公司的时候,帮忙整理公司申请软件著作权都是需要代码量的,根据代码多少行来评估软件的大小,万一老板知道了非但没有帮公司增加代码量,还减少了,会不会立即把开掉?没敢给们老板展示优秀的成果,所幸,这段代码非但没有出过任何问题,还避免了以前同事老是在新增一个类之后,把代码复制过来,但是没有正确修改的问题,大大提高了效率。虽然,没敢大事宣布劳动成果,但是这次成功的修改,则彻底让走上了代码重构的不归路。
看到这里,大家应该知道这个案例是否真实的了吧。相信,从08年开始的码农们,看到这种类似的代码绝对不比少。那么,想告诉你们的是什么呢?
少用代码生成器
们来分析一下,为什么之前的前辈会写出上面的代码。归结起来有以下几点:
至今为止,还是很多人使用代码生成器,那么们应该怎么对待这个问题呢。认为,代码生成器确实可以减少你不少工作,但是少用,那些重复性的工作,除了部分确实是没有办法的,其他大部分都是可以通过框架解决的,举例来说,像三层架构,真正需要用到代码生成器的,也就是Model类而已,其他的完全可以在框架中完成。因此你要竭尽全力的思考怎么在框架中来减少重复性工作,而不是依赖于代码生成器。
另外,如果你还是在用相关的代码生成工具,请重新定义 动软代码生成器 的代码模板,自己写一个模板;或者使用CodeSmith来完全制定自己的代码生成,因为动软给的代码模板真心乱,比如下面这段代码:
for (int n = 0; n rowsCount; n++)
model = new DBAccess.Model.eventweek();
if(dt.Rows[n][ GroupNo ].ToString()!= )
model.GroupNo=int.Parse(dt.Rows[n][ GroupNo ].ToString());
if(dt.Rows[n][ Week0 ].ToString()!= )
model.Week0=int.Parse(dt.Rows[n][ Week0 ].ToString());
if(dt.Rows[n][ Week1 ].ToString()!= )
model.Week1=int.Parse(dt.Rows[n][ Week1 ].ToString());
}
首先,你就不能用 var row=dt.Rows[n] 替代吗?其次,直接用int.Parse效率多低?再次,dt.Rows[n][ Week0 ]为NULL怎么办?
们再来看看其他的一些代码:
public List string GetDevices(string dev){
List string devs=new List string
int start=0;
for(int i=0;i dev.Length;i++){
if(dev[i]=='^'){
devs.Add(dev.SubString(start,i));
start=i+1;
return devs;
}
有没有很眼熟,没错,这就是对String.Split()函数的简单实现。前辈应该是从c++程序员转过来的,习惯了各种功能自己实现一遍,但是他忽略了C#的很多东西。们不去评判这段代码的优劣,而实际上他在很长一段时间都运行得很好。们来看看使用这一段代码有什么不好的地方:
那么,们应该怎样去避免重复发明轮子呢?从个人的经历来提出以下几点,希望能够对各位有所帮助:
这里再举一个自己的例子。在现有的程序中,发现需要越来越多的线程来执行一些简单的任务,比如在每天检测一下硬盘是否达到90%了,每天9点要控制一下空调的开启而在网上6点的时候把空调关掉。线程使用越来越多,越是觉得浪费,因为这些现场仅仅只需完成一次或者有限的几次,大部分时间都是没有意义的,那么怎么办呢?决定自己写一个任务类,来完成相关的事情。说干就干,很快把这个类写出来了。
public abstract class MissionBase : IMission
private DateTime _nextExecuteTime;
protected virtual DateTime[] ExecuteTimePoints { get; private set; }
protected virtual int IntervalSeconds { get; private set; }
protected IEngine Engine { get; private set; }
public bool IsCanceled{get{ }}
public bool IsExecuting{get{ }}
public bool IsTimeToExecute{get{ }}
public abstract bool Enable { get; }
public abstract string Name { get; }
protected MissionBase(IEngine engine)
ExecuteTimePoints = null;//默认采用间隔的方式
IntervalSeconds = 60 * 60;//默认的间隔为1个小时
Engine = engine;
/// 任务的执行方法
public void Done()
if (Interlocked.CompareExchange(ref _isExecuting, 1, 0) == 1) return;
finally
Interlocked.CompareExchange(ref _isExecuting, 0, 1);
///实际方法的执行
protected abstract void DoneReal();
}
但是,实际上这个任务方法,并不好用,要写的代码不少,而且可靠性还没有保障。当然,可以继续完善这个类,但是决定搜索一下是否还有其他的方法。直到有一天,再次阅读《CLR Via C#》,看到线程这一章,讲到了System.Threading.Timer以及ThreadPool类时,就知道了,使用Timer类完全可以解决这个用尽量少的线程完成定时任务的问题。
因为从原理上来说,Timer类无论你声明了多少个,其实就只有一个线程在执行。当你到了执行时间时,这个管理线程会用ThreadPool来执行Timer中的函数,因为使用的ThreadPool,执行完成之后,线程就马上回收了,这个其实就完全实现了所需要的功能。
等你无法重构的时候再考虑重写
带过很多优秀的程序员,也与很多优秀的程序员共事过。有一大部分的程序员在看到一套系统不是那么满意,或者存在某些明显的问题,就总是忍不住要把整套系统按自己觉得可以优化的方向来重写,结果,重写结构往往并不令人满意。系统中确实存在很多不合理的地方,但是有不少的这种代码,恰恰是为了解决一些特定场景下的问题的。也就是说,所有的规范以及编程的原则,其实也是有条件限制的,他可能在大部分的时候是正确的,能够指导你完成任务,但是,并不是在所有地方都是适用的。比如数据库范式,但实际中们的设计往往会考虑冗余,这是违背范式的,但是为什么还有那么多人趋之若鹜呢?因为们可能需要用空间换时间。
如果们一开始就考虑重写,那么你可能会陷入以下的困境:
举个例子,说明如何通过重构更好的利用现有代码的。
有一个非常庞大的系统,其中有一块功能是用于数据采集、存储、告警管理以及电话、短信等告警通知。大致的结构如下:
class MainEngine:IEngine{
public MainEngine(ConfigSettings config){
public void Start();
public void Stop();
}
需要增加新的业务功能时,程序员写的代码往往是这样的:首先时修改配置类
class ConfigSettings{
public bool NewFuncEnable{get;private set;}
public ConfigSettings(){
NewFuncEnable=xx;//从配置文件读取
}
接着修改主程序:
class MainEngine:IEngine{
private NewFuncClass newCls=new NewFuncClass();
public MainEngine(ConfigSettings config){
public void Start(){
if(config.NewFuncEnable)
newCls.Start();
public void Stop(){
if(config.NewFuncEnable)
newCls.Stop();
}
在修改的过程中,往往是根据配置文件来判断新功能是否启用。上面代码会造成什么问题呢:
那么们如何对这段代码进行重构呢。首先,们把新功能注册的代码抽取出来,通过反射来实现新的功能的注册。
private void RegisterTaskHandlerBundles()
var bundles = xxx.BLL.Caches.ServiceBundleCache.Instance.GetBundles( TaskHandlerBundle
if (bundles != null bundles.Count 0)
var asmCache = new Dictionary string, Assembly
foreach (var bundle in bundles)
if (!asmCache.ContainsKey(bundle.Category)) asmCache.Add(bundle.Category, Assembly.Load(bundle.AssemblyName));
var handler = (ITaskHandler)asmCache[bundle.Category].CreateInstance(bundle.ClassName, false, BindingFlags.Default, null,
new object[] { this, bundle }, null, null);
_taskHandlerBundles.Add(bundle, handler);
catch (Exception e)
NLogHelper.Instance.Error( 加载bundle[Name:{0},Assembly:{1}:Class:{2}]异常:{3} , bundle.Name, bundle.AssemblyName, bundle.ClassName, e.Message);
}
修改MainEngine代码
class MainEngine:IEngine{
private NewFuncClass newCls=new NewFuncClass();
public MainEngine(ConfigSettings config){
RegisterTaskHandlerBundles();
public void Start(){
_taskHandlerBundles.Start();
public void Stop(){
_taskHandlerBundles.Stop();
}
OK,现在们再来看看怎么实现原来的新增功能:你只需按规范新建一个类,继承ITaskHandler接口,并实现接口的方法。最后在XTGL_ServiceBundle表中新增一条记录即可。们再来看看这么做有什么好处:
重构的目标之一,就是把框架和业务完全分离。
有志于深入了解的同学,可以了解下反射、Ioc和插件话编程等。
学会单元测试,培养重构意识
可能上面说了这么多,还是有很多人并不理解重构。没关系,在这里教你们一个快速入门的办法,就是单元测试。什么是单元测试,请自行google。单元测试有什么要求?就是要求你要把每个方法都弄成尽量可以测试的。尽量让方法变成是可测试的,就是培养你重构意识的利器。在你要求把方法变成可测试的过程,你就会发现你必须得不断的修改方法,让它的职责尽量单一,让它尽量的与上下文无关,让它尽可能通过方法参数的输入输出就能完成相关的功能,让依赖的类都尽量改为接口而不是实例。最终,你就会发觉,这就是重构!而且是在不知不觉中,你重构的功力就会大大提升,你编程的水平也会大大提升!
看到这里,有经验的程序员就会问,你这是在鼓励使用TDD吗?不,不是的。TDD(Test-Driven Development)鼓励的是测试驱动开发,未开发之前先编写单元测试用例代码,测试代码确定需要编写什么产品代码。这是一种比较先进的开发方法,但是在编程的实践过程中,认为它过于繁琐,很多中小企业很难实施,更别提们个人开发者。这里提倡你用单元测试培养重构意识,可以说是一种后驱动,用于提高重构能力和重构愿望,你完全可以把这个方法称为 TDR(Test-Driven Refactoring) 测试驱动重构 。当然,在开发之前如果你有意识的让方法可测试,那么你写出来的函数将会是比较高质量的代码。当函数都是一个个可重用性高的函数之时,你将会发现,写代码其实就像堆积木一样,可以把一个大型的需求分解成无数细小的功能,很快的把需求实现。
以下是一个超大方法中的一段代码,如果你懂得怎样让这段代码编程一个可测试的方法,那么,恭喜你,你入门了。
所谓重构
如果你有耐心看到这里,你应该知道,并非一个标题党,而这篇文章也许称为 如何在编程中应用重构的思想 更为贴切,但是不想用这么严肃的标题。
很多编程初学者,或者有多年编程经验的人都觉得阅读别人的代码非常困难,重构更是无从谈起,他们要么对这些代码望洋兴叹,要么就是推翻从来。但是,如果们有重构的意识,以及在编程的过程中熟悉一些代码调整和优化的小技巧,你自然而然就会培养出重构的能力。
重构,其实很简单:
如果你坚持这么去做了,一段时间之后感觉自然就出来了。
重构的目的,是让代码更为精简、稳定、能够重用,是最大程度的让功能和业务分离。在重构的过程中,阅读代码的能力、写出优秀代码的能力以及系统架构能力都会稳步提升。你成为一个优秀的程序员将指日可待。