iT邦幫忙

2023 iThome 鐵人賽

DAY 16
0
Software Development

軟體架構備忘錄系列 第 16

Day 16 程式架構 - 行為型設計模式 (知識點080~084)

  • 分享至 

  • xImage
  •  

想要解決的問題

如何將可能變動的邏輯抽出,讓該邏輯視需求動態抽換?

架構設計中,有許多邏輯都會可能會變動。需要事先辨認那些是可能變動的部分。
並抽出這些變動的邏輯,後續再通過 覆寫、呼叫中介者、呼叫註冊的觀察者等方式進行動作。
常見作法包含:模板方法模式、中介者模式、觀察者模式、策略模式、狀態模式


模板方法模式

描述

範本方法模式 (Template Method Pattern) 是一種設計模式,它將共用的方法放在父類中,並定義一個演算法的骨架,將一些步驟推遲到子類實現。父類專注於共通的流程步驟,而子類負責提供特定的實現細節。

使用情境

  • 延遲實現:希望由父類別定義共通邏輯與框架,部分變動邏輯由子類別實現

案例1:經典範本方法模型

一個經典的例子是 Java Servlet 的 GenericServlet 類,它是 Servlet API 中的一個實現。GenericServlet 將一些常用的 Servlet 方法作為共用方法實現,但最關鍵的 service 方法是由子類實現,以延遲具體的響應內容的生成。

public class MyServlet extends GenericServlet {

    @Override
    public void service(ServletRequest request, ServletResponse response) throws ServletException, IOException {
        response.setContentType("text/html");
        PrintWriter out = response.getWriter();
        out.println("<html>");
        out.println("<body>");
        out.println("<h1>Hello, World!</h1>");
        out.println("</body>");
        out.println("</html>");
    }
}

案例2:套用範本方法模型概念

在現代框架中,如 Spring Boot,也使用了範本方法模式的概念。例如,Spring Boot 的 Controller 類中的請求處理方法可以視為範本方法,而具體的業務邏輯則由子類(Controller 類)實現。在下方的示例中,@GetMapping 用於指定服務的 URL,而 @PathVariable 用於提取 URL 中的變數。

@RestController
public class RestController {

    @GetMapping(value = "/student/{studentId}")
    public Student getTestData(@PathVariable Integer studentId) {
        Student student = new Student();
        student.setName("Peter");
        student.setId(studentId);

        return student;
    }
}

中介者模式

描述

中介者模式 (Mediator Pattern) 使用於程式元素之間的相互聯繫。若是N個元素之間,都有可以互相影響其他元素的呈現狀態。為了避免之間的耦合性過複雜,而且有多個重複控制出現於不同元素的處理程式碼。應抽出獨立的中介者程式,負責與其他元素進行控制。

使用情境

  • 與外部系統、組件進行界接:由於界接時都需要處理認證、請求與回傳剖析、錯誤處理等邏輯。將這些邏輯統一放置於中介者,以便於未來依需求統一調整溝通細節。

案例1: 前端的中介者模式

由於前端的各畫面區塊的表單元素之間,可能會有複雜的交互關係。

應該通過中介者進行統一事件處理,以避免表單元素之間的耦合過於複雜。

另外,為了避免單一中介者負責太多的表單元素控制,可依照畫面區塊分成多個中介者。

// 中介者物件
const Form1Mediator = {
    handleEvent: (event, source) => {
        if (event === "change") {
            console.log(`Input "${source.id}" changed to: ${source.value}`);
        } else if (event === "click") {
            console.log(`Button "${source.id}" clicked.`);
        }
    },
};

//每個元素分別註冊事件發生時,通知中介者
document.getElementById("inputName").addEventListener("change", (event) => {
    Form1Mediator.handleEvent("change", event.target);
});
document.getElementById("inputDate").addEventListener("change", (event) => {
    Form1Mediator.handleEvent("change", event.target);
});
document.getElementById("btnOk").addEventListener("click", (event) => {
    Form1Mediator.handleEvent("click", event.target);
});
document.getElementById("btnCancel").addEventListener("click", (event) => {
    Form1Mediator.handleEvent("click", event.target);
});

