Update 2023.11.17 2017.05.05

Python デザインパターン サンプルコード Mediator
Mark Summerfield『実践 Python 3』デザインパターンのサンプルコード
Python3(3.11)で動くソースコード(.pyファイル .ipynbファイル)あります
「anaconda3」on .py「PyCharm」.ipynb「Jupyter Notebook」

著作権の問題があるので,本に書いてないことだけを解説します。つまり,視点を変えて解説します。

◆◆Mediator パターンの使われる場面◆◆

C++ サンプルも Java サンプルも GUI のダイアログです。ラジオボタン,押しボタン,テキストボックスなどの各パーツの入力条件や操作条件が複雑に絡み合うのが普通です。これらの条件を各パーツのオブジェクトに実装してしまうとオブジェクト間の通信が密になり複雑になってしまいます。

各パーツのオブジェクト間の通信を一切なくし,各パーツのオブジェクトは mediator(調停者)とだけ通信を行い,mediator(調停者)だけが入力条件や操作条件を実装します。このような実装を mediator パターンと言います。

GUI のダイアログに限らずパーツどうしが複雑に絡み合うものならば mediator パターンが適用できます。

(2023-11-17)Python3.11で動作確認済み


◆◆Mediator パターンとは◆◆

GoFによれば,Mediator パターンの目的は, 「オブジェクト群の相互作用をカプセル化するオブジェクトを定義する。Mediator パターンは,オブジェクト同士がお互いを明示的に参照し合うことがないようにして,結合度を低めることを促進する。それにより,オブジェクトの相互作用を独立に変えることができるようになる。 」

GoFによれば,Mediator パターンは次のような場合に使う。
・しっかりと定義されているが複雑な方法で,オブジェクトの集まりが通信する場合。その結果,オブジェクト間の依存関係が構造化できず,理解が難しい。
・あるオブジェクトが他の多くのオブジェクトに対して参照を持ち,それらと通信するので,それを再利用するのが難しい場合。
・いくつかのクラス間に分配された振る舞いを,できるだけサブクラス化を行わずにカスタマイズしたい場合。

GoFによれば,Mediator パターンの関連するパターンは次のようなものである。
Facade パターン:Facade パターンは、その目的がより便利なインタフェースを提供するためにサブシステムを構成しているオブジェクトを抽象化するという点で、Mediator パターンとは異なっている。そのプロトコルは一方向である。すなわち、Facade オブジェクトはサブシステムを構成しているオブジェクトに対して要求を出すが、その逆はない。それとは対照的に、Mediator パターンは、Colleague オブジェクトが提供していない、または提供できない協調的な振る舞いを可能にし、そのプロトコルは双方向である。
Observer パターン:Colleague オブジェクトは、Observer パターンを使って mediator と通信することができる。

◆◆Mediator パターンのサンプルのクラス構成◆◆

ソースコードは巻末にあり,ソースファイルはこのWebの左上隅にありダウンロードできます。
【mediator1.py】【mediator1d.py】
      Mediator                         Medeated
    (on_change)                       (on_change)
                                  ↑              ↑
                                Button            Text
                           (click, __str__)    (text, __str__)

    Form
(creat_widgets, create_mediator, update_ui, clicked)
【mediator2.py】【mediator2d.py】
                                      Medeated
                                     (on_change)
                                  ↑              ↑
                                Button            Text
                           (click, __str__)    (text, __str__)

    Form
(creat_widgets, create_mediator, _update_ui_mediator, _clicked_mediator)

class Form は,ウィジェットを設定し,その関係を決め,mediator を設定する
class Mediator は,mediator であり,ウィジェットと通信する(mediator2.pyには無い)
class Medeated は,mediated のスーパークラスです
class Button, Text は,ウィジェットの変化を mediator に知らせる mediated です

◆◆Mediator パターンの実装◆◆

【mediator1.py】

サンプルは,GUIを想定していますが,実際はGUIを作っていません。つもりだけです。

ウィジェット(パーツ)は,文字入力が2つ,ボタンが2つです。ウィジェットどうしの関係は,文字入力が2つとも埋まっていないと,「OK」ボタンが有効になりません。それだけです。

