TkinterでのMediator実装 - 複雑なウィンドウ間の依存関係を整理する -

## はじめに

Tkinterを使用してGUIアプリケーションを開発していると、ウィンドウ間の通信や状態管理が複雑になりがちです。特に以下のような課題に直面することがあります：

- メインウィンドウとサブウィンドウ間でのデータの受け渡し
- 複数のウィンドウ間での状態の同期
- イベント発生時の複数ウィンドウの更新
- ウィンドウ間の依存関係による可読性の低下

この記事では、Mediatorパターンを使用してこれらの課題を解決する実装例を紹介します。在庫管理システムの実装を通して、具体的な実装方法を見ていきましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/4502478d-3f19-bf2b-1538-ea3b263117e3.png)

## 完成イメージ

この記事で実装する在庫管理システムは以下の機能を持ちます：

1. メインウィンドウ
   - 現在の総在庫数の表示
   - 商品追加画面の表示
   - 在庫一覧画面の表示

2. 商品追加ウィンドウ
   - 商品名と数量の入力
   - バリデーションチェック
   - 商品の追加

3. 在庫一覧ウィンドウ
   - 登録された商品の一覧表示
   - 商品の削除機能

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/aabacbfd-a5d6-26d6-d9cf-6def0abbfaf6.png)

商品追加画面で追加すると、在庫管理システム、在庫一覧の画面にデータが通知される

## Mediatorパターンとは

Mediatorパターンは、オブジェクト間の通信を仲介する「メディエーター」オブジェクトを導入することで、オブジェクト間の結合度を下げるデザインパターンです。

### メリット
- オブジェクト間の直接的な参照が減り、結合度が下がる
- 通信のロジックが一箇所にまとまり、見通しが良くなる
- 新しいウィンドウの追加が容易になる

### デメリット
- メディエーターが肥大化する可能性がある
- 小規模なアプリケーションでは過剰な設計になる可能性がある

## 実装の解説

### 1. データモデルとストアの実装

まず、データを管理するためのモデルとストアを実装します。今回はシンプルなインメモリデータストアを使用します。

```python
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. メディエーターのインターフェースと実装

次に、ウィンドウ間の通信を仲介するメディエーターを実装します。

```python
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. ウィンドウの基底クラス

各ウィンドウに共通する機能を基底クラスとして実装します。

```python
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. 具体的なウィンドウの実装

各ウィンドウの具体的な実装は別ファイルとして提供していますが、ポイントとなる部分を解説します：

#### メインウィンドウのポイント
- 在庫数の表示と更新
- サブウィンドウの表示制御

```python
def update_inventory_count(self):
    count = self.mediator.store.get_total_count()
    self.inventory_label.config(text=f"在庫数: {count}")
```

#### 商品追加ウィンドウのポイント
- 入力値のバリデーション
- メディエーターへの通知

```python
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})
```

#### 在庫一覧ウィンドウのポイント
- リストの表示と更新
- 商品の削除機能

```python
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
```

## 実際の使用方法

アプリケーションの実行は以下のように行います：

```python
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パターン実装のポイント

1. **責務の明確化**
   - メディエーターには通信の仲介のみを担当させる
   - 各ウィンドウは自身のUI操作のみを担当
   - データ操作はストアに集中させる

2. **イベントの設計**
   - イベント名は明確で一貫性のある命名を心がける
   - イベントと共に渡すデータは必要最小限にする

3. **エラーハンドリング**
   - 入力値のバリデーション
   - ユーザーへのフィードバック
   - 例外処理の実装

4. **UI/UXの考慮**
   - ウィンドウの表示/非表示の適切な制御
   - 操作結果のフィードバック
   - スクロールバーなどの使いやすさの向上

## まとめ

Mediatorパターンを使用することで、以下のような利点が得られました：

1. **コードの整理**
   - ウィンドウ間の依存関係が整理され、見通しが良くなった
   - 各クラスの責務が明確になった
   - データの流れが把握しやすくなった

2. **保守性の向上**
   - 新機能の追加が容易になった
   - バグ修正の影響範囲が限定的になった
   - テストが書きやすくなった

3. **拡張性の確保**
   - 新しいウィンドウの追加が容易
   - イベントの追加・変更が容易
   - データストアの実装変更が容易

## おわりに

Mediatorパターンは、複数のウィンドウを持つGUIアプリケーションの実装に非常に有効です。ただし、アプリケーションの規模や要件に応じて、適切なパターンを選択することが重要です。

## 補足：発展的な実装について

より実践的なアプリケーションを作成する場合、以下のような機能の追加を検討すると良いでしょう：

1. **データの永続化**
   - SQLiteなどのデータベースの利用
   - ファイルへの保存機能

2. **検索・フィルター機能**
   - 商品名での検索
   - 在庫数での絞り込み

3. **在庫の履歴管理**
   - 入出庫履歴の記録
   - 履歴の表示機能

