設計模式-第2篇-軟件設計七大原則

文章目錄

設計模式-軟件設計七大原則

1.開閉原則(Open Closed Principle)

1.1 說明

定義一個軟件實體如類、模塊和函數應該對外擴展開放,對修改關閉。

  • 抽象構建框架,用實現擴展細節
  • 本質就是面向抽象的接口編程

優點:提高軟件系統的可複用性及可維護性


1.2 Code Demo

以一個Demo爲例,闡述開閉原則。

假設現在有個銀行的支付接口IBankPay,有三個方法。付款、收款、獲取餘額。所有的第三方支付工具都應該遵守這個接口規範。

1.2.1 IBankPay接口

/**
 * 銀行提供支付接口
 * @author MuZiYu
 */
public interface IBankPay {
    /**
     * 付款接口
     */
    void pay(BigDecimal payMoney);
    /**
     * 收款接口
     */
    void collect(BigDecimal collectMoney);
    /**
     * 獲取賬戶餘額
     */
    BigDecimal getBalance();
}

假設現在有兩個支付平臺,比如我們熟悉的支付寶和微信。分別實現銀行的支付接口IBankPay

1.2.2 Alipay

/**
 * 支付寶實現類
 * @Author MuZiYu
 */
public class Alipay implements IBankPay{

    private BigDecimal balance;

    public Alipay(BigDecimal balance) {
        this.balance = balance;
    }

    @Override
    public void pay(BigDecimal payMoney) {
        this.balance = balance.subtract(payMoney);
        System.out.println("支付寶付款成功,付款金額"+payMoney);
    }

    @Override
    public void collect(BigDecimal collectMoney) {
        this.balance = balance.add(collectMoney);
        System.out.println("支付寶收款成功,收款金額"+collectMoney);
    }

    @Override
    public BigDecimal getBalance() {
        return this.balance;
    }
}

1.2.3 WeiChatPay

/**
 * 微信支付
 * @Author MuZiYu
 */
public class WeiChatPay implements IBankPay{

    private BigDecimal balance;

    public WeiChatPay(BigDecimal balance) {
        this.balance = balance;
    }
    
    @Override
    public void pay(BigDecimal payMoney) {
        this.balance = balance.subtract(payMoney);
        System.out.println("微信支付成功,付款金額"+payMoney);
    }

    @Override
    public void collect(BigDecimal collectMoney) {
        this.balance = balance.add(collectMoney);
        System.out.println("微信收款成功,收款金額"+collectMoney);
    }

    @Override
    public BigDecimal getBalance() {
        return this.balance;
    }
}

1.2.4 Test

那麼,我們有個Test類,去測試這兩種實現方式的方法。我們同樣地初始餘額設置爲1000元,分別支付500元,再收款200元,看最終餘額。

public class Test {
    public static void main(String[] args) {
        IBankPay payWay1 = new Alipay(new BigDecimal("1000"));
        payWay1.pay(new BigDecimal("500"));
        payWay1.collect(new BigDecimal("200"));
        System.out.println("AliPay Balance:"+payWay1.getBalance());

        IBankPay payWay2 = new WeiChatPay(new BigDecimal("1000"));
        payWay2.pay(new BigDecimal("500"));
        payWay2.collect(new BigDecimal("200"));
        System.out.println("WeChatPay Balance:"+payWay2.getBalance());
    }
}

在這裏插入圖片描述

1.3 需求變動

假設現在需求需要變動,支付寶或者微信需要實現一個新功能,比如支付時需要收取不同費率的手續費。我們需要在各自的pay方法修改實現。

   /**
   	* 支付寶收取手續費0.5%
   	*/
	@Override
    public void pay(BigDecimal payMoney) {
        this.balance = balance.subtract(payMoney.multiply(new BigDecimal("1.005")));
        System.out.println("支付寶付款成功,付款金額"+payMoney);
    }
   /**
   	* 微信收取手續費1%
   	*/
	@Override
    public void pay(BigDecimal payMoney) {
        this.balance = balance.subtract(payMoney.multiply(new BigDecimal("1.01")));
        System.out.println("微信支付成功,付款金額"+payMoney);
    }