Mediator パターンでは,ウィジェットどうしの通信はしません。mediator とだけ通信します。ウィジェットのことを mediated と言います。

ウィジェットどうしが通信することのデメリットは巻頭に書いてあるとおりです。要は,密な構造になり,複雑になり,bug りやすいです。

サンプルは,実際には,GUIを作っていないので,メインルーチンはテストプログラムです。
  def main
  def test_user_interaction_with
のやっていることは,2つのボタンをクリックすることと,2つのテキスト窓に文字を入力することだけです。その間に,「OK」ボタンが有効であるかを問い合わせています。

テキスト窓の2つに文字を入力すれば,「OK」ボタンが有効になります。クリックすれば,「OK」と表示します。

84行目の
  class Button
102行目の
  class Text
をインスタンス化するときに(28行目),2つのボタン,2つの文字入力を識別します。

これらのウィジェットが操作されたとき(変化したとき)は,いずれも,メソッド
  on_change(94, 118行目)
を呼びます。このメソッドは,スーパークラスで定義されていて(79行目),mediator のメソッド
  on_change(63行目)
を,自分のインスタンスを引数にして呼んでいます。その引数をキーにして,辞書
  callablesForWidget
から,辞書のバリュー,つまり,そのウィジェットが変化したときの動作 callables を導出します。callables は,リストであり,caller は,リストの中身です。

その内容は,
  class Mediator
のコンストラクタが,35行目のインスタンス化の引数,つまり,ウィジェットとその動作のペアを,57行目で辞書に格納したものです。

つまり,上の caller は,メソッド
  update_ui(条件により「OK」ボタンを有効にする)
  clicked(「OK」「Cancel」を表示する)
のいずれかです。66行目で for in ループがありますが,中身は1つです。

これが,ウィジェットが変化したときの動作のすべてです。

ちなみに,ウィジェットクラスの特殊メソッド
  __str__
は,インスタンスを print するとき呼ばれます。これがないとアドレスを print するだけです。

オブジェクト指向のデザインパターンは,このMediator パターンのように,インスタンスを渡す,あるいは,インスタンスを辞書に格納する,ということにより,動作の切り替えで,if 文はまったく使われていません。if 文は条件の確認だけに使われています。

【mediator1d.py】

ウィジェットクラスのスーパークラスをクラスデコレータにしています。それだけです。

【mediator2.py】

42, 52行目のメソッド
  def _update_ui_mediator
  def _clicked_mediator
をデコレータにより,コルーチンにしています。

コルーチンは,ジェネレータのように yield を持っていますが,その yield の動作がジェネレータとは逆になります。両者とも yield で一時停止しますが,ジェネレータは,for in ループから値を pull されます。コルーチンは,無限ループの中に yield があり,値を push します。

このサンプルでは,class Mediatorがありません。代わりに35行目の
  self.mediator = self._update_ui_mediator(self._clicked_mediator())
がパイプラインになっています。パイプラインのコンポネントはコルーチンです。

次の行では,各ウィジェットの mediator 属性にパイプラインを設定しています。前のサンプルでは,これは,ウィジェットとその動作がペアになった辞書だったわけです。

ウィジェットに変化があったとき,スーパーclass Mediatedのメソッド
  on_change
が呼ばれ,さらに,パイプラインが呼ばれます。パイプラインの1番目の要素
  _update_ui_mediator
が呼ばれ,条件が合えば処理がなされ,サンプルのパイプラインには引数に後任が有るので,パイプラインの2番目の要素
  _clicked_mediator
が呼ばれ,条件が合えば処理がなされ,サンプルのパイプラインには引数に後任が無いので終了します。

これが,ウィジェットが変化したときの動作のすべてです。

前のサンプルは古典的なデザインパターンと言われます。GoF本の考え方を引き継いでいるからです。このサンプルはコルーチンが使われ,Pythonic と言ってよいでしょう。

【mediator2d.py】

ウィジェットクラスのスーパークラスをクラスデコレータにしています。それだけです。