4. **ユーザー認証**
   - ログイン機能
   - 権限管理

これらの機能を追加する際も、Mediatorパターンの基本的な構造を維持することで、整理された実装を続けることができます。

## クラス図

```mermaid
classDiagram
    %% 抽象クラスとインターフェース
    class WindowMediator {
        <<abstract>>
        +notify(sender: object, event: str, data: dict)
    }
    class BaseWindow {
        <<abstract>>
        #mediator: WindowMediator
        #window: Toplevel
        +setup_ui()
        +hide_window()
        +show_window()
    }

    %% データモデルとストア
    class InventoryItem {
        +id: int
        +name: str
        +quantity: int
        +created_at: datetime
    }
    class InventoryStore {
        -_items: Dict[int, InventoryItem]
        -_next_id: int
        +add_item(name: str, quantity: int): InventoryItem
        +get_all_items(): List[InventoryItem]
        +delete_item(item_id: int): bool
        +get_total_count(): int
    }

    %% 具象メディエーター
    class InventoryMediator {
        +store: InventoryStore
        +main_window: MainWindow
        +add_item_window: AddItemWindow
        +inventory_list_window: InventoryListWindow
        +notify(sender: object, event: str, data: dict)
    }

    %% 具象ウィンドウクラス
    class MainWindow {
        -inventory_label: Label
        +setup_ui()
        +update_inventory_count()
        +open_add_item_window()
        +open_inventory_list()
    }
    class AddItemWindow {
        -name_entry: Entry
        -quantity_entry: Entry
        +setup_ui()
        +add_item()
        +clear_inputs()
    }
    class InventoryListWindow {
        -listbox: Listbox
        -_item_ids: Dict[int, int]
        +setup_ui()
        +refresh_list()
        +delete_selected()
    }

    %% 関係性の定義
    WindowMediator <|-- InventoryMediator
    BaseWindow <|-- MainWindow
    BaseWindow <|-- AddItemWindow
    BaseWindow <|-- InventoryListWindow
    
    InventoryMediator --> InventoryStore
    InventoryMediator --> MainWindow
    InventoryMediator --> AddItemWindow
    InventoryMediator --> InventoryListWindow

    InventoryStore --> InventoryItem

    %% リンク説明
    BaseWindow --> WindowMediator : has mediator
    InventoryStore ..> InventoryItem : creates
```

## シーケンス図

```mermaid
sequenceDiagram
    %% 参加者定義
    participant User as ユーザー
    participant Main as MainWindow
    participant Add as AddItemWindow
    participant List as InventoryListWindow
    participant Med as InventoryMediator
    participant Store as InventoryStore

    %% 商品追加フロー
    Note over User,Store: 商品追加フロー
    User->>Main: 商品追加ボタンクリック
    Main->>Med: open_add_item_window()要求
    Med->>Add: show_window()
    
    User->>Add: 商品情報入力
    User->>Add: 追加ボタンクリック
    
    Add->>Add: 入力値検証
    
    Add->>Med: notify("add_item", {name, quantity})
    Med->>Store: add_item(name, quantity)
    Store-->>Med: return new_item
    
    Med->>Main: update_inventory_count()
    Main->>Store: get_total_count()
    Store-->>Main: return total
    Main->>Main: 在庫数表示更新
    
    Med->>List: refresh_list()
    List->>Store: get_all_items()
    Store-->>List: return items
    List->>List: リスト更新

    Med->>Add: clear_inputs()
    Add->>Add: 入力フォームクリア

    %% 商品削除フロー
    Note over User,Store: 商品削除フロー
    User->>Main: 在庫一覧ボタンクリック
    Main->>Med: open_inventory_list()要求
    Med->>List: show_window()
    
    List->>Store: get_all_items()
    Store-->>List: return items
    List->>List: リスト表示
    
    User->>List: 商品選択
    User->>List: 削除ボタンクリック
    
    List->>List: 削除確認ダイアログ
    Note over List: ユーザー確認待ち
    
    List->>Med: notify("delete_item", {item_id})
    Med->>Store: delete_item(item_id)
    Store-->>Med: return success
    
    Med->>Main: update_inventory_count()
    Main->>Store: get_total_count()
    Store-->>Main: return total
    Main->>Main: 在庫数表示更新
    
    Med->>List: refresh_list()
    List->>Store: get_all_items()
    Store-->>List: return items
    List->>List: リスト更新

    %% 画面遷移フロー
    Note over User,Store: 画面遷移フロー
    User->>Main: ウィンドウ遷移要求
    Main->>Med: 画面遷移イベント
    Med->>Med: 現在の状態確認
    
    alt 画面表示
        Med->>Add: show_window()
        Add-->>Med: 表示完了
    else 画面非表示
        Med->>Add: hide_window()
        Add-->>Med: 非表示完了
    end
    
    Med->>Main: 遷移完了通知
```