JAVA 設計的七大原則


一、開閉原則

  開閉原則(Open-Closed Principle, OCP)是指一個軟件實體如類、模塊和函數應該對 擴展開放,對修改關閉。

  所謂的開閉,也正是對擴展和修改兩個行為的一個原則。強調 的是用抽象構建框架,用實現擴展細節。

  可以提高軟件系統的可復用性及可維護性。開 閉原則,是面向對象設計中最基礎的設計原則。它指導我們如何建立穩定靈活的系統,實現開閉原則的核心思想就是面向抽象編程。

二、依賴倒置原則

  依賴倒置原則(Dependence Inversion Principle,DIP)是指設計代碼結構時,高層模 塊不應該依賴底層模塊,二者都應該依賴其抽象。

  通過依賴倒置,可以減少類與類之間的耦合性,提高系統的穩定性,提高代碼的 可讀性和可維護性,並能夠降低修改程序所造成的風險。

  下面我們通過一個例子來具體闡述:

  首先創建一個MyCar類,假如你現在有兩輛車

public class MyCar {

    public void AudiCarDriving() {
        System.out.println("AudiCar  is drving ……");
    }

    public void BWMCarDriving() {
        System.out.println("BWMCar is drving ……");
    }
}

  但是隨着你經濟的逐漸飆升,又買下一輛車,這個時候,業務擴展,我們的代碼要從底層到高層(調用層)依次修改代碼。在 MyCar 類中增加 xxxCarDriving()的方法,在高層也要追加調用。如此一來,系統發布以后,實際上是非 常不穩定的,在修改代碼的同時也會帶來意想不到的風險。接下來我們優化代碼,創建 一個課程的抽象 CarDriving 接口

public interface CarDriving {
    void driving();
}
/**
* 創建AudiCar 類
*/ public class AudiCar implements CarDriving { @Override public void driving() { System.out.println(this.getClass().getSimpleName()+"is drving ……"); } } /**
* 創建BWMCar類
*/ public class BWMCar implements CarDriving { @Override public void driving() { System.out.println(this.getClass().getSimpleName()+"is driving ……"); } } /**
* 優化之前的MyCar 類
*/ public class MyCar { public void driving(CarDriving carDriving) { carDriving.driving(); } } //調用 public static void main(String[] args) { MyCar myCar = new MyCar(); myCar.driving(new AudiCar()); myCar.driving(new BWMCar()); }

  我們這時候再看來代碼,無論你有多少車,對於新車,我只需要新建一 類,通過傳參的方式告訴MyCar,而不需要修改底層代碼。實際上這是一種大家非常熟悉 的方式,叫依賴注入。注入的方式還有構造器方式和 setter 方式。我們來看構造器注入 方式:

/**
 * @Description 構造器注入
 * @Date 2019\5\20
 */
public class MyCar_01 {

    private CarDriving carDriving;

    public MyCar_01(CarDriving carDriving) {
        this.carDriving = carDriving;
    }

    public void driving() {
        carDriving.driving();
    }
}
   /**
     * @Description 調用的時候一樣
     */
    public static void main(String[] args) {
        new MyCar_01(new AudiCar()).driving();
        new MyCar_01(new BWMCar()).driving();
    }

  根據構造器方式注入,在調用時,每次都要創建實例。那么,如果 MyCar 是全局單例,則 我們就只能選擇用 Setter 方式來注入,繼續修改 MyCar  類的代碼:

/**
 * @Description setter 注入
 * @Date 2019\5\20
 */
public class MyCar_02 {

    private CarDriving carDriving;

    public void setCarDriving(CarDriving carDriving) {
        this.carDriving = carDriving;
    }

    public void driving() {
        carDriving.driving();
    }
}

/*
 * @Description 調用方法如下
 */
public static void main(String[] args) {
    MyCar_02 myCar = new MyCar_02();
    myCar.setCarDriving(new AudiCar());
    myCar.driving();
    myCar.setCarDriving(new BWMCar());
    myCar.driving();
}

  執行結果

AudiCaris drving ……
BWMCaris driving ……

三、單一職責原則

  單一職責(Simple Responsibility Pinciple,SRP)是指不要存在多於一個導致類變更的原因。

  假設我們有一個 Class 負責兩個職責,一旦發生需求變更,修改其中一個職責的邏輯代碼,有可能會導致另一個職責的功能發生故障。這樣一來,這個 Class 存在兩個導 致類變更的原因。如何解決這個問題呢?我們就要給兩個職責分別用兩個 Class 來實現, 進行解耦。后期需求變更維護互不影響。這樣的設計,可以降低類的復雜度,提高類的 可 讀 性 , 提高系統的可維護性,降低變更引起的風險。總體來說就是一個Class/Interface/Method 只負責一項職責。