◆◆ソースコード◆◆

このWebページの左上隅からダウンロードできます。

ソースファイルは5つです。
・mediator1.py;Mediator パターンのPythonサンプル(その1)
・mediator1d.py;Mediator パターンのPythonサンプル(その1)クラスデコレータを使う
・mediator2.py;Mediator パターンのPythonサンプル(その2)
・mediator2d.py;Mediator パターンのPythonサンプル(その2)クラスデコレータを使う
・Qtrac.py;mediator2.py, mediator2d.py がインポートする

【mediator1.py】
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
#!/usr/bin/env python3
# Copyright c 2012-13 Qtrac Ltd. All rights reserved.
# This program or module is free software: you can redistribute it
# and/or modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version. It is provided for
# educational purposes and is distributed in the hope that it will be
# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.

import collections


def main():
    form = Form()
    test_user_interaction_with(form)


class Form:

    def __init__(self):
        self.create_widgets()
        self.create_mediator()


    def create_widgets(self):
        self.nameText = Text()
        self.emailText = Text()
        self.okButton = Button("OK")
        self.cancelButton = Button("Cancel")


    def create_mediator(self):
        self.mediator = Mediator(((self.nameText, self.update_ui),
                (self.emailText, self.update_ui),
                (self.okButton, self.clicked),
                (self.cancelButton, self.clicked)))
        self.update_ui()


    def update_ui(self, widget=None):
        self.okButton.enabled = (bool(self.nameText.text) and
                                 bool(self.emailText.text))


    def clicked(self, widget):
        if widget == self.okButton:
            print("OK")
        elif widget == self.cancelButton:
            print("Cancel")


class Mediator:

    def __init__(self, widgetCallablePairs):
        self.callablesForWidget = collections.defaultdict(list)
        for widget, caller in widgetCallablePairs:
            self.callablesForWidget[widget].append(caller)
            widget.mediator = self


    def on_change(self, widget):
        callables = self.callablesForWidget.get(widget)
        if callables is not None:
            for caller in callables:
                caller(widget)
        else:
            raise AttributeError("No on_change() method registered for {}"
                    .format(widget))


class Mediated:

    def __init__(self):
        self.mediator = None


    def on_change(self):
        if self.mediator is not None:
            self.mediator.on_change(self)


class Button(Mediated):

    def __init__(self, text=""):
        super().__init__()
        self.enabled = True
        self.text = text


    def click(self):
        if self.enabled:
            self.on_change()


    def __str__(self):
        return "Button({!r}) {}".format(self.text,
                "enabled" if self.enabled else "disabled")


class Text(Mediated):

    def __init__(self, text=""):
        super().__init__()
        self.__text = text


    @property
    def text(self):
        return self.__text


    @text.setter
    def text(self, text):
        if self.text != text:
            self.__text = text
            self.on_change()


    def __str__(self):
        return "Text({!r})".format(self.text)


def test_user_interaction_with(form):
    form.okButton.click()           # Ignored because it is disabled
    print(form.okButton.enabled)    # False
    form.nameText.text = "Fred"
    print(form.okButton.enabled)    # False
    form.emailText.text = "fred@bloggers.com"
    print(form.okButton.enabled)    # True
    form.okButton.click()           # OK
    form.emailText.text = ""
    print(form.okButton.enabled)    # False
    form.cancelButton.click()       # Cancel


if __name__ == "__main__":
    main()

【mediator1d.py】
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
#!/usr/bin/env python3
# Copyright c 2012-13 Qtrac Ltd. All rights reserved.
# This program or module is free software: you can redistribute it
# and/or modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version. It is provided for
# educational purposes and is distributed in the hope that it will be
# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.

import collections


def main():
    form = Form()
    test_user_interaction_with(form)


