Update 2023.11.19 2017.05.05

Python デザインパターン サンプルコード Mediator
結城 浩「Java言語で学ぶデザインパターン入門」をPython化
Python3(3.11)で動くソースコード(.pyファイル .ipynbファイル)あります
「anaconda3」on .py「PyCharm」.ipynb「Jupyter Notebook」

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

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

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

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


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


◆◆Mediator パターンとは◆◆

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

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

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

◆◆Python の tkinter のくせと Java サンプルとの違い◆◆

各パーツのパラメータの設定が大きく違うのは当たり前ですが,それだけだとオブジェクトの構成はほとんど同じになるはずです。しかし,tkinter のくせというか仕様は Java の GUI と大きく違っていて,実装はかなりくせのあるものになりました。ただ,応用は簡単にできそうです。

Java サンプルでは,各パーツを配置するオブジェクトと各パーツを操作するオブジェクト(これを colleague (同僚)と言います)が共通です。

Python サンプルでは,これらを共通にできませんので,別のインスタンス名を付けるのですが,同じものを指していることを判りやすくするために微妙なインスタンス名になっています。

メインルーチンから呼ばれるクラスが mediator(調停者)となるわけですが,mediator じしんのインスタンスを各パーツ colleague (同僚)に渡し,colleague から mediator への通信に使う(mediator のメソッドを呼ぶ)のです。

渡したインスタンスが渡した直後にはきちんと働いているのですが,各パーツのイベントドリブン(変化によるトリガー)のときにはインスタンスが消失しているようなのです。

そこで,渡されたインスタンスを使うことを止め,インスタンス化したときのオブジェクトをそのまま使うことにしました。複数人の大規模開発には向かない使い方ですので注意してください。プログラムとしては正しく動作します。

◆◆ダイアログの入力条件◆◆

ダイアログの入力条件は Java サンプルによると次のようになります。

このダイアログは,次のように使います。
・ゲストログイン[Guest]か,ユーザログイン[Login]かを選択する
・ユーザログインの場合には,ユーザ名[Username]とパスワード[Password]を入力する
・ログインするなら[OK]ボタン,やめるなら[Cancel]ボタンを押す
(サンフ。ルプログラムでは,どこにもログインしません。ボタンを押したら単に終了するだけです)

プログラムの動作条件をさらに詳細にみてみると次のようになります。

・ゲストログインが選ばれているときには,ユーザ名とパスワードを「無効状態Jにして,文字列が入力できないようにする
・ユーザログインが選ばれているときには,ユーザ名は「有効状態」になり,文字列が入力できるようにする
・ユーザ名に文字が1文字も入っていない場合には,パスワードは「無効状態」になる
・ユーザ名に文字が1文字でも入っていたら,パスワードは「有効状態」になる(もちろんゲストログインの場合には,パスワードは「無効状態」である)
・ユーザ名とパスワードの両方に文字が1文字でも入っている場合には,OKボタンは「有効状態」になり,押せる状態になるが,ユーザ名とパスワードのどちらか一方でも空ならば, OKボタンは「無効状態」になって,押せない状態になる(もちろんゲストログインの場合には,OKボタンは常に「有効状態」である)
・Cancelボタンは常に「有効状態」で,いつでも押せる状態になっている

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

ソースコードは巻末にあり,ソースファイルはこのWebの左上隅にありダウンロードできます。
               Mediator                         Colleague
(createColleagues, colleagueChanged)  (setMediator, setColleagueEnabled)
     ↑                            ↑        ↑              ↑
                    ColleagueButton  ColleagueTextField  ColleagueCheckbox
                        (do)     (do, textValueChanged)  (do, itemStateChanged)
     ↑
  LoginFrame
(colleagueChanged)

class Mediator は,mediator のテンプレートです。Java では interface です。
class Colleague は,colleague のテンプレートです。Java では interface です。
class LoginFrame は,mediator です。さらに tkinter のサブクラスです。
class ColleagueButton, ColleagueTextField, ColleagueCheckbox は,colleague です。

◆◆tkinter の実装◆◆

tkinter モジュールには大きく分けて,tkinter.Canvas クラスと tkinter.Frame クラスがあり,前者が自由なお絵かきソフトであり,後者がこのサンプルのようにパーツを配置するソフトです。このサンプルでは後者だけ使います。

ダイアログには,2つのラジオボタン,2つのラベル,2つのエントリーボックス,2つの押しボタンがあります。tkinter では,ソースコードを見れば判るようにもう一つ全体を囲むフレームを配置します。

各パーツのパラメータ設定の詳細は他のWebサイトを参照してください。各パーツを配置するのに pack を使わないで grid を使っています。こちらの方がきれいにレイアウトすることができます。

この tkinter のポイントは,各パーツの状態が変化したときのイベントドリブンのパラメータの書き方です。パーツの種類によってパラメータがかなり違いますのでソースコードをよく見てください。