public class DriveCar {

    public void driving(String carType){
        if("SUV".equals(carType)){
            System.out.println(carType+"動力強悍,適合越野");
        }else {
            System.out.println(carType+"乘坐舒適");
        }
    }
}

/*調用如下*/
public static void main(String[] args) {
    DriveCar driveCar = new DriveCar();
    driveCar.driving("SUV");
    driveCar.driving("MPV");
}

  從上面代碼來看,DriveCar 類承擔了兩種處理邏輯。假如,現在要對SUV做更多處理,那么 SUV和MPV的大小、功能都不一樣,必須要修改代碼。而修改代碼邏輯勢必會相互影 響容易造成不可控的風險。我們對職責進行分離解耦,來看代碼,分別創建兩個類 SUVDrive 和 MPVDrive:

public class SUVDrive {
    public void driving(String carType){
        System.out.println(carType + "動力強悍,適合越野");
    }
}

public class MPVDrive {
    public void driving(String carType){
        System.out.println(carType + "乘坐舒適");
    }
}

/*調用如下*/
public static void main(String[] args) {
    SUVDrive suv = new SUVDrive();
    suv.driving("SUV");
    MPVDrive mpv = new MPVDrive();
    mpv.driving("MPV");
}

  如果有跟多的業務需要,可以設計一個頂層接口,然后再根據情況拆分成不同的接口,使其滿足單一職責原則,便於后期維護

四、接口隔離原則

  接口隔離原則(Interface Segregation Principle, ISP)是指用多個專門的接口,而不使 用單一的總接口,客戶端不應該依賴它不需要的接口。

  這個原則指導我們在設計接口時 應當注意一下幾點:

     1、一個類對一類的依賴應該建立在最小的接口之上。

    2、建立單一接口,不要建立龐大臃腫的接口。

    3、盡量細化接口,接口中的方法盡量少(不是越少越好,一定要適度)。 接口隔離原則符合我們常說的高內聚低耦合的設計思想,從而使得類具有很好的可讀性、 可擴展性和可維護性。我們在設計接口的時候,要多花時間去思考,要考慮業務模型,  包括以后有可能發生變更的地方還要做一些預判。所以,對於抽象,對業務模型的理解 是非常重要的。

public interface IAnimal {
    void eat();
    void fly();
    void swim();
}

/**
 * @Description Bird 類實現:
 */
public class Bird implements IAnimal{
    @Override
    public void eat() { }

    @Override
    public void fly() { }

    @Override
    public void swim() { }
}

/**
 * @Description Dog 類實現
 */
public class Dog implements IAnimal {
    @Override
    public void eat() {}

    @Override
    public void fly() {}

    @Override
    public void swim() {}
}

  可以看出,Bird 的 swim()方法可能只能空着,Dog 的 fly()方法顯然不可能的。這時候, 我們針對不同動物行為來設計不同的接口,分別設計 IEatAnimal,IFlyAnimal 和 ISwimAnimal 接口,來看代碼:

public interface IEatAnimal{
    void eat();
}

public interface IFlyAnimal {
    void fly();
}

public interface ISwimAnimal {
    void swim();
}

/**
 * @Description Dog 只實現 IEatAnimal 和 ISwimAnimal 接口
 */
public class Dog implements ISwimAnimal,IEatAnimal {
    @Override
    public void eat() {}
    @Override
    public void swim() {}
}

五、迪米特法則

  迪米特原則(Law of Demeter LoD)是指一個對象應該對其他對象保持最少的了解,又 叫最少知道原則(Least Knowledge Principle,LKP),盡量降低類與類之間的耦合。

  迪米特原則主要強調只和朋友交流,不和陌生人說話。出現在成員變量、方法的輸入、輸 出參數中的類都可以稱之為成員朋友類,而出現在方法體內部的類不屬於朋友類。現在來設計一個權限系統,Boss 需要查看目前發布到線上的課程數量。這時候,Boss 要找到 TeamLeader 去進行統計,TeamLeader 再把統計結果告訴 Boss。接下來我們還是來看代碼:

public class Course {
}

import java.util.List;

public class TeamLeader {
    public void checkNumberOfCourses(List<Course> courseList){
        System.out.println("目前已發布的課程數量是:"+courseList.size());
    }
}

import java.util.ArrayList;
import java.util.List;

public class Boss {
    public void commandCheckNumber(TeamLeader teamLeader){
    //模擬 Boss 一頁一頁往下翻頁,TeamLeader 實時統計
        List<Course> courseList = new ArrayList<Course>();
        for (int i= 0; i < 20 ;i ++){
            courseList.add(new Course());
        }
        teamLeader.checkNumberOfCourses(courseList);
    }
}

public static void main(String[] args) {
    Boss boss = new Boss();
    TeamLeader teamLeader = new TeamLeader();
    boss.commandCheckNumber(teamLeader);
}

  寫到這里,其實功能已經都已經實現,代碼看上去也沒什么問題。根據迪米特原則,Boss 只想要結果,不需要跟 Course 產生直接的交流。而 TeamLeader 統計需要引用 Course 對象。Boss 和 Course 並不是朋友,從下面的類圖就可以看出來

下面來對代碼進行改造:

import java.util.ArrayList;
import java.util.List;

public class TeamLeader {
    public void checkNumberOfCourses(){
        List<Course> courseList = new ArrayList<Course>();
        for(int i = 0 ;i < 20;i++){
            courseList.add(new Course());
        }
        System.out.println("目前已發布的課程數量是:"+courseList.size());
    }
}


public class Boss {
    public void commandCheckNumber(TeamLeader teamLeader){
        teamLeader.checkNumberOfCourses();
    }
}

  再來看下面的類圖,Course 和 Boss 已經沒有關聯了

六、里氏替換原則

  里氏替換原則(Liskov Substitution Principle,LSP)是指如果對每一個類型為 T1 的對 象 o1,都有類型為 T2 的對象 o2,使得以 T1 定義的所有程序 P 在所有的對象 o1 都替換成 o2 時,程序 P 的行為沒有發生變化,那么類型 T2 是類型 T1 的子類型。 定義看上去還是比較抽象,我們重新理解一下,可以理解為一個軟件實體如果適用一個 父類的話,那一定是適用於其子類,所有引用父類的地方必須能透明地使用其子類的對 象,子類對象能夠替換父類對象,而程序邏輯不變。根據這個理解,我們總結一下: 引申含義:子類可以擴展父類的功能,但不能改變父類原有的功能。

    1、子類可以實現父類的抽象方法,但不能覆蓋父類的非抽象方法。

    2、子類中可以增加自己特有的方法。

    3、當子類的方法重載父類的方法時,方法的前置條件(即方法的輸入/入參)要比父類 方法的輸入參數更寬松。

    4、當子類的方法實現父類的方法時(重寫/重載或實現抽象方法),方法的后置條件(即 方法的輸出/返回值)要比父類更嚴格或相等。

  使用里氏替換原則有以下優點: 1、約束繼承泛濫,開閉原則的一種體現。 2、加強程序的健壯性,同時變更時也可以做到非常好的兼容性,提高程序的維護性、擴 展性。降低需求變更時引入的風險。 現在來描述一個經典的業務場景,用正方形、矩形和四邊形的關系說明里氏替換原則, 我們都知道正方形是一個特殊的長方形,那么就可以創建一個長方形父類 Rectangle 類:

/**
 * @Description 創建一個長方形父類 Rectangle 類
 */
public class Rectangle {
    private long height;
    private long width;

    public long getWidth() {
        return width;
    }

    public void setWidth(long width) {
        this.width = width;
    }

    public long getHeight() {
        return height;
    }

    public void setHeight(long height) {
        this.height = height;
    }
}

/**
 * @Description 創建正方形 Square 類繼承長方形
 */
public class Square extends Rectangle{
    private long length;
    public long getLength() {
        return length;
    }
    public void setLength(long length) {
        this.length = length;
    }
    @Override
    public long getWidth() {
        return getLength();
    }
    @Override
    public long getHeight() {
        return getLength();
    }
    @Override
    public void setHeight(long height) {
        setLength(height);
    }
    @Override
    public void setWidth(long width) {
        setLength(width);
    }
}

public class LspMain {
    public static void resize(Rectangle rectangle){
        while (rectangle.getWidth() >= rectangle.getHeight()){
            rectangle.setHeight(rectangle.getHeight() + 1);
            System.out.println("width:"+rectangle.getWidth() + ",height:"+rectangle.getHeight());
        }
        System.out.println("resize 方法結束" +
                "\nwidth:"+rectangle.getWidth() + ",height:"+rectangle.getHeight());
    }

    public static void main(String[] args) {
        Rectangle rectangle = new Rectangle();
        rectangle.setWidth(20);
        rectangle.setHeight(10);
        resize(rectangle);
    }
}

  運行結果:

width:20,height:11
width:20,height:12
width:20,height:13
width:20,height:14
width:20,height:15
width:20,height:16
width:20,height:17
width:20,height:18
width:20,height:19
width:20,height:20
width:20,height:21
resize 方法結束
width:20,height:21

  發現高比寬還大了,在長方形中是一種非常正常的情況。現在我們再來看下面的代碼, 把長方形 Rectangle 替換成它的子類正方形 Square,修改測試代碼:

public static void main(String[] args) {
        Square square = new Square();
        square.setLength(10);
        resize(square);
    }

  這時候我們運行的時候就出現了死循環,違背了里氏替換原則,將父類替換為子類后, 程序運行結果沒有達到預期。因此,我們的代碼設計是存在一定風險的。里氏替換原則 只存在父類與子類之間,約束繼承泛濫。我們再來創建一個基於長方形與正方形共同的 抽象四邊形 Quadrangle 接口:

public interface Quadrangle {
    long getWidth();
    long getHeight();
}

/**
 * @Description 修改 Rectangle 類
 */
public class Rectangle implements Quadrangle{
    private long height;
    private long width;
    @Override
    public long getWidth() {
        return width;
    }
    public long getHeight() {
        return height;
    }
    public void setHeight(long height) {
        this.height = height;
    }
    public void setWidth(long width) {
        this.width = width;
    }
}

/**
 * @Description 修改 Square 類
 */
public class Square implements Quadrangle{
    private long length;
    public long getLength() {
        return length;
    }
    public void setLength(long length) {
        this.length = length;
    }
    @Override
    public long getWidth() {
        return length;
    }
    @Override
    public long getHeight() {
        return length;
    }
}

  

此時,如果我們把 resize()方法的參數換成四邊形 Quadrangle 類,方法內部就會報錯。 因為正方形 Square 已經沒有了 setWidth()和 setHeight()方法了。因此,為了約束繼承 泛濫,resize()的方法參數只能用 Rectangle 長方形

七、合成復用原則

  合成復用原則(Composite/Aggregate Reuse Principle,CARP)是指盡量使用對象組 合(has-a)/聚合(contanis-a),而不是繼承關系達到軟件復用的目的。可以使系統更加靈 活,降低類與類之間的耦合度,一個類的變化對其他類造成的影響相對較少。 繼承我們叫做白箱復用,相當於把所有的實現細節暴露給子類。組合/聚合也稱之為黑箱 復用,對類以外的對象是無法獲取到實現細節的。要根據具體的業務場景來做代碼設計, 其實也都需要遵循 OOP 模型。還是以數據庫操作為例,先來創建 DBConnection 類:

public class DBConnection {
    public String getConnection(){
        return "MySQL 數據庫連接";
    }
}

public class ProductDao {
    private DBConnection dbConnection;
    public void setDbConnection(DBConnection dbConnection) {
        this.dbConnection = dbConnection;
    }
    public void addProduct(){
        String conn = dbConnection.getConnection();
        System.out.println("使用"+conn+"增加產品");
    }
}

  這就是一種非常典型的合成復用原則應用場景。但是,目前的設計來說,DBConnection 還不是一種抽象,不便於系統擴展。目前的系統支持 MySQL 數據庫連接,假設業務發生 變化,數據庫操作層要支持 Oracle 數據庫。當然,我們可以在 DBConnection 中增加對 Oracle 數據庫支持的方法。但是違背了開閉原則。其實,我們可以不必修改 Dao 的代碼, 將 DBConnection 修改為 abstract,來看代碼:

 

public abstract class DBConnection {
    public abstract String getConnection();
}


/**
 * @Description 將 MySQL 的邏輯抽離
 */
public class MySQLConnection extends DBConnection {
    @Override
    public String getConnection() {
        return "MySQL 數據庫連接";
    }
}


/**
 * @Description 創建 Oracle 支持的邏輯
 */
public class OracleConnection extends DBConnection {
    @Override
    public String getConnection() {
        return "Oracle 數據庫連接";
    }
}

  具體選擇交給應用層,來看一下類圖

設計原則總結

  學習設計原則,學習設計模式的基礎。在實際開發過程中,並不是一定要求所有代碼都 遵循設計原則,我們要考慮人力、時間、成本、質量,不是刻意追求完美,要在適當的 場景遵循設計原則,體現的是一種平衡取舍,幫助我們設計出更加優雅的代碼結構。


免責聲明!

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



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