設計模式六大原則(六)----開閉原則

盛開的太陽發表於2021-06-15

一. 什麼是開閉原則?

開放封閉原則(OCP,Open Closed Principle)是所有物件導向原則的核心。軟體設計本身所追求的目標就是封裝變化、降低耦合,而開放封閉原則正是對這一目標的最直接體現。其他的設計原則,很多時候是為實現這一目標服務的.

1.1 先來看開閉原則的定義:

Software entities like classes,modules and functions should be open for extension but closed for modifications
一個軟體實體, 如類, 模組, 函式等應該對擴充套件開放, 對修改封閉.

這也是開放封閉原則的核心思想:對擴充套件開放,對修改封閉.

1.2 這是什麼含義呢?

  • 對擴充套件開放,意味著有新的需求或變化時,可以對現有程式碼進行擴充套件,以適應新的情況。
  • 對修改封閉,意味著類一旦設計完成,就可以獨立完成其工作,而不要對已有程式碼進行任何修改

二. 如何實現開放封閉原則呢?

“需求總是變化”、“世界上沒有一個軟體是不變的”。這裡投射出的意思是:需求總是變化的, 可是對於軟體設計者來說, 如何才能做到不對原有系統修改的前提下, 實現靈活的擴充套件. 這就是開閉原則要實現的.

我們在設計系統的時候, 不可能設想一次性把需求確定後, 後面就不改變了.這不科學也不現實的. 既然需求是一定會變化的, 那麼我們要如何優雅的面對這種變化呢? 如何設計可以使軟體相對容易修改, 不至於需求一變, 就要把整個程式推到重來?

開封-封閉原則. 設計軟體要容易維護且不容易出問題的最好辦法, 就是多擴充套件, 少修改.

2.1 依賴與抽象

實現開放封閉的核心思想就是面對抽象程式設計,而不是面對具體程式設計,因為抽象相對穩定。
讓類依賴於固定的抽象,所以對修改是封閉的;而通過物件導向的繼承和多型機制,可以實現對抽象體的繼承,通過覆寫其方法來改變固有行為,實現新的擴充套件方法,所以對於擴充套件就是開放的。這是實施開放封閉原則的基本思路。

2.2 如何落地開閉原則

如果當前的設計不符合開放封閉原則,則必須進行重構。常用的設計模式主要有模板方法(Template Method)設計模式策略(Strategy)設計模式。而封裝變化,是實現這一原則的重要手段,將經常發生變化的部分封裝為一個類。

2.3 開閉原則的重要性

1.開閉原則對測試的影響

開閉原則可是保持原有的測試程式碼仍然能夠正常執行,我們只需要對擴充套件的程式碼進行測試就可以了。

2.開閉原則可以提高複用性

在物件導向的設計中,所有的邏輯都是從原子邏輯組合而來的,而不是在一個類中獨立實現一個業務邏輯。只有這樣程式碼才可以複用,粒度越小,被複用的可能性就越大。

3.開閉原則可以提高可維護性

物件導向開發的要求。

2.4 如何使用開閉原則

1.抽象約束

第一,通過介面或者抽象類約束擴充套件,對擴充套件進行邊界限定,不允許出現在介面或抽象類中不存在的public方法;
第二,引數型別、引用物件儘量使用介面或者抽象類,而不是實現類;
第三,抽象層儘量保持穩定,一旦確定即不允許修改。

2.後設資料(metadata)控制模組行為

後設資料就是用來描述環境和資料的資料,通俗地說就是配置引數,引數可以從檔案中獲得,也可以從資料庫中獲得。
Spring容器就是一個典型的後設資料控制模組行為的例子,其中達到極致的就是控制反轉(Inversion of Control)

3.制定專案章程

在一個團隊中,建立專案章程是非常重要的,因為章程中指定了所有人員都必須遵守的約定,對專案來說,約定優於配置。

4.封裝變化

對變化的封裝包含兩層含義:
第一,將相同的變化封裝到一個介面或者抽象類中;
第二,將不同的變化封裝到不同的介面或抽象類中,不應該有兩個不同的變化出現在同一個介面或抽象類中。

三. 案例分析

案例一: 畫形狀

需求: 有圓形, 有橢圓形, 根據要求畫出相應的形狀

public class GraphicEditor {

    public void draw(Shape shape) {
        if (shape.m_type == 1) {
            drawRectangle();
        } else if(shape.m_type == 2) {
            drawCircle();
        }
    }

