Linux.中国 - 开源社区

 找回密码
 骑士注册

QQ登录

微博登录


从把三千行代码重构成15行代码谈起

2014-12-8 10:10    评论: 33 收藏: 4 分享: 37    

等你无法重构的时候再考虑重写

我带过很多优秀的程序员,也与很多优秀的程序员共事过。有一大部分的程序员在看到一套系统不是那么满意,或者存在某些明显的问题,就总是忍不住要把整套系统按自己觉得可以优化的方向来重写,结果,重写结构往往并不令人满意。系统中确实存在很多不合理的地方,但是有不少的这种代码,恰恰是为了解决一些特定场景下的问题的。也就是说,所有的规范以及编程的原则,其实也是有条件限制的,他可能在大部分的时候是正确的,能够指导你完成你的任务,但是,并不是在所有地方都是适用的。比如数据库范式,但实际中我们的设计往往会考虑冗余,这是违背范式的,但是为什么还有那么多人趋之若鹜呢?因为我们可能需要用空间换时间。

如果我们一开始就考虑重写,那么你可能会陷入以下的困境:

  • 需要花更大的精力来完成一些看似简单的BUG
    你要知道,有一部分看似错误或者非常不优美的代码,其实恰恰是为了解决一些非常刁钻的问题的。
  • 再也无法兼容老的系统了
    你急于把原有系统重写,却往往忽略了对原有系统的兼容,那么你新的系统的推进则会十分缓慢。而老系统的维护,又会陷入及其尴尬的情况。
  • 过度设计,导致重写计划迟迟无法完成
    有重写冲动的程序员往往是在架构设计上有一些读到的见解,他们善于利用所学的各种设计模式和架构技巧来建立系统,但是越是想尽可能的利用设计模式,越是陷入过度设计的困局,导致重写的计划迟迟都无法完成。
  • 无法有效利用现有系统已经完成并测试的代码
    如果你确实有必要进行重写,我还是建议你把代码尽可能的重构。因为重构之后的系统,能够让你更轻易的重写,又最大限度了保留以前可用的业务代码

我举个例子,说明如何通过重构更好的利用现有代码的。

我有一个非常庞大的系统,其中有一块功能是用于数据采集、存储、告警管理以及电话、短信等告警通知。大致的结构如下:

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();
	}
}

在修改的过程中,往往是根据配置文件来判断新功能是否启用。上面代码会造成什么问题呢:

  • 主程序代码和扩展功能耦合性太强,每增加一个功能都要修改主程序代码,这里非常非常容易出错。尤其是新的人进度开发组,很容易就忘主程序中增加了一些致命性的代码。比如上述的扩展功能,可能是在特定的项目中才会有这个扩展功能,但是,写代码的人忘记增加是否启用的配置选项了,导致所有的项目都应用了这个功能,而这个功能需要特定的表,这样就悲剧了。即使是你增加了配置,也是非常的不美观,因为在通用的版本中使用了这个配置,往往会让定制项目以外的人员感到困惑
  • 增加扩展功能的人还需对整个MainEngine代码有一定的熟悉,否则,他根本就不知道在Start方法和Stop方法进行newClas的对应方法的调用
  • 如果你打算对这段代码进行重写,那么,你会感到非常的困难,因为你分不清楚newCls这个新实例的作用,要么你花大精力去把所有代码理清楚,要么直接就把这段新增的业务代码去掉了。

