淺談代碼結構的設計


本文來自網易雲社區

 

作者:陸秋煒

引言 :很久之前,在做中間件測試的時候,看到開發人員寫的代碼,有人的代碼,看起來總是特別舒服,但有的開發代碼,雖然邏輯上沒有什么問題,但總給人感覺特別難受。后來成為了一位專職開發人員,漸漸發現,自己的代碼也是屬於“比較難受”的那種。后來隨着代碼的增加,編寫代碼時,總有一些比較乖巧的方式,這就是之前不懂的“設計模式”。之前代碼架構比較少(只是寫一些測試工具),用不到這些,只有自己慢慢做了一些架構工作后,才用得到,並去主動了解。

 

但今天想說的,並不是具體的哪一種設計模式的優劣,而是想記錄一下,設計模式中存在的一些設計思想。有了這些設計思想,某些設計模式就自然而然的出現了。所以說,所謂的“設計模式”並不是被發明出來的,而是被我們自己“發現”的。

 

一,設計是一個逐步分解的過程,而不是一個功能合成的過程

之前無論是作為開發還是測試,習慣性的覺得,別人提供了什么功能,就用什么樣的功能,這樣做天經地義。然而,在自己的架構設計過程中,如果有了這樣額思維,很容易讓自己的程序設計陷入困境。

打個裝修的比喻,我們一定是有設計師設計相關方案(具體的風格),然后分解成對應的家具,然后再購買材料,打造對應的家具。如果我們將這一過程倒過來,先有什么材料,然后看這些材料能打造出什么家具,再把家具組合起來,那么最后的裝修效果一定會非常差。

圖1 正確的設計方式

圖2 自底向上的設計結果,一定是最后的整合有問題

所以優秀的設計一定是從整體到局部設計出來的。從局部構造整體,不可能得到優秀的設計。

 

二:對於一個整體的概念性理解,一定是在理解最初的功能(實現目標)為基礎的

了解清楚某個功能模塊(或者整個功能)具體要干什么事情,我們才能夠知道具體要如何做設計。而不是找一個設計方案,能夠實現主要功能就行了,其他功能再次基礎上修修補補。

再舉一個簡單的例子:比如說我們要喝水(表面功能/基礎目標),那么我們就需要找相關盛水的容器(設計實現)。我們找到了以下容器(可能的實現方案):

圖三 各種盛水容器的實現

三種容器都能喝水,但具體要使用哪個呢?如果隨便選一個酒杯,但具體實現(或者未來可能的功能)要求能夠帶到戶外去,總不能給酒杯再加個蓋子吧;同理,如果我們要品酒,卻選了個保溫杯的實現,到時候直接設計推倒重來了。所以,要有合適的設計,一定要對產品本身的需求(以及未來可能的需求)做詳細的分析和了解,然后確定設計方案。

 

三:在設計關聯關系時,優先使用對象組合,而非繼承關系

在學習“面向對象”的語言時,我們首先被教會“封裝、繼承、多態”。從此,感覺有點關系的都要進行繼承,覺得這樣能節省好多代碼。然后我們的代碼中便出現了繼承的亂用

正常情況下,這樣做沒有問題,但問題的起源在於,我們的需求是不斷的修改和添加的,如果使用了繼承,在超類中的方法改動,會影響到子類,並可能引起引起子類之間出現冗余代碼。

舉個汽車的例子吧,一輛汽車開行(drive)是一樣的,但車標(logo)是不一樣的,所以用繼承

public abstract class Car {    /**
     * 駕駛汽車
     */
    public void drive(){
        System.out.print("drive");
    }    /**
     * 每輛車的車標是不一樣的,所以抽象
     */
    public abstract void logo() ;
}class BMW extends Car{    @Override
    public void logo() {
        System.out.print("寶馬");
    }
}class Benz extends Car{    @Override
    public void logo() {
        System.out.print("奔馳");
    }
}class Tesla extends Car{    @Override
    public void logo() {
        System.out.print("特斯拉");
    }
}