但是如果此時又有一個需求到來,支付寶或者微信要支持使用優惠券支付。如果我們直接修改接口,增加一個方法payByUseCoupon,那麼很多實現IBankPay接口的其它支付工具都需要去實現這個方法。可是他們並不支持這個方法。

需要注意:當接口定義好,應當儘量避免直接修改接口。

所以我們可以使用繼承,用實現去擴展細節

1.3.1 CouponAlipay

public class CouponAlipay extends Alipay {

    public CouponAlipay(BigDecimal balance) {
        super(balance);
    }
	/**
	 * 使用優惠券支付
	 */
    public void payByUserCoupon(BigDecimal payMoney, BigDecimal couponMoney){
        super.pay(payMoney.subtract(couponMoney));
    }
}

1.3.2 Test

    public static void main(String[] args) {
        IBankPay payWay1 = new Alipay(new BigDecimal("1000"));
        payWay1.pay(new BigDecimal("500"));
        System.out.println("AliPay Balance:"+payWay1.getBalance());

        System.out.println("----------------------");
        
        IBankPay payWay3 = new CouponAlipay(new BigDecimal("1000"));
        CouponAlipay pay = (CouponAlipay) payWay3;
        pay.payByUserCoupon(new BigDecimal("500"),new BigDecimal("100"));
        System.out.println("AliPay Balance:"+pay.getBalance());
        System.out.println("Pay Money Count By Using Coupon:"+pay.getCouponPayMoneyCount());
    }

在這裏插入圖片描述

1.3.3 UML類圖

在這裏插入圖片描述

1.4 總結

開閉原則告訴我們應儘量通過擴展軟件實體的行爲來實現變化,而不是通過修改已有的代碼來完成變化,它是爲軟件實體的未來事件而制定的對現行開發設計進行約束的一個原則。

2.依賴倒置原則(Dependency Inversion Principle)

2.1 定義

  • 高層模塊不應該依賴低層模塊,二者都應該依賴其抽象
  • 抽象不應該依賴細節,細節應該依賴抽象
  • 針對接口編程,不要針對實現編程

2.2 優點

減少類間的耦合性、提高系統穩定性,提高代碼可讀性和可維護性,可降低修改程序所造成的風險。

2.3 Code Demo

依賴倒置原則的核心就是針對接口編程

現在來看一個Demo,假設計算機學科的學生需要學習多門語言課程。如果是面向實現編程,則需要不斷地修改這個ComputerStudent類,增加方法。

public class ComputerStudent {
    public void studyJava(){
        System.out.println("學習java課程");
    }

    public void studyPython(){
        System.out.println("學習python課程");
    }
    
    ......
}

public class Test {
    public static void main(String[] args) {
        ComputerStudent student = new ComputerStudent();
        student.studyJava();
        student.studyPython();
        ......
    }
}

Test這個類和ComputerStudent相比,相當於高層模塊。此時該高層模塊依賴低層模塊ComputerStudent,並且ComputerStudent面向實現編程,導致強耦合。


2.4 抽取接口,面向接口編程

2.4.1 ICourse接口

public interface ICourse {
    void studyCourse();
}

2.4.2 JavaCourse實現類

public class JavaCource implements ICourse {

    @Override
    public void studyCourse() {
        System.out.println("學習java課程");
    }
}

2.4.3 PythonCourse實現類

public class PythonCourse implements ICourse{

    @Override
    public void studyCourse() {
        System.out.println("學習python課程");
    }
}

2.4.4 ComputerStudent改造

public class ComputerStudent {
    public void study(ICourse iCourse){
        iCourse.studyCourse();
    }
}

可以通過方法傳參的方式傳入ICourse接口的實現類,具體實現取決於傳入的ICourse接口的實現類。【接口方法方式注入】

2.4.5 Test

public class Test {
    public static void main(String[] args) {
//        ComputerStudent student = new ComputerStudent();
//        student.studyJava();
//        student.studyPython();

        ComputerStudent student = new ComputerStudent();
        student.study(new JavaCource());
        student.study(new PythonCourse());
    }
}

此時不論計算機學生需要擴展學習其他任何的課程,ComputerStudent這個類都不需要去動。只需要不斷地實現ICourse接口的實現類即可。