もう一つのポイントは,各パーツの入力を有効・無効にするパラメータの書き方です。このパラメータの書き方はパーツの種類が違っても同じです。が,参照するWebサイトによって書き方がマチマチなのですが,このサンプルの書き方が普通だと思います。

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

メインルーチンは tkinter を呼ぶ典型的な書き方であり他には知りません。

class LoginFrame は mediator です。コンストラクタでは,tkinter の各パーツを配置し,パラメータを設定します。

各パーツが変化するときのイベントドリブンでは colleague のメソッドを呼びますので,各パーツを配置する前に,すべてのパーツをインスタンス化してしまい,そのインスタンスを colleague のメソッドを呼ぶときに使います。

次に,このクラスのインスタンスを mediator として(引数 self を) colleague に渡し, colleague から mediator のメソッドを呼ぶときにそのインスタンスを使います。ただ,debug の結果はそのインスタンスが消失しているようなので,実際はメインルーチンでこのクラスをインスタンス化したときのオブジェクトをそのまま使っています。

各パーツのすべてのイベントドリブンは mediator の同じメソッドを呼びます。誰から呼ばれたかはまったく関知しません。常に同じ条件で実装します。その条件は最初に書いたダイアログの入力条件であり,そのままプログラミングしてあります。

これで実装の説明は終わりなのですが,ポイントは colleague どうしは一切通信はしないで, mediator とだけ通信することです。通信とはお互いのメソッドを呼ぶことなのですが,呼ぶ相手を間違わないようにインスタンスを渡していることに注目してください。

GoFの23のデザインパターンはオブジェクト指向プログラミングと言われるのですが,一番のポイントはインスタンスをやりとりすることにあります。この方法は普通のテキストには書いてありませんのでぜひデザインパターンを参考にしてください。

◆◆ソースコード◆◆

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

ソースファイルは1つです。
・Mediator.py;Mediator パターンのPythonサンプル

【Mediator.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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Mediator.py
import sys
from abc import ABCMeta, abstractmethod
import tkinter as Tk

class Mediator(metaclass = ABCMeta):

    @abstractmethod
    def createColleagues(self):
        pass
    @abstractmethod
    def colleagueChanged(self):
        pass

class Colleague(metaclass = ABCMeta):

    @abstractmethod
    def setMediator(self, mediator):
        pass
    @abstractmethod
    def setColleagueEnabled(self, enabled):
        pass

class ColleagueButton(Colleague):
    def setMediator(self, mediator):
        # Mediatorを保持
        self.mediator = mediator
    def setColleagueEnabled(self, obj, enabled):
        # Mediatorから有効・無効が指示される
        if enabled:
            obj.configure(state=Tk.NORMAL)
        else:
            obj.configure(state=Tk.DISABLED)

class ColleagueTextField(Colleague):
    def setMediator(self, mediator):
        # Mediatorを保持
        self.mediator = mediator
    def setColleagueEnabled(self, obj, enabled):
        # Mediatorから有効・無効が指示される
        if enabled:
            obj.configure(state=Tk.NORMAL)
        else:
            obj.configure(state=Tk.DISABLED)
        # 無効にするときは色を変える???
    def textValueChanged(TextEvent):
        # 文字列が変化したらMediatorに通知
        #self.mediator.colleagueChanged() # インスタンス消失?
        login_frame.colleagueChanged()

class ColleagueCheckbox(Colleague):
    def setMediator(self, mediator):
        # Mediatorを保持
        self.mediator = mediator
    def setColleagueEnabled(self, enabled):
        # Mediatorから有効・無効が指示される
        pass
    def itemStateChanged(self):
        # 状態が変化したらMediatorに通知
        #self.mediator.colleagueChanged() # インスタンス消失?
        login_frame.colleagueChanged()

