文章目錄
- 什麼是開閉原則
- 簡單執行個體
- 實戰執行個體
- 如何了解“對修改關閉”?修改代碼就一定違背開閉原則嗎
- 參考資料
什麼是開閉原則
開閉原則的英文全稱是 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 類添加單元測試,老的單元測試都不會失敗,也不用修改。
如何了解“對修改關閉”?修改代碼就一定違背開閉原則嗎
對修改關閉的前提是:對擴充開放。
,添加一個新功能,不可能任何子產品、類、方法的代碼都不“修改”,這個是做不到的。類需要建立、組裝、并且做一些初始化操作,才能建構成可運作的的程式,這部分代碼的修改是在所難免的。我們要做的是盡量讓修改操作更集中、更少、更上層,盡量讓最核心、最複雜的那部分邏輯代碼滿足開閉原則。
也就是說,對拓展開放是為了應對變化(需求),對修改關閉是為了保證已有代碼的穩定性;最終結果是為了讓系統更有彈性!
添加一個新的功能,如果能夠保證老的核心代碼不會被修改,那麼這就是符合開閉原則的。
熟練使用各種設計模式、并且應用到實際工作中,是我們開發者一生都要去學習的。
但是,熟悉了“開閉原則”,這并不意味着你需要随時随地都要考慮擴充。需求永遠是在不斷變化的,即便我們對業務、對系統有足夠的了解,那也不可能識别出所有的擴充點,即便你能識别出所有的擴充點,為這些地方都預留擴充點,這樣做的成本也是不可接受的。我們沒必要為一些遙遠的、不一定發生的需求去提前買單,做過度設計。
最合理的做法是,對于一些比較确定的、短期内可能就會擴充,或者需求改動對代碼結構影響比較大的情況,或者實作成本不高的擴充點,在編寫代碼的時候之後,我們就可以事先做些擴充性設計。但對于一些不确定未來是否要支援的需求,或者實作起來比較複雜的擴充點,我們可以等到有需求驅動的時候,再通過重構代碼的方式來支援擴充的需求。