天天看點

設計原則之【開閉原則】

文章目錄

  • ​​什麼是開閉原則​​
  • ​​簡單執行個體​​
  • ​​實戰執行個體​​
  • ​​如何了解“對修改關閉”?修改代碼就一定違背開閉原則嗎​​
  • ​​參考資料​​

什麼是開閉原則

開閉原則的英文全稱是 Open Closed Principle,簡寫為 OCP。它的英文描述是:software entities (modules, classes, functions, etc.) should be open for extension , but closed for modification。我們把它翻譯成中文就是:軟體實體(子產品、類、方法等)應該“對擴充開放、對修改關閉”。

可擴充性,就是,添加一個新的功能應該是,在已有代碼基礎上擴充代碼(新增子產品、類、方法等),而非修改已有代碼(修改子產品、類、方法等)。是衡量代碼品質最重要的标準之一。

開閉原則了解起來并不難,難的是能夠靈活的應用到實際開發工作中。

簡單執行個體

以下代碼相信也不陌生,這就是典型的使用面向對象語言做着面向過程的事:

package com.study;

public class Ocp {

    public static void main(String[] args) {
        GraphicEditor graphicEditor = new GraphicEditor();
        graphicEditor.drawShape(new Rectangle());
        graphicEditor.drawShape(new Circle());
        graphicEditor.drawShape(new Triangle());
    }
}

//這是一個用于繪圖的類 [使用方]
class GraphicEditor {
    //接收 Shape 對象,然後根據 type,來繪制不同的圖形
    public void drawShape(Shape s) {
        if (s.m_type == 1){
            drawRectangle(s);
        } else if (s.m_type == 2){
            drawCircle(s);
        } else if (s.m_type == 3){
            drawTriangle(s);
        }
    }

    //繪制矩形
    public void drawRectangle(Shape r) {
        System.out.println(" 繪制矩形 ");
    }

    //繪制圓形
    public void drawCircle(Shape r) {
        System.out.println(" 繪制圓形 ");
    }

    //繪制三角形
    public void drawTriangle(Shape r) {
        System.out.println(" 繪制三角形 ");
    }
}

//Shape 類,基類
class Shape {
    int m_type;
}

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

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

//新增畫三角形
class Triangle extends Shape {
    Triangle() {
        super.m_type = 2;
    }
}      

優化後:

package com.study;

public class Ocp {

    public static void main(String[] args) {
        GraphicEditor graphicEditor = new GraphicEditor();
        graphicEditor.drawShape(new Rectangle());
        graphicEditor.drawShape(new Circle());
        graphicEditor.drawShape(new Triangle());
    }
}

//這是一個用于繪圖的類 [使用方]
class GraphicEditor {
    //接收 Shape 對象,然後根據 type,來繪制不同的圖形
    public void drawShape(Shape s) {
        s.draw();
    }
}

//Shape 類,基類
abstract class Shape {
    abstract void draw();
}

class Rectangle extends Shape {
    @Override
    void draw() {
        System.out.println(" 繪制矩形 ");
    }
}

class Circle extends Shape {
    @Override
    void draw() {
        System.out.println(" 繪制圓形 ");
    }
}

//新增畫三角形
class Triangle extends Shape {
    @Override
    void draw() {
        System.out.println(" 繪制三角形 ");
    }
}      

實戰執行個體

現在有一個登入邏輯:

public class Login {
  private AccountService accountService;

  @Autoware
  public Login (AccountService accountService) {
    this.accountService = accountService;
  }

  public boolean loginCheck(String password, String name) {
    // 檢查賬号密碼
    if(!accountService.checkPwd(password, name)){
    return false;
  }
  // 檢查登入失敗次數
  if(accountService.getLoginErrorCount() >= 5){
    return false;
    }
    return true;
  }
}      

上述代碼相信很多小夥伴都會遇到,看上去似乎并沒有什麼大問題。

但是,現在新增一個邏輯:登入時,我要校驗驗證碼是否正确,代碼可能需要做以下改動:

public class Login {
  private AccountService accountService;

  @Autoware
  public Login (AccountService accountService) {
    this.accountService = accountService;
  }

  // 改動1:方法添加參數
  public boolean loginCheck(String password, String name, String verificationCode) {
    // 檢查賬号密碼
    if(!accountService.checkPwd(password, name)){
    return false;
  }
  // 檢查登入失敗次數
  if(accountService.getLoginErrorCount() >= 5){
    return false;
    }
    // 改動2:檢查驗證碼
    if(!accountService.getVerificationCode().equals(verificationCode)){
    return false;
    }
    return true;
  }
}      