class Form:

    def __init__(self):
        self.create_widgets()
        self.create_mediator()


    def create_widgets(self):
        self.nameText = Text()
        self.emailText = Text()
        self.okButton = Button("OK")
        self.cancelButton = Button("Cancel")


    def create_mediator(self):
        self.mediator = Mediator(((self.nameText, self.update_ui),
                (self.emailText, self.update_ui),
                (self.okButton, self.clicked),
                (self.cancelButton, self.clicked)))
        self.update_ui()


    def update_ui(self, widget=None):
        self.okButton.enabled = (bool(self.nameText.text) and
                                 bool(self.emailText.text))


    def clicked(self, widget):
        if widget == self.okButton:
            print("OK")
        elif widget == self.cancelButton:
            print("Cancel")


class Mediator:

    def __init__(self, widgetCallablePairs):
        self.callablesForWidget = collections.defaultdict(list)
        for widget, caller in widgetCallablePairs:
            self.callablesForWidget[widget].append(caller)
            widget.mediator = self


    def on_change(self, widget):
        callables = self.callablesForWidget.get(widget)
        if callables is not None:
            for caller in callables:
                caller(widget)
        else:
            raise AttributeError("No on_change() method registered for {}"
                    .format(widget))


def mediated(Class):
    setattr(Class, "mediator", None)
    def on_change(self):
        if self.mediator is not None:
            self.mediator.on_change(self)
    setattr(Class, "on_change", on_change)
    return Class


@mediated
class Button:

    def __init__(self, text=""):
        super().__init__()
        self.enabled = True
        self.text = text


    def click(self):
        if self.enabled:
            self.on_change()


    def __str__(self):
        return "Button({!r}) {}".format(self.text,
                "enabled" if self.enabled else "disabled")


@mediated
class Text:

    def __init__(self, text=""):
        super().__init__()
        self.__text = text


    @property
    def text(self):
        return self.__text


    @text.setter
    def text(self, text):
        if self.text != text:
            self.__text = text
            self.on_change()


    def __str__(self):
        return "Text({!r})".format(self.text)


def test_user_interaction_with(form):
    form.okButton.click()           # Ignored because it is disabled
    print(form.okButton.enabled)    # False
    form.nameText.text = "Fred"
    print(form.okButton.enabled)    # False
    form.emailText.text = "fred@bloggers.com"
    print(form.okButton.enabled)    # True
    form.okButton.click()           # OK
    form.emailText.text = ""
    print(form.okButton.enabled)    # False
    form.cancelButton.click()       # Cancel


if __name__ == "__main__":
    main()

【mediator2.py】
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
#!/usr/bin/env python3
# Copyright c 2012-13 Qtrac Ltd. All rights reserved.
# This program or module is free software: you can redistribute it
# and/or modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version. It is provided for
# educational purposes and is distributed in the hope that it will be
# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.

from Qtrac import coroutine


def main():
    form = Form()
    test_user_interaction_with(form)


class Form:

    def __init__(self):
        self.create_widgets()
        self.create_mediator()


    def create_widgets(self):
        self.nameText = Text()
        self.emailText = Text()
        self.okButton = Button("OK")
        self.cancelButton = Button("Cancel")


    def create_mediator(self):
        self.mediator = self._update_ui_mediator(self._clicked_mediator())
        for widget in (self.nameText, self.emailText, self.okButton,
                self.cancelButton):
            widget.mediator = self.mediator
        self.mediator.send(None)


    @coroutine
    def _update_ui_mediator(self, successor=None):
        while True:
            widget = (yield)
            self.okButton.enabled = (bool(self.nameText.text) and
                                     bool(self.emailText.text))
            if successor is not None:
                successor.send(widget)


    @coroutine
    def _clicked_mediator(self, successor=None):
        while True:
            widget = (yield)
            if widget == self.okButton:
                print("OK")
            elif widget == self.cancelButton:
                print("Cancel")
            elif successor is not None:
                successor.send(widget)


class Mediated:

    def __init__(self):
        self.mediator = None


    def on_change(self):
        if self.mediator is not None:
            self.mediator.send(self)


class Button(Mediated):

    def __init__(self, text=""):
        super().__init__()
        self.enabled = True
        self.text = text


    def click(self):
        if self.enabled:
            self.on_change()


    def __str__(self):
        return "Button({!r}) {}".format(self.text,
                "enabled" if self.enabled else "disabled")


class Text(Mediated):

    def __init__(self, text=""):
        super().__init__()
        self.__text = text


    @property
    def text(self):
        return self.__text


    @text.setter
    def text(self, text):
        if self.text != text:
            self.__text = text
            self.on_change()


    def __str__(self):
        return "Text({!r})".format(self.text)


def test_user_interaction_with(form):
    form.okButton.click()           # Ignored because it is disabled
    print(form.okButton.enabled)    # False
    form.nameText.text = "Fred"
    print(form.okButton.enabled)    # False
    form.emailText.text = "fred@bloggers.com"
    print(form.okButton.enabled)    # True
    form.okButton.click()           # OK
    form.emailText.text = ""
    print(form.okButton.enabled)    # False
    form.cancelButton.click()       # Cancel


if __name__ == "__main__":
    main()

【mediator2d.py】
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
#!/usr/bin/env python3
# Copyright c 2012-13 Qtrac Ltd. All rights reserved.
# This program or module is free software: you can redistribute it
# and/or modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version. It is provided for
# educational purposes and is distributed in the hope that it will be
# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.

from Qtrac import coroutine


def main():
    form = Form()
    test_user_interaction_with(form)


class Form:

    def __init__(self):
        self.create_widgets()
        self.create_mediator()


    def create_widgets(self):
        self.nameText = Text()
        self.emailText = Text()
        self.okButton = Button("OK")
        self.cancelButton = Button("Cancel")


    def create_mediator(self):
        self.mediator = self._update_ui_mediator(self._clicked_mediator())
        for widget in (self.nameText, self.emailText, self.okButton,
                self.cancelButton):
            widget.mediator = self.mediator
        self.mediator.send(None)


    @coroutine
    def _update_ui_mediator(self, successor=None):
        while True:
            widget = (yield)
            self.okButton.enabled = (bool(self.nameText.text) and
                                     bool(self.emailText.text))
            if successor is not None:
                successor.send(widget)


    @coroutine
    def _clicked_mediator(self, successor=None):
        while True:
            widget = (yield)
            if widget == self.okButton:
                print("OK")
            elif widget == self.cancelButton:
                print("Cancel")
            elif successor is not None:
                successor.send(widget)

def mediated(Class):
    setattr(Class, "mediator", None)
    def on_change(self):
        if self.mediator is not None:
            self.mediator.send(self)
    setattr(Class, "on_change", on_change)
    return Class


@mediated
class Button:

    def __init__(self, text=""):
        super().__init__()
        self.enabled = True
        self.text = text


    def click(self):
        if self.enabled:
            self.on_change()


    def __str__(self):
        return "Button({!r}) {}".format(self.text,
                "enabled" if self.enabled else "disabled")


@mediated
class Text:

    def __init__(self, text=""):
        super().__init__()
        self.__text = text


    @property
    def text(self):
        return self.__text


    @text.setter
    def text(self, text):
        if self.text != text:
            self.__text = text
            self.on_change()


    def __str__(self):
        return "Text({!r})".format(self.text)


def test_user_interaction_with(form):
    form.okButton.click()           # Ignored because it is disabled
    print(form.okButton.enabled)    # False
    form.nameText.text = "Fred"
    print(form.okButton.enabled)    # False
    form.emailText.text = "fred@bloggers.com"
    print(form.okButton.enabled)    # True
    form.okButton.click()           # OK
    form.emailText.text = ""
    print(form.okButton.enabled)    # False
    form.cancelButton.click()       # Cancel


if __name__ == "__main__":
    main()

【Qtrac.py】
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#!/usr/bin/env python3
# Copyright c 2012-13 Qtrac Ltd. All rights reserved.
# This program or module is free software: you can redistribute it
# and/or modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version. It is provided for
# educational purposes and is distributed in the hope that it will be
# useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.

import abc
import collections
import errno
import functools
import os
import sys


def coroutine(function):
    @functools.wraps(function)
    def wrapper(*args, **kwargs):
        generator = function(*args, **kwargs)
        next(generator)
        return generator
    return wrapper

以上

トップページに戻る
inserted by FC2 system