那么我们如何对这段代码进行重构呢。首先,我们把新功能注册的代码抽取出来,通过反射来实现新的功能的注册。

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)
            {
                try
                {
                    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表中新增一条记录即可。我们再来看看这么做有什么好处:

  • 新增的类只需按规范写即可,完全对MainEngine代码没有任何影响。你甚至可以把这个MainEngine代码写在一个新建的Dll中。
  • 新增功能的这个业务类跟原来的代码解耦,非常方便进行新功能的业务测试,而无需考虑原有框架的影响
  • 新增功能的业务类与架构完全分离,我们在重写代码中只要保证接口的稳定性,无论我们怎么把系统架构重写,我们可以马上就重用上原有的业务功能代码。

重构的目标之一,就是把框架和业务完全分离

有志于深入了解的同学,可以了解下反射、Ioc和插件话编程等。

学会单元测试,培养你的重构意识

可能上面说了这么多,还是有很多人并不理解重构。没关系,在这里我教你们一个快速入门的办法,就是单元测试。什么是单元测试,请自行google。单元测试有什么要求?就是要求你要把每个方法都弄成尽量可以测试的。尽量让你的方法变成是可测试的,就是培养你重构意识的利器。在你要求把方法变成可测试的过程,你就会发现你必须得不断的修改你的方法,让它的职责尽量单一,让它尽量的与上下文无关,让它尽可能通过方法参数的输入输出就能完成相关的功能,让依赖的类都尽量改为接口而不是实例。最终,你就会发觉,这就是重构!而且是在不知不觉中,你重构的功力就会大大提升,你编程的水平也会大大提升!

看到这里,有经验的程序员就会问,你这是在鼓励我使用TDD吗?不,不是的。TDD(Test-Driven Development)鼓励的是测试驱动开发,未开发之前先编写单元测试用例代码,测试代码确定需要编写什么产品代码。这是一种比较先进的开发方法,但是在编程的实践过程中,我认为它过于繁琐,很多中小企业很难实施,更别提我们个人开发者。我这里提倡你用单元测试培养你的重构意识,可以说是一种后驱动,用于提高你的重构能力和重构愿望,你完全可以把我的这个方法称为“TDR(Test-Driven Refactoring)——测试驱动重构”。当然,在开发之前如果你有意识的让方法可测试,那么你写出来的函数将会是比较高质量的代码。当你的函数都是一个个可重用性高的函数之时,你将会发现,写代码其实就像堆积木一样,可以把一个大型的需求分解成无数细小的功能,很快的把需求实现。

以下是一个超大方法中的一段代码,如果你懂得怎样让这段代码编程一个可测试的方法,那么,恭喜你,你入门了。

所谓重构

如果你有耐心看到这里,你应该知道,我并非一个标题党,而这篇文章也许称为“如何在编程中应用重构的思想”更为贴切,但是我不想用这么严肃的标题。

很多编程初学者,或者有多年编程经验的人都觉得阅读别人的代码非常困难,重构更是无从谈起,他们要么对这些代码望洋兴叹,要么就是推翻从来。但是,如果我们有重构的意识,以及在编程的过程中熟悉一些代码调整和优化的小技巧,你自然而然就会培养出重构的能力。

重构,其实很简单:

  • 把基础打牢固
  • 多看点优秀的代码
  • 避免复制粘贴,如果看见重复代码时应该有意识要消灭它
  • 减少对代码生成器的依赖
  • 在处理现有代码时尽量用重构代替重写,在重写之前一定要先重构
  • 尽量让所有的方法都是可测试的

如果你坚持这么去做了,一段时间之后感觉自然就出来了。

重构的目的,是让你的代码更为精简、稳定、能够重用,是最大程度的让功能和业务分离。在重构的过程中,你的阅读代码的能力、写出优秀代码的能力以及系统架构能力都会稳步提升。你成为一个优秀的程序员将指日可待。

12
查看其它分页:

发表评论


最新评论

我也要发表评论

qq229614108 2014-12-15 15:19
高大上
回复
来自 - 江苏南京 的 Safari/Linux 用户 2014-12-10 00:39
当年的神器,不搞.net好多年了
回复
理工动物园副园长 2014-12-8 13:33  新浪微博网友评论
//@Linux中国:→_→//@Viz_V:Design pattern 学好没有错,重构糟糕的代码也没有错,但这么在国内做就是错的。为什么呢?很多国内码农的工资与代码量挂钩,印度程序员还知道loop里的代码复制粘贴十遍呢~有的时候不是不会写好程序,而是……(此处省略1000字)
回复
韦恩卑鄙 2014-12-8 13:33  新浪微博网友评论
反射的.ctor都不做成delegate 缓存 不是新手难道是老鸟。有IoC容器不用也挺愁的。//@thomas--y: 这特么是新手文章//@Linux中国:但是这篇文章的思路其实是更值得学习的;再次这篇我觉得也不是新手级别 //@韦恩卑鄙: linux 中国转博客园的.net 新手文章合适嘛?
回复
飞-翔-鸟521 2014-12-8 13:33  新浪微博网友评论
mark
回复
我是铁人007 2014-12-8 13:33  新浪微博网友评论
//@Linux中国:首先我觉得不应该有狭隘的门户之见,非Linux的技术也应该传播,只是我们偏重 Linux;其次即便不用.NET,但是这篇文章的思路其实是更值得学习的;再次这篇我觉得也不是新手级别吧;最后转载也明确注明了来源和作者,谢谢作者@马非码。
回复
绿色圣光 2014-12-8 13:21
还没干过这么夸张的,最多把几百行精简成几十行。
7 回复
thomas--y 2014-12-8 13:03  新浪微博网友评论
回复
mdemo 2014-12-8 13:03  新浪微博网友评论
[哈哈]//@赌场游侠:动软代码生成器!想起大二时候做项目用的时候,还有些小激动呢!满满的回忆啊!@mdemo
回复
大栗哥 2014-12-8 13:03  新浪微博网友评论
收下看//@Linux中国:首先我觉得不应该有狭隘的门户之见,非Linux的技术也应该传播,只是我们偏重 Linux;其次即便不用.NET,但是这篇文章的思路其实是更值得学习的;再次这篇我觉得也不是新手级别吧;最后转载也明确注明了来源和作者,谢谢作者@马非码。您看呢? //@韦恩卑鄙: linux 中国转博客园的.
回复
LiuWei_Find 2014-12-8 13:03  新浪微博网友评论
→_→//@Linux中国: →_→//@Viz_V:Design pattern 学好没有错,重构糟糕的代码也没有错,但这么在国内做就是错的。为什么呢?很多国内码农的工资与代码量挂钩,印度程序员还知道loop里的代码复制粘贴十遍呢~有的时候不是不会写好程序,而是……(此处省略1000字)
回复
yueliangku 2014-12-8 12:33  新浪微博网友评论
快去看看你家的sqlsrv又死机了没[拜拜]//@Linux中国: (删)其次即便不用.NET,但是这篇文章的思路其实是更值得学习的;再次这篇我觉得也不是新手级别吧;(删) //@韦恩卑鄙: linux 中国转博客园的.net 新手文章合适嘛?
回复
郑淳猫奴日记 2014-12-8 12:33  新浪微博网友评论
//@Linux中国:→_→//@Viz_V:Design pattern 学好没有错,重构糟糕的代码也没有错,但这么在国内做就是错的。为什么呢?很多国内码农的工资与代码量挂钩,印度程序员还知道loop里的代码复制粘贴十遍呢~有的时候不是不会写好程序,而是……(此处省略1000字)
回复
赌场游侠 2014-12-8 12:33  新浪微博网友评论
动软代码生成器!想起大二时候做项目用的时候,还有些小激动呢!满满的回忆啊!@mdemo
回复
韦恩卑鄙 2014-12-8 12:33  新浪微博网友评论
右边先把自己性别搞清楚行么//@_小噗: 为什么软狗随时随地都能gc呢?快去看看你家的sqlsrv又死机了没[拜拜]//@Linux中国: (删)其次即便不用.NET,但是这篇文章的思路其实是更值得学习的;再次这篇我觉得也不是新手级别吧;(删) //@韦恩卑鄙: linux 中国转博客园的.net 新手文章合适嘛?
回复
韦恩卑鄙 2014-12-8 12:33  新浪微博网友评论
我觉得你的订户不觉得犯 我就没问题。是不是新手文吗.net专业的都知道 您做linux的小编就别嘴硬啦。//@Linux中国: 首先我觉得不应该有狭隘的门户之见,非Linux的技术也应该传播,只是我们偏重 Linux;其次即便不用.NET,但是这篇文章的思路其实是更值得学习的;再次这篇我觉得也不是新手级别吧;
回复
ZengYi 2014-12-8 12:06
构想主题都很好,但是用C#做为例子不太好。
回复
uptown1919 2014-12-8 12:03  新浪微博网友评论
还有和代码量挂钩的..如果公司懂的话一般不会看代码量 不懂的代码完全都不会看//@Linux中国:→_→//@Viz_V:Design pattern 学好没有错,重构糟糕的代码也没有错,但这么在国内做就是错的。为什么呢?很多国内码农的工资与代码量挂钩,印度程序员还
回复
_小噗 2014-12-8 12:03  新浪微博网友评论
为什么软狗随时随地都能gc呢?快去看看你家的sqlsrv又死机了没[拜拜]//@Linux中国: (删)其次即便不用.NET,但是这篇文章的思路其实是更值得学习的;再次这篇我觉得也不是新手级别吧;(删) //@韦恩卑鄙: linux 中国转博客园的.net 新手文章合适嘛?
回复
果壳中的米特尼克 2014-12-8 12:03  新浪微博网友评论
还有缺陷密度,项目总缺陷数目,甚至Lint,klocwork的分析。甚至关注一下UT,ST?可惜可执行性呢?另外不是所有程序员都会为新feature努力,可惜没人看的到
回复
12下一页

热点评论

绿色圣光 2014-12-8 13:21
还没干过这么夸张的,最多把几百行精简成几十行。
7
返回顶部

分享到微信朋友圈

打开微信,点击底部的“发现”,
使用“扫一扫”将网页分享至朋友圈。