🗂

[システム設計]疎結合にするための工夫4選

2023/06/23に公開

はじめに

システム開発系の仕事をしている方なら分かると思いますが、見やすい・保守のしやすいコードを書きたくなりますよね。そもそも、チーム開発をしていると汚いコードを書くこと自体ギルティになってきます(汚いコード書くとめっちゃ怒られます)。
そこで、見やすい・保守性の高いコードを書く上で大切な"疎結合"という考え方(および具体例)について纏めてみました。

疎結合とは

システム開発における「疎結合」(Loose Coupling)とは、システムの各部分が他の部分に対して最小限の依存性を持つ設計のことを指します。これは、各部分がそれぞれ独立して機能し、他の部分の変更に対して影響を受けにくい状態を意味します。

そもそも、疎結合には、以下のような利点を持ちます:

  1. 変更の容易さ:各部分が独立しているため、一部を変更しても他の部分に影響を与えにくい。これにより、システムのメンテナンスや拡張が容易になります。

  2. 再利用性:各部分が独立しているため、特定の部分を他のシステムやコンテキストで再利用することが容易になります。

  3. テストの容易さ:各部分が独立しているため、個々の部分を単体でテストすることが容易になります。

一方、各部分が独立しているため、全体としての統一感や一貫性を保つことが難しくなる場合もあり、このあたりは注意が必要です。

疎結合にするための工夫4選

Pythonでクラス同士を疎結合にする方法を具体的なコードを使って説明します。

  1. 依存性注入(Dependency Injection)

依存性注入は、クラスが他のクラスに依存する場合に、その依存性を外部から注入することでクラス間の結合度を下げるテクニックです。

class Engine:
    def start(self):
        return "Engine started"

class Car:
    def __init__(self, engine):
        self.engine = engine

    def start(self):
        return self.engine.start()

engine = Engine()
car = Car(engine)
print(car.start())  # Engine started

この例では、CarクラスはEngineクラスに依存していますが、その依存性はCarのコンストラクタによって注入されます。これにより、Carクラスは特定のEngineの実装ではなく、Engineのインターフェース(startメソッドを持つ任意のオブジェクト)に依存するようになります。

  1. 抽象基本クラスの使用

Pythonではインターフェースは直接サポートされていませんが、抽象基本クラス(ABC)を使用して同様の効果を得ることができます。

from abc import ABC, abstractmethod

class AbstractEngine(ABC):
    @abstractmethod
    def start(self):
        pass

class Engine(AbstractEngine):
    def start(self):
        return "Engine started"

class Car:
    def __init__(self, engine):
        if not isinstance(engine, AbstractEngine):
            raise ValueError("engine must be an instance of AbstractEngine")
        self.engine = engine

    def start(self):
        return self.engine.start()

engine = Engine()
car = Car(engine)
print(car.start())  # Engine started

この例では、CarクラスはAbstractEngineインターフェースに依存しています。これにより、Carクラスは具体的なEngineクラスではなく、startメソッドを持つ任意のオブジェクトに依存するようになります。

  1. イベント駆動プログラミング

イベント駆動プログラミングは、プログラムがユーザーのアクション(クリックやキーボード入力など)やシステムのイベント(タイマーの終了、新しいデータの到着など)に反応して動作するような設計パターンを指します。

Pythonでの具体的な例としては、GUI(Graphical User Interface)ライブラリの一つであるTkinterを使用した簡単なアプリケーションを考えてみましょう。以下のコードは、ボタンをクリックするとメッセージが表示される簡単なGUIアプリケーションです。

import tkinter as tk

def on_button_click():
    print("Button clicked!")

root = tk.Tk()
button = tk.Button(root, text="Click me!", command=on_button_click)
button.pack()

root.mainloop()

このコードでは、on_button_click関数がイベントハンドラとして定義されています。この関数は、ボタンがクリックされたというイベントが発生したときに呼び出されます。command=on_button_clickの部分で、ボタンのクリックイベントに対してこの関数を関連付けています。

root.mainloop()は、イベントループを開始します。このループは、新しいイベントが発生するのを待ち、イベントが発生すると対応するイベントハンドラを呼び出します。このように、プログラムはイベントが発生するまで待機し、イベントが発生したときに特定のアクションを実行する、というのがイベント駆動プログラミングの基本的な考え方です。

  1. メディエーターパターン

メディエーターパターンは、ソフトウェア設計パターンの一つで、オブジェクト間の通信をカプセル化し、オブジェクトが直接通信しないようにすることで、オブジェクト間の結合度を低減することを目指します。

Pythonでメディエーターパターンを実装する例を以下に示します。この例では、チャットルーム(メディエータ)とユーザー(コンポーネント)を模しています。

class ChatRoom:
    def __init__(self):
        self.users = []

    def add_user(self, user):
        self.users.append(user)

    def send_message(self, message, user):
        for u in self.users:
            if u != user:
                u.receive_message(message)


class User:
    def __init__(self, name, chat_room):
        self.name = name
        self.chat_room = chat_room
        self.chat_room.add_user(self)

    def send_message(self, message):
        print(f"{self.name} says: {message}")
        self.chat_room.send_message(message, self)

    def receive_message(self, message):
        print(f"{self.name} received: {message}")


# Usage
chat_room = ChatRoom()

bob = User("Bob", chat_room)
alice = User("Alice", chat_room)
charlie = User("Charlie", chat_room)

bob.send_message("Hello, Alice!")
alice.send_message("Hi, Bob!")
charlie.send_message("Hi, everyone!")

このコードでは、ChatRoomクラスがメディエーターの役割を果たし、Userクラスがコンポーネントの役割を果たします。ユーザーはメッセージを送信すると、そのメッセージはチャットルームに送られ、チャットルームがそのメッセージを他の全てのユーザーに転送します。これにより、各ユーザーは他のユーザーと直接通信することなく、メッセージを交換することができます。

これらのテクニックは、クラス間の結合度を下げ、コードの再利用性とテスト容易性を向上させるのに役立ちます。

Discussion