一切看起來解決的很完美。突然加了一個需求,要求有個充電(change)需求,這時候,只有特斯拉(tesla)才有充電方法。但如果使用繼承,在父類添加change方法的同時,就需要在BMW和Benz實現無用的change方法,對於子類的影響非常大。但如果使用組合,使用ChangeBehavior,問題就得到了有效解決,

public interface ChargeBehavior {    void charge() ;
}public abstract class Car {    protected ChargeBehavior chargeBehavior ;    /**
     * 駕駛汽車
     */
    public void drive(){
        System.out.print("drive");
    }    /**
     * 每輛車的車標是不一樣的,所以抽象
     */
    public abstract void logo() ;    /**
     * 充電
     */
    public void change(){        /**
         * 不用關心具體充電方式,委托ChargeBehavior子類實現
         */
        if (chargeBehavior!=null) {
            chargeBehavior.charge();
        }
    }

}class Benz extends Car{    @Override
    public void logo() {
        System.out.print("奔馳");
    }
}class BMW extends Car{    @Override
    public void logo() {
        System.out.print("寶馬");
    }
}class Tesla extends Car{    @Override
    public void logo() {
        System.out.print("特斯拉");
    }    public Tesla() {        super();
        chargeBehavior = new TeslaChargeBehavior() ;
    }
}class TeslaChargeBehavior implements ChargeBehavior{    @Override
    public void charge() {
        System.out.print("charge");
    }
}

通過將充電的行為委托給changeBehavior接口,子類如果不需要的話,就可以做到無感知接入。

這樣的代碼有三個優勢

  • 1,代碼不需要子類中重復實現

  • 2,子類不想要的東西,可以無感知實現

  • 3,子類運行的行為,可以委托給behavior實現,子類本省本身無需任何改動

 

四:對於接口和類的再次理解

在剛剛接觸面向對象的時候,封裝,對我們來說就是類,實例化后就是對象。最基本功能是對於數據進行隱藏,對於行為進行開放(如JavaBean)。慢慢用多了以后漸漸發現,其實我們可以封裝跟多東西,比如某些實現的細節(私有方法方法),實例化規則(構造器)等。

1,對於變化本身進行封裝

由於我們的代碼是分層和分模塊的,但我們的需求又是經常要變化的,我們希望修改新功能,對於除了模塊本身外,調用方是無感知的。所以,我們的類(或者說是模塊吧)變封裝了變化本身。對於調用方來說,只需要知道不會變的功能名(方法名)就夠了,而不需要了解可能變化的內容。

 

圖四 變化本身進行封裝

2,從共性和可變性到抽象類

在一類實現中,我們其實可以分析發現,代碼的實現上是有一些共性的,比如說處理的流程(如何調用一些方法的順序),也有一些完全一致的操作(比如上文提到的car都可以drive,實現一致的方法)。但也有一些可變性:如必須存在(共性),但實現不一致的操作(如上文car里面的logo方法,必須有,但不一致)。這時候,我們就可以對這些實現進行一些簡單的抽象,成為抽象類。抽象類就是將共性變為以實現的方法,而將可變性變為抽象方法,讓子類予以實現。

圖五,共性和抽象類

 

總結:

代碼看多了,寫多了,便會發現,看起來舒服的代碼,在可維護性,可讀性,可擴展性上相對來說都比較高。代碼界也有“顏值即戰斗力”這一說法,頗有一番玄學的味道。但分析具體的原因,其實可以發現,優秀的編碼設計,在其抽象,封裝,都有其合理之處,其整體的架構設計上,亦有其獨到之處。

 

網易雲大禮包:https://www.163yun.com/gift

本文來自網易雲社區,經作者陸秋煒授權發布

 

相關文章:
【推薦】 流式斷言器AssertJ介紹


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM