java核心技術(shù)筆記

第一章、面向?qū)ο笪宕蠡驹瓌t SOLID原則

1.1 單一職責(zé)

參考:http://www.itdecent.cn/p/63bd557f6ca4
單一職責(zé)原則的英文是 Single Responsibility Principle,縮寫為 SRP。他的英文描述是:A class or module should have a single reponsibility。翻譯過來就是:一個類或者模塊只負責(zé)完成一個職責(zé)(或者功能)。

意思是一個類只負責(zé)完成一個職責(zé)或者功能。也就是說,不要設(shè)計大而全的類,要設(shè)計粒度小、功能單一的類。換個角度來講就是,一個類包含了兩個或者兩個以上業(yè)務(wù)不相干的功能,那我們就說它職責(zé)不夠單一,應(yīng)該將它拆分成多個功能更加單一、粒度更細的類。

這個原則看似比較簡單,但實際在開發(fā)中判斷一個類是否單一是比較難拿捏的。因為在不同的應(yīng)用場景和業(yè)務(wù)的不同階段,對同一個類職責(zé)判斷是否單一都是有可能不一樣的。在當(dāng)前需求下一個類可能已經(jīng)滿足了單一職責(zé)的判斷,但如果換一個應(yīng)用場景或者未來的某個需求背景下可能就不滿足了,需要繼續(xù)拆分成粒度更細的類。所以通常我們可以先寫一個粗粒度的類,滿足業(yè)務(wù)需求。隨著業(yè)務(wù)的發(fā)展如果粗粒度的類越來越龐大的時候,我們再將這個粗粒度的類拆分成幾個更細粒度的類。

判斷一個類是否需要拆分有以下幾個原則供參考:

  • 類中的代碼行數(shù)、函數(shù)或?qū)傩赃^多,會影響代碼的可讀性和可維護性,我們就需要考慮對類進行拆分;
  • 類依賴的其他類過多,或者依賴類的其他類過多,不符合高內(nèi)聚、低耦合的設(shè)計思想,我們就需要考慮對類進行拆分;
  • 私有方法過多,我們就要考慮能否將私有方法獨立到新的類中,設(shè)置為 public 方法,供更多的類使用,從而提高代碼的復(fù)用性;
  • 比較難給類起一個合適名字,很難用一個業(yè)務(wù)名詞概括,或者只能用一些籠統(tǒng)的 Manager、Context 之類的詞語來命名,這就說明類的職責(zé)定義得可能不夠清晰;
  • 類中大量的方法都是集中操作類中的某幾個屬性,那就可以考慮將這幾個屬性和對應(yīng)的方法拆分出來。

1.2 開閉原則

開閉原則的英文全稱是 Open Closed Principle,簡寫為 OCP。它的英文描述是:software entities (modules, classes, functions, etc.) should be open for extension , but closed for modification。翻譯過來就是:軟件實體(模塊、類、方法等)應(yīng)該“對擴展開放、對修改關(guān)閉”。

這個描述比較簡略,詳細表述一下就是:添加一個新的功能應(yīng)該是在已有代碼基礎(chǔ)上擴展代碼(新增模塊、類、方法等),而非修改已有代碼(修改模塊、類、方法等)。

這里說的不修改已有代碼并不是指完全杜絕修改代碼,在軟件開發(fā)需求迭代中修改代碼是在所難免的,我們要做的是盡量讓修改操作更集中、更少、更上層,盡量讓最核心、最復(fù)雜的那部分邏輯代碼滿足開閉原則。

打個比方說,有一個插入用戶信息:姓名,年齡,電話的方法。

class Demo{
      public void insertUserInfo(String name,int age,String tel){
          ....
      }

}

如果隨著業(yè)務(wù)的發(fā)展這個方法需要把用戶的email也插入進去,如果我們直接修改這個方法添加一個email參數(shù)

class Demo{
      public void insertUserInfo(String name,int age,String tel,String email){
          ....
      }

}

這樣的修改就有違背開閉原則,代碼上所有調(diào)用這個方法的地方都需要修改。

假如是這樣的代碼

class Demo{
      public void insert(User user){
          ....
      }

}

class User{
    String name;
    int age;
    String tel;
}

我們?yōu)榱藵M足業(yè)務(wù)的需求,需要在插入用戶信息的時候把email也插入進去,那么我們直接在User類中添加一個email屬性,這樣的修改就沒有違背開閉原則。

其實也可以這樣理解,開閉原則說的是軟件實體(模塊、類、方法等)應(yīng)該“對擴展開放、對修改關(guān)閉”。我們可以看出來,開閉原則可以應(yīng)用在不同粒度的代碼中,可以是模塊,也可以類,還可以是方法(及其屬性)。同樣一個代碼改動,在粗代碼粒度下,被認定為“修改”,在細代碼粒度下,又可以被認定為“擴展”。比如,添加屬性和方法相當(dāng)于修改類,在類這個層面,這個代碼改動可以被認定為“修改”;但這個代碼改動并沒有修改已有的屬性和方法,在方法(及其屬性)這一層面,它又可以被認定為“擴展”。

再則我們通過上面滿足同一需求的方法代碼來看,下面那個傳User對象的方法比上面的方法更方便擴展一些,這也就是為什么要遵循開閉原則來寫代碼的原因。

1.3 里式替換原則

里式替換原則的英文翻譯是:Liskov Substitution Principle,縮寫為 LSP。這個原則最早是在 1986 年由 Barbara Liskov 提出,他是這么描述這條原則的:

If S is a subtype of T, then objects of type T may be replaced with objects of type S, without breaking the program。

在 1996 年,Robert Martin 在他的 SOLID 原則中,重新描述了這個原則,英文原話是這樣的:

Functions that use pointers of references to base classes must be able to use objects of derived classes without knowing it。

綜合兩者的描述,將這條原則用中文描述出來,是這樣的:子類對象(object of subtype/derived class)能夠替換程序(program)中父類對象(object of base/parent class)出現(xiàn)的任何地方,并且保證原來程序的邏輯行為(behavior)不變及正確性不被破壞。

原則里說“子類替換父類出現(xiàn)的地方”咋一看有點像面向?qū)ο罄锒鄳B(tài)的意思,其實不是。多態(tài)是面向?qū)ο缶幊痰囊淮筇匦?,也是面向?qū)ο缶幊陶Z言的一種語法。它是一種代碼實現(xiàn)的思路。而里式替換是一種設(shè)計原則,是用來指導(dǎo)繼承關(guān)系中子類該如何設(shè)計的,子類的設(shè)計要保證在替換父類的時候,不改變原有程序的邏輯以及不破壞原有程序的正確性。直接來講就是按照協(xié)議來設(shè)計。

舉個例子

class SecurityService{

    public boolean isRightfulPassword(String password){
          if(password==null || password.length < 6){
                return false;
          }
          return true;
    }
}

class SonSecurityService extends SecurityService{
     @Override
     public boolean isRightfulPassword(String password){
          if(password==null){
               throw new ParameterNullRunTimeException();
          }
          return super.isRightfulPassword(password);
    }
}

在父類SecurityService中isRightfulPassword參數(shù)password為空的時候返回的結(jié)果是false,而子類SonSecurityService在替換父類后,當(dāng)password為空時卻拋了一個異常,這就違背了里式替換原則。

里式替換原則是用來指導(dǎo)繼承關(guān)系中子類該如何設(shè)計的一個原則。理解里式替換原則,最核心的就是理解“按照協(xié)議來設(shè)計(design by contract)”。父類定義了函數(shù)的“約定”(或者叫協(xié)議),那子類可以改變函數(shù)的內(nèi)部實現(xiàn)邏輯,但不能改變函數(shù)原有的“約定”。這里的約定包括:函數(shù)聲明要實現(xiàn)的功能;對輸入、輸出、異常的約定;業(yè)務(wù)上的特殊限制;

有一個比較簡單的方法來判斷是否違背了里式替換原則。那就是拿父類的單元測試去驗證子類的代碼。如果某些單元測試運行失敗,就有可能說明,子類有可能違背了里式替換原則。

1.4 接口隔離原則

接口隔離原則的英文翻譯是“ Interface Segregation Principle”,縮寫為 ISP。Robert Martin 在 SOLID 原則中是這樣定義它的:“Clients should not be forced to depend upon interfaces that they do not use?!敝弊g成中文的話就是:客戶端不應(yīng)該強迫依賴它不需要的接口。其中的“客戶端”,可以理解為接口的調(diào)用者或者使用者。

舉個例子:

public interface UserService {
      //注冊
      boolean register(String username, String password);
      //登錄
      boolean login(String username, String password);
      //查詢用戶
      User getUserById(int id);
     //刪除用戶
      boolean deleteUserById(int id);
}

有一個用戶服務(wù)的接口,里面有注冊,登錄,查詢,刪除幾個函數(shù)暴露給客戶端,但是很明顯刪除函數(shù)只有后臺管理模塊可以用到,而且其他模塊如果都可以使用的話就有可能照成誤刪用戶。這就違背了接口隔離原則,我們需要把刪除函數(shù)單獨抽離出來給后臺管理模塊使用。

public interface UserService {
      //注冊
      boolean register(String username, String password);
      //登錄
      boolean login(String username, String password);
      //查詢用戶
      User getUserById(int id);
      
}

public interface AdminUserService {
        //刪除用戶
      boolean deleteUserById(int id);
}

其中“接口”也可以有不同的含義:

  • 可以把它理解為微服務(wù)的一組接口,如果部分接口只被部分調(diào)用者使用,我們就需要將這部分接口隔離出來,單獨給這部分調(diào)用者使用,而不強迫其他調(diào)用者也依賴這部分不會被用到的接口;
  • 可以把它理解成api接口或者函數(shù),部分調(diào)用者只需要函數(shù)中的部分功能,那我們就需要把函數(shù)拆分成粒度更細的多個函數(shù),讓調(diào)用者只依賴它需要的那個細粒度函數(shù)。
  • 可以把它理解成面向?qū)ο缶幊讨卸x的接口語法,那接口的設(shè)計要盡量單一,不要讓接口的實現(xiàn)類和調(diào)用者,依賴不需要的接口函數(shù)。

說起接口隔離其實和單一原則比較類似,都是指在代碼設(shè)計上盡量單一。不同的是單一原則針對的是模塊,類,接口的設(shè)計。接口隔離原則不光說的是接口的設(shè)計,還提供了一種判斷接口職責(zé)是否單一的標(biāo)準(zhǔn):如果調(diào)用者只使用部分接口或接口的部分功能,那接口的設(shè)計就不夠職責(zé)單一。

1.5 依賴倒置原則(Dependence Inversion Principle ,DIP)

參考:http://www.itdecent.cn/p/377830cb1112

定義:
①高級模塊不應(yīng)該依賴于低級模塊,兩者都應(yīng)該依賴于抽象。
②抽象不應(yīng)該依賴于細節(jié)。
③細節(jié)應(yīng)該依賴于抽象。

那么高級模塊、低級模塊,抽象,細節(jié)各指的是什么呢?
每一個邏輯的實現(xiàn)都是由原子邏輯組成,不可分割的原子邏輯就是低級模塊。而低級模塊組裝就是高級模塊。
在Java中,抽象就是指接口或抽象類,兩者并不能直接被實例化。
細節(jié)就是實現(xiàn)類,繼承抽象類或?qū)崿F(xiàn)接口的類就是細節(jié)。特點是可以被直接實例化。

那么依賴倒置原則定義在Java語言中可以表示為:
①模塊間的依賴關(guān)系、實現(xiàn)類間的依賴關(guān)系都是通過接口或抽象類產(chǎn)生。
②接口與抽象類不依賴實現(xiàn)類。
③實現(xiàn)類依賴接口或抽象。
從上述定義我們可以看出,依賴倒置原則的核心思想就是:面向接口編程

代碼重現(xiàn):

司機類

public class Driver {
   public void drive(Benz benz){
      benz.run();
   }
}

奔馳類

public class Benz {
   public void run(){
      System.out.println("開奔馳車上路");
   }
}

測試類

public class Demo_01 {
   public static void main(String[] args) {
         Driver sanmao = new Driver();
         sanmao.drive(new Benz());
   }
}
--------------------output---------------------------
開奔馳車上路

通過以上代碼完成了司機類開奔馳車的場景。
但是隨著業(yè)務(wù)需求的變更,現(xiàn)在要求這個司機還能開寶馬車,那么上面代碼必須要重寫(耦合性太強),才能滿足業(yè)務(wù)需求,顯然上面代碼在設(shè)計上有錯誤。
那么我們依據(jù)依賴倒置原則重構(gòu)以上代碼:

司機接口類

public interface IDriver {
    void drive(ICar iCar);
}

司機實現(xiàn)類

public class Driver implements IDriver{
   @Override
   public void drive(ICar iCar) {
      iCar.run();
   }
}