這樣的代碼修改實際上存在挺多問題的。一方面,我們對接口進行了修改,這就意味着調用這個接口的代碼都要做相應的修改。另一方面,修改了 loginCheck() 函數内容,相應的單元測試及其他邏輯都需要修改。

接下來我們重構一下Login的代碼,讓其具備擴充性,重構包括兩部分:

  • 第一部分是将 loginCheck() 函數的多個入參封裝成 LoginCheckInfo類;
  • 第二部分是引入 handler 的概念,将 if 判斷邏輯分散在各個 handler 中。
public class LoginCheckInfo {//省略constructor/getter/setter方法
  private String name;
  private String password;
  private String verificationCode;
}

public class Login {
  private List<LoginCheckHandler> checkHandlers = new ArrayList<>();
  
  public void addLoginCheckHandler(CheckHandler checkHandler) {
    this.checkHandlers.add(checkHandler);
  }

  public void check(LoginCheckInfo loginCheckInfo) {
    for (LoginCheckHandlerhandler : checkHandlers) {
      boolean result = handler.check(loginCheckInfo);
      // 處理邏輯
    }
  }
}

public abstract class CheckHandler {
  private AccountService accountService;

  @Autoware
  public CheckHandler (AccountService accountService) {
    this.accountService = accountService;
  }
  public abstract void check(LoginCheckInfo loginCheckInfo);
}

public class PwdCheckHandler extends CheckHandler {
  public PwdCheckHandler(AccountService accountService) {
    super(accountService);
  }

  @Override
  public boolean check(LoginCheckInfo loginCheckInfo) {
    return accountService.checkPwd(loginCheckInfo.getPassword(), loginCheckInfo.getName());
  }
}

public class LoginCountCheckHandler extends CheckHandler {
  public LoginCountCheckHandler(AccountService accountService) {
    super(accountService);
  }

  @Override
  public boolean check(LoginCheckInfo loginCheckInfo) {
    return accountService.getLoginErrorCount() >= 5;
  }
}      

要想使用我們重構後的登入邏輯:

@Bean
public Login login(){
  Login login = new Login();
  login.addLoginCheckHandler(new PwdCheckHandler());
  login.addLoginCheckHandler(new LoginCountCheckHandler());
}

// 隻需要在service層注入login,然後調用login.check();即可      

此時,我們如果想要擴充驗證碼功能:

public class VerificationCodeCheckHandler extends CheckHandler {
  public VerificationCodeCheckHandler(AccountService accountService) {
    super(accountService);
  }

  @Override
  public boolean check(LoginCheckInfo loginCheckInfo) {
    return accountService.getVerificationCode().equals(loginCheckInfo.getVerificationCode());
  }
}

// 然後在定義Login的bean中将這個處理器添加進去即可。
login.addLoginCheckHandler(new VerificationCodeCheckHandler());      

重構之後的代碼更加靈活和易擴充。如果我們要想添加新的告警邏輯,隻需要基于擴充的方式建立新的 handler 類即可,不需要改動原來的 check() 函數的邏輯。而且,我們隻需要為新的 handler 類添加單元測試,老的單元測試都不會失敗,也不用修改。

如何了解“對修改關閉”?修改代碼就一定違背開閉原則嗎

對修改關閉的前提是:對擴充開放。

,添加一個新功能,不可能任何子產品、類、方法的代碼都不“修改”,這個是做不到的。類需要建立、組裝、并且做一些初始化操作,才能建構成可運作的的程式,這部分代碼的修改是在所難免的。我們要做的是盡量讓修改操作更集中、更少、更上層,盡量讓最核心、最複雜的那部分邏輯代碼滿足開閉原則。

也就是說,對拓展開放是為了應對變化(需求),對修改關閉是為了保證已有代碼的穩定性;最終結果是為了讓系統更有彈性!

添加一個新的功能,如果能夠保證老的核心代碼不會被修改,那麼這就是符合開閉原則的。

熟練使用各種設計模式、并且應用到實際工作中,是我們開發者一生都要去學習的。

但是,熟悉了“開閉原則”,這并不意味着你需要随時随地都要考慮擴充。需求永遠是在不斷變化的,即便我們對業務、對系統有足夠的了解,那也不可能識别出所有的擴充點,即便你能識别出所有的擴充點,為這些地方都預留擴充點,這樣做的成本也是不可接受的。我們沒必要為一些遙遠的、不一定發生的需求去提前買單,做過度設計。

最合理的做法是,對于一些比較确定的、短期内可能就會擴充,或者需求改動對代碼結構影響比較大的情況,或者實作成本不高的擴充點,在編寫代碼的時候之後,我們就可以事先做些擴充性設計。但對于一些不确定未來是否要支援的需求,或者實作起來比較複雜的擴充點,我們可以等到有需求驅動的時候,再通過重構代碼的方式來支援擴充的需求。