    public void drawRectangle() {
        System.out.println("畫長方形");
    }

    public void drawCircle() {
        System.out.println("畫圓形");
    }

    class Shape {
        int m_type;
    }

    class Rectangle extends Shape {
        Rectangle() {
            super.m_type=1;
        }
    }

    class Circle extends Shape {
        Circle() {
            super.m_type=2;
        }
    }

}

我們來看看, 這個程式碼, 初看是符合要求了, 再想想, 要是我增加一種形狀呢? 比如增加三角形.
首先, 要增加一個三角形的類, 繼承自Shape
第二, 要增加一個畫三角形的方法drawTrriage()
第三, 在draw方法中增加一種型別type=3的處理方案.
這就違背了開閉原則-對擴充套件開發, 對修改關閉. 增加一個型別, 修改了三處程式碼.

我們來看看合適的設計

public class GraphicEditor1 {

    public void draw(Shape shape) {
        shape.draw();
    }



    interface Shape {
        void draw();
    }

    class Rectangle implements Shape {

        @Override
        public void draw() {
            System.out.println("畫矩形");
        }
    }

    class Circle implements Shape {

        @Override
        public void draw() {
            System.out.println("畫圓形");
        }
    }

}

各種型別的形狀自己規範自己的行為, 而GraphicEditor.draw()只負責畫出來. 當增加一種型別三角形. 只需要
第一: 增加一個三角形的類,實現Shape介面
第二, 呼叫draw方法,劃出來就可以了.

整個過程都是在擴充套件, 而沒有修改原來的類. 這個設計是符合開閉原則的.

案例二:

比如現在有一個銀行業務, 存錢, 取錢和轉賬. 最初我們會怎麼思考呢?

  1. 首先有一個銀行業務類, 用來處理銀行的業務
  2. 銀行有哪些業務呢? 存錢,取錢,轉賬, 這都是銀行要執行的操作
  3. 那外部說我要存錢, 我要取錢,我要轉賬, 通過一個型別來告訴我們
    程式碼就生成了
package com.lxl.www.designPatterns.sixPrinciple.openclosePrinciple.bank;

/**
* 銀行業務
*/
public class BankBusiness {

   public void operate(int type) {
       if (type == 1) {
           save();
       } else if(type == 2) {
           take();
       } else if(type == 3) {
           transfer();
       }
   }

   public void save(){
       System.out.println("存錢");
   }

   public void take(){
       System.out.println("取錢");
   }

   public void transfer() {
       System.out.println("轉賬");
   }
}

咋一看已經實現了需求. 但是現在有新的需求來了, 銀行要增加功能---理財. 理財是銀行業務的一種, 自然是新增一個方法.
然後在operate()方法裡增加一種型別. 這就是一個糟糕的設計, 增加新功能, 但是卻修改了原來的程式碼

我們設計成介面抽象的形式, 上程式碼

public interface Business {
    public void operate();
}

public class Save implements Business{
    @Override
    public void operate() {
        System.out.println("存錢業務");
    }
}

public class Take implements Business {
    @Override
    public void operate() {
        System.out.println("取錢業務");
    }
}

public class Transfer implements Business {
    @Override
    public void operate() {
        System.out.println("轉賬業務");
    }
}


/**
 * 銀行業務類
 */
public class BankBusinesses {
    /**
     * 處理銀行業務
     * @param business
     */
    public void operate(Business business) {
        System.out.println("處理銀行業務");
        business.operate();
    }
}

通過介面抽象的形式方便擴充套件, 加入要新增理財功能. 只需新增一個理財類, 其他業務程式碼都不需要修改.

其實, 在日常工作中, 經常會遇到這種情況. 因為我們平時寫業務邏輯會更多一些, 而業務就像流水賬, 今天一個明天一個一點一點的增加. 所以,當業務增加到3個的時候, 我們就要思考, 如何寫能夠方便擴充套件j

3.3 關於作答鏈路的思考

作答鏈路包括
拉題-->初始化-->答題-->訂正-->加積分-->主觀題批改-->主觀題批改回傳等流程.
那麼這麼一條鏈路是否可以通過介面抽象的形式規範程式碼,實現開閉原則呢? 不至於後面增加一種型別, 就需要新增一個方法.
其實, 我覺得是可以的.

四. 總結:

  1. 遵守開閉原則可以提高軟體擴充套件性和維護性。
  2. 大部分的設計模式和設計原則都是在實現開閉原則。

相關文章