天天看點

偷偷看了同僚的代碼找到了優雅代碼的秘密

作者:慕楓技術筆記

#頭條創作挑戰賽#

引言

對于一個軟體平台來說,軟體平台代碼的好壞直接影響平台整體的品質與穩定性。同時也會影響着寫代碼同學的創作激情。想象一下如果你從 git 上面 clone 下來的的工程代碼亂七八糟,代碼晦澀難懂,難以快速上手,有種想推到重寫的沖動,那麼程式猿在這個工程中寫好代碼的初始熱情都沒了。相反,如果 clone 下的代碼結構清晰,代碼優雅易懂,那麼你在寫代碼的時候都不好意思寫爛代碼。這其中的差别相信工作過的同學都深有體會,那麼我們看了那麼多代碼之後,到底什麼樣的代碼才是好代碼呢?它們有沒有一些共同的特征或者原則?本文通過闡述優雅代碼的設計原則來和大家聊聊怎麼寫好代碼。

代碼設計原則

好代碼是設計出來的,也是重構出來的,更是不斷疊代出來的。在我們接到需求,經過概要設計過後就要着手進行編碼了。但是在實際編碼之前,我們還需要進行領域分層設計以及代碼結構設計。那麼怎麼樣才能設計出來比較優雅的代碼結構呢?有一些大神們總結出來的優雅代碼的設計原則,我們分别來看下。

SRP

所謂 SRP(Single Responsibility Principle)原則就是職責單一原則,從字面意思上面好像很好了解,一看就知道什麼意思。但是看的會不一定就代表我們就會用,有的時候我們以為我們自己會了,但是在實際應用的時候又會遇到這樣或者那樣的問題。原因就是實際我們沒有把問題想透,沒有進行深度思考,知識還隻是知識,并沒有轉化為我們的能力。就比如這裡所說的職責單一原則指的是誰的單一職責,是類還是子產品還是域呢?域可能包含多個子產品,子產品也可以包含多個類,這些都是問題。

為了友善進行說明,這裡以類來進行職責單一設計原則的說明。對于一個類來說,如果它隻負責完成一個職責或者功能,那麼我們可以說這個類時符合單一職責原則。請大家回想一下,其實我們在實際的編碼過程中,已經有意無意地在使用單一職責設計原則了。因為實際它是符合我們人思考問題的方式的。為什麼這麼說呢?想想我們在整理衣櫃的時候,為了友善拿衣服我們會把夏天的衣服放在一個櫃子中,冬天的衣服放在一個櫃子。這樣季節變化的時候,我們隻要到對應的櫃子直接拿衣服就可以了。否則如果冬天和夏天的衣服都放在一個櫃子中,我們找衣服的時候可就費勁了。放到軟體代碼設計中,我們也需要采用這樣的分類思維。在進行類設計的時候,要設計粒度小、功能單一的類,而不是大而全的類。

舉個栗子,在學生管理系統中,如果一個類中既有學生資訊的操作比如建立或者删除動作,又有關于課程的建立以及修改動作,那麼我們可以認為這個類時不滿足單一職責的設計原則的,因為它将兩個不同業務域的業務混雜在了一起,是以我們需要進行拆分,将這個大而全的類拆分為學生以及課程兩個業務域,這樣粒度更細,更加内聚。

偷偷看了同僚的代碼找到了優雅代碼的秘密

筆者根據自身的經驗,總結了需要考慮進行單一職責拆分的幾個場,希望對大家判斷是否需要進行拆分有個簡單的判斷的标準:

1、不同的業務域需要進行拆分,就像上面的例子,另外如果與其他類的依賴過多,也需要考慮是不是應該進行拆分;

2、如果我們在類中編寫代碼的時候發現私有方法具有一定的通用性,比如判斷 ip 是不是合法,解析 xml 等,那我們可以考慮将這些方法抽出來形成公共的工具類,這樣其他類也可以友善地進行使用。

另外單一職責的設計思想不止在代碼設計中使用,我們在進行微服務拆分的時候也會一定程度地遵循這個原則。

OCP

OCP(Open Closed Principle)即對修改關閉,對擴充開放原則,個人覺得這是設計原則中最難的原則。不僅了解起來有一定的門檻,在實際編碼過程中也是不容易做到的。

首先我們得先搞清楚這裡的所說的修改以及擴充的差別在什麼地方,說實話一開始看到這個原則的時候,我總覺得修改和開放說的不是一個意思嘛?想來想去都覺得有點迷糊。後來在不斷的項目實踐中,對這個設計原則的了解逐漸加深了。

