開閉原則 | Open/Closed Principle

Posted by Ian Tsai on Friday, September 11, 2020

本系列文章同步分享於IT邦幫忙第12屆鐵人賽


Design Pattern 系列文章導讀


Design Pattern可以說是開發上大家都會遇到的一個課題, 這系列文會從Design Principles、各種design pattern到最後的Anti-Patterns & Code Smells介紹下去,讓我們可以更了解各種pattern的使用時機與場合。 預計目標主題如下,若有哪部分不熟的章節可以直接點進去看
註:可以利用Online Java Compiler IDE

  1. 設計模式 - 入門篇
  2. 設計模式 - 原則篇 | Design Principles
  3. 設計模式 - 創建型模型篇 | Creational Patterns
  4. 設計模式 - 結構型模型篇 | Structural Patterns
  5. 設計模式 - 行為型模型篇 | Behavioural Patterns
  6. 設計模式 - 番外篇
  • 定義


Software entities like classes, modules and functions should be open for extension but closed for modifications.

翻譯年糕:軟體實體像是類別、模組和函式,應該只對延展開放,但對修改關閉。

簡單來說,就是當系統需要被擴充時,應該藉由新增新的程式碼來擴充新功能,而非修改原本的程式碼來擴充新功能。

先來看個例子,我們以前到百貨公司買東西,可以刷信用卡付錢,也可以刷金融卡付錢(兩者的差別在於金融卡帳戶有錢才能刷)。對於銀行而言他們會判斷你用的是哪種卡,而去做扣款的動作。

public class Payment {
    private String msg;
    public void pay(){
        System.out.println(msg);
    }
    public void setPaymentType(String type){
        if ("creditCard".equals(type)) {
            msg = "選擇信用卡付錢!!";
        } else if ("debitCard".equals(type)) {
            msg = "選擇金融卡付錢!!";
        }
    }
}

如上述程式,銀行判斷是刷哪種卡,在用該類的進行付款。但隨著科技越來越進步,現在不只這兩種付費方式。電子支付、悠遊卡…等等都是現在可以選擇的付費方式。如果銀行要跟著時代進步,勢必也要增加付款的功能才行。

public class Payment {
    private String msg;
    public void pay(){
        System.out.println(msg);
    }
    public void setPaymentType(String type){
        String msg;
        if ("creditCard".equals(type)) {
            msg = "選擇信用卡付錢!!";
        } else if ("debitCard".equals(type)) {
            msg = "選擇金融卡付錢!!";
        } else if ("easyCard".equals(type)) {
            msg = "選擇悠遊卡付錢!!";
        } else if ("electronicPayment".equals(type)) {
            msg = "選擇電子支付!!";
        } else {
            msg = ("不支援付款選項");
        }
        this.msg = msg;
    }
}

public class Bank {
    public static void main(String args[]) {
     Payment p = new Payment();
     p.setPaymentType("creditCard");
     p.pay();
    }
}

output

選擇信用卡付錢!!

程式經過修改後,增加了悠遊卡以及電子支付的方法進Payment內了!但這顯然不是一個好方法。如果再擴充的時候,把原本的方法改錯了,這樣會造成系統的問題。所以我們需要一個讓系統方便擴充,但同時又不能影響到原本的程式方法。

  • 業務邏輯?附加邏輯?


剛剛有提到,我們不能在既有的程式碼內做擴充,因為存在著風險。那我們要如何去擴充程式?首先我們要先分清楚業務邏輯以及附加邏輯。

業務邏輯為程式的核心邏輯,基本上系統可能有80%以上是圍繞著它,如同上述程式碼中的pay()。不管你選擇怎樣付款,會後都會進行pay()的動作進行付款。而剩下20%就是附加邏輯。附加邏輯會隨著新需求而增加,如同setPaymentType()內選擇付款方式從兩種變四種。

  • 應用 Open/Closed Principle


現在我們知道了不能在既有的程式碼做修改,也知道了業務邏輯與附加邏輯間的差異,那我們就可以開始對剛剛的付款範例進行OCP的修改!

abstract class Payment {
    private String msg;
    public void pay(){
        this.msg = setPaymentType();
        System.out.println(msg);
    }
    public abstract String setPaymentType();
}

class CreditCard extends Payment {
    @Override
    public String setPaymentType() {
        return "選擇信用卡付錢!!";
    };
}

// 其他三種略...

public class Bank {
    public static void main(String args[]) {
     CreditCard cc = new CreditCard();
     cc.pay();
    }
}

現在我們將原本的Paymant業務邏輯獨立出來一個abstract class,並把setPaymentType定義為抽象方法讓繼承的類別去實作他。接著把每個付款項目各自建立一個class繼承Payment,並且Override setPaymentType()。這樣若未來銀行想要增加虛擬貨幣付款,就不需要更動Payment的內容(符合修改封閉),直接建立一個新的class並繼承Payment即可(符合擴充開放)。

  • 小結


OCP的優點
1. 降低耦合
2. 增加擴展性
3. 易於維護
OCP目標

擴充新功能應藉由新增新的程式碼,而非修改原本的程式碼

  • 範例程式碼


範例1:未使用OCP

範例2:使用OCP

  • References


  • 物件導向設計原則:開放封閉原則,定義、解析與實踐
  • [物件導向]開放封閉原則(Open/closed principle)
  • [ 設計原則 ] - 開放封閉原則(OCP,Open-Closed Principle)