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 相依在 ConsoleAction,而 Action 相依在 MessageBoxConsole,在更複雜的情境中,物件若是像這樣彼此相依,就會像一團大毛線球,將這些物件緊緊糾在一起,解不開理還亂,變得難以維護。

來個調解者?

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;
    }
}

好棒!MessageBoxAction 等,不用知道彼此的存在,,現在只要知道 Mediator 就好了!?

你在開玩笑嗎?

在 Gof 提出的模式中,Mediator 是最隱晦不明的一個模式,口述了一個圖形介面對話方塊的情境,畫了幾張圖,然後說有個 Mediator 就可以解決彼此間的相依關係。

只不過為什麼圖形介面會產生那些相依關係呢?書中沒有任何程式碼作為觀察對象,我能想像到的是,確實是有不少人會繼承圖形元件,建立一個子類別來擴充元件功能,然後讓子類別本身實作多個事件傾聽器,使得子類別有太多的任務,從而需要知道其他圖形元件的存在;不過,畢竟這是你讓一個物件肩負太多職責的問題,不一定或沒必要透過 Mediator 來解決!

就算不是這個情境,圖形介面真的產生彼此間錯綜複雜的相依,你也得觀察實際的程式碼,查明到底是什麼樣的相依關係,才能解開各自不同的相依關係,並不是有個 Mediator 就好了。

例如,方才的 MessageBoxAction 等,看似只需要知道 Mediator 的存在,其他就都被隱藏了嗎?並沒有!來看看 MessageBoxsetMessage,你還是得知道,該呼叫 Mediator 的哪個方法吧!

    void setMessage(String message) {
        this.message = message;
        mediator.consoleClean();
        if(message.length() < 8) {
            mediator.consoleLog("長度不足 8 個字元");
            mediator.actionDisabe();
        }
        else {
            mediator.actionEnable();
        }
    }

來看看 ActionsetMessage,你還是知道了有主控台之類的東西,會需要清空、會作為做 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,就表示它只面對一個單元,實際上,consoleCleanactionDisable 等方法的呼叫,不也代表著 MessageBox 知道了 Mediator 進一步的細節,它接觸了 consoleCleanactionDisable 等單元不是嗎?

沒有情境,沒有需求,沒有實際的程式碼,就無法討論最少知識原則下,該怎麼重構程式碼,那只是個原則;沒有情境,沒有需求,沒有實際的程式碼,你說用個 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 不用知道 leftright 的細節了,設定訊息時可以如下:

messageBox.setMessage("some messasge", 
    message -> {
        console.clean();
        console.warning("長度不足 8 個字元");
        action.disable();
    },
    message -> {
        console.clean();
        action.enable();
    }
);

也許你也可以設計一個 MessageServiceMessageBox

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 的概念吧!