偷偷看了同僚的代碼找到了優雅代碼的秘密

設計原則中所說的修改指的是對原有代碼的修改,而擴充指的是在原有代碼基礎上的能力的擴充,并不修改原先已有的代碼。這是修改與擴充的最大的差別,一個需要修改原來的代碼邏輯,另一個不修改。是以才叫對修改關閉但是對擴充開放。弄清楚修改和擴充的差別之後,我們再想一想為什麼要對修改關閉,而要對擴充開放呢? 我們都知道軟體平台都是不斷進行更新疊代的,是以我們需要不斷在原先的代碼中進行開發。

那麼就會涉及到一個問題如果我們的代碼設計得不好,擴充性不強,那麼每次進行功能疊代的時候都會修改原先已有的代碼,有修改就有可能引入 bug,造成系統平台的不穩定。是以我們為了平台的穩定性,需要對修改關閉。但是我們要添加新的功能怎麼辦呢?那就是通過擴充的方式來進行,是以需要實作對擴充開放。

這裡我們以一個例子來進行說明,否則可能還是有點抽象。在一個監控平台中,我們需要對服務所占用 CPU、記憶體等運作資訊進行監控,第一版代碼如下。

public class Alarm {
	private AlarmRule alarmRule;
    private AlarmNotify alarmNotify;
    
    public Alarm(AlarmRule alarmRule, AlarmNotify alarmNotify) {
        this.alarmRule = alarmRule;
        this.alarmNotify = alarmNotify;
    }
    
    public void checkServiceStatus(String serviecName, int cpu, int memory) {
        if(cpu > alarmRule.getRule(ServiceConstant.Status).getCpuThreshold) {
            alarmNotify.notify(serviecName +  alarmRule.getRule(ServiceConstant.Status).getDescription)
        }
        
         if(memory > alarmRule.getRule(ServiceConstant.Status).getMemoryThreshold) {
            alarmNotify.notify(serviecName +  alarmRule.getRule(ServiceConstant.Status).getDescription)
        } 
    
    }


}

           

代碼邏輯很簡單,就是根據對應的告警規則中的門檻值判斷是否達到觸發告警通知的條件。如果此時來了個需求,需要增加判斷的條件,就是根據服務對應的狀态,判斷需不需要進行告警通知。我們來先看下比較 low 的修改方法。我們在 checkServiceStatus 方法中增加了服務狀态的參數,同僚在方法中增加了判斷狀态的邏輯。

public class Alarm {
	private AlarmRule alarmRule;
    private AlarmNotify alarmNotify;
    
    public Alarm(AlarmRule alarmRule, AlarmNotify alarmNotify) {
        this.alarmRule = alarmRule;
        this.alarmNotify = alarmNotify;
    }
    
    public void checkServiceStatus(String serviecName, int cpu, int memory, int status) {
        if(cpu > alarmRule.getRule(ServiceConstant.Status).getCpuThreshold) {
            alarmNotify.notify(serviecName +  alarmRule.getRule(ServiceConstant.Status).getDescription)
        }
        
         if(memory > alarmRule.getRule(ServiceConstant.Status).getMemoryThreshold) {
            alarmNotify.notify(serviecName +  alarmRule.getRule(ServiceConstant.Status).getDescription)
        } 
        
         if(status == alarmRule.getRule(ServiceConstant.Status).getStatusThreshold) {
            alarmNotify.notify(serviecName +  alarmRule.getRule(ServiceConstant.Status).getDescription)
        } 
    
    }


}

           

很顯然這種修改方法非常的不友好,為什麼這麼說呢?首先修改了方法參數,那麼調用該方法的地方可能也需要修改,另外如果改方法有單元測試方法的話,單元測試用例必定也需要修改,在原有測試過的代碼中添加新的邏輯,也增加了 bug 引入的風險。是以這種修改的方式我們需要進行避免。那麼怎麼修改才能夠展現對修改關閉以及對擴充開放呢?

首先我們可以先将關于服務狀态的屬性抽象為一個 ServiceStatus 實體,在對應的檢查方法中以 ServiceStatus 為入參,這樣以後如果還有服務狀态的屬性增加的話,隻需要在 ServiceStatus 中添加即可,并不需要修改方法中的參數以及調用方法的地方,同樣單元測試的方法也不用修改。

@Data
public class ServiceStatus {
    String serviecName;
    int cpu;
    int memory;
    int status;

}
           

