iT邦幫忙

2021 iThome 鐵人賽

DAY 24
0
Software Development

也該是時候學學 Design Pattern 了系列 第 24

Day 24: Behavioral patterns - Observer

目的

當需要多個物件同時「監聽」一個相同物件,一旦該物件發生變化時,監聽的物件們將自動採取相對應的處理。

說明

通常是發生在單個物件的狀態變化發生時,其他物件將會動態地跟著調整、執行各自對應的行為。

在模式內是這樣稱呼的:

  • 需要「監聽」的物件:觀察者(稱作 Observer、Subscriber)。
  • 「被監聽」的物件:通知者(稱作:Subject、Publisher)。

作法是:

  1. 建立觀察者的虛擬層親代,通常使用 interface,使用 update() 作為共同方法。
  2. 建立通知者的虛擬層親代,通常使用 interface,會建立三個常用的方法:
    1. 負責增加觀察者的:add()
    2. 負責移除觀察者的:remove()
    3. 負責通知觀察者的:notify()
  3. 建立觀察者子代,實作 update() 的細節。
  4. 建立通知者子代,實作三個常用方法的細節。
    1. 關於 notify() 要思考的是,通知者因為哪些行為改變自身狀態後才需要「通知」觀察者。
  5. 通知者子代除了實作三個常用方法的細節,還要考慮如何註冊觀察者們,要使用 Array(JS)、List(Java) 或是 Object(JS)、Map(Java)。
  6. 當使用者要使用時,先建立通知者,再讓通知者註冊觀察者。

以下範例以「簡易判斷 Log 等級後發出警示」為核心製作。

UML 圖

Observer Pattern UML Diagram

使用 Java 實作

製作 Log 資料:Log

public class Log {
    private String title;
    private String level;
    private String message;
    private String type;

    public Log(String title, String level, String message, String type) {
        this.title = title;
        this.level = level;
        this.message = message;
        this.type = type;
    }

    public String getTitle() {
        return title;
    }

    public String getLevel() {
        return level;
    }

    public String getMessage() {
        return title + ", " + message;
    }

    public String getType() {
        return type;
    }
}

製作觀察者的虛擬層親代:Siren

public interface Siren {
    void update(String logLevel, String message);
}

製作通知者的虛擬層親代:LogCollectorMethods

public interface LogCollectorMethods {
    void addSubject(String subject);

    void addSubscriber(String subject, Siren siren);

    void removeSubscriber(String subject, Siren siren);

    void notifySubscribers(String subject, String level, String message);
}

製作觀察者子代:ApiPassFailedSirenLogInFailedSiren(Observer 物件)

public class ApiPassFailedSiren implements Siren {
    private String name;
    private String level;

    public ApiPassFailedSiren(String name, String level) {
        this.name = name;
        this.level = level;
    }

    @Override
    public void update(String logLevel, String message) {
        if (level.equals("low")) {
            alert(message);
        } else if (level.equals("medium")) {
            if (logLevel.equals("high") || logLevel.equals("medium")) {
                alert(message);
            }
        } else if (level.equals("high") && logLevel.equals("high")) {
            alert(message);
        }
    }

    private void alert(String message) {
        System.out.println("The Siren: '" + name + "' is working now, and the message is:\n" + message);
    }
}

public class LogInFailedSiren implements Siren {
    private String name;
    private String level;

    public LogInFailedSiren(String name, String level) {
        this.name = name;
        this.level = level;
    }

    @Override
    public void update(String logLevel, String message) {
        if (level.equals("low")) {
            alert(message);
        } else if (level.equals("medium")) {
            if (logLevel.equals("high") || logLevel.equals("medium")) {
                alert(message);
            }
        } else if (level.equals("high") && logLevel.equals("high")) {
            alert(message);
        }
    }

    private void alert(String message) {
        System.out.println("The 'Log In Failed' " + name + " Siren is working now, and the message is:\n" + message);
    }
}

製作通知者子代:ApiPassFailedSiren(Subject 物件)

public class LogCollector implements LogCollectorMethods {
    private Map<String, List<Siren>> subscribers;

    public LogCollector() {
        this.subscribers = new HashMap<>();
    }

