Mediator
January 9, 2022程式中的相依關係有時錯綜複雜,甚至形成一種多個物件間彼此相依的情況,來隨便舉個例子好了,你有一個訊息盒、一個主控台,一個動作物件。
如果訊息盒被設置了訊息,會清空主控台、檢查訊息長度,若是長度不足,會在主控台設置警訊,將動作物件設為不可用,否則就將動作物件設為可用:
class MessageBox {
private String message = "";
private Console console;
private Action action;
MessageBox(Console console, Action action) {
this.console = console;
this.action = action;
}
void setMessage(String message) {
this.message = message;
console.clean();
if(message.length() < 8) {
console.warning("長度不足 8 個字元");
action.disable();
}
else {
action.enable();
}
}
String getMessage() {
return message;
}
}
動作物件在可用的情況下,會從訊息盒取得訊息,清空主控台,然後 log 訊息:
class Action {
private boolean enabled;
private MessageBox messageBox;
private Console console;
Action(MessageBox messageBox, Console console) {
this.messageBox = messageBox;
this.console = console;
}
void execute() {
if(this.enabled) {
String message = messageBox.getMessage();
console.clean();
console.log(message);
}
}
void disable() {
enabled = false;
}
void enable() {
enabled = true;
}
}
這只是個簡單示範,顯然地,MessageBox
相依在 Console
、Action
,而 Action
相依在 MessageBox
、Console
,在更複雜的情境中,物件若是像這樣彼此相依,就會像一團大毛線球,將這些物件緊緊糾在一起,解不開理還亂,變得難以維護。
來個調解者?
Gof 說了,這時來個調解者,物件就可以不用知道彼此的存在!還有句不知道起源自哪的話,電腦科學的問題,都可以通過增加一個中間層來解決!於是你就定義了一個 Mediator
:
class Mediator {
MessageBox messageBox;
Action action;
Console console;
Mediator(MessageBox messageBox, Action action, Console console) {
this.messageBox = messageBox;
this.action = action;
this.console = console;
}
String getMessage() {
return messageBox.getMessage();
}
void setMessage(String message) {
messageBox.setMessage(message);
}
void actionEnable() {
action.enable();
}
void actionDisabe() {
action.disable();
}
void consoleClean() {
console.clean();
}
void consoleLog(String message) {
console.log(message);
}
void consoleWarning(String message) {
console.warning(message);
}
}
然後,物件需要做什麼,就都要求這個 Mediator
:
class MessageBox {
private String message = "";
private Mediator mediator;
public MessageBox(Mediator mediator) {
this.mediator = mediator;
}
void setMessage(String message) {
this.message = message;
mediator.consoleClean();
if(message.length() < 8) {
mediator.consoleWarning("長度不足 8 個字元");
mediator.actionDisabe();
}
else {
mediator.actionEnable();
}
}
String getMessage() {
return message;
}
}
class Action {
private boolean enabled;
private Mediator mediator;
public Action(Mediator mediator) {
this.mediator = mediator;
}
void execute() {
if(this.enabled) {
String message = mediator.getMessage();
mediator.consoleClean();
mediator.consoleLog(message);
}
}
void disable() {
enabled = false;
}
void enable() {
enabled = true;
}
}
好棒!MessageBox
、Action
等,不用知道彼此的存在,,現在只要知道 Mediator
就好了!?
你在開玩笑嗎?
在 Gof 提出的模式中,Mediator 是最隱晦不明的一個模式,口述了一個圖形介面對話方塊的情境,畫了幾張圖,然後說有個 Mediator
就可以解決彼此間的相依關係。
只不過為什麼圖形介面會產生那些相依關係呢?書中沒有任何程式碼作為觀察對象,我能想像到的是,確實是有不少人會繼承圖形元件,建立一個子類別來擴充元件功能,然後讓子類別本身實作多個事件傾聽器,使得子類別有太多的任務,從而需要知道其他圖形元件的存在;不過,畢竟這是你讓一個物件肩負太多職責的問題,不一定或沒必要透過 Mediator
來解決!
就算不是這個情境,圖形介面真的產生彼此間錯綜複雜的相依,你也得觀察實際的程式碼,查明到底是什麼樣的相依關係,才能解開各自不同的相依關係,並不是有個 Mediator
就好了。
例如,方才的 MessageBox
、Action
等,看似只需要知道 Mediator
的存在,其他就都被隱藏了嗎?並沒有!來看看 MessageBox
的 setMessage
,你還是得知道,該呼叫 Mediator
的哪個方法吧!
void setMessage(String message) {
this.message = message;
mediator.consoleClean();
if(message.length() < 8) {
mediator.consoleLog("長度不足 8 個字元");
mediator.actionDisabe();
}
else {
mediator.actionEnable();
}
}
來看看 Action
的 setMessage
,你還是知道了有主控台之類的東西,會需要清空、會作為做 log 的對象吧!
void execute() {
if(this.enabled) {
String message = mediator.getMessage();
mediator.consoleClean();
mediator.consoleLog(message);
}
}
這些物件需要知道的東西有比較少?你在開玩笑吧!
迪米特法則
Mediator 與其說是個模式,不如說像是迪米特法則(Law of Demeter)這類的最少知識原則(Principle of least knowledge),也就是「各單元對其他單元所知應當有限:只瞭解與目前單元最相關之單元」。
然而,「單元」不見得是以物件單位,就像上面的 MessageBox
並不因為只面對 Mediator
,就表示它只面對一個單元,實際上,consoleClean
、actionDisable
等方法的呼叫,不也代表著 MessageBox
知道了 Mediator
進一步的細節,它接觸了 consoleClean
、actionDisable
等單元不是嗎?
沒有情境,沒有需求,沒有實際的程式碼,就無法討論最少知識原則下,該怎麼重構程式碼,那只是個原則;沒有情境,沒有需求,沒有實際的程式碼,你說用個 Mediator
就能減少接觸的單元,那是個笑話!
上面還只是個陽春範例,如果是實際的應用程式,你的 Mediator
本身,不就是一團大毛線球嗎?而且這團大毛線球還成了全知全能的上帝物件(god object)呢!你覺得這樣的設計有比較好?
就一開始的 MessageBox
而言,你應該怎麼重構呢?單就這個陽春範例而言,至少你應該辨識出,MessageBox
被設定訊息時,想做事其實是格式正確與否時的對應動作,至於重構的方向呢?需要有進一步的需求才知道,也許很多人對格式正確與否時的對應動作有興趣,那就設計個事件處理:
class MessageBox {
private String message = "";
void setMessage(String message, Consumer<String> left, Consumer<String> right) {
this.message = message;
if(message.length() < 8) {
left.accept(message);
}
else {
right.accept(message);
}
}
String getMessage() {
return message;
}
}
這麼一來,MessageBox
不用知道 left
、right
的細節了,設定訊息時可以如下:
messageBox.setMessage("some messasge",
message -> {
console.clean();
console.warning("長度不足 8 個字元");
action.disable();
},
message -> {
console.clean();
action.enable();
}
);
也許你也可以設計一個 MessageService
給 MessageBox
:
class MessageBox {
private String message = "";
private MessageService messageService;
MessageBox(MessageService messageService) {
this.messageService = messageService
}
void setMessage(String message, Consumer<String> left, Consumer<String> right) {
this.message = message;
if(message.length() < 8) {
messageService.processError("長度不足 8 個字元");
}
else {
messageService.processMessage(message);
}
}
String getMessage() {
return message;
}
}
或許在不同的需求下,你會有不同的重構方式,無論如何,絕不是用個像是上帝物件的 Mediator
,把全部的相依塞進去,以為只面對一個 Mediator
,就是去除了跟其他物件的相依性了。
真有 Mediator?
有沒有什麼情境,可以使用一個居中協調物件,就可以讓各物件彼此合作?有!不過,這些物件必然有一定的行為規範,訊息上必然有對應的定義。
例如,在〈Interpreter〉中最後的那些 Parser
:
class BlockParser implements Parser {
@Override
public Command parse(Source source) {
var block = new Block();
while(source.hasNextToken()) {
var cmd = source.nextToken();
if(cmd.equals("print")) {
block.add(new PrintParser().parse(source));
}
else if(cmd.equals("repeat")) {
block.add(new RepeatParser().parse(source));
}
}
return block;
}
}
class PrintParser implements Parser {
@Override
public Command parse(Source source) {
return new Print(source.nextToken());
}
}
class RepeatParser implements Parser {
@Override
public Command parse(Source source) {
var times = Integer.parseInt(source.nextToken());
return new Repeat(times, new BlockParser().parse(source));
}
}
這些 Parser
就目前程式碼來看,其實不需要狀態,如果想要的話,可以設計一個 Parsers
來管理 Parser
實例:
class Parsers {
private static Map<String, Parser> parsers = Map.of(
"block", source -> {
var block = new Block();
while(source.hasNextToken()) {
var cmd = source.nextToken();
block.add(Parsers.get(cmd).parse(source));
}
return block;
},
"print", source -> new Print(source.nextToken()),
"repeat", source -> {
var times = Integer.parseInt(source.nextToken());
return new Repeat(times, Parsers.get("block").parse(source));
}
);
static Parser get(String cmd) {
return parsers.get(cmd);
}
}
不用管實際的 Parser
型態,只要知道它們都有 parse
行為,這個時候 Parsers
在角色上,就有點像是 Mediator。
這也就是為什麼,有些人會說,像是多人連線聊天室、訊息廣播、頻道的發佈訂閱,是 Mediator 的實際應用,或說是有 Mediator 的概念,畢竟聊天室、廣播的連線客戶端,行為上會有規範,訊息格式上也會有定義,由一個居中的伺服器來協調,客戶端、頻道發佈/訂閱者彼此之間,就不用知曉各自的存在。
想用個上帝物件就切開一組物件間錯綜複雜的相依性,那只是掩耳盜鈴或說是駝鳥行為,沒有那種便宜事,面對錯綜複雜的相依性,你要有真實情境、需求,才能逐一分析相依性,才能知道朝哪個方向重構。
如果最後你真的可以找出物件之間,其實具有一定的行為規範,也可以找到對應的訊息定義,或許你重構後的結果,才會具有 Mediator 的概念吧!