为您推荐:
精华内容
最热下载
问答
  • 5星
    4KB weixin_42696271 2021-09-11 16:43:22
  • 5星
    10KB weixin_42696333 2021-09-10 17:44:54
  • 5星
    19.58MB Yao__Shun__Yu 2021-05-31 15:44:57
  • 5星
    7.3MB qq_17695025 2021-02-18 19:50:32
  • 5星
    1.36MB qq_17695025 2021-07-13 21:23:37
  • 5星
    39KB weixin_45317919 2021-06-04 11:27:28
  • 3.97MB qq1235656 2019-05-28 14:54:19
  • 5星
    3.55MB astrophysics 2015-06-05 11:14:45
  • 5星
    3.6MB astrophysics 2015-08-14 10:02:17
  • 软件设计与重构的六个原则 再读《重构》和《架构整洁之道》 目录 开放封闭原则 --- OCP 依赖倒置原则 --- DIP 单一职责原则 --- SRP Liskov替换原则 --- LSP 接口隔离原则 -- ISP 迪米特法则(最小知识原则) -- ...

    软件设计与重构的六个原则

    再读《重构》和《架构整洁之道》

    目录
    开放封闭原则 --- OCP
    依赖倒置原则 --- DIP
    单一职责原则 --- SRP
    Liskov替换原则 --- LSP
    接口隔离原则 -- ISP
    迪米特法则(最小知识原则) -- LOD

    一、开放封闭原则 --- OCP

    软件中的基础结构(函数、类或模块)对于功能扩展是开放的,但是对于修改是封闭的。

    可实施的具体行为

    面向接口编程,不要面向实现编程
    依赖倒置原则
    Liskov替换原则

    eg.

    //Method One
    int DoSomeFunction(code,  param1, param2)
    {
        int rtnCode = ...
        switch(code)
        {
        case CODE_A:
        {
            code block A
            break;
        }
        case CODE_B:
        {
             code block B
            break;
        }
        ....
        default:
            rtnCode = not support error code
    
        return rtnCode;
    }
    
    //Method Two
    int ProcessForCodeA(param1, param2)
    {
        code block A
    }
    
    int ProcessForCodeB(param1, param2)
    {
        code block B
    }
    
    struct 
    {
        int code;
        int (*Processor)(param1, param2);
    }ITEM;
    
    //Table-Driven Methods
    static ITEM processItems[] = 
    {
        {CODE_A, ProcessForCodeA},
        {CODE_B, ProcessForCodeB},
         ......
    };
    
    int DoSomeFunction(code,  param1, param2)
    {
        for_each(item in processItems)
        {
            if(item.code == code)
            {
                 return item.Processor(param1, param2);
            }
        }
    
        return not supoort error code;
    }
    
    

     

     

     

    #define  MAX_PROCESS_ITEMS    32
    
    ITEM processItems[MAX_PROCESS_ITEMS] = { 0 };
    
    int DoSomeFunction(code,  param1, param2)
    {
        ......
    }
    
    bool RegisterProcessor(int code, Processor func)
    {
        add {code, func} to processItems[]
    }
    
    void DeregisterProcessor(int code)
    {
        remove code process item from processItems[]
    }
    
    

     

     

    可实施的设计模式:

    Strategy 策略模式
    Template Method 模板方法模式
    Visitor 访问者模式

    开放封闭原则 --- Strategy 模式

    意图:定义一些列算法,把它们一个个封装起来,并且使它们可以互相替换。本模式使得算法可独立于使用它的客户而变化

    适用性:

    • 许多相关的类仅仅是行为有差异,这个模式提供一种用多个行为中的一个来配置一个类的方法
    • 需要使用一个算法的不同变体
    • 算法使用客户不应该知道的数据,使用策略模式可以避免暴露复杂的、与算法相关的数据结构
    • 一个类定义了多种行为,并且这些行为在这个类的操作中以多个条件语句的形式出现。将相关的条件分支移入它们各自的Strategy类中以代替这些条件语句。

    开放封闭原则 --- Template Method 模式

    意图:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。本模式使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。

    适用性:

    • 一次性定义一个算法的不变部分,并将可变的行为留给子类来实现
    • 各子类中公共的行为应被提取出来并集中到一个公共父类中以避免代码重复 控制子类扩展。
    • 模板方法只在特定的点调用“Hook”操作,遮掩个旧只允许在这些点进行扩展。

    开放封闭原则 --- Visitor 模式

    意图:表示一个作用于某对象结构中的各个元素的操作。本模式使得你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。

    适用性:

    • 一个对象结构包含很多类对象,它们有不同的接口,而你想对这些对象实施一些依赖与其具体类的操作。
    • 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而你想避免让这些操作“污染”这些对象的类。Visitor使得你可以将相关的操作集中起来定义在一个类中。
    • 定义对象结构的类很少改变,但经常需要在此结构上定义新的操作。改变对象结构类需要重定义对所有访问者的接口,这可能需要很大的代价。如果对象结构类经常改变,那么可能还是在这些类中定义这些操作比较好。
    class Client
    {
        Element *element;
    
        void SomeOperation()
        {
            生成一个ContreteVisitor1的实例 v
            element->Accept(&v);
            v.ShowSomething(); //用ContreteVisitor1定义的方式输出结果
        }
        void AnotherOperation()
        {
            生成一个ContreteVisitor2的实例 v
            element->Accept(&v);
            v.ShowSomething(); //用ContreteVisitor2定义的方式输出结果
        }
    };
    

     

    二、依赖倒置原则 --- DIP

    传统的层次化设计模型,上层和下层业务分离,上层依赖下层提供的功能,下层不能反向依赖上层。
    依赖倒置不是简单的依赖方向翻转,它的核心仍然是抽象接口。
    1、高层模块不应该依赖于底层模块(二者都应该依赖于抽象)
    2、抽象不应该依赖于实现,实现应该依赖于抽象

        

    可实施的具体行为:

    关键是抽象,面向接口编程
    Liskov替换原则

    依赖倒置原则 --- ATM提款机

    取款业务逻辑流程是稳定的
    提款机的实现是变化的

        

    A:  bool  Withdraw(账户验证参数,金额)
    B: int GeiQian(其他参数,账户验证参数,金额)

    将取款业务流程中对提款机的操作接口抽象出来,定义一组提款的逻辑接口。
    抽象接口的提出,解除了取款业务和提款机的耦合关系,在满足Liskov替换原则的基础上,替换不同厂家的提款机变的非常容易。

    class Teller
    {
    public:
       ...
        bool IsConnected(环境参数) = 0;
        bool GetBalance(位置信息, 余额信息) = 0;
        bool Pay(上下文信息,支付金额) = 0;
        ...
    };
    
    //具体不同厂家的提款机,就是这个抽象接口的实现者
    class IntelTeller : public Teller
    {
        IntelTeller(TellerMgmt& tm)
        {    tm.Register(this); }
        ...
        bool IsConnected(环境参数)
        {    Intel 的实现   }
        bool GetBalance(位置信息, 余额信息)
        { Intel 的实现    }
        bool Pay(上下文信息,支付金额)
        { Intel 的实现    }
        ...
    };
    class MoftTeller : public Teller
    {
        ...
        bool IsConnected(环境参数)
        { Moft 的实现 }
        bool GetBalance(位置信息, 余额信息)
        { Moft 的实现 }
        bool Pay(上下文信息,支付金额)
        { Moft 的实现 }
        ...
    };
    
    //提供注册接口,由能提供者注册自己提供的功能
    class TellerMgmt
    {
        void Register(Teller *teller) 
        { ...... }
        Teller *GetTeller(...) 
        { ...... }
    protected:
        //管理注册的Teller们;
    }
    TellerMgmt& GetTellerMgmtObj()
    {
        static TellerMgmt tm;
    
        if(tm 没有初始化)
        {
            对tm初始化,并设置初始化标志
        }
    
        return tm;  
    }
    
    //取款业务模块
    Teller *teller = tm.GetTeller();
    
    if(teller->IsConnected(...))
    {
        ......
    }
    

    依赖倒置原则 --- Singleton单实例模式

    意图:
    保证一个类仅有一个实例,并提供一个访问它的全局访问点。

    适用性:
    当类只有一个实例,并且客户可以从一个众所周知的访问点访问它时

    重要:
    不要把单实例模式当全局变量用

    三、单一职责原则 --- SRP

    描述:对一个类而言,应该只有一个引起它变化原因。
    现在的描述:任何一个软件模块都应该只对某一类行为者负责

    • 降低代码复杂度,一个类只负责一项职责,逻辑也简单
    • 提高代码可读性
    • 当发生变化的时候,能减少变化影响的范围,并且受影响的类的变化情况更好预知

        

       

    ConnectionMgmt::AddModem(Connection *modem)
    {
        Add modem to modems list
    }
    
    ConnectionMgmt::Connect(...user param...)
    {
        for_each(modem in modems list)
        {
            modem->Dial(...);
        }
    }
    
    ModemImplemention modem1;
    ModemImplemention modem2;
    
    ......
    
    ConnectionMgmt cm;
    cm.AddModem(&modem1);
    cm.AddModem(&modem2);
    ......
    cm.Connect(...);
    

    可实施的具体行为:

    高内聚原则
    分离变化的部分和不变的部分
    TDD 测试驱动开发
    最小知识原则 LOD

    可实施的设计模式:

    Facade 外观模式
    Proxy 代理模式
    Adapter 适配器模式

    单一职责原则 --- Facade 模式

    意图:为子系统中的一组接口提供一个一致的界面。本模式定义了一个高层接口,这个接口使得这一子系统更加容易使用

    单一职责原则 --- Proxy 模式

    意图:为其他对象提供一种代理,以控制对这个对象的访问。本模式定义了一个代理对象,通过代理对象屏蔽原对象的一些接口

    单一职责原则 --- Adapter 模式

    意图:将一个类的接口转换成客户希望的另一个接口。本模式使得原本由于接口不兼容而不能在一起工作的那些类可以在一起工作

    AdapterA::OperationA(...)
    {
        ...
        opl->Function1(...);
    }
    
    AdapterB::OperationB(...)
    {
        ...
        opl->Function3(...);
    }
    

    四、Liskov替换原则 --- LSP

    描述:子类型(subtype)必需能够替换掉它们的基类型

    OO背后的主要机制是抽象和多态,在C++和Java这样的静态语言中,支持抽象和多态的关键机制之一就是继承。如果继承体系中某个类的实现不满足LSP原则,那么这个体系就会变的很脆弱,失去健壮性。

    违反LSP原则,将导致对OCP原则的违反。

    class A 
    {
    }
    
    class B : public A
    {
        bool Operation();
    };
    
    class C : public A
    {
        bool Operation();
    };
    
    void TestFunc(const A& ta)
    {
        if(ta is a B)
        {
            static_cast<const B&>(ta).Operation();
        }
        else if(ta is a C)
        {
            static_cast<const C&>(ta).Operation();
        }
        else if(...)
        {
        ...
        }
        ...
    }
    //new design
    class A 
    {
        virtual bool Operation();
    };
    
    void TestFunc(const A& ta)
    {
        ...
        ta.Operation();
        ...
    }
    /*
    正是子类型的可替换性才使得使用基类类型的模块在无需修改的情况下就可以扩展。
    比如增加一个新类D: 
    */
    class D : public A 
    {     
        bool Operation(); 
    }; 
    //使用基类类型的函数TestFunc()不需要做任何修改就可以支持D: 
    D d; 
    TestFunc(d)

     

    类的继承体系设计要遵循 IS-A 原则,并且这个 IS-A 是关于行为的,不是关于数据的。  
    IS-A 原则只能作为子类型定义的含义过于宽容,应该将子类型的“可替换性”作为子类型定义的必要条件。

    潜在的违反LSP的情况:
    没有 IS-A 关系的继承
    派生类中退化了某个函数
    还有一些语言层面上的错误,会导致违反LSP原则,比如某个子类的Operation()内部抛出了异常(而不是按照约定返回错误值),这会使得TestFunc的行为发生不可控的变化。

    五、接口隔离原则 --- ISP

    描述:不应该强迫客户依赖于它们不用的方法。换句话说,一个类对另一个类的依赖应该是建立在最小的接口范围上的。

    强迫客户依赖它们不使用的方法,那么客户就要面临着这些未使用的方法的改变所带来的变更,无形中增加了不必要的耦合关系,潜在地违反SRP原则。

    可实施的具体行为:
    通过拆分职责分离接口
    使用委托分离接口
    分离变化的部分和不变的部分
    使用多继承分离接口

    使用委托分离接口

    class TimedDoor: public Door
    {
        void DoorTimeOut(...)
    }
    
    class TimerClient
    {
        virtual void TimeOut() = 0;
    }
    
    class DoorTimedAdapter: public TimerClient
    {
        DoorTimedAdapter(TimedDoor& door) { aTimedDoor = door; }
        virtual void TimeOut() { aTimedDoor.DoorTimeOut(); }
        TimedDoor  aTimedDoor;
    }
    
    class Timer
    {
        RegisterTimeClient(TimerClient *tc)  
        { 
            //add tc to tcs 列表
        }
        
    //事件发生时:
    for_each( tc in tcs)
        tc->TimeOut(...);
    }
    
    
    
    TimedDoor door;
    
    Timer timer;
    
    timer.RegisterTimeClient(new DoorTimedAdapter(door));
    
    door.Open();
    
    class Timer
    {
        friend static Timer& GetTimer()
        public:
        RegisterTimeClient(TimerClient *tc)  { add tc to tcs 列表}
        protected:
        Timer() {}
    //事件发生时:
    for_each( tc in tcs)
        tc->TimeOut(...);
    }
    
    Timer& Timer::GetTimer()
    {
        static Timer timer;
        if(....)
        {
        }
    
        return timer;
    }
    
    
    class TimedDoor: public Door
    {
        TimedDoor()
        {
            Timer& timer = Timer::GetTimer();
             timer.RegisterTimeClient(new DoorTimedAdapter(*this));
        }
        virtual void DoorTimeOut(...);
    }
    
    
    TimedDoor door;
    
    door.Open();
    

    使用多重继承分离接口

    class TimerClient
    {
        virtual void TimeOut() = 0;
    }
    
    class TimedDoor: public Door, public TimerClient
    {
        TimedDoor()
        {
            Timer& timer = Timer::GetTimer();
             timer.RegisterTimeClient(this);
        }
        virtual void TimeOut()
        {
            do alarm report
        }
        // other interface inherit from Door
    }
    
    TimedDoor door;
    
    door.Open();
    

    可实施的设计模式:
    Facade 外观模式
    Proxy 代理模式

    ISP原则和SRP原则
    SRP原则强调的是“只有一个原因能造成对象的改变”,这就潜在地对一个类的接口个数和接口内方法的个数提出了要求。一般来说,接口个数越多,接口内的方法个数越多,越容易违反SRP原则。所以具体实施的时候,都要求一个类只实现一个接口。
    ISP原则强调的是“隔离”,对一个类实现的接口数量和各个接口内方法的数量都没有要求,只要求这些接口之间相互隔离,并且没有多余的接口。ISP在具体实施的时候,可以使用多继承的方式使用那些相互隔离的接口。
    虽然ISP所采用多继承的时候会潜在地造成一个类的接口的增加,但是这两个原则本质上是不矛盾的,因为每一个被分离出来的接口都应该是满足SRP原则的。

    六、迪米特法则 --- LOD

    描述:迪米特法则(Law of Demeter)又叫作最少知识原则,一个对象应当对其他对象有尽量少的了解。

    “不要和陌生人说话”,一个软件实体应该尽量少的与其他软件实体发生相互作用,换句话说,对其他软件实体有尽量少的知识(了解)。
    迪米特法则的初衷是降低类之间的耦合,减少对其他类的依赖。

    可实施的设计模式:
    Facade 外观模式
    Mediator 中介者模式

    迪米特法则 --- Mediator 模式

    意图:用一个中介对象封装一些列的对象交互。本模式使得对象不需要显示地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互

        

    三个对象的协作:TextField,Button 和 StaticText

    class WelcomeDialog
    {
        TextField aUserNameText;
        Button aNextButton;
        StaticText aStaticWelcome;
    
        WelcomeDialogMediator mediator;
        ......
    };
    
    //WelcomeDialog类的初始化部分:
    
    ......
    aUserNameText.SetMediator(mediator);
    aNextButton.SetMediator(mediator);
    aStaticWelcome.SetMediator(mediator);
    
    mediator.btnNext = aNextButton;
    mediator.staticWelcome = aStaticWelcome;
        ......
    };
    
    
    void TextField::TextChange(String text)
    {
        ......
        mediator.OnTextChange(this->self_ID, text);
        ......
    };
    
    void WelcomeDialogMediator::OnTextChange(int id, String text)
    {
        //检查 id 是否是 aUserNameText,不同的控件触发的TextChange可以有不同的响应处理
        boolean isEnable = text.IsEmpty() ? false : true;
        ...
        btnNext.SetState(isEnable);
        staticWelcome.SetText(text);
        ...
    }
    

    适用性:

    • 一组对象以定义良好但是复杂的方式进行通信,产生的相互依赖关系结构混乱且难以理解。
    • 一个对象引用其他很多对象并且直接与这些对象通信,导致该对象难以复用。
    • 想定制一个分布在多个类中的行为,而又不想生成太多的子类。
    展开全文
    danxibaoxxx 2019-06-05 11:06:13
  • “ 这几天突然有个想法,要是我们把整个软件行业想象成一个项目,而我是整个项目的架构师,该如何重构这个项目。图片来自 Pexels这个假设当然是不可能的,没有一家软件公司会"听我的&...

    这几天突然有个想法,要是我们把整个软件行业想象成一个项目,而我是整个项目的架构师,该如何重构这个项目。

    图片来自 Pexels

    这个假设当然是不可能的,没有一家软件公司会"听我的",但如果我"重构"的好,还是很可能成为现实,因为科技总会趋向于好的方向发展,一个好的重构搞不好就和未来不谋而合了。

    我们先来整体看下我们"软件行业"这个项目:从范围来说,它包括用户界面,各种应用软件 APP,再到操作系统,都是我们的管辖范围。

    然后,我们再来采访下我们的客户:

    资深宅男甲:我先提一个,以后我看电影能不能不用下载,一个蓝光动作片几十个 G,我换了新电脑还要拷贝下载。

    游戏玩家乙:你都别说下载了,我们有时候根本玩不了,比如 PS4 上的赛达尔,Windows 还不让玩。好多其他软件也是,我想玩 LOL 就要买电脑,想玩王者竟然还要求我买个手机。

    独肾王子丙:设备能不能别要钱,随便一个电脑,手机,平板哪个不是动不动就大几千,互相还不兼容,讨不讨厌。

    霸道总裁丁:我在家里拷贝的内容,竟然在公司复制不了,你们是不是**,都是我的电脑,这么简单的功能,为啥都实现不了!!!

    作为一个专业的软件从业人员,用户永远是对的,而架构师的职责就是解决他们的痛点。

    之所以存在这些业务痛点,一定是设计在某些地方没有做好解耦和隔离。

    设计领域一个流行很久的词叫 MVC,主要理念是一个水平分层的概念。

    从我们个人用的电脑,手机这些智能设备看,也有比较明显的三层:

    • 外设(显示器,手机屏,键鼠,手柄,体感设备等等)

    • APP(各种应用,游戏,PC 软件等等)

    • 操作系统(连接外设和应用)

    那么上面这些客户的痛点是因为这三层没有设计好,有很强的耦合吗?

    我们的显示器依赖于操作系统或者某个 APP 吗,并不会,显示器搬到哪都能用,他们之间有非常成熟且标准的接口进行隔离。

    那么应用软件依赖操作系统吗,确实如此(即使不依赖 OS 的也是做了适配),这也导致我们的软件在操作系统间可移植性很差。

    但这不是根因,因为这个只会影响到软件开发商,而且影响不大(市面上的操作系统类型并不多,一般软件开发商只会针对特定的操作系统进行开发)。

    那么问题出在哪呢?其实是耦合层次的问题!

    我们上面分析过,外设和操作系统是解耦的,APP 虽然耦合操作系统但无伤大雅,操作系统这一个底层模块更称得上完美,完全不会依赖上层的外设和应用。

    但是我们都忽略了一个点:这些解耦都只是接口上的解耦,他们其实有很强的物理距离依赖。

    怎么理解,很简单:外设必须和主机放一块。

    用户依赖前端设备(用户要使用软件,必须得有显示器,鼠标键盘之类的前端设备才行),前端设备对操作系统的地理位置有强依赖,软件又依赖操作系统,操作系统又依赖硬件。

    所以我们为了玩软件,就得把软件,操作系统,主机硬件全部弄到本地放一起才行。

    那么是不是我们解决掉这个距离依赖,很多痛点就都不存在了呢?

    确实如此,上面那些刁钻客户的问题全部迎刃而解,而且整个行业会有翻天覆地的变化!

    01

     所有人共用一个超级大电脑

    以前十个人一人一台电脑打游戏,现在每个人只留一个显示器,键盘鼠标,十台电脑都搬到云上。试想想下,这十台电脑会还是十台吗?装软件还是装十份?

    肯定不可能,我相信未来软件开发商应该看不到操作系统,他们只需要跟云厂商谈好要买多少 CPU,多少内存,多少带宽,多少存储空间(或者根据实际使用量动态收费)就好,然后在云上装一份自己的软件就行。

    就像一台电脑连了无数根无限长的鼠标键盘线一样。

    02

     云操作系统出现

    当所有软件上云,一定会促使操作系统发生进化。

    我们现在开发软件还经常讲高并发,微服务,分布式,动态扩容等等,本质上来说这些东西都是软件对硬件的直接耦合产生的结果(也可以说是操作系统没有做好软件应用和硬件的隔离)。

    而且这些东西对任何软件都一样,技术门槛还高,每家公司都搞得头大,大家不同的只是软件的内容本身。

    我相信这些雷同的东西最终一定会被云操作系统给收编直至完全不可见。你的每一次下盘,你以为是一次简单的硬盘读写,其实底下是一个超级巨型的分布式智能存储软件,你既不用担心读写性能,也不用担心数据丢失。

    你以为你就是单机部署了一下,云操作系统早就根据实时流量跟你做了云化部署和动态扩容,这些你都看不见而已。

    简单说就是:就是云操作系统会完全隔离软件和硬件,软件看到的只是一个操作系统,而操作系统去做到万物互联。

    现在云厂商让一个传统企业上云跟上坟一样,本质上还是因为我们看得见云,需要做代码架构调整,运维改革。

    我相信未来,上云不会有太多代价,因为就是从本地机房的电脑换个更大的电脑而已。

    云操作系统也可能只是在现有的基础操作系统上虚拟的一个适配层,不管怎样,我们对未来的技术实现不做过多猜测,但他一定会来。

    03

     资源利用最大化

    当所有的资源都被云操作系统集中接管,资源肯定会被最大化利用。以前一万个人看一部电影,一人要下载一份。现在就只需要一份即可。

    以前每个人一台电脑,不用的时候就是浪费,现在共用一台超级电脑,总资源消耗肯定会大大减少(参考二手宽带厂商如何把 100M 带宽卖给 100 户人家,而且每户带宽是 10M)。

    对用户来说拷贝任何一个文件,都会是秒级的!

    我相信未来的云系统本身就已经做好了各种备份容灾,用户层面的拷贝都只是引用拷贝。

    而且即使用户需要修改文件,也会出现非常智能的存储软件能通过延迟拷贝,增量记录等手段节约大量资源,实际情况可能需要考虑存储介质的 IO 瓶颈,还是会有多份,但相比当前来说存储空间依然是成千上万倍的减少。

    04

     BS/CS 模式慢慢消亡

    用户的电脑上云了,软件开发商的应用也上云了,都部署到一块了,还有客户端的概念吗?

    没有了客户端,还有 C 端,浏览器的概念吗?所有的软件只需要定义好自己的本地 UI,通过操作系统的接口传给外设即可。

    当然,现在我们所谓的 C 端不可能一下子消失,这些东西一定有一个过渡期,但最终一定会慢慢消失。

    不过更加可能的一种情况,以后的 C 端就仅仅是指我们的显示器,键鼠这些了。

    05

     个人操作系统消失,云操作系统崛起

    这个不需要太多解释,云上的电脑肯定根本不需要界面,像 Window 这类桌面级的操作系统肯定会慢慢衰落,而面向于云的操作系统肯定会崛起。

    这类操作系统要有很强的整合硬件的能力(这个我比较看好华为的鸿蒙,至少他的理念是对的。

    鸿蒙有一个流转的概念,其实真正的流转就是"不需要流转",把一个东西从 A 传给 B,总会有同步传输的消耗,如果 A 和 B 本来就是同一个人,还需要流转吗?)。

    06

     智能存储软件崛起

    由于个人电脑普遍上云,专业的存储软件会替换现在的硬盘,存储变得更为智能。相当于一块超级大的智慧硬盘。

    主要功能如下:

    • 自动备份,自动修复:再也不用担心硬盘损坏,数据丢失。(基本的存储软件都会有 raid,容灾等功能)

    • 弹性伸缩:可以随时插入新盘进行扩容,也可以自动给没有使用的硬盘下电,节约能耗。

    • 自动整理:自动整理磁盘碎片,比如 100 块同样大小的盘,每个盘都有 10% 的数据,那么可以把这些数据整理到 10 块盘中,其他的 90 块盘可以闲置。

    • 自动压缩:比如存储两个 1G 的文件,其中一个文件是拷贝的另外一个文件仅做了少量修改。最终使用的存储空间约等于 1G+ 修改部分的大小。

    • 极速拷贝:其实和自动压缩很像,你拷贝仅仅只拷贝引用,修改就只存储修改的部分。

    07

     云操作系统称霸

    我不认为前端设备和操作系统的接口能够标准化,我更倾向于到时候会出现几个大的云操作系统厂商,各自有自己的标准。

    为什么这样说,因为一旦标准归一,就代表任何厂家都可以生产外设(就跟浏览器一样,百花齐放),而外设又是流量的总入口,这么大快肥肉没人想拱手让出。

    软件和操作系统之间的接口会统一标准吗?我觉得答案不重要,统一更好,不统一也会出现类似于 JVM 这样的适配层出现。

    08

     外设会发生变革

    现在的前端设备都是带一些 HDMI 插口,以后可能就只有一个网口,或者 WiFi 模块。

    每个前段设备都会自带一个协议解析模块,将云操作系统的数据解析为影音数据。

    其实 TC 盒子之类的云桌面早就出现了,只不过 TC 盒子还有点大,像一个小的操作系统,可以接键鼠显示器,未来一定会越变越小,一直小到看不见(比如集成在显示器上)。

    我没有去研究 TC 盒子的实现原理,我不知道他是如何把数据传到前端来的,键鼠操作也还流畅,只不过画面不是很清晰,我不清楚这种如果用来玩那种大型的单机或网游,性能会怎么样。

    不过不重要,最基础的云桌面都实现了,随着各项技术的发展,那种无限接近于近端连接的云桌面还会远吗?

    我猜测,未来外设会特别便宜,甚至免费,因为你使用了我的前端设备,就代表你选择了我的云操作系统和我所支持的所有软件,前端设备的费用最终可以转嫁到软件服务费中。

    09

     显示器,鼠标键盘这些外设厂家衰落

    软件开发商和用户都上云了,接口又是云厂商私有的,以前的外设厂家通过 USB 接口,耳机孔,HDMI 线是没法连到云上的。

    所以未来外设厂商会沦落到孙子的角色,好点的情况,云厂商卖协议转换模块给他们,狠点的话云厂商就自己造外设了。

    像大米,oqqo 这些没有自己操作系统的终端厂家可就惨了,未来手机就只剩一个屏幕了,下面的接口还是别人的,应该只有西北风了。

    10

     真正的小程序出现

    现在炒的火热的小程序,他至少还是个程序。未来应用商城的 APP,可能连程序都不是了,完全只是个链接,他只是存了个云服务器的地址而已。

    你通过账号密码登陆上去,和云电脑通信直接就能享受软件服务,软件和系统早就已经准备好了,你只需要告诉我你想玩啥。

    11

     芯片

    这个要特别说一说,现在我们国家芯片被美国佬卡死了,如果手机、终端都上云了,芯片的核心竞争力就变成了性能,而不是体积大小了,我那么大个机房,我差你那几 nm?

    当然,某些领域的专业外设可能还是需要高级芯片,但芯片危机肯定不会像现在这么严峻,搞不好到那时候国产芯片超过了美国也不是不可能!

    12

     软件开发和维护变得更简单

    上面已经讲过,云系统会屏蔽掉网络,底层硬件,所有软件在开发上都类似于单体应用,而且不区分客户端和服务端(当然还是有 UI 和后端服务的分层)。

    软件和用户之间不再直接关联,而是通过云操作系统连接,那么外挂问题,安全问题,版本升级,硬件的性能瓶颈可能都没了,不要太香。

    13

     网络安全被重新定义

    同上,软件和外层网络已经没有了接口,他只跟操作系统和内部网络直接交互,所有的安全问题全部转移到操作系统之上。

    我相信安全这个行业会被重新定义。以前还说模拟客户端发个网络请求来攻击服务器,现在你就拿个键盘鼠标,我看你咋攻击!

    当然如果你牛逼,把云操作系统和外设的接口都给破解了,又能干嘛,传输一些按钮键盘点击事件过去,能做的估计也很有限(当然到时候外设肯定会有专门的认证接口,只要这个做好防护应该就不会有大问题)。

    14

     显卡声卡之类的东东

    云操作系统肯定是不需要显卡声卡的(到时候肯定有一个本地视频影音数据转网络数据的模块,不知道会叫什么),即使有也很可能会转移到显示器上,我只是感觉这些东西会有很大的变革,等着看吧。

    15

     其他的一些影响

    用户的习惯被改变了,很多东西都会变。像现在这种游戏主机厂商应该会被淘汰,没人会愿意买一台大主机回家,还要下载游戏,还天天更新,而且你支持的那么几款游戏还不兼容其他系统,我呸!

    网吧很多年前很多人预研说会消失,因为大家都有钱了买得起电脑了,谁还去网吧,结果呢,还活得好好的。

    但这次是真的要凉凉了,电脑都没了,网吧放啥啊,就放个显示器?别说,还真有可能继续活着。

    个人数据都上云了,安全和隐私会变得格外重要,应该会有完善的法律法规出现。哪家公司敢挑战这个,一准凉凉。

    远程会议,远程定位这种实现会变得格外简单。

    现在所谓的网络游戏可能没了,都会变成一个超大的“局域网”游戏,数据全在“本地”交互。

    ....

    16

     总结

    总体来说:

    • 未来会形成以云操作系统为中心的不同阵营。

    • 一刀把显示器和电脑的的连接线砍断换成了网线,手机也只剩一块屏幕。

    • 用户会以软件为单位购买服务,而不是买整个手机或电脑回去。

    以后大家想玩啥就直接玩,没有下载,没有更新。把家里的东西拷到公司怎么整?还需要拷贝吗?记好你的账号,在哪登录都是一样的。

    上面这东西总体来说都是假设,是否真的能实现呢?万一技术上不可行,不都是瞎扯吗,而且就算技术可行,大家愿意用上云吗?

    其实我觉得这个问题毋庸置疑,试想一下,以前交通不发达的时候,大家会在家里屯粮食,因为出门买东西不方便,现在呢,交通已经很便利了,但是还是不够便利,至少跨个省还是要几个小时。

    但是如果有一天交通异常发达了,你今天想去纽约喝杯咖啡两分钟就能到,试问,谁还会在家里冰箱装一堆一堆的东西。

    我相信只要解决了个人隐私和安全问题,大家一定还是愿意上云的,买个电脑要占家里空间,自己要装系统,装软件,坏了要修,用个几年还要买新的,万一出个差电脑还带不走,不用的时候还浪费。

    上云了这些问题都不存在,按需定制付费。技术问题就更不用说了:云操作系统更多的是实现问题,没有无法逾越的技术难题。

    至于网络带宽和时延,短短十几年我们就从 2G,到 3G,4G 再到 5G,网络接口的效率达到和本地接口一样只是一个时间问题,不用操这种咸心。

    作者:王兵

    简介:现任鼎桥技术有限公司架构师,从事软件开发和软件设计十二年,精通 Java,对设计原则和设计模式等有深刻理解,平时喜欢撸代码,做逻辑智力题,打打游戏。

    编辑:陶家龙

    来源:51CTO技术栈

    精彩文章推荐:

    刘朋:程序员如何练就领导力

    2021-07-29

    山哥新作:架构师必备技能之业务分析

    2021-07-26

    浅谈云原生架构的 7 个原则

    2021-07-23

    资深架构师十几年的架构干货经验总结分享!

    2021-07-19

    资深架构专家聊小团队中微服务困境及分布式事务解决方案

    2021-07-16

    阿里专家晨末:什么是技术一号位?

    2021-07-08

    展开全文
    k6T9Q8XKs6iIkZPPIFq 2021-07-31 00:17:56
  • 重构(Refactoring)就是在不改变软件现有功能的基础上,通过调整程序代码改善软件的质量、性能,使其程序的设计模式和架构更趋合理,提高软件的扩展性和维护性。  也许有人会问,为什么不在项目开始时多花些时间...
       重构(Refactoring)就是在不改变软件现有功能的基础上,通过调整程序代码改善软件的质量、性能,使其程序的设计模式和架构更趋合理,提高软件的扩展性和维护性。 
    

      也许有人会问,为什么不在项目开始时多花些时间把设计做好,而要以后花时间来重构呢?要知道一个完美得可以预见未来任何变化的设计,或一个灵活得可以容纳任何扩展的设计是不存在的。系统设计人员对即将着手的项目往往只能从大方向予以把控,而无法知道每个细枝末节,其次永远不变的就是变化,提出需求的用户往往要在软件成型后,始才开始"品头论足",系统设计人员毕竟不是先知先觉的神仙,功能的变化导致设计的调整再所难免。所以"测试为先,持续重构"作为良好开发习惯被越来越多的人所采纳,测试和重构像黄河的护堤,成为保证软件质量的法宝。

    一、为什么要重构(Refactoring)

          在不改变系统功能的情况下,改变系统的实现方式。为什么要这么做?投入精力不用来满足客户关心的需求,而是仅仅改变了软件的实现方式,这是否是在浪费客户的投资呢?

          重构的重要性要从软件的生命周期说起。软件不同与普通的产品,他是一种智力产品,没有具体的物理形态。一个软件不可能发生物理损耗,界面上的按钮永远不会因为按动次数太多而发生接触不良。那么为什么一个软件制造出来以后,却不能永远使用下去呢?

          对软件的生命造成威胁的因素只有一个:需求的变更。一个软件总是为解决某种特定的需求而产生,时代在发展,客户的业务也在发生变化。有的需求相对稳定一些,有的需求变化的比较剧烈,还有的需求已经消失了,或者转化成了别的需求。在这种情况下,软件必须相应的改变。

          考虑到成本和时间等因素,当然不是所有的需求变化都要在软件系统中实现。但是总的说来,软件要适应需求的变化,以保持自己的生命力。

          这就产生了一种糟糕的现象:软件产品最初制造出来,是经过精心的设计,具有良好架构的。但是随着时间的发展、需求的变化,必须不断的修改原有的功能、追加新的功能,还免不了有一些缺陷需要修改。为了实现变更,不可避免的要违反最初的设计构架。经过一段时间以后,软件的架构就千疮百孔了。bug越来越多,越来越难维护,新的需求越来越难实现,软件的构架对新的需求渐渐的失去支持能力,而是成为一种制约。最后新需求的开发成本会超过开发一个新的软件的成本,这就是这个软件系统的生命走到尽头的时候。

          重构就能够最大限度的避免这样一种现象。系统发展到一定阶段后,使用重构的方式,不改变系统的外部功能,只对内部的结构进行重新的整理。通过重构,不断的调整系统的结构,使系统对于需求的变更始终具有较强的适应能力。

      通过重构可以达到以下的目标:

      ·持续偏纠和改进软件设计

      重构和设计是相辅相成的,它和设计彼此互补。有了重构,你仍然必须做预先的设计,但是不必是最优的设计,只需要一个合理的解决方案就够了,如果没有重构、程序设计会逐渐腐败变质,愈来愈像断线的风筝,脱缰的野马无法控制。重构其实就是整理代码,让所有带着发散倾向的代码回归本位。

      ·使代码更易为人所理解

      Martin Flower在《重构》中有一句经典的话:"任何一个傻瓜都能写出计算机可以理解的程序,只有写出人容易理解的程序才是优秀的程序员。"对此,笔者感触很深,有些程序员总是能够快速编写出可运行的代码,但代码中晦涩的命名使人晕眩得需要紧握坐椅扶手,试想一个新兵到来接手这样的代码他会不会想当逃兵呢?

      软件的生命周期往往需要多批程序员来维护,我们往往忽略了这些后来人。为了使代码容易被他人理解,需要在实现软件功能时做许多额外的事件,如清晰的排版布局,简明扼要的注释,其中命名也是一个重要的方面。一个很好的办法就是采用暗喻命名,即以对象实现的功能的依据,用形象化或拟人化的手法进行命名,一个很好的态度就是将每个代码元素像新生儿一样命名,也许笔者有点命名偏执狂的倾向,如能荣此雅号,将深以此为幸。

      对于那些让人充满迷茫感甚至误导性的命名,需要果决地、大刀阔斧地整容,永远不要手下留情!

      ·帮助发现隐藏的代码缺陷

      孔子说过:温故而知新。重构代码时逼迫你加深理解原先所写的代码。笔者常有写下程序后,却发生对自己的程序逻辑不甚理解的情景,曾为此惊悚过,后来发现这种症状居然是许多程序员常患的"感冒"。当你也发生这样的情形时,通过重构代码可以加深对原设计的理解,发现其中的问题和隐患,构建出更好的代码。

      ·从长远来看,有助于提高编程效率

      当你发现解决一个问题变得异常复杂时,往往不是问题本身造成的,而是你用错了方法,拙劣的设计往往导致臃肿的编码。

      改善设计、提高可读性、减少缺陷都是为了稳住阵脚。良好的设计是成功的一半,停下来通过重构改进设计,或许会在当前减缓速度,但它带来的后发优势却是不可低估的。

    二、何时着手重构(Refactoring)

      新官上任三把火,开始一个全新的项目时,程序员往往也会燃起三把火:紧锣密鼓、脚不停蹄、加班加点,一支声势浩大的千军万"码"夹裹着程序员激情和扣击键盘的鸣金奋力前行,势如破竹,攻城掠地,直指"黄龙府"。

      开发经理是这支浩浩汤汤代码队伍的统帅,他负责这支队伍的命运,当齐恒公站在山顶上看到管仲训练的队伍整齐划一地前进时,他感叹说"我有这样一支军队哪里还怕没有胜利呢?"。但很遗憾,你手中的这支队伍原本只是散兵游勇,在前进中招兵买马,不断壮大,所以队伍变形在所难免。当开发经理发觉队伍变形时,也许就是克制住攻克前方山头的诱惑,停下脚步整顿队伍的时候了。

      Kent Beck提出了"代码坏味道"的说法,和我们所提出的"队伍变形"是同样的意思,队伍变形的信号是什么呢?以下列述的代码症状就是"队伍变形"的强烈信号:

      ·代码中存在重复的代码

      中国有118 家整车生产企业,数量几乎等于美、日、欧所有汽车厂家数之和,但是全国的年产量却不及一个外国大汽车公司的产量。重复建设只会导致效率的低效和资源的浪费。

      程序代码更是不能搞重复建设,如果同一个类中有相同的代码块,请把它提炼成类的一个独立方法,如果不同类中具有相同的代码,请把它提炼成一个新类,永远不要重复代码。


      ·过大的类和过长的方法

      过大的类往往是类抽象不合理的结果,类抽象不合理将降低了代码的复用率。方法是类王国中的诸侯国,诸侯国太大势必动摇中央集权。过长的方法由于包含的逻辑过于复杂,错误机率将直线上升,而可读性则直线下降,类的健壮性很容易被打破。当看到一个过长的方法时,需要想办法将其划分为多个小方法,以便于分而治之。

      ·牵一毛而需要动全身的修改

      当你发现修改一个小功能,或增加一个小功能时,就引发一次代码地震,也许是你的设计抽象度不够理想,功能代码太过分散所引起的。

      ·类之间需要过多的通讯

      A类需要调用B类的过多方法访问B的内部数据,在关系上这两个类显得有点狎昵,可能这两个类本应该在一起,而不应该分家。

      ·过度耦合的信息链

      "计算机是这样一门科学,它相信可以通过添加一个中间层解决任何问题",所以往往中间层会被过多地追加到程序中。如果你在代码中看到需要获取一个信息,需要一个类的方法调用另一个类的方法,层层挂接,就象输油管一样节节相连。这往往是因为衔接层太多造成的,需要查看就否有可移除的中间层,或是否可以提供更直接的调用方法。

      ·各立山头干革命

      如果你发现有两个类或两个方法虽然命名不同但却拥有相似或相同的功能,你会发现往往是因为开发团队成员协调不够造成的。笔者曾经写了一个颇好用的字符串处理类,但因为没有及时通告团队其他人员,后来发现项目中居然有三个字符串处理类。革命资源是珍贵的,我们不应各立山头干革命。

      ·不完美的设计

      在笔者刚完成的一个比对报警项目中,曾安排阿朱开发报警模块,即通过Socket向指定的短信平台、语音平台及客户端报警器插件发送报警报文信息,阿朱出色地完成了这项任务。后来用户又提出了实时比对的需求,即要求第三方系统以报文形式向比对报警系统发送请求,比对报警系统接收并响应这个请求。这又需要用到Socket报文通讯,由于原来的设计没有将报文通讯模块独立出来,所以无法复用阿朱开发的代码。后来我及时调整了这个设计,新增了一个报文收发模块,使系统所有的对外通讯都复用这个模块,系统的整体设计也显得更加合理。

      每个系统都或多或少存在不完美的设计,刚开始可能注意不到,到后来才会慢慢凸显出来,此时唯有勇于更改才是最好的出路。

      ·缺少必要的注释

      虽然许多软件工程的书籍常提醒程序员需要防止过多注释,但这个担心好象并没有什么必要。往往程序员更感兴趣的是功能实现而非代码注释,因为前者更能带来成就感,所以代码注释往往不是过多而是过少,过于简单。人的记忆曲线下降的坡度是陡得吓人的,当过了一段时间后再回头补注释时,很容易发生"提笔忘字,愈言且止"的情形。

      曾在网上看到过微软的代码注释,其详尽程度让人叹为观止,也从中体悟到了微软成功的一个经验。

    三、重构(Refactoring)的难题

          学习一种可以大幅提高生产力的新技术时,你总是难以察觉其不适用的场合。通常你在一个特定场景中学习它,这个场景往往是个项目。这种情况下你很难看出什么会造成这种新技术成效不彰或甚至形成危害。十年前,对象技术(object tech.)的情况也是如此。那时如果有人问我「何时不要使用对象」,我很难回答。并非我认为对象十全十美、没有局限性 — 我最反对这种盲目态度,而是尽管我知道它的好处,但确实不知道其局限性在哪儿。

          现在,重构的处境也是如此。我们知道重构的好处,我们知道重构可以给我们的工作带来垂手可得的改变。但是我们还没有获得足够的经验,我们还看不到它的局限性。
    这一小节比我希望的要短。暂且如此吧。随着更多人学会重构技巧,我们也将对它有更多了解。对你而言这意味:虽然我坚决认为你应该尝试一下重构,获得它所提供的利益,但在此同时,你也应该时时监控其过程,注意寻找重构可能引入的问题。请让我们知道你所遭遇的问题。随着对重构的了解日益增多,我们将找出更多解决办法,并清楚知道哪些问题是真正难以解决的。

          数据库(Databases)

          「重构」经常出问题的一个领域就是数据库。绝大多数商用程序都与它们背后的database schema(数据库表格结构)紧密耦合(coupled)在一起,这也是database schema如此难以修改的原因之一。另一个原因是数据迁移(migration)。就算你非常小心地将系统分层(layered),将database schema和对象模型(object model)间的依赖降至最低,但database schema的改变还是让你不得不迁移所有数据,这可能是件漫长而烦琐的工作。

          在「非对象数据库」(nonobject databases)中,解决这个问题的办法之一就是:在对象模型(object model)和数据库模型(database model)之间插入一个分隔层(separate layer),这就可以隔离两个模型各自的变化。升级某一模型时无需同时升级另一模型,只需升级上述的分隔层即可。这样的分隔层会增加系统复杂度,但可以给你很大的灵活度。如果你同时拥有多个数据库,或如果数据库模型较为复杂使你难以控制,那么即使不进行重构,这分隔层也是很重要的。

          你无需一开始就插入分隔层,可以在发现对象模型变得不稳定时再产生它。这样你就可以为你的改变找到最好的杠杆效应。

          对开发者而言,对象数据库既有帮助也有妨碍。某些面向对象数据库提供不同版本的对象之间的自动迁移功能,这减少了数据迁移时的工作量,但还是会损失一定时间。如果各数据库之间的数据迁移并非自动进行,你就必须自行完成迁移工作,这个工作量可是很大的。这种情况下你必须更加留神classes内的数据结构变化。你仍然可以放心将classes的行为转移过去,但转移值域(field)时就必须格外小心。数据尚未被转移前你就得先运用访问函数(accessors)造成「数据已经转移」的假象。一旦你确定知道「数据应该在何处」时,就可以一次性地将数据迁移过去。这时惟一需要修改的只有访问函数(accessors),这也降低了错误风险。

          修改接口(Changing Interfaces)

          关于对象,另一件重要事情是:它们允许你分开修改软件模块的实现(implementation)和接口(interface)。你可以安全地修改某对象内部而不影响他人,但对于接口要特别谨慎 — 如果接口被修改了,任何事情都有可能发生。

          一直对重构带来困扰的一件事就是:许多重构手法的确会修改接口。像Rename Method(273)这么简单的重构手法所做的一切就是修改接口。这对极为珍贵的封装概念会带来什么影响呢?

          如果某个函数的所有调用动作都在你的控制之下,那么即使修改函数名称也不会有任何问题。哪怕面对一个public函数,只要能取得并修改其所有调用者,你也可以安心地将这个函数易名。只有当需要修改的接口系被那些「找不到,即使找到也不能修改」的代码使用时,接口的修改才会成为问题。如果情况真是如此,我就会说:这个接口是个「已发布接口」(published interface)— 比公开接口(public interface)更进一步。接口一旦发行,你就再也无法仅仅修改调用者而能够安全地修改接口了。你需要一个略为复杂的程序。

          这个想法改变了我们的问题。如今的问题是:该如何面对那些必须修改「已发布接口」的重构手法?

          简言之,如果重构手法改变了已发布接口(published interface),你必须同时维护新旧两个接口,直到你的所有用户都有时间对这个变化做出反应。幸运的是这不太困难。你通常都有办法把事情组织好,让旧接口继续工作。请尽量这么做:让旧接口调用新接口。当你要修改某个函数名称时,请留下旧函数,让它调用新函数。千万不要拷贝函数实现码,那会让你陷入「重复代码」(duplicated code)的泥淖中难以自拔。你还应该使用Java提供的 deprecation(反对)设施,将旧接口标记为 "deprecated"。这么一来你的调用者就会注意到它了。

          这个过程的一个好例子就是Java容器类(collection classes)。Java 2的新容器取代了原先一些容器。当Java 2容器发布时,JavaSoft花了很大力气来为开发者提供一条顺利迁徙之路。

          「保留旧接口」的办法通常可行,但很烦人。起码在一段时间里你必须建造(build)并维护一些额外的函数。它们会使接口变得复杂,使接口难以使用。还好我们有另一个选择:不要发布(publish)接口。当然我不是说要完全禁止,因为很明显你必得发布一些接口。如果你正在建造供外部使用的APIs,像Sun所做的那样,肯定你必得发布接口。我之所以说尽量不要发布,是因为我常常看到一些开发团队公开了太多接口。我曾经看到一支三人团队这么工作:每个人都向另外两人公开发布接口。这使他们不得不经常来回维护接口,而其实他们原本可以直接进入程序库,径行修改自己管理的那一部分,那会轻松许多。过度强调「代码拥有权」的团队常常会犯这种错误。发布接口很有用,但也有代价。所以除非真有必要,别发布接口。这可能意味需要改变你的代码拥有权观念,让每个人都可以修改别人的代码,以运应接口的改动。以搭档(成对)编程(Pair Programming)完成这一切通常是个好主意。

          不要过早发布(published)接口。请修改你的代码拥有权政策,使重构更顺畅。

          Java之中还有一个特别关于「修改接口」的问题:在throws子句中增加一个异常。这并不是对签名式(signature)的修改,所以你无法以delegation(委托手法)隐藏它。但如果用户代码不作出相应修改,编译器不会让它通过。这个问题很难解决。你可以为这个函数选择一个新名字,让旧函数调用它,并将这个新增的checked exception(可控式异常)转换成一个unchecked exception(不可控异常)。你也可以拋出一个unchecked异常,不过这样你就会失去检验能力。如果你那么做,你可以警告调用者:这个unchecked异常日后会变成一个checked异常。这样他们就有时间在自己的代码中加上对此异常的处理。出于这个原因,我总是喜欢为整个package定义一个superclass异常(就像java.sql的SQLException),并确保所有public函数只在自己的throws子句中声明这个异常。这样我就可以随心所欲地定义subclass异常,不会影响调用者,因为调用者永远只知道那个更具一般性的superclass异常。

          难以通过重构手法完成的设计改动

          通过重构,可以排除所有设计错误吗?是否存在某些核心设计决策,无法以重构手法修改?在这个领域里,我们的统计数据尚不完整。当然某些情况下我们可以很有效地重构,这常常令我们倍感惊讶,但的确也有难以重构的地方。比如说在一个项目中,我们很难(但还是有可能)将「无安全需求(no security requirements)情况下构造起来的系统」重构为「安全性良好的(good security)系统」。

          这种情况下我的办法就是「先想象重构的情况」。考虑候选设计方案时,我会问自己:将某个设计重构为另一个设计的难度有多大?如果看上去很简单,我就不必太担心选择是否得当,于是我就会选最简单的设计,哪怕它不能覆盖所有潜在需求也没关系。但如果预先看不到简单的重构办法,我就会在设计上投入更多力气。不过我发现,这种情况很少出现。

          何时不该重构?

          有时候你根本不应该重构 — 例如当你应该重新编写所有代码的时候。有时候既有代码实在太混乱,重构它还不如从新写一个来得简单。作出这种决定很困难,我承认我也没有什么好准则可以判断何时应该放弃重构。

          重写(而非重构)的一个清楚讯号就是:现有代码根本不能正常运作。你可能只是试着做点测试,然后就发现代码中满是错误,根本无法稳定运作。记住,重构之前,代码必须起码能够在大部分情况下正常运作。

          一个折衷办法就是:将「大块头软件」重构为「封装良好的小型组件」。然后你就可以逐一对组件作出「重构或重建」的决定。这是一个颇具希望的办法,但我还没有足够数据,所以也无法写出优秀的指导原则。对于一个重要的古老系统,这肯定会是一个很好的方向。

          另外,如果项目已近最后期限,你也应该避免重构。在此时机,从重构过程赢得的生产力只有在最后期限过后才能体现出来,而那个时候已经时不我予。Ward Cunningham对此有一个很好的看法。他把未完成的重构工作形容为「债务」。很多公司都需要借债来使自己更有效地运转。但是借债就得付利息,过于复杂的代码所造成的「维护和扩展的额外开销」就是利息。你可以承受一定程度的利息,但如果利息太高你就会被压垮。把债务管理好是很重要的,你应该随时通过重构来偿还一部分债务。

          如果项目已经非常接近最后期限,你不应该再分心于重构,因为已经没有时间了。不过多个项目经验显示:重构的确能够提高生产力。如果最后你没有足够时间,通常就表示你其实早该进行重构。

    四、重构(Refactoring)与设计

          「重构」肩负一项特别任务:它和设计彼此互补。初学编程的时候,我埋头就写程序,浑浑噩噩地进行开发。然而很快我便发现,「事先设计」(upfront design)可以助我节省回头工的高昂成本。于是我很快加强这种「预先设计」风格。许多人都把设计看作软件开发的关键环节,而把编程(programming)看作只是机械式的低级劳动。他们认为设计就像画工程图而编码就像施工。但是你要知道,软件和真实器械有着很大的差异。软件的可塑性更强,而且完全是思想产品。正如Alistair Cockburn所说:『有了设计,我可以思考更快,但是其中充满小漏洞。』

          有一种观点认为:重构可以成为「预先设计」的替代品。这意思是你根本不必做任何设计,只管按照最初想法开始编码,让代码有效运作,然后再将它重构成型。事实上这种办法真的可行。我的确看过有人这么做,最后获得设计良好的软件。极限编程(Extreme Programming)[Beck, XP] 的支持者极力提倡这种办法。

          尽管如上所言,只运用重构也能收到效果,但这并不是最有效的途径。是的,即使极限编程(Extreme Programming)爱好者也会进行预先设计。他们会使用CRC卡或类似的东西来检验各种不同想法,然后才得到第一个可被接受的解决方案,然后才能开始编码,然后才能重构。关键在于:重构改变了「预先设计」的角色。如果没有重构,你就必须保证「预先设计」正确无误,这个压力太大了。这意味如果将来需要对原始设计做任何修改,代价都将非常高昂。因此你需要把更多时间和精力放在预先设计上,以避免日后修改。

          如果你选择重构,问题的重点就转变了。你仍然做预先设计,但是不必一定找出正确的解决方案。此刻的你只需要得到一个足够合理的解决方案就够了。你很肯定地知道,在实现这个初始解决方案的时候,你对问题的理解也会逐渐加深,你可能会察觉最佳解决方案和你当初设想的有些不同。只要有重构这项武器在手,就不成问题,因为重构让日后的修改成本不再高昂。

          这种转变导致一个重要结果:软件设计朝向简化前进了一大步。过去未曾运用重构时,我总是力求得到灵活的解决方案。任何一个需求都让我提心吊胆地猜疑:在系统寿命期间,这个需求会导致怎样的变化?由于变更设计的代价非常高昂,所以我希望建造一个足够灵活、足够强固的解决方案,希望它能承受我所能预见的所有需求变化。问题在于:要建造一个灵活的解决方案,所需的成本难以估算。灵活的解决方案比简单的解决方案复杂许多,所以最终得到的软件通常也会更难维护 — 虽然它在我预先设想的方向上的确是更加灵活。就算幸运走在预先设想的方向上,你也必须理解如何修改设计。如果变化只出现在一两个地方,那不算大问题。然而变化其实可能出现在系统各处。如果在所有可能的变化出现地点都建立起灵活性,整个系统的复杂度和维护难度都会大大提高。当然,如果最后发现所有这些灵活性都毫无必要,这才是最大的失败。你知道,这其中肯定有些灵活性的确派不上用场,但你却无法预测到底是哪些派不上用场。为了获得自己想要的灵活性,你不得不加入比实际需要更多的灵活性。

          有了重构,你就可以通过一条不同的途径来应付变化带来的风险。你仍旧需要思考潜在的变化,仍旧需要考虑灵活的解决方案。但是你不必再逐一实现这些解决方案,而是应该问问自己:『把一个简单的解决方案重构成这个灵活的方案有多大难度?』如果答案是「相当容易」(大多数时候都如此),那么你就只需实现目前的简单方案就行了。
    重构可以带来更简单的设计,同时又不损失灵活性,这也降低了设计过程的难度,减轻了设计压力。一旦对重构带来的简单性有更多感受,你甚至可以不必再预先思考前述所谓的灵活方案 — 一旦需要它,你总有足够的信心去重构。是的,当下只管建造可运行的最简化系统,至于灵活而复杂的设计,唔,多数时候你都不会需要它。

          劳而无获— Ron Jeffries

          Chrysler Comprehensive Compensation(克莱斯勒综合薪资系统)的支付过程太慢了。虽然我们的开发还没结束,这个问题却已经开始困扰我们,因为它已经拖累了测试速度。

          Kent Beck、Martin Fowler和我决定解决这个问题。等待大伙儿会合的时间里,凭着我对这个系统的全盘了解,我开始推测:到底是什么让系统变慢了?我想到数种可能,然后和伙伴们谈了几种可能的修改方案。最后,关于「如何让这个系统运行更快」,我们提出了一些真正的好点子。

          然后,我们拿Kent的量测工具度量了系统性能。我一开始所想的可能性竟然全都不是问题肇因。我们发现:系统把一半时间用来创建「日期」实体(instance)。更有趣的是,所有这些实体都有相同的值。

          于是我们观察日期的创建逻辑,发现有机会将它优化。日期原本是由字符串转换而生,即使无外部输入也是如此。之所以使用字符串转换方式,完全是为了方便键盘输入。好,也许我们可以将它优化。

          于是我们观察日期怎样被这个程序运用。我们发现,很多日期对象都被用来产生「日期区间」实体(instance)。「日期区间」是个对象,由一个起始日期和一个结束日期组成。仔细追踪下去,我们发现绝大多数日期区间是空的!

          处理日期区间时我们遵循这样一个规则:如果结束日期在起始日期之前,这个日期区间就该是空的。这是一条很好的规则,完全符合这个class的需要。采用此一规则后不久,我们意识到,创建一个「起始日期在结束日期之后」的日期区间,仍然不算是清晰的代码,于是我们把这个行为提炼到一个factory method(译注:一个著名的设计模式,见《Design Patterns》),由它专门创建「空的日期区间」。

          我们做了上述修改,使代码更加清晰,却意外得到了一个惊喜。我们创建一个固定不变的「空日期区间」对象,并让上述调整后的factory method每次都返回该对象,而不再每次都创建新对象。这一修改把系统速度提升了几乎一倍,足以让测试速度达到可接受程度。这只花了我们大约五分钟。

          我和团队成员(Kent和Martin谢绝参加)认真推测过:我们了若指掌的这个程序中可能有什么错误?我们甚至凭空做了些改进设计,却没有先对系统的真实情况进行量测。

          我们完全错了。除了一场很有趣的交谈,我们什么好事都没做。

          教训:哪怕你完全了解系统,也请实际量测它的性能,不要臆测。臆测会让你学到一些东西,但十有八九你是错的。

    五、重构与性能(Performance)

          译注:在我的接触经验中,performance一词被不同的人予以不同的解释和认知:效率、性能、效能。不同地区(例如台湾和大陆)的习惯用法亦不相同。本书一遇performance我便译为性能。efficient译为高效,effective译为有效。

          关于重构,有一个常被提出的问题:它对程序的性能将造成怎样的影响?为了让软件易于理解,你常会作出一些使程序运行变慢的修改。这是个重要的问题。我并不赞成为了提高设计的纯洁性或把希望寄托于更快的硬件身上,而忽略了程序性能。已经有很多软件因为速度太慢而被用户拒绝,日益提高的机器速度亦只不过略微放宽了速度方面的限制而已。但是,换个角度说,虽然重构必然会使软件运行更慢,但它也使软件的性能优化更易进行。除了对性能有严格要求的实时(real time)系统,其它任何情况下「编写快速软件」的秘密就是:首先写出可调(tunable)软件,然后调整它以求获得足够速度。

          我看过三种「编写快速软件」的方法。其中最严格的是「时间预算法」(time budgeting),这通常只用于性能要求极高的实时系统。如果使用这种方法,分解你的设计时就要做好预算,给每个组件预先分配一定资源 — 包括时间和执行轨迹(footprint)。每个组件绝对不能超出自己的预算,就算拥有「可在不同组件之间调度预配时间」的机制也不行。这种方法高度重视性能,对于心律调节器一类的系统是必须的,因为在这样的系统中迟来的数据就是错误的数据。但对其他类系统(例如我经常开发的企业信息系统)而言,如此追求高性能就有点过份了。

          第二种方法是「持续关切法」(constant attention)。这种方法要求任何程序员在任何时间做任何事时,都要设法保持系统的高性能。这种方式很常见,感觉上很有吸引力,但通常不会起太大作用。任何修改如果是为了提高性能,通常会使程序难以维护,因而减缓开发速度。如果最终得到的软件的确更快了,那么这点损失尚有所值,可惜通常事与愿违,因为性能改善一旦被分散到程序各角落,每次改善都只不过是从「对程序行为的一个狭隘视角」出发而已。

          关于性能,一件很有趣的事情是:如果你对大多数程序进行分析,你会发现它把大半时间都耗费在一小半代码身上。如果你一视同仁地优化所有代码,90% 的优化工作都是白费劲儿,因为被你优化的代码有许多难得被执行起来。你花时间做优化是为了让程序运行更快,但如果因为缺乏对程序的清楚认识而花费时间,那些时间都是被浪费掉了。

           第三种性能提升法系利用上述的 "90%" 统计数据。采用这种方法时,你以一种「良好的分解方式」(well-factored manner)来建造自己的程序,不对性能投以任何关切,直至进入性能优化阶段 — 那通常是在开发后期。一旦进入该阶段,你再按照某个特定程序来调整程序性能。

         在性能优化阶段中,你首先应该以一个量测工具监控程序的运行,让它告诉你程序中哪些地方大量消耗时间和空间。这样你就可以找出性能热点(hot spot)所在的一小段代码。然后你应该集中关切这些性能热点,并使用前述「持续关切法」中的优化手段来优化它们。由于你把注意力都集中在热点上,较少的工作量便可显现较好的成果。即便如此你还是必须保持谨慎。和重构一样,你应该小幅度进行修改。每走一步都需要编译、测试、再次量测。如果没能提高性能,就应该撤销此次修改。你应该继续这个「发现热点、去除热点」的过程,直到获得客户满意的性能为止。关于这项技术,McConnell [McConnell] 为我们提供了更多信息。

          一个被良好分解(well-factored)的程序可从两方面帮助此种优化形式。首先,它让你有比较充裕的时间进行性能调整(performance tuning),因为有分解良好的代码在手,你就能够更快速地添加功能,也就有更多时间用在性能问题上(准确的量测则保证你把这些时间投资在恰当地点)。其次,面对分解良好的程序,你在进行性能分析时便有较细的粒度(granularity),于是量测工具把你带入范围较小的程序段落中,而性能的调整也比较容易些。由于代码更加清晰,因此你能够更好地理解自己的选择,更清楚哪种调整起关键作用。

          我发现重构可以帮助我写出更快的软件。短程看来,重构的确会使软件变慢,但它使优化阶段中的软件性能调整更容易。最终我还是有赚头。

    六、重构起源何处?

          我曾经努力想找出重构(refactoring)一词的真正起源,但最终失败了。优秀程序员肯定至少会花一些时间来清理自己的代码。这么做是因为,他们知道简洁的代码比杂乱无章的代码更容易修改,而且他们知道自己几乎无法一开始就写出简洁的代码。

          重构不止如此。本书中我把重构看作整个软件开发过程的一个关键环节。最早认识重构重要性的两个人是Ward Cunningham和Kent Beck,他们早在1980s之前就开始使用Smalltalk,那是个特别适合重构的环境。Smalltalk是一个十分动态的环境,你可以很快写出极具功能的软件。Smalltalk的「编译/连结/执行」周期非常短,因此很容易快速修改代码。它是面向对象,所以也能够提供强大工具,最大限度地将修改的影响隐藏于定义良好的接口背后。Ward和Kent努力发展出一套适合这类环境的软件开发过程(如今Kent把这种风格叫作极限编程 [Beck, XP])。他们意识到:重构对于提高他们的生产力非常重要。从那时起他们就一直在工作中运用重构技术,在严肃而认真的软件项目中使用它,并不断精炼这个程序。

          Ward和Kent的思想对Smalltalk社群产生了极大影响,重构概念也成为Smalltalk文化中的一个重要元素。Smalltalk社群的另一位领袖是Ralph Johnson,伊利诺斯大学乌尔班纳分校教授,著名的「四巨头」 [Gang of Four] 之一。Ralph最大的兴趣之一就是开发软件框架(framework)。他揭示了重构对于灵活高效框架的开发帮助。

         Bill Opdyke是Ralph的博士研究生,对框架也很感兴趣。他看到重构的潜在价值,并看到重构应用于Smalltalk之外的其它语言的可能性。他的技术背景是电话交换系统的开发。在这种系统中,大量的复杂情况与时俱增,而且非常难以修改。Bill的博士研究就是从工具构筑者的角度来看待重构。通过研究,Bill发现:在C++ framework开发项目中,重构很有用。他也研究了极有必要的「语义保持性(semantics-preserving)重构」及其证明方式,以及如何以工具实现重构。时至今日,Bill的博士论文 [Opdyke] 仍然是重构领域中最有价值、最丰硕的研究成果。此外他为本书撰写了第13章。

          我还记得1992年OOPSLA大会上见到Bill的情景。我们坐在一间咖啡厅里,讨论当时我正为保健业务构筑的一个概念框架(conceptual framework)中的某些工作。Bill跟我谈起他的研究成果,我还记得自己当时的想法:『有趣,但并非真的那么重要』。唉,我完全错了。

          John Brant和Don Roberts将重构中的「工具」构想发扬光大,开发了一个名为「重构浏览器」(Refactoring Browser)的Smalltalk重构工具。他们撰写了本书第14章,其中对重构工具做了更多介绍。

          那么,我呢?我一直有清理代码的倾向,但从来没有想到这会有那么重要。后来我和Kent一起做了个项目,看到他使用重构手法,也看到重构对生产性能和产品质量带来的影响。这份体验让我相信:重构是一门非常重要的技术。但是,在重构的学习和推广过程中我遇到了挫折,因为我拿不出任何一本书给程序员看,也没有任何一位专家打算写出这样一本书。所以,在这些专家的帮助下,我写下了这本书。

          优化一个薪资系统— Rich Garzaniti
          将Chrysler Comprehensive Compensation(克莱斯勒综合薪资系统)交给GemStone公司之前,我们用了相当长的时间开发它。开发过程中我们无可避免地发现程序不够快,于是找了Jim Haungs — GemSmith中的一位好手 — 请他帮我们优化这个系统。

          Jim先用一点时间让他的团队了解系统运作方式,然后以GemStone的ProfMonitor特性编写出一个性能量测工具,将它插入我们的功能测试中。这个工具可以显示系统产生的对象数量,以及这些对象的诞生点。

          令我们吃惊的是:创建量最大的对象竟是字符串。其中最大的工作量则是反复产生12,000-bytes的字符串。这很特别,因为这字符串实在太大了,连GemStone惯用的垃圾回收设施都无法处理它。由于它是如此巨大,每当被创建出来,GemStone都会将它分页(paging)至磁盘上。也就是说字符串的创建竟然用上了I/O子系统(译注:分页机制会动用I/O),而每次输出记录时都要产生这样的字符串三次﹗

          我们的第一个解决办法是把一个12,000-bytes字符串缓存(cached)起来,这可解决一大半问题。后来我们又加以修改,将它直接写入一个file stream,从而避免产生字符串。

          解决了「巨大字符串」问题后,Jim的量测工具又发现了一些类似问题,只不过字符串稍微小一些:800-bytes、500-bytes……等等,我们也都对它们改用file stream,于是问题都解决了。

          使用这些技术,我们稳步提高了系统性能。开发过程中原本似乎需要1,000小时以上才能完成的薪资计算,实际运作时只花40小时。一个月后我们把时间缩短到18小时。正式投入运转时只花12小时。经过一年的运行和改善后,全部计算只需9小时。

          我们的最大改进就是:将程序放在多处理器(multi-processor)计算器上,以多线程(multiple threads)方式运行。最初这个系统并非按照多线程思维来设计,但由于代码有良好分解(well factored),所以我们只花三天时间就让它得以同时运行多个线程了。现在,薪资的计算只需2小时。

          在Jim提供工具使我们得以在实际操作中量度系统性能之前,我们也猜测过问题所在。但如果只靠猜测,我们需要很长的时间才能试出真正的解法。真实的量测指出了一个完全不同的方向,并大大加快了我们的进度。

    展开全文
    xgbing 2014-04-12 09:05:17
  • 【笔记】以下是通用的代码重构规则python代码重构技巧看这里:Python重构代码的一些模式==========================重构是项目做到 一定程度后必然要做的事情。代码重构,可以改善既有的代码设计,增强既有工程的可...

    【笔记】

    以下是通用的代码重构规则

    python代码重构技巧看这里:Python重构代码的一些模式

    ==========================

    重构是项目做到 一定程度后必然要做的事情。代码重构,可以改善既有的代码设计,增强既有工程的可扩充、可维护性。随着项目需求的不断迭代,需求的不断更新,我们在项目中 所写的代码也在时时刻刻的在变化之中。在一次新的需求中,你添加了某些功能模块,但这些功能模块有可能在下一次需求中不在适用。或者你因为需求迭代与变 更,使你原有的方法或者类变得臃肿,以及各个模块或者层次之间耦合度增加。此时,你要考虑重构了。

     

    重构,在《重构,改善既有代码的设计》这本经典的书中给出了定义,大概就是:在不改变代码对外的表现的情况下,修改代码的内部特征。说白了,就是我们的测试用例不变,然后我们对既有的代码的结构进行修改。重构在软件开发中是经常遇到的,也是非常重要的。在需求迭代,Debug,Code Review时,你都可以对你既有的代码进行重构。

     

    在接下来的几篇博文中,我想与大家一块去窥探一下代码重构的美丽,学习一下代码重构的一些规则。当然在每个规则中都有小的Demo, 在本篇博客以及相关内容的博客是使用Swift语言实现的。当然,和设计模式相同,重构重要的是手法和思想,和使用什么样的语言关系不大。经典的重构书籍中是使用Java语言来实现的,如果你对PHP, Python等其他语言比较熟悉,完全可以使用这些语言来测试一些重构手法。

     

    本篇博客的主题就是通过一些列的重构手法,对既有的需要重构的函数或者方法进行重 构。并且会将每个示例在GitHub上进行分享,感兴趣的小伙伴可以对其进行下载。有的小伙伴说了,我没有Mac,怎么对你写的Swift代码进行编译 呢?这个问题好解决,你可以看我之前发表的这篇博客窥探Swift之使用Web浏览器编译Swift代码以及Swift中的泛型。你可以将相关代码进行拷贝,在浏览器中观察结果。因为在线编译的网站是国外的,访问起来也许会有一些卡顿,不过是可以用的。好前面扯了这么多了,进入今天的主题。

    目录:

    一、将大函数按模块拆分成几个小的函数

    二、将微不足道的小函数通过inline进行整合

    三、将一些临时变量使用函数替换(以函数查询替代临时变量)

    四、将复杂表达式拆分成多个表达式(以多个解释性临时变量拆分复杂表达式)

    五、将在不同语义下具有不同含义的临时行变量进行拆分

    六、移除对函数参数的赋值(引入另一个临时性变量)

    七、以对象取代函数

     

    一、Extract Method(提取函数)-------将大函数按模块拆分成几个小的函数

    Extract Method被翻 译成中文就是提取函数的意思,这一点在代码重构中用的非常非常的多。在重构时提倡将代码模块进行细分,因为模块越小,可重用度就越大。不要写大函数,如果 你的函数过大,那么这意味着你的函数需要重构了。因为函数过大,可维护性,可理解性就会变差。并且当你实现类似功能的时候就容易产生重复代码。写代码时, 最忌讳的就是代码重复。这也就是经常所说的DRY(Don`t Repeat Yourself)原则。所以当函数过长时,你需要将其细分,将原函数拆分成几个函数。

     

    下 方将会通过一个示例来直观的感受一下Extract Method,当然这些示例不是我原创的,是《重构:改善既有代码的设计》中Java示例演变的Swift版,在写Swift代码时,对原有的示例进行了 一些修改,算是伪原创吧。不过目的只有一个:希望与大家交流分享。实在是没有必要再找其他的例子说明这些重构规则,因为《重构:改善既有的代码的设计》这 本书真的是太经典了。

     

    1.需要重构的代码如下所示。下方代码中的MyCustomer类中有两个常量属性,并且该类提供了一个构造器。该类还提供了一个输出方法,就是第一该类中的属性进行打印说明,其实该类中没有什么功能。

      

     

    在写好需要重构的类后,我们要为该类写一个测试用例。这便于在我们重构时对重构的 正确性进行验证,因为每次重构后都要去执行该测试用例,以保证我们重构是正确的。下方截图就是为上方示例写的测试用例以及该测试用例的打印结果。当然重构 后我们也需要调用该测试用例,并观察打印结果是否与之前的一致。当然如果你不想自己观察,你可以为上面的类添加相应的单元测试,这也是在常规项目中经常使 用的。至于如果添加测试用例,我们会在后面的博客中进行详细介绍。下方就是上述类的测试用例和输出结果:

          

     

    2.接下来我们对上面类中的printOwning函数进行分析。上述类可以正常 工作,这是肯定的。但是printOwning()函数写的不够好。因为它干了太多的事情,也就是说该函数包括了其他子模块,需要对其进行拆分。由上面截 图中的红框可以看出,每个红框就代表着一个独立的功能模块,就说明这一块代码可以被拆分成独立的函数。在拆分子函数时,我们要为该函数起一个与改代码块功 能相符合的名字。也就是说当你看到该函数名字时,你就应该知道该函数是干嘛的。

     

    下方代码段就是我们重构后的类。说白的,就是对函数中可以进行独立的模块进行提 取,并为提取的新的函数命一个合适的名称。经过重构后printOwing()函数就只有两行代码,这样看其中调用的函数名也是很容易理解其作用的。下方 拆分出来的三个函数也是一个独立的模块,因为函数短小,所以易于理解,同时也易于重用。经过Extract Method,当然好处是多多的。经过重构后的代码,我在调用上述的测试用例,输出结果和原代码是一直的,如果不一致的话,那么说明你的重构有问题呢,需 要你进行Debug。

         

     

     

    二. Inline Method ---- 内联函数:将微不足道的小函数进行整合

    看过《周易》的小伙伴应该都知道,《周易》所表达的思想有一点就是“物极必反”。 《周易》中的六十四卦中的每一卦的“上九”(第六位的阳爻)或者“上六”(第六位的阴爻)都是物极必反的表现。其实《周易》其实就是计算机科学中二进制的 表象,因为太极生两仪(2进制中的2),两仪生四象(2的平方为4),四象生八卦(4 x 2 = 8),八卦有演变出六十四卦。六十四卦的就是2进制中的0-1排列。九五至尊,九六就物极必反了。wo kao, 扯远了,言归正传,当然这提到《周易》不是说利用周易如何去算卦,如何去预测,本宝宝不信这东西。不过《周易》中的哲学还是很有必要学习一下的。有所取, 有所不取。

     

    回到本博客的主题,Inline Method其实是和Extract Method相对的。当你在重构或者平时编程时,对模块进行了过度的封装,也就是使用Extract Method有点过头了,把过于简单的东西进行了封装,比如一个简单的布尔表达式,而且该表达式只被用过一次。此时就是过度的使用Extract Method的表现了。物极必反,所以我们需要使用Inline Method进行中和,将过度封装的函数在放回去,或者将那些没有必要封装的函数放回去。也就是Extract Method相反的做法。

    至于Inline Method规则的示例呢,在此就不做过多的赘述了,因为只需要你将Extract Method的示例进行翻转即可。

     

    三.Replace Temp with Query----以查询取代临时变量: 将一些临时变量使用函数替代

    1.Replace Temp with Query说白了就是将那些有着复杂表达式赋值并且多次使用的临时变量使用查询函数取代,也就是说该临时变量的值是通过函数的返回值来获取的。这样一来在 实现类似功能的函数时,这些复杂的临时变量就可以进行复用,从而减少代码的重复率。下方就是Replace Temp with Query规则的一个特定Demo,接下来我们要对getPrice()函数使用Replace Temp with Query规则进行重构。

      

     

     

    对上面的小的demo创建对应的测试用例是少不了的,因为我们要根据测试用例还测试我重构后的代码是否一致,下方截图就是该代码的测试用例以及输出结果,具体如下所示。

       

     

    2.接下来就是对Procut类中的getPrice()函数进行分析并重构了。在getPrice()函数中的第一个红框中有一个 basePrice临时常量,该常量有一个较为复杂的赋值表达式,我们可以对其使用Replace Temp with Query进行重构,可就是创建一个函数来返回该表达式的值。第二个红框中的discountFactor临时变量被多次使用,我们可以对其通过 Replace Temp with Query规则进行重构,具体重构后的代码如下所示。

    由重构后的代码容易看出,上面我们提到的临时常量或者变量都不存在了,取而代之的是两个查询方法,对应的查询方法返回的就是之前消除的临时变量或常量的值。

       

     

    四、Inline Temp ---内联临时变量:与上面的Replace Temp with Query相反

    当临时变量只被一个简单的表达式赋值,而该临时变量妨碍了其他重构手法。此时我们就不应 该使用Replace Temp with Query。之所以有时我们会使用到Inline Temp规则,是因为Replace Temp with Query规则使用过度造成的情况,还是物极必反,使用Replace Temp with Query过度时,就需要使用Inline Temp进行修正,当然Inline Temp的示例与Replace Temp with Query正好相反,在此就不做过多的赘述了。

     

    五、Introduce Explaining Variable---引入解释性变量:将复杂的表达式拆分成多个变量

    当一个函数中有一个比较复杂的表达式时,我们可以将 表达式根据功能拆分成不同的变量。拆分后的表达式要比之前未拆分的表达式的可读性更高。将表达式拆分成相应的临时变量,也就是Introduce Explaining Variable,如果临时变量被多次使用的话,我们还可以尝试着使用Replace Temp with Query规则去除临时变量,也就是将临时变量换成查询函数。

    1.在下方Product类中的getPrice()方法中返回了一个比较长的表达式,第一眼看这个函数感觉会非常的不舒服。因为它返回的表达式太长了,而且可读性不太好。在这种情况下就很有必要将该表达式进行拆分。

       

     

    2.接下来就可以使用Introduce Explaining Variable规则,引入解释性变量。顾名思义,我们引入的变量是为了解释该表达式中的一部分的功能的,目的在于让该表达式具有更好的可读性。使用 Introduce Explaining Variable规则,就相当于为该表达式添加上相应的注释。下方截图就是使用 Introduce Explaining Variable规则进行重构后的结果。

       

     

    3.引入临时变量是为了更好的可读性,如果临时变量所代表的表达式多次使用,我们就可以对上述函数在此使用Replace Temp with Query规则进行重构。也就是去除经常使用而且表达式比较复杂的临时变量,下方代码段是对上述函数进行Replace Temp with Query重构,去掉临时变量,再次重构后的结果如下所示。

        

     

    六、Split Temporary Variable-----分解临时变量:一心不可二用

    什么叫分解临时变量的,具体说来就是在一个函数中一个临时变量不能做两种事 情,也就是一个临时变量不能赋上不同意义的值。如果你这么做了,那么对不起,请对该重复使用的临时变量进行分解,也就是说你需要创建一个新的临时变量来接 收第二次分配给第一个临时变量的值,并为第二个临时变量命一个确切的名字。

    下方第一个函数是重构前的,可以看出temp被重复的赋值了两次的值,如果 这两个值关系不大,而且temp不足以对两个值的意思进行说明。那么就说明该段代码就应该被重构了。当然,重构的做法也是非常简单的,只需要术业有专攻即 可,各司其职,并且为每个临时变量命一个合适的名字即可。具体做法如下所示。

     

     

     七、Remove Assignments to Parameters----移除对参数的赋值

    “移除对参数的赋值”是什么意思呢?顾名思义,就是在函数中不要对函数参数 进行赋值。也就是说你在函数的作用域中不要对函数的参数进行赋值(当然,输入输出参数除外),当直接对函数的参数进行修改时,对不起,此时你应该对此重 构。因为这样会是参数的原始值丢失,我们需要引入临时变量,然后对这个临时变量进行操作。

    1.下方这个discount()函数就做的不好,因为在 discount()函数中直接对非inout参数inputVal进行了修改并且返回了,我们不建议这样做。遇到这种情况,我们需要使用Remove Assignments to Parameters规则对下方的函数进行重构。

       

      

    2.当然重构的手法也特别简单,就是需要将上面的inputVal使用函数的临时变量进行替代即可,下方就是重构后的函数。

       

     

    八.Replace Method with Method Object----以函数对象取代函数

    当一个特别长的函数,而且函数中含有比较复杂的临时变量,使用上述那些方法 不好进行重构时,我们就要考虑将该函数封装成一个类了。这个对应的类的对象就是函数对象。我们可以将该场函数中的参数以及临时变量转变成类的属性,函数要 做的事情作为类的方法。将函数转变成函数类后,我们就可以使用上述的某些方法对新的类中的函数进行重构了。具体做法请看下方示例。

    1.下方示例中的discount函数有过多的参数(当然,现实项目工程中参数比这个还要多),并函数中含有多个临时变量,假设函数功能比较复杂,而且比较长。下方示例对该函数使用上述那些规则进行重构会比较复杂,此时我们就可以将该函数抽象成一个类。

       

     

    2.重构的第一步就是将上述discount()函数抽象成Discount类。在Discount类中有六个属性,这六个属性分别对应着 discount()函数的不同参数。除了添加参数属性外,我们在函数类提取时还添加了一个Account的委托代理对象。该委托代理对象是为了在 Discount类中访问Account类中依赖的数据,下方是第一次重构后的代码。

       

     

    3.接着,我们就可以在新的Discount类中的compute()方法中使用 我们上述介绍的规则进行重构。对compute()方法进行分析,我们发现importandValue等属性是可以通过Replace Temp with Qurey 规则进行消除的。所为我们可以再次对上述方法进行重构,重构后的具体代码如下:

         

     

    今天的博客主要讲了如何对既有代码中的函数进行重构,在本篇博客中提到了8大规 则。这8大规则在函数代码重构时时非常实用的,并且也是非常重要的。还是那句话,虽然代码是使用Swift语言实现的,但是代码重构的手法和思想和语言无 关。接下来还会继续更新关于代码重构的博客,敬请期待吧。

    转自:https://www.cnblogs.com/tianzhiyi/p/5340869.html

    展开全文
    ztf312 2018-01-06 15:43:18
  • qq_44537408 2021-01-21 22:05:29
  • devcloud 2020-12-21 11:16:06
  • m0_55208453 2021-07-28 08:04:58
  • Giberson1 2012-11-08 09:10:23
  • cym492224103 2016-08-30 13:48:23
  • lvlei19911108 2021-04-29 11:29:49
  • digyso888 2009-04-12 12:05:00
  • ccsss22 2020-09-08 04:07:52
  • ByteDanceTech 2021-05-20 00:35:51
  • dilla_lmj 2021-09-07 14:32:20
  • ByteDanceTech 2021-06-17 00:38:05
  • Pieces_thinking 2021-10-24 01:09:07
  • weixin_33599503 2021-05-21 06:47:16
  • FanceFu 2018-02-23 21:06:03
  • pengbin790000 2020-12-28 20:27:29
  • github_38592071 2021-07-18 00:30:50
  • weixin_38405253 2021-03-28 00:15:14
  • yytoo2 2019-02-26 12:47:23
  • likandmydeer 2012-07-27 13:53:17
  • weixin_33160876 2021-05-06 03:59:13
  • qq_35616167 2018-06-09 11:33:27
  • GarfieldEr007 2017-10-15 18:44:32

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 24,501
精华内容 9,800
关键字:

原图重构软件