はじめに
Tkinterを使用してGUIアプリケーションを開発していると、ウィンドウ間の通信や状態管理が複雑になりがちです。特に以下のような課題に直面することがあります:
- メインウィンドウとサブウィンドウ間でのデータの受け渡し
- 複数のウィンドウ間での状態の同期
- イベント発生時の複数ウィンドウの更新
- ウィンドウ間の依存関係による可読性の低下
この記事では、Mediatorパターンを使用してこれらの課題を解決する実装例を紹介します。在庫管理システムの実装を通して、具体的な実装方法を見ていきましょう。
完成イメージ
この記事で実装する在庫管理システムは以下の機能を持ちます:
-
メインウィンドウ
- 現在の総在庫数の表示
- 商品追加画面の表示
- 在庫一覧画面の表示
-
商品追加ウィンドウ
- 商品名と数量の入力
- バリデーションチェック
- 商品の追加
-
在庫一覧ウィンドウ
- 登録された商品の一覧表示
- 商品の削除機能
商品追加画面で追加すると、在庫管理システム、在庫一覧の画面にデータが通知される
Mediatorパターンとは
Mediatorパターンは、オブジェクト間の通信を仲介する「メディエーター」オブジェクトを導入することで、オブジェクト間の結合度を下げるデザインパターンです。
メリット
- オブジェクト間の直接的な参照が減り、結合度が下がる
- 通信のロジックが一箇所にまとまり、見通しが良くなる
- 新しいウィンドウの追加が容易になる
デメリット
- メディエーターが肥大化する可能性がある
- 小規模なアプリケーションでは過剰な設計になる可能性がある
実装の解説
1. データモデルとストアの実装
まず、データを管理するためのモデルとストアを実装します。今回はシンプルなインメモリデータストアを使用します。
from dataclasses import dataclass
from datetime import datetime
@dataclass
class InventoryItem:
id: int
name: str
quantity: int
created_at: datetime
class InventoryStore:
def __init__(self):
self._items: Dict[int, InventoryItem] = {}
self._next_id: int = 1
def add_item(self, name: str, quantity: int) -> InventoryItem:
item = InventoryItem(
id=self._next_id,
name=name,
quantity=quantity,
created_at=datetime.now()
)
self._items[item.id] = item
self._next_id += 1
return item
def get_all_items(self) -> List[InventoryItem]:
return list(self._items.values())
def delete_item(self, item_id: int) -> bool:
if item_id in self._items:
del self._items[item_id]
return True
return False
def get_total_count(self) -> int:
return sum(item.quantity for item in self._items.values())
2. メディエーターのインターフェースと実装
次に、ウィンドウ間の通信を仲介するメディエーターを実装します。
class WindowMediator(ABC):
@abstractmethod
def notify(self, sender: object, event: str, data: Optional[Dict[str, Any]] = None) -> None:
pass
class InventoryMediator(WindowMediator):
def __init__(self, store: InventoryStore):
self.store = store
self.main_window = None
self.add_item_window = None
self.inventory_list_window = None
def notify(self, sender: object, event: str, data: Optional[Dict[str, Any]] = None) -> None:
if event == "add_item":
# 商品追加時の処理
if data and all(key in data for key in ["name", "quantity"]):
self.store.add_item(data["name"], data["quantity"])
self.main_window.update_inventory_count()
self.inventory_list_window.refresh_list()
self.add_item_window.clear_inputs()
messagebox.showinfo("成功", "商品を追加しました")
elif event == "delete_item":
# 商品削除時の処理
if data and "item_id" in data:
if self.store.delete_item(data["item_id"]):
self.main_window.update_inventory_count()
self.inventory_list_window.refresh_list()
messagebox.showinfo("成功", "商品を削除しました")
3. ウィンドウの基底クラス
各ウィンドウに共通する機能を基底クラスとして実装します。
class BaseWindow(ABC):
def __init__(self, mediator: WindowMediator):
self.mediator = mediator
self.window = tk.Toplevel()
self.window.protocol("WM_DELETE_WINDOW", self.hide_window)
self.setup_ui()
def setup_ui(self):
pass
def hide_window(self):
self.window.withdraw()
def show_window(self):
self.window.deiconify()
4. 具体的なウィンドウの実装
各ウィンドウの具体的な実装は別ファイルとして提供していますが、ポイントとなる部分を解説します:
メインウィンドウのポイント
- 在庫数の表示と更新
- サブウィンドウの表示制御
def update_inventory_count(self):
count = self.mediator.store.get_total_count()
self.inventory_label.config(text=f"在庫数: {count}")
商品追加ウィンドウのポイント
- 入力値のバリデーション
- メディエーターへの通知
def add_item(self):
name = self.name_entry.get().strip()
if not name:
messagebox.showerror("エラー", "商品名を入力してください")
return
try:
quantity = int(self.quantity_entry.get())
if quantity <= 0:
raise ValueError("数量は1以上である必要があります")
except ValueError as e:
messagebox.showerror("エラー", "有効な数量を入力してください")
return
self.mediator.notify(self, "add_item", {"name": name, "quantity": quantity})
在庫一覧ウィンドウのポイント
- リストの表示と更新
- 商品の削除機能
def refresh_list(self):
self.listbox.delete(0, tk.END)
self._item_ids.clear()
items = self.mediator.store.get_all_items()
for idx, item in enumerate(items):
display_text = f"{item.name} - {item.quantity}個"
self.listbox.insert(tk.END, display_text)
self._item_ids[idx] = item.id
実際の使用方法
アプリケーションの実行は以下のように行います:
def main():
root = tk.Tk()
root.withdraw() # メインウィンドウを非表示にする
# データストアの作成
store = InventoryStore()
# メディエーターの作成
mediator = InventoryMediator(store)
# 各ウィンドウの作成
main_window = MainWindow(mediator)
add_item_window = AddItemWindow(mediator)
inventory_list_window = InventoryListWindow(mediator)
# メディエーターに各ウィンドウを登録
mediator.main_window = main_window
mediator.add_item_window = add_item_window
mediator.inventory_list_window = inventory_list_window
root.mainloop()
Mediatorパターン実装のポイント
-
責務の明確化
- メディエーターには通信の仲介のみを担当させる
- 各ウィンドウは自身のUI操作のみを担当
- データ操作はストアに集中させる
-
イベントの設計
- イベント名は明確で一貫性のある命名を心がける
- イベントと共に渡すデータは必要最小限にする
-
エラーハンドリング
- 入力値のバリデーション
- ユーザーへのフィードバック
- 例外処理の実装
-
UI/UXの考慮
- ウィンドウの表示/非表示の適切な制御
- 操作結果のフィードバック
- スクロールバーなどの使いやすさの向上
まとめ
Mediatorパターンを使用することで、以下のような利点が得られました:
-
コードの整理
- ウィンドウ間の依存関係が整理され、見通しが良くなった
- 各クラスの責務が明確になった
- データの流れが把握しやすくなった
-
保守性の向上
- 新機能の追加が容易になった
- バグ修正の影響範囲が限定的になった
- テストが書きやすくなった
-
拡張性の確保
- 新しいウィンドウの追加が容易
- イベントの追加・変更が容易
- データストアの実装変更が容易
おわりに
Mediatorパターンは、複数のウィンドウを持つGUIアプリケーションの実装に非常に有効です。ただし、アプリケーションの規模や要件に応じて、適切なパターンを選択することが重要です。
補足:発展的な実装について
より実践的なアプリケーションを作成する場合、以下のような機能の追加を検討すると良いでしょう:
-
データの永続化
- SQLiteなどのデータベースの利用
- ファイルへの保存機能
-
検索・フィルター機能
- 商品名での検索
- 在庫数での絞り込み
-
在庫の履歴管理
- 入出庫履歴の記録
- 履歴の表示機能
-
ユーザー認証
- ログイン機能
- 権限管理
これらの機能を追加する際も、Mediatorパターンの基本的な構造を維持することで、整理された実装を続けることができます。