class LoginFrame(Tk.Frame, Mediator):
    # コンストラクタ
    def __init__(self, master=None):
        Tk.Frame.__init__(self, master, bg='light gray')
        self.master.title('Mediator Sample')
        # Colleagueたちを生成する。
        self.instanceColleagues()
        # フレーム全体をつくる
        self.root = Tk.Frame(self, bg='light gray')
        self.root.pack()
        # 各パーツを配置する
        # Radiobutton(ラジオボタン)
        self.RadiobuttonValue = Tk.IntVar()
        self.RadiobuttonValue.set(1)
        self.checkGuest_ = Tk.Radiobutton(
            self.root, text='Guest', variable=self.RadiobuttonValue, value=0,
            bg='light gray', font=("Times", 12),
            command= self.checkGuest.itemStateChanged)
        self.checkGuest_.grid(row=0, column=0, padx=5, pady=5)
        self.checkLogin_ = Tk.Radiobutton(
            self.root, text='Login', variable=self.RadiobuttonValue, value=1,
            bg='light gray', font=("Times", 12),
            command= self.checkLogin.itemStateChanged)
        self.checkLogin_.grid(row=0, column=1, padx=5, pady=5)
        # Label(ラベル)
        self.userName_ = Tk.Label(
            self.root, text='Username', bg='light gray',
            relief=Tk.RIDGE, bd=0, font=("Times", 12))
        self.userName_.grid(row=1, column=0, padx=5, pady=5)
        self.passWord_ = Tk.Label(
            self.root, text='Password', bg='light gray',
            relief=Tk.RIDGE, bd=0, font=("Times", 12))
        self.passWord_.grid(row=2, column=0, padx=5, pady=5)
        # Entry(エントリーボックス;1行入力)
        self.textUserBuffer = Tk.StringVar()
        self.textUserBuffer.trace(
            "w", lambda name, index, mode: self.textUser.textValueChanged())
        self.textUser_ = Tk.Entry(
            self.root, width=15, textvariable=self.textUserBuffer,
            font=("Times", 12))
        self.textUser_.grid(row=1, column=1, padx=5, pady=5)
        self.textPassBuffer = Tk.StringVar()
        self.textPassBuffer.trace(
            "w", lambda name, index, mode: self.textPass.textValueChanged())
        self.textPass_ = Tk.Entry(
            self.root, width=15, textvariable=self.textPassBuffer,
            font=("Times", 12))
        self.textPass_.grid(row=2, column=1, padx=5, pady=5)
        # Button(押しボタン)
        self.buttonOk_ = Tk.Button(
            self.root, text='OK', bg='light gray', width=12,
            font=("Times", 12), command = self.buttonOkPushed)
        self.buttonOk_.grid(row=3, column=0, padx=5, pady=5)
        self.buttonCancel_ = Tk.Button(
            self.root, text='Cacel', bg='light gray', width=12,
            font=("Times", 12), command = self.buttonCancelPushed)
        self.buttonCancel_.grid(row=3, column=1, padx=5, pady=5)
        # Colleagueたちの生成
        self.createColleagues()
        #self.debugDsabled() # debug
        #self.debugEnabled() # debug
        # 有効・無効の初期指定
        self.colleagueChanged()
    # コンストラクタ終り
    # 押しボタン「OK」が押された(サンプルは何もしない)
    def buttonOkPushed(self):
        print("buttonOkPushed")
    # 押しボタン「Cancel」が押された(サンプルは何もしない)
    def buttonCancelPushed(self):
        print("buttonCancelPushed")
    # 各パーツの有効・無効をdebugする
    # textUser, textPass, buttonOkについては有効・無効が設定可
    def debugDsabled(self):
        self.textUser.setColleagueEnabled(self.textUser_, False)
        self.textPass.setColleagueEnabled(self.textPass_, False)
        self.buttonOk.setColleagueEnabled(self.buttonOk_, False)
    def debugEnabled(self):
        self.textUser.setColleagueEnabled(self.textUser_, True)
        self.textPass.setColleagueEnabled(self.textPass_, True)
        self.buttonOk.setColleagueEnabled(self.buttonOk_, True)
    #  Colleagueたちを生成する。
    def instanceColleagues(self):
        # 生成
        #CheckboxGroup = CheckboxGroup()
        self.checkGuest = ColleagueCheckbox()
        self.checkLogin = ColleagueCheckbox()
        self.textUser = ColleagueTextField()
        self.textPass = ColleagueTextField()
        self.buttonOk = ColleagueButton()
        self.buttonCancel = ColleagueButton()
    def createColleagues(self):
        # Mediatorのセット
        self.checkGuest.setMediator(self)
        self.checkLogin.setMediator(self)
        self.textUser.setMediator(self)
        self.textPass.setMediator(self)
        self.buttonOk.setMediator(self)
        self.buttonCancel.setMediator(self)
    # Colleageからの通知で各Colleageの有効・無効を判定する。
    def colleagueChanged(self):
        #print(self.RadiobuttonValue.get()) #debug
        if self.RadiobuttonValue.get()==0:   # Guest mode
            self.textUser.setColleagueEnabled(self.textUser_, False)
            self.textPass.setColleagueEnabled(self.textPass_, False)
            self.buttonOk.setColleagueEnabled(self.buttonOk_, True)
        else:                   # Login mode
            self.textUser.setColleagueEnabled(self.textUser_, True)
            self.userpassChanged()
    # textUserまたはtextPassの変更があった。
    # 各Colleageの有効・無効を判定する。
    def userpassChanged(self):
        #print(self.textUserBuffer.get()) #debug
        if (len(self.textUserBuffer.get()) > 0):
            self.textPass.setColleagueEnabled(self.textPass_, True)
            if (len(self.textPassBuffer.get()) > 0):
                self.buttonOk.setColleagueEnabled(self.buttonOk_, True)
            else:
                self.buttonOk.setColleagueEnabled(self.buttonOk_, False)
        else:
            self.textPass.setColleagueEnabled(self.textPass_, False)
            self.buttonOk.setColleagueEnabled(self.buttonOk_, False)

if __name__ == '__main__':
    login_frame = LoginFrame()
    login_frame.pack()
    login_frame.mainloop()

以上

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