    @Override
    public void addSubject(String subject) {
        if (!subscribers.containsKey(subject)) {
            subscribers.put(subject, new ArrayList<>());
        }
    }

    @Override
    public void addSubscriber(String subject, Siren siren) {
        if (!subscribers.containsKey(subject)) {
            subscribers.put(subject, new ArrayList<>());
        }

        subscribers.get(subject).add(siren);
    }

    @Override
    public void removeSubscriber(String subject, Siren siren) {
        List<Siren> sirens = subscribers.get(subject);
        sirens.remove(siren);
    }

    @Override
    public void notifySubscribers(String subject, String level, String message) {
        List<Siren> sirens = subscribers.get(subject);

        for (Siren siren : sirens) {
            siren.update(level, message);
        }

        System.out.println("");
    }

    public void receiveLog(Log log) {
        notifySubscribers(log.getType(), log.getLevel(), log.getMessage());
    }
}

測試,建立通知者後,依序註冊觀察者,最後試著發送訊息:LogCollectorObserverSample

public class LogCollectorObserverSample {
    public static void main(String[] args) {
        LogCollector logCollector = new LogCollector();
        logCollector.addSubject("api");
        logCollector.addSubscriber("api", new ApiPassFailedSiren("/users", "low"));
        logCollector.addSubscriber("api", new ApiPassFailedSiren("/photos", "medium"));
        logCollector.addSubscriber("api", new ApiPassFailedSiren("/cars", "low"));
        logCollector.addSubscriber("api", new ApiPassFailedSiren("/keys", "high"));

        logCollector.addSubject("logIn");
        logCollector.addSubscriber("logIn", new LogInFailedSiren("users", "high"));
        logCollector.addSubscriber("logIn", new LogInFailedSiren("admin", "low"));

        logCollector.receiveLog(new Log("api pass failed", "high", "the client uses wrong token", "api"));
        logCollector.receiveLog(new Log("log in failed", "low", "the account is wrong", "logIn"));
    }
}

使用 JavaScript 實作

製作 Log 資料:Log

class Log {
  /**
   * @param {string} title
   * @param {string} level
   * @param {string} message
   * @param {string} type
   */
  constructor(title, level, message, type) {
    this.title = title;
    this.level = level;
    this.message = message;
    this.type = type;
  }

  getTitle() {
    return this.title;
  }

  getLevel() {
    return this.level;
  }

  getMessage() {
    return this.title + ", " + this.message;
  }

  getType() {
    return this.type;
  }
}

製作觀察者的虛擬層親代:Siren

/** @abstract */
class Siren {
  /**
   * @param {string} name
   * @param {string} level
   */
  constructor(name, level) {
    this.name = name;
    this.level = level;
  }

  /**
 * @override
 * @param {string} logLevel
 * @param {string} message
 */
  update(logLevel, message) {
    if (this.level === "low") {
      this.alert(message);
    } else if (this.level === "medium") {
      if (logLevel === "high" || logLevel === "medium") {
        this.alert(message);
      }
    } else if (this.level === "high" && logLevel === "high") {
      this.alert(message);
    }
  }

  /** @param {string} message */
  alert(message) { return; }

  /** @returns {string} */
  getName() { return; }
}

製作通知者的虛擬層親代:LogCollectorMethods

/** @abstract */
class LogCollectorMethods {
  constructor() {
    /** @type {Map<String, Siren[]>} */
    this.subscribers = new Map();
  }
  /** @param {string} subject */
  addSubject(subject) { return; }

  /**
   * @param {string} subject
   * @param {Siren} siren
   */
  addSubscriber(subject, siren) { return; }

  /**
 * @param {string} subject
 * @param {Siren} siren
 */
  removeSubscriber(subject, siren) { return; }

  /**
   * @param {string} subject
   * @param {string} level
   * @param {string} message
   */
  notifySubscribers(subject, level, message) { return; }
}

製作觀察者子代:ApiPassFailedSirenLogInFailedSiren(Observer 物件)

class ApiPassFailedSiren extends Siren {
  /**
   * @param {string} name
   * @param {string} level
   */
  constructor(name, level) {
    super(name, level);
  }