此時,ComputerStudent在學習任何課程的時候,具體的實現類交給高層模塊Test去決定(傳具體的課程學習對象),而不是針對ComputerStudent面向實現編程。

也就是說,我們是面向ICourse接口編程,而不是面向具體的ComputerStudent編程。

2.4.6 UML

在這裏插入圖片描述

此時只需要水平擴展即可。ComputerStudentTest是解耦的,同時也是和ICourse的具體實現類是解耦的。

但是ComputerStudentICourse是依賴關係。


2.4.7 改造:通過構造注入

public class ComputerStudent {
	// 其實這和Spring的依賴注入就已經很相似了
    private ICourse iCourse;

    public ComputerStudent(ICourse iCourse) {
        this.iCourse = iCourse;
    }

    public void study(){
        iCourse.studyCourse();
    }

//    public void study(ICourse iCourse){
//        iCourse.studyCourse();
//    }
}
public class Test {
    public static void main(String[] args) {
//        ComputerStudent student = new ComputerStudent();
//        student.studyJava();
//        student.studyPython();

//        ComputerStudent student = new ComputerStudent();
//        student.study(new JavaCource());
//        student.study(new PythonCourse());

        ComputerStudent student1 = new ComputerStudent(new JavaCource());
        student1.study();
        ComputerStudent student2 = new ComputerStudent(new PythonCourse());
        student2.study();
    }
}

不過這樣有點不好,就是每次都得重新new一個ComputerStudent對象才行。所以可以再改造,提供Getter/Setter修改ICourse實現類。

public class ComputerStudent {

    private ICourse iCourse;

    public ComputerStudent(ICourse iCourse) {
        this.iCourse = iCourse;
    }

    public void study(){
        iCourse.studyCourse();
    }

    public ICourse getICourse() {
        return iCourse;
    }

    public void setICourse(ICourse iCourse) {
        this.iCourse = iCourse;
    }
}
public class Test {
    public static void main(String[] args) {
//        ComputerStudent student = new ComputerStudent();
//        student.studyJava();
//        student.studyPython();

//        ComputerStudent student = new ComputerStudent();
//        student.study(new JavaCource());
//        student.study(new PythonCourse());

//        ComputerStudent student1 = new ComputerStudent(new JavaCource());
//        student1.study();
//        ComputerStudent student2 = new ComputerStudent(new PythonCourse());
//        student2.study();

        ComputerStudent student = new ComputerStudent(new JavaCource());
        student.study();
        student.setICourse(new PythonCourse());
        student.study();
    }
}

2.5 總結

依賴倒置原則的本質就是通過抽象(接口或抽象類)使各個類或模塊的實現彼此獨立,不互相影響,實現模塊間的鬆耦合。需要遵循以下的幾個規則。

每個類儘量都有接口或抽象類,或者抽象類和接口兩者都具備

這是依賴倒置的基本要求,接口和抽象類都是屬於抽象的,有了抽象纔可能依賴倒置。

任何類都不應該從具體類派生

如果一個項目處於開發狀態,確實不應該有從具體類派生出子類的情況,但這也不是絕對的,因爲人都是會犯錯誤的,有時設計缺陷是在所難免的,因此一些情況下的繼承都是可以忍受的。
特別是項目維護階段,維護工作基本上都是進行擴展開發,修復行爲。通過一個繼承關係,覆寫一個方法就可以修正一個很大的Bug,何必去繼承最高的基類呢?(當然這種情況儘量發生在不甚瞭解父類或者無法獲得父類代碼的情況下。)

儘量不要覆寫基類的方法

如果基類是一個抽象類,而且這個方法已經實現了,子類儘量不要覆寫。
類間依賴的是抽象,覆寫了抽象方法,對依賴的穩定性會產生一定的影響。

結合里氏替換原則使用

接口負責定義public屬性和方法,並且聲明與其他對象的依賴關係。
抽象類負責公共構造部分的實現,實現類準確的實現業務邏輯,同時在適當的時候對父類進行細化。

3.單一職責(Single Responsibility Principle)

3.1 定義

  • 不要存在 多於一個 導致類變更的 原因
  • 一個類/接口/方法 只負責 一項職責

3.2 優點