另外在檢測方法中,我們怎麼修改才能展現可擴充呢?而不是在檢測方法中添加處理邏輯。一個比較好的實作方式就是通過抽象檢測方法,具體地實作在各個實作類中。這樣即使新增檢測邏輯,隻需要擴充檢測實作方法就可,不需要在修改原先代碼的邏輯,實作代碼的可擴充。

LSP

LSP(Liskov Substitution Principle)裡氏替換原則,這個設計原則我覺得相較于前面的兩個設計原則來說要簡單些。它的内容為子類對象(object of subtype/derived class)能夠替換程式(program)中父類對象(object of base/parent class)出現的任何地方,并且保證原來程式的邏輯行為(behavior)不變及正确性不被破壞。

裡式替換原則是用來指導,繼承關系中子類該如何設計的一個原則。了解裡式替換原則,最核心的就是了解“design by contract,按照協定來設計”這幾個字。父類定義了函數的“約定”(或者叫協定),那子類可以改變函數的内部實作邏輯,但不能改變函數原有的“約定”。這裡的約定包括:函數聲明要實作的功能;對輸入、輸出、異常的約定;甚至包括注釋中所羅列的任何特殊說明。

我們怎麼判斷有沒有違背 LSP 呢?我覺得有兩個關鍵點可以作為判斷的依據,一個是子類有沒有改變父類申明需要實作的業務功能,另一個是否違反父類關于輸入、輸出以及異常抛出的規定。

ISP

ISP(Interface Segregation Principle)接口隔離原則,簡單了解就是隻給調用方需要的接口,它不需要的就不要硬塞給他了。這裡我們舉個栗子,以下是關于産品的接口,其中包含了建立産品、删除産品、根據 ID 擷取産品以及更新産品的接口。如果此時我們需要對外提供一個根據産品的類别擷取産品的接口,我們應該怎麼辦?很多同學會說,這還不簡單,我們直接在這個接口裡面添加根據類别查詢産品的接口就 OK 了啊。大家想想這個方案有沒有什麼問題。

public interface ProductService { 
    boolean createProduct(Product product); 
    boolean deleteProductById(long id); 
    Product getProductById(long id); 
    int updateProductInfo(Product product);
}


public class UserServiceImpl implements UserService { //...}

           

這個方案看上去沒什麼問題,但是再往深處想一想,外部系統隻需要一個根據産品類别查詢商品的功能,,但是實際我們提供的接口中還包含了删除、更新商品的接口。如果這些接口被其他系統誤調了可能會導緻産品資訊的删除或者誤更新。是以我們可以将這些第三方調用的接口都隔離出來,這樣就不存在誤調用以及接口能力被無序擴散的情況了。

public interface ProductService { 
    boolean createProduct(Product product); 
    boolean deleteProductById(long id); 
    Product getProductById(long id); 
    int updateProductInfo(Product product);
}


public interface ThirdSystemProductService{
    List<Product> getProductByType(int type);
}


public class UserServiceImpl implements UserService { //...}

           

LOD

LOD(Law of Demeter)即迪米特法則,這是我們要介紹的最後一個代碼設計法則了,光從名字上面上看,有點不明覺厲的感覺,看不出來到底到底表達個什麼意思。我們可以來看下原文是怎麼描述這個設計原則的。

Each unit should have only limited knowledge about other units: only units “closely” related to the current unit. Or: Each unit should only talk to its friends; Don’t talk to strangers.

按照我自己的了解,這迪米特設計原則的最核心思想或者說最想達到的目的就是盡最大能力減小代碼修改帶來的對原有的系統的影響。是以需要實作類、子產品或者服務能夠實作高内聚、低耦合。不該有直接依賴關系的類之間,不要有依賴;有依賴關系的類之間,盡量隻依賴必要的接口。迪米特法則是希望減少類之間的耦合,讓類越獨立越好。每個類都應該少了解系統的其他部分。一旦發生變化,需要了解這一變化的類就會比較少。打個比方這就像抗戰時期的的地下組織一樣,相關聯的聚合到一起,但是與外部保持盡可能少的聯系,也就是低耦合。

偷偷看了同僚的代碼找到了優雅代碼的秘密

總結

本文總結了軟體代碼設計中的五大原則,按照我自己的了解,這五大原則就是程式猿代碼設計的内功,而二十三種設計模式實際就是内功催生出來的程式設計招式,是以深入了解五大設計原則是我們用好設計模式的基礎,也是我們在平時設計代碼結構的時候需要遵循的一些常見規範。隻有不斷的在設計代碼-》遵循規範-》編寫代碼-》重構這個循環中磨砺,我們才能編寫出優雅的代碼。