  /**
   * @override
   * @param {string} message
   */
  alert(message) {
    console.log("The Siren: '" + this.name + "' is working now, and the message is:\n" + message);
  }

  /** @override */
  getName() {
    return this.name;
  }
}

class LogInFailedSiren extends Siren {
  /**
   * @param {string} name
   * @param {string} level
   */
  constructor(name, level) {
    super(name, level);
  }

  /**
   * @override
   * @param {string} message
   */
  alert(message) {
    console.log("The 'Log In Failed' " + this.name + " Siren is working now, and the message is:\n" + message);
  }

  /** @override */
  getName() {
    return this.name;
  }
}

製作通知者子代:ApiPassFailedSiren(Subject 物件)

class LogCollector extends LogCollectorMethods {
  constructor() {
    super();
  }

  /**
   * @override
   * @param {string} subject
   */
  addSubject(subject) {
    if (!this.subscribers.has(subject)) {
      this.subscribers.set(subject, []);
    }
  }

  /**
   * @override
   * @param {string} subject
   * @param {Siren} siren
   */
  addSubscriber(subject, siren) {
    if (!this.subscribers.has(subject)) {
      this.subscribers.set(subject, []);
    }

    this.subscribers.get(subject).push(siren);
  }

  /**
   * @override
   * @param {string} subject
   * @param {Siren} siren
   */
  removeSubscriber(subject, siren) {
    const sirens = this.subscribers.get(subject);
    const removeSpecificSiren = sirens.filter(item => item.getName() !== siren.getName())
    this.subscribers.set(subject, removeSpecificSiren);
  }

  /**
   * @override
   * @param {string} subject
   * @param {string} level
   * @param {string} message
   */
  notifySubscribers(subject, level, message) {
    const sirens = this.subscribers.get(subject);

    for (const siren of sirens) {
      siren.update(level, message);
    }

    console.log("");
  }

  /** @param {Log} log */
  receiveLog(log) {
    this.notifySubscribers(log.getType(), log.getLevel(), log.getMessage());
  }
}

測試,建立通知者後,依序註冊觀察者,最後試著發送訊息:logCollectorObserverSample

const logCollectorObserverSample = () => {
  const logCollector = new LogCollector();
  logCollector.addSubject("api");
  logCollector.addSubscriber("api", new ApiPassFailedSiren("/users", "low"));
  logCollector.addSubscriber("api", new ApiPassFailedSiren("/photos", "medium"));
  logCollector.addSubscriber("api", new ApiPassFailedSiren("/cars", "low"));
  logCollector.addSubscriber("api", new ApiPassFailedSiren("/keys", "high"));

  logCollector.addSubject("logIn");
  logCollector.addSubscriber("logIn", new LogInFailedSiren("users", "high"));
  logCollector.addSubscriber("logIn", new LogInFailedSiren("admin", "low"));

  logCollector.receiveLog(new Log("api pass failed", "high", "the client uses wrong token", "api"));
  logCollector.receiveLog(new Log("log in failed", "low", "the account is wrong", "logIn"));
};

logCollectorObserverSample();

總結

Observer 模式還有另一個稱呼:Publish/Subscribe,這稱呼直接了當地表達該模式的重點。

自己實作時觀察到:

  • 如何管理觀察者們?
    • 如果需要依照觸發類別分門別類,那適合使用 Object 搭配 Array(或是 Map 搭配 List)。
    • 如果需要依照加入的順序,那使用 Array(或是 List)即可。
  • 如何搭配語言特性,簡化「觀察 <-> 訂閱 <-> 通知」的模式?
    • 在「大話設計模式」中有提到運用 delegate 可以簡化。

最後要提一點,Observer 模式與 Mediator 模式,兩者的初衷不同,前者是建立單向的連結,後者是消除物件們複雜的依賴關係,而實作騎來卻十分相似,想想這或許是讓程式碼維持高彈性、減少複雜度等目的後,必然的結果也說不定。

明天將介紹 Behavioural patterns 的第八個模式:State 模式。


上一篇
Day 23: Behavioral patterns - Memento
下一篇
Day 25: Behavioral patterns - State
系列文
也該是時候學學 Design Pattern 了31

尚未有邦友留言

立即登入留言