降低類的複雜度,提高類的可讀性,提高系統的可維護性,降低變更引起的風險

3.3 類的單一職責Code

3.3.1 多職責的類

在Bird類裏定義一個移動方式的方法move

public class Bird {
    public void move(String name){
        System.out.println(name+":用翅膀飛");
    }
}
public class Test {
    public static void main(String[] args) {
        Bird bird = new Bird();
        bird.move("大雁");
        bird.move("鴕鳥");
    }
}

在這裏插入圖片描述

很明顯,鴕鳥用翅膀飛就不合理。大雁和鴕鳥雖然都是鳥,但是它們的移動方式完全不同,所以用一個類強行表現這多種情況,就會讓這個類的職責變得非常複雜。

public class Bird {
    public void move(String name){
        if("鴕鳥".equals(name)){
            System.out.println(name+":用腳走");
        }else{
            System.out.println(name+":用翅膀飛");
        }
    }
}

3.3.2 拆分職責

我們針對可以飛的鳥和用腳走路的鳥,拆分成兩種類

public class FlyBird {
    public void move(String name){
        System.out.println(name+":用翅膀飛");
    }
}
public class FootBird {
    public void move(String name) {
        System.out.println(name + ":用腳走");
    }
}
public class Test {
    public static void main(String[] args) {
//        Bird bird = new Bird();
//        bird.move("大雁");
//        bird.move("鴕鳥");

        FlyBird flyBird = new FlyBird();
        flyBird.move("大雁");
        FootBird footBird = new FootBird();
        footBird.move("鴕鳥");
    }
}

在這裏插入圖片描述


3.4 接口的單一職責

3.4.1 多職責的接口

現在有一個接口,FoodStore 食品店的接口。

public interface FoodStore {
    // 生產食物
    void product();
    // 加工食物
    void process();
    
    //銷售
    void sale();
}

接口既負責生產、加工這些屬於工廠操作的職責,又要負責銷售這個職責。所以這個接口的職責不是單一的,那麼這是不易擴展的。想象一下,在實際生活中,假如一個商店的規模越來越大,那麼它還能既負責生產加工、又負責銷售嗎?所以最好將其分離開來,形成兩個接口。這樣生產加工和銷售兩不誤。


3.4.2 拆分職責的接口

將食物生產加工的方法抽取到一個FoodStoreFactory接口中,將食物銷售的接口抽取到FoodStoreSale接口中

public interface FoodStoreFactory {
    // 生產食物
    void product();
    // 加工食物
    void process();
}
public interface FoodStoreSale {
    //銷售
    void sale();
}

此時,新建一個實現類SteamedBunFoodStore饅頭食品店的類,同時繼承兩個接口就具有了兩種接口的行爲,既可以生產加工,也可以銷售。如果後期業務擴展,可以只繼承一個FoodStoreSale或者 FoodStoreFactory接口,只專注單向業務。

public class SteamedBunFoodStore implements FoodStoreFactory,FoodStoreSale {
    @Override
    public void product() {

    }

    @Override
    public void process() {

    }

    @Override
    public void sale() {

    }
}

在這裏插入圖片描述


3.5 方法的單一職責

3.5.1 多職責的方法

其實我們很多時候有如下操作。根據傳入的boolean的值不同,執行不同的方法。這就是一個方法中有兩個職責,此時可以將其拆分出來,維護起來也更加明顯。

public class UserMethod {
    public void method(String arg1,String arg2,boolean b){
        if(b){
            //do something
        }else{
            //do something
        }
    }
}

3.6 總結

總的來說,最好保證接口方法的單一職責原則,儘量保證。不過實際開發中對於類的職責還是要分實際情況來決定。

4.接口隔離( Interface Segregation Principle)

4.1 定義

用多個專門的接口,而不使用單一的總接口,客戶端不應該依賴它不需要的接口

  • 一個類對一個類的依賴應該建立在最小的接口上
  • 建立單一接口,不要建立龐大臃腫的接口
  • 儘量細化接口,接口中的方法儘量少
  • 注意適度原則,一定要適度

4.2 優點

符合高內聚、低耦合的設計思想,從而使類具有很好的可讀性、可擴展性、可維護性。