在前端也可以使用事件委任機制,將所有事件控制集中於特定表單區塊,以減少事件註冊的數量。

// 中介者物件
const Form1Mediator = {
    handleEvent: (event, source) => {
        if (event === "change") {
            if (source.tagName === "INPUT") {
                console.log(`Input "${source.id}" changed to: ${source.value}`);
            }
        } else if (event === "click") {
            if (source.tagName === "BUTTON") {
                console.log(`Button "${source.id}" clicked.`);
            }
        }
    },
};

// 獲取父級div元素
const myDiv = document.getElementById("myDiv");
// 使用事件委派由容器元素統一處理所有事件
myDiv.addEventListener("click", (event) => {
    Form1Mediator.handleEvent("click", event.target);
});
myDiv.addEventListener("change", (event) => {
    Form1Mediator.handleEvent("change", event.target);
});

觀察者模式

描述

觀察者模式 (Observer Pattern) 用於需要在某物件的狀態改變時(發佈者),呼叫其他物件(觀察者)時。其實這很類似於實作簡單事件註冊機制。

使用情境

  • 動態通知其他物件狀態改變:事件觸發的位置事先不需要知道那些程式需要引用此事件。可解偶觀察者與發佈者

案例1: 經典的觀察者

通過建立主題實現物件,並提供觀察者註冊機制。當事件發生時,通知已註冊的觀察者。

// 觀察者介面
interface Observer {
    void update(String message);
}

// 觀察者實現類
class UserMessageObserver implements Observer {
    @Override
    public void update(String message) {
        System.out.println("收到使用者消息:" + message);
        // 在這裡執行觀察者的動作,例如顯示通知、處理消息等
    }
}

// 主題介面
interface Subject {
    void addObserver(Observer observer);
    void removeObserver(Observer observer);
    void notifyObservers(String message);
}

// 主題實現類
class UserMessageSubject implements Subject {
    private List<Observer> observers = new ArrayList<>();

    @Override
    public void addObserver(Observer observer) {
        observers.add(observer);
    }

    @Override
    public void removeObserver(Observer observer) {
        observers.remove(observer);
    }

    @Override
    public void notifyObservers(String message) {
        for (Observer observer : observers) {
            observer.update(message);
        }
    }
}

案例2:前端的觀察者模式

在JS或其他語言,已支援可直接傳遞方法進入觀察者中。

因此這種模式下,實作觀察者會簡單很多。

//主題實現物件
var UserMessageSubject = {
  observers: [],// 觀察者陣列,用於儲存回呼函數
  addObserver: function(callback) {//新增觀察者
    observers.push(callback);
  },
  removeObserver: function(callback) {// 移除觀察者
    const index = observers.indexOf(callback);
    if (index !== -1) {
      observers.splice(index, 1);
    }
  },
  notifyObservers: function(message) {// 通知觀察者
    observers.forEach((observer) => {
      observer(message);
    });
  }
}

// 新增觀察者
UserMessageSubject.addObserver((message)=>{'Observer 1 收到消息:' + message});
UserMessageSubject.addObserver((message)=>{'Observer 2 收到消息:' + message});

// 模擬使用者傳送消息
const userMessage = '這是一條使用者消息。';
UserMessageSubject.notifyObservers(userMessage);

策略模式

描述

策略模式 (Strategy Pattern) 將系統中某部分的邏輯抽出,透過指定邏輯interface的傳入、傳回值 與行為。讓呼叫系統的程式可以動態決定要使用哪一種方式進行那部分的邏輯。

使用情境

  • 由呼叫端自行決定部分邏輯:如果運行中有一部分的邏輯是可動態抽換的,則可以使用策略模式

案例

在前端JS中,可以設計一個元件讓使用者傳入一個表單,此元件會自動進行資料驗證。

並且可讓使用者自行指定客製化檢查的策略模式邏輯。

var customValidator = (form) => {//自定義的檢查邏輯
  var validResult = []; 
  if($(form).find('input_1').val.length < 5 )  {
    validResult.push('input_1 必須至少包含5個字元');
  }
  if(!/^\d+$/.test($(form).find('input_2').val))  {
    validResult.push('input_2 須為數字);
  }
  return validResult;
});
//傳入自定義檢查邏輯進行驗證
var validateResult = ValidationLibrary.validate($('#targetForm'), customValidator);
validateResult.showResult();//在畫面上呈現檢查結果

狀態模式

描述

狀態模式 (State Pattern) 是基於有限狀態機 (Inite State Machine,FSM) 的概念。如果某個模組有多個不同的狀態,並且在不同狀態間可以執行的動作,行為模式有所差異。為了避免狀態控制的邏輯過於複雜而難以維護,將各狀態分別以不同的類別進行描述其行為細節。

使用情境

  • 多狀態且狀態控制複雜:如果模組中有多個不同狀態,而且需要於不同狀態間切換,以及處理不同的事項。分別存放不同狀態邏輯,有助於維護以及提升易理解性。

案例1:

使用狀態模式控制在Lock狀態時只有備註欄位可以輸入。

// 表單狀態接口
interface FormState {
    void handleForm(Map<String, JTextField> formComponents);
}

// 鎖定狀態
class LockedState implements FormState {
    @Override
    public void handleForm(Map<String, JTextField> formComponents) {
        // 鎖定表單中的所有元素,除了 "comment"
        for (Map.Entry<String, JTextField> entry : formComponents.entrySet()) {
            String fieldName = entry.getKey();
            JTextField textField = entry.getValue();
            if (!fieldName.equals("comment")) {
                textField.setEditable(false);
            }
        }
    }
}

// 非鎖定狀態
class UnlockedState implements FormState {
    @Override
    public void handleForm(Map<String, JTextField> formComponents) {
        // 解鎖表單中的所有元素
        for (Map.Entry<String, JTextField> entry : formComponents.entrySet()) {
            JTextField textField = entry.getValue();
            textField.setEditable(true);
        }
    }
}

案例2:

如果狀態控制更為複雜時,甚至有專門套件處理狀態邏輯。

例如前端的xstate套件就是參考FSM模型的形式建置的狀態控制工具。

import { createMachine, interpret } from "xstate";

// 建立一個名為 "form" 的狀態機,用於管理表單狀態
const formStateMachine = createMachine({
  id: "form", // 狀態機的唯一識別碼
  initial: "idle", // 初始狀態為 "idle",即表單空閒狀態
  states: {
    idle: {
      // 在 "idle" 狀態下可以觸發的事件
      on: {
        LOCK: "locked", // 當觸發 "LOCK" 事件時,切換到 "locked" 狀態
      }
    },
    locked: {
      // "locked" 狀態下可以觸發的事件
      on: {
        UNLOCK: "idle", // 當觸發 "UNLOCK" 事件時,切換回 "idle" 狀態
      }
    }
  }
});

// 建立狀態機實例
const formService = interpret(formStateMachine).start();

// 表單元素引用
const nameInput = document.getElementById("name");
const emailInput = document.getElementById("email");
const commentInput = document.getElementById("comment");
const lockButton = document.getElementById("lock-button");

// 根據狀態停用或啟用表單欄位
formService.onTransition(state => {
  if (state.matches("idle")) {
    nameInput.disabled = false;
    emailInput.disabled = false;
    commentInput.disabled = false;
  } else if (state.matches("locked")) {
    nameInput.disabled = true;
    emailInput.disabled = true;
    commentInput.disabled = false; // 在鎖定後只有備註欄位可以輸入
  }
});

// 觸發 LOCK 事件,切換到 locked 狀態
formService.send("LOCK");
// 觸發 UNLOCK 事件,切換回 idle 狀態
formService.send("UNLOCK");

上一篇
Day 15 程式架構 - 結構型設計模式 (知識點076~079)
下一篇
Day 17 程式架構 - 網頁應用程式安全 (知識點085~089)
系列文
軟體架構備忘錄30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言