这一顿神操做!从把3000行代码重构成15行代码谈起!

若是你认为这是一个标题党,那么我真诚的恳请你耐心的把文章的第一部分读完,而后再下结论。若是你认为可以戳中您的 G 点,那么请随手点个赞。c++

把三千行代码重构为 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]得改一遍?

不要重复发明轮子

咱们再来看看其余的一些代码:

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#的不少东西。咱们不去评判这段代码的优劣,而实际上他在很长一段时间都运行得很好。咱们来看看使用这一段代码有什么很差的地方:

  • 重复发明轮子。花费了额外的时间,函数的健壮性和不好
  • 可读性差。实际上是一个很简单的功能,可是用上了这么一段函数,起初我还觉得有什么特别的功能。

那么,咱们应该怎样去避免重复发明轮子呢?我从我的的经从来提出如下几点,但愿可以对各位有所帮助:

  • 了解你所学的编程语言的特性。你能够看一本基础的入门书籍,把全部的特性浏览一遍,或者上MSDN,把相关的内容过一遍。
  • 在你决定动手发明一个轮子以前,先搜索一下现成的解决方案。你还能够到CodeProject、GitHub之类的网站搜索一下。在知乎上有不少人都在批评这么一种现象,总是问一些重复性的问题,而后又职责知乎没落了,没有人回答他的问题,实际上相关问题已经有了很详细的解答,那提问以前,不能首先去搜一下是否有现成的答案,反而指责没有回答他的问题呢?
  • 你有必定的基础以后,还应该去读一下相关的经典书籍,深刻了解其中的原理。好比,你以为你有必定的基础了,我建议你去把《CLR Via 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;

        try
        {
            ……
        }
        finally
        {
            Interlocked.CompareExchange(ref _isExecuting, 0, 1);
        }
    }

    ///实际方法的执行
    protected abstract void DoneReal();
}

可是,实际上这个任务方法,并很差用,要写的代码很多,并且可靠性尚未保障。固然,我能够继续完善这个类,可是我决定搜索一下是否还有其余的方法。直到有一天,我再次阅读《CLR Via C#》,看到线程这一章,讲到了System.Threading.Timer以及ThreadPool类时,我就知道了,使用Timer类彻底能够解决个人这个用尽可能少的线程完成定时任务的问题

由于从原理上来讲,Timer类不管你声明了多少个,其实就只有一个线程在执行。当你到了执行时间时,这个管理线程会用ThreadPool来执行Timer中的函数,由于使用的ThreadPool,执行完成以后,线程就立刻回收了,这个其实就彻底实现了我所须要的功能。

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

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

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

  • 须要花更大的精力来完成一些看似简单的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)——测试驱动重构”。固然,在开发以前若是你有意识的让方法可测试,那么你写出来的函数将会是比较高质量的代码。当你的函数都是一个个可重用性高的函数之时,你将会发现,写代码其实就像堆积木同样,能够把一个大型的需求分解成无数细小的功能,很快的把需求实现。

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

所谓重构

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

不少编程初学者,或者有多年编程经验的人都以为阅读别人的代码很是困难,重构更是无从谈起,他们要么对这些代码望洋兴叹,要么就是推翻历来。可是,若是咱们有重构的意识,以及在编程的过程当中熟悉一些代码调整和优化的小技巧,你天然而然就会培养出重构的能力。
重构,其实很简单:

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

若是你坚持这么去作了,一段时间以后感受天然就出来了。

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

在这分享一份整理了2个月的Android进阶面试解析笔记文档,包括了知识点笔记和高频面试问题解析及部分知识点视频讲解给你们!为了避免影响阅读,在这以图片展现部份内容于目录截图,有须要的朋友麻烦点赞后点击下面在线连接获取免费领取方式吧!
阿里P6P7【安卓】进阶资料分享+加薪跳槽必备面试题
相关文章
相关标签/搜索