汽車接口類

public interface ICar {
    void run();
}

汽車實現(xiàn)類

public class Benz implements ICar{
   @Override
   public void run() {
      System.out.println("開奔馳上路");
   }
}
public class BMW implements ICar {
   @Override
   public void run() {
      System.out.println("開寶馬上路");
   }
}

測試類

public class Demo_01 {
   public static void main(String[] args) {
    IDriver sanmao = new Driver();
    ICar benz = new Benz();
    ICar BMW = new BMW();
    sanmao.drive(benz);
    sanmao.drive(BMW);
   }
}
--------------------output---------------------------
開奔馳上路
開寶馬上路

Demo_01類就是高級模塊,他對所有低級模塊的依賴都建立在抽象類或接口上

在IDriver接口中傳入ICar接口,實現(xiàn)模塊間的依賴關(guān)系是通過接口或抽象類產(chǎn)生。在Driver實現(xiàn)類中也傳入了ICar接口,究竟使用Car的哪個子類還得在測試類中聲明。
在測試類中,我們貫徹“②接口與抽象類不依賴實現(xiàn)類。”,所以在測試類Demo_01中,我們都是聲明了各類的抽象。

注意:在Java中,只要定義變量就必然要有類型。一個變量可以有兩種類型:表面類型與實際類型。表面類型是在定義時賦予的類型,實際類型是對象的類型,如sanmao的表現(xiàn)類型是IDriver,實際類型為Driver。

依賴的三種寫法
依賴是可以傳遞的。A對象依賴B對象,B依賴C,C依賴D...,
只要做到抽象依賴,即使是多層的依賴傳遞也無所畏懼。
對象的依賴關(guān)系有以下三種方式傳遞,可以參考Spring的依賴注入

①構(gòu)造函數(shù)傳遞依賴關(guān)系

public interface IDriver {
    void drive();
}

public class Driver implements IDriver{
   private ICar iCar;
   public Driver(ICar _iCar){
      this.iCar = _iCar;
   }
   
   public void drive() {
      this.iCar.run();
   }
}

②Setter方法傳遞依賴關(guān)系

public interface IDriver {
    void drive();
    void setCar(ICar _iCar);
}

public class Driver implements IDriver{
   private ICar iCar;
   @Override
   public void drive() {
      this.iCar.run();
   }
   @Override
   public void setCar(ICar _iCar) {
      this.iCar = _iCar;
   }
}

③接口聲明依賴關(guān)系

public interface IDriver {
    void drive(ICar iCar);
}

public class Driver implements IDriver{
   @Override
   public void drive(ICar iCar) {
      iCar.run();
   }
}

依賴倒置原則的本質(zhì)就是通過接口或抽象類使得各模塊間相互獨立,互不影響,實現(xiàn)松耦合。在使用這個原則時,我們需要注意以下幾個原則:

①每個類都盡可能的都有接口或抽象類,或者抽象類和接口兩者都具備。(依賴倒置原則的基本要求)
②變量的表面類型盡量是接口或者抽象類。(工具類不需要接口或者抽象類,使用類的Clone方法,必須使用實現(xiàn)類,這是JDK的規(guī)范)
③任何類都不應(yīng)該從具體類派生。
④盡量不要覆寫基類的方法。(若基類是一個抽象類,此方法已經(jīng)實現(xiàn),那么子類就不應(yīng)該覆寫)
⑤結(jié)合里氏替換原則使用
接口負責(zé)public屬性與方法,并且聲明與其他對象的依賴關(guān)系,抽象類負責(zé)公共構(gòu)造部分的實現(xiàn),實現(xiàn)類準(zhǔn)確的實現(xiàn)業(yè)務(wù)邏輯,同時在適當(dāng)?shù)臅r候?qū)Ω割愡M行細化。

上面講完了依賴關(guān)系與原則,那么什么叫倒置呢?
要理解倒置,首先我們得知道什么叫“正置”。依賴正置就是類之間的依賴是實現(xiàn)類間的依賴,也就是面向?qū)崿F(xiàn)編程。但是編寫程序需要的是對現(xiàn)實世界的事物進行抽象,轉(zhuǎn)化為我們熟知的抽象類或接口,這樣我們就可以將模塊間的依賴關(guān)系、實現(xiàn)類間的依賴關(guān)系都是通過接口或抽象類產(chǎn)生。這就是我們所說的“倒置”。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容