4.3 Code Demo

4.3.1 未遵循接口隔離原則的接口

public interface IAnimalAction {
    void swim();
    void fly();
    void run();
}

4.3.2 實現類

public class Dog implements IAnimalAction {
    @Override
    public void swim() {
        System.out.println("Dog Swimming...");
    }

    @Override
    public void fly() {
        //無實現
    }

    @Override
    public void run() {
        System.out.println("Dog Running...");
    }
}

public class Bird implements IAnimalAction {
    @Override
    public void swim() {
         //無實現
    }

    @Override
    public void fly() {
        System.out.println("Bird Fly....");
    }

    @Override
    public void run() {
		 //無實現
    }
}

可以看到,Dog繼承IAnimalAction接口,實現了swim、fly、run三個方法。但是fly方法是被強制去依賴實現的,但是Dog根本不需要這個方法。

同理,Bird繼承IAnimalAction接口,swim和run方法對其是多餘的。

所以這個IAnimalAction接口設計的就很有問題,是一個“胖接口”,需要遵循接口隔離原則。


4.4.3 遵循接口隔離原則後的改造

我們按照接口隔離原則,將IAnimalAction接口改造爲三個接口。

public interface IFlyAnimalAction {
    void fly();
}
public interface IRunAnimalAction {
    void run();
}
public interface ISwimAnimalAction {
    void swim();
}

如此,再看我們的實現類DogBird

public class Dog implements IRunAnimalAction,ISwimAnimalAction {
    @Override
    public void swim() {
        System.out.println("Dog Swimming...");
    }

    @Override
    public void run() {
        System.out.println("Dog Running...");
    }
}
public class Bird implements IFlyAnimalAction {
    @Override
    public void fly() {
        System.out.println("Bird Fly....");
    }
}

遵循了接口隔離原則,避免了 客戶端 依賴它不需要的接口這個問題。

4.4 與單一職責原則的區別

  • 接口隔離針對的層面是接口,強調的是它的實現類不應該被強制實現一些根本不需要的方法(客戶端不應該被迫依賴於他們不使用的接口),是一種對接口層面的約束。希望一個接口方法越少越好,最好是單一接口(比如有一個IWorker接口,有eat方法和work方法,工人類去實現這個接口自然沒什麼問題。但如果有個機器人類也實現這個接口,就需要被迫實現eat方法,但實際上這個接口方法被實現後是沒任何實際邏輯的(默認實現或空實現)。所以這個接口被污染了
  • 而單一職責主要強調是職責,是業務邏輯上的劃分,針對的是接口的實現和細節。比如上述的食品店接口,既負責生產加工,又負責銷售。該接口具備兩樣職責,不利於後續維護。再比如,一個IUser接口,既包含了獲取用戶信息的相關接口,又包含了對用戶信息操作的接口,這就是一個接口有多個職責,應當對職責進行拆分。變成一個只獲取用戶信息的接口IUserInfo,一個操作用戶信息的接口IUserDAO

4.5 總結

用於處理胖接口(fat interface)所帶來的問題。如果類的接口定義暴露了過多的行爲,則說明這個類的接口定義內聚程度不夠好。換句話說,類的接口可以被分解爲多組功能函數的組合,每一組都服務於不同的客戶類,而不同的客戶類可以選擇使用不同的功能分組。

接口隔離必須平衡適度,如果劃分地過細,導致接口數量過多,也是不利於維護的。

5.迪米特原則(最少知道原則)(Least Knowledge Principle)

5.1 定義

一個對象應當對其他對象保持最少的瞭解。

  • 儘量降低類與類之間的耦合
  • 強調只和朋友交流,不和陌生人交流(什麼是朋友?出現在成員變量方法的輸入方法輸出的類,就是成員朋友類。出現在方法體內部的類不屬於朋友類,該類如果出現代表不滿足迪米特法則,這個類是陌生朋友

其實就是,對外部引用的類越少越好。類自身儘量多使用privateprotected關鍵字

5.2 優點

降低類與類之間的耦合

5.3 Code Demo

5.3.1 一個違背迪米特原則的例子

public class Boss {
    /**
     * 獲取僱員的數量
     */
    public int getEmployeeCount(DepartmentManager departmentManager){
        //模擬從數據庫查詢---此時Employee不是Boss的直接朋友類
        List<Employee> employees = new ArrayList<>();
        for (int i = 0; i < 20; i++) {
            employees.add(new Employee());
        }
        return departmentManager.countEmployees(employees);
    }
}
public class DepartmentManager {
    /**
     *  計算僱員的數量
     */
    public int countEmployees(List<Employee> employees){
        return employees.size();
    }
}
public class Employee {

}
public class Test {
    public static void main(String[] args) {
        Boss boss = new Boss();
        System.out.println(boss.getEmployeeCount(new DepartmentManager()));
    }
}

在上述的例子中。Boss想要統計自己公司的員工人數,他只需要和部門主管DepartmentManager打交道即可,不應該和員工Employee產生直接的耦合。在Boss類中,Employee屬於方法體內的類,不是成員朋友類,因此Boss不應該和Employee直接打交道。

在這裏插入圖片描述

5.3.2 遵守迪米特原則

public class Boss {
    public int getEmployeeCount(DepartmentManager departmentManager){
        return departmentManager.countEmployees();
    }
}
public class DepartmentManager {
    public int countEmployees(){
        //模擬從數據庫查詢
        List<Employee> employees = new ArrayList<>();
        for (int i = 0; i < 20; i++) {
            employees.add(new Employee());
        }
        return employees.size();
    }
}

其實就是把Boss對Employee的依賴捨去,將相關操作和依賴放到DepartmentManager中。這就實現了Boss與Employee的解耦。

在這裏插入圖片描述

5.4 總結

迪米特原則與實際開發中比較貼切的就是MVC分層開發,Controller層應該減少相關的耦合,儘量不要直接出現DAO的類,將相關依賴和業務處理放在Service中完成。

  • 迪米特只是要求儘量降低耦合,但是並不是杜絕。完全的沒有依賴基本不可能

在實際應用中經常會出現這樣一個方法:放在本類中也可以,放在其他類中也沒有錯,那怎麼去衡量呢?
你可以堅持這樣一個原則:如果一個方法放在本類中,既不增加類間關係,也對本類不產生負面影響,那就放置在本類中。

6.里氏替換原則(Liskov Substitution Principle)

6.1 定義

  • 所有引用基類的地方必須能透明地使用其子類的對象。

通俗點講,就是隻要父類能出現的地方子類就可以出現,而且替換爲子類也不會產生任何錯誤或異常,使用者可能根本就不需要知道是父類還是子類。但是,反過來就不行了,有子類出現的地方,父類未必就能適應。

  • 如果對每一個類型爲S的對象o1,都有類型爲T的對象o2,使得以T定義的所有程序P在所有的對象o1都代換成o2時,程序P的行爲沒有發生變化,那麼類型S是類型T的子類型。

6.2 優點

  • 爲良好的繼承提供了規範

6.3 注意

如果子類不能完整地實現父類的方法,或者父類的某些方法在子類中已經發生“畸變”,則建議斷開父子繼承關係,採用依賴、聚合、組合等關係代替繼承。

6.4 Code demo

6.4.1 違背里氏替換原則

public class A {
    public void fun(int a,int b){
        System.out.println(a+"+"+b+"="+(a+b));
    }
}

public class B extends A{
    @Override
    public void fun(int a,int b){
        System.out.println(a+"-"+b+"="+(a-b));
    }
}

public class demo {
    public static void main(String[] args){
        System.out.println("父類的運行結果");
        A a=new A();
        a.fun(1,2);
        //父類存在的地方,可以用子類替代
        //子類B替代父類A
        System.out.println("子類替代父類後的運行結果");
        B b=new B();
        b.fun(1,2);
    }
}

子類運行的結果很明顯和父類的fun方法調用結果不一致,這就違背了里氏替換原則。此處父類A並不能被子類B完全替代。


6.4.2 子類可以有特有方法

public class Father {
    public void f(HashMap map){
        System.out.println("Father f() invoke...");
    }
}
public class Son extends Father {
    public void f2(){
        System.out.println("Son f2() invoke...");
    }
}
public class Test {
    public static void main(String[] args) {
        HashMap map = new HashMap();
        Father father = new Father();
        father.f(map);
        System.out.println("-------------使用子類替換--------------");
        Son son = new Son();
        son.f(map);
        son.f2();
    }
}
打印結果:
Father f() invoke...
-------------使用子類替換--------------
Father f() invoke...
Son f2() invoke...

6.4.3 子類覆蓋或實現父類方法時,子類方法形參 要比 父類方法形參 範圍大

public class Father {
    public void f(HashMap map){
        System.out.println("Father f() invoke...");
    }
}
public class Son extends Father {

   // @Override---這裏使用@Override註解會報錯,因爲這是方法重載而不是重寫
    public void f(Map map){
        System.out.println("Son f() invoke...");
    }

//    @Override---這是方法重寫
//    public void f(HashMap map){
//        System.out.println("Son f() invoke...");
//    }
}
public class Test {
    public static void main(String[] args) {
        HashMap map = new HashMap();
        Father father = new Father();
        father.f(map);
        System.out.println("-------------使用子類替換--------------");
        Son son = new Son();
        son.f(map);
    }
}

讓我們想象一下結果,這會違背里氏替換原則嗎?

Father f() invoke...
-------------使用子類替換--------------
Father f() invoke...

答案是不會,因爲子類在方法定義時,形參使用的是Map,它是重載從父類繼承過來的形參爲HashMap的方法f(HashMap map)。所以當傳入HashMap類型的參數時,並沒有調用這個重載的方法,而是調用的是從父類繼承過來的原先的方法。


假如我們調換下範圍,讓父類方法的形參是小範圍(HashMap),子類方法的形參是大範圍(Map)。又會怎樣?

public class Father {
    public void f(Map map){
        System.out.println("Father f() invoke...");
    }
}
public class Son extends Father {
    public void f(HashMap map){
        System.out.println("Son f() invoke...");
    }
}
public class Test {
    public static void main(String[] args) {
        HashMap map = new HashMap();
        Father father = new Father();
        father.f(map);
        System.out.println("-------------使用子類替換--------------");
        Son son = new Son();
        son.f(map);
    }
}
Father f() invoke...
-------------使用子類替換--------------
Son f() invoke...

假設我們傳入的參數類型是Map類型

public class Test {
    public static void main(String[] args) {
        Map map = new HashMap();
        Father father = new Father();
        father.f(map);
        System.out.println("-------------使用子類替換--------------");
        Son son = new Son();
        son.f(map);
    }
}
Father f() invoke...
-------------使用子類替換--------------
Father f() invoke...

則符合里氏替換原則。


除此以外,使用繼承時,子類方法的返回值類型不能比父類方法返回值類型大,否則報錯。

在這裏插入圖片描述

在這裏插入圖片描述

6.5 總結

  • 里氏替換原則要求任何時候父類替換爲子類時,能夠保證程序不出錯,並且不改變原有的邏輯。那就要求子類不要重寫父類的方法。這和多態的出發點不一樣,里氏替換原則就是爲了避免繼承的一些缺陷。而多態就是爲了擴展父類,讓子類有更多的行爲,讓子類擁有更多的個性,所以要求重寫父類方法。

    1. 繼承是侵入性的。只要繼承,就必須擁有父類的所有屬性和方法。
    2. 降低了代碼的靈活性。因爲繼承時,父類會對子類有一種約束。
    3. 增強了耦合性。當需要對父類的代碼進行修改時,必須考慮到對子類產生的影響。有時修改了一點點代碼都有可能需要對打斷程序進行重構。
  • 里氏替換原則中,子類不應該去實現父類已經實現好的方法,但需要實現父類的抽象方法。

  • 子類可以有自己的個性,就是子類可以在自己的類中定義別的方法

7.合成/複用原則(組合/複用原則)

原則就是使用 組合和聚合代替繼承

在這裏插入圖片描述


爲了解決繼承的強耦合(is a),使用組合和聚合去代替(has a


設計原則的核心思想

  • 找出應用中可能需要變化之處,把它們獨立出來,不要和那些不需要變化的代碼混在一起。
  • 針對接口編程,而不是針對實現編程
  • 爲了交互對象之間的鬆耦合設計而努力
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章