ソフトウェア設計原則

1. 単一責任原則(Single Responsibility Principle, SRP)

クラスは変更の理由を一つだけ持つべきです。つまり、各クラスは特定の業務ロジックのみを担当すべきです。これにより、クラスの責務が明確になり、保守性が向上します。

たとえば、ユーザー情報を管理するUserProfileクラスが、データベースへの保存と表示機能を同時に処理している場合、これは原則に違反します。

class UserProfile:
    def __init__(self, name, email):
        self.name = name
        self.email = email

    def save_user(self):
        print(f"{self.name} をデータベースに保存")

    def display_user(self):
        print(f"ユーザー: {self.name}, メール: {self.email}")

このように、保存と表示の両方を行うクラスは、単一責任原則に従って分割する必要があります。

class UserStorage:
    def save_user(self, user):
        print(f"{user.name} をデータベースに保存")

class UserDisplay:
    def display_user(self, user):
        print(f"ユーザー: {user.name}, メール: {user.email}")

上記のように、それぞれの機能を別々のクラスに分離することで、各クラスの責務が明確になります。

2. 開閉原則(Open-Closed Principle, OCP)

ソフトウェアの要素(クラスやモジュールなど)は、拡張には開いており、修正には閉じているべきです。つまり、既存コードを変更することなく新しい機能を追加できる状態にすべきです。

図形描画アプリケーションを例に考えます。基本となるShapeクラスと、それを描画するGraphicEditorクラスがあります。

class Shape:
    def draw(self):
        pass

class Circle(Shape):
    def draw(self):
        print("円を描画")

class Square(Shape):
    def draw(self):
        print("四角形を描画")

class GraphicEditor:
    def draw_shape(self, shape):
        shape.draw()

この場合、GraphicEditorは拡張には開いており、既存コードの変更なしに新しい図形を追加できます。

class Triangle(Shape):
    def draw(self):
        print("三角形を描画")

新しい図形を追加する際も、GraphicEditorのコードを変更する必要はありません。

3. 里氏置換原則(Liskov Substitution Principle, LSP)

サブクラスのオブジェクトは、親クラスのオブジェクトと置き換えてもプログラムの正しさに影響を与えないべきです。つまり、サブクラスは親クラスの仕様に従うべきです。

鳥のクラス構造を考えます。親クラスBirdにはflyメソッドがあり、そのサブクラスとしてSparrowとOstrichがあります。

class Bird:
    def fly(self):
        pass

class Sparrow(Bird):
    def fly(self):
        print("スズメは飛べる")

class Ostrich(Bird):
    def fly(self):
        print("タカツブリは飛べない")

この場合、Ostrichが飛べないという特性に反してflyメソッドを実装するため、原則に違反しています。

より適切な設計では、Birdを抽象化し、飛べる鳥と飛べない鳥を分けて定義します。

class Bird:
    pass

class FlyingBird(Bird):
    def fly(self):
        pass

class NonFlyingBird(Bird):
    pass

class Sparrow(FlyingBird):
    def fly(self):
        print("スズメは飛べる")

class Ostrich(NonFlyingBird):
    pass

こうすることで、それぞれのクラスが正しい振る舞いを保ち、原則を尊重できます。

4. インターフェース分離原則(Interface Segregation Principle, ISP)

クライアントは、使用しないインターフェースに依存すべきではありません。大きなインターフェースを小さなインターフェースに分割し、必要な機能のみを実装するべきです。

複数の機能を持つプリンターのインターフェースを考えてみましょう。

class MultiFunctionPrinter:
    def print_document(self):
        pass

    def scan_document(self):
        pass

    def copy_document(self):
        pass

印刷のみを行うプリンターがこのインターフェースを実装する場合、不要な機能に依存することになり、原則に違反します。

インターフェースを細分化して、各機能を独立したインターフェースに分けます。

class Printer:
    def print_document(self):
        pass

class Scanner:
    def scan_document(self):
        pass

class Copier:
    def copy_document(self):
        pass

それぞれの機能に対応するクラスを作成します。

class SimplePrinter(Printer):
    def print_document(self):
        print("文書を印刷")

class AdvancedPrinter(Printer, Scanner, Copier):
    def print_document(self):
        print("文書を印刷")

    def scan_document(self):
        print("文書をスキャン")

    def copy_document(self):
        print("文書をコピー")

これにより、必要最小限のインターフェースのみを使用できるようになります。

5. 依存関係逆転原則(Dependency Inversion Principle, DIP)

高レベルモジュールは低レベルモジュールに依存せず、どちらも抽象に依存すべきです。具体的な実装ではなく、抽象インターフェースを介して依存関係を定義します。

メッセージ送信機能の例を見てみましょう。メールとSMSの送信をサポートする必要があります。

class MessageSender:
    def send(self, message):
        pass

class EmailSender(MessageSender):
    def send(self, message):
        print(f"メール送信: {message}")

class SMSSender(MessageSender):
    def send(self, message):
        print(f"SMS送信: {message}")

送信方法を抽象化することで、具体的な実装に依存せずに柔軟に切り替えが可能です。

class Notification:
    def __init__(self, sender: MessageSender):
        self.sender = sender

    def notify(self, message):
        self.sender.send(message)

このように、Notificationは具体的な送信手段に依存せず、抽象インターフェースを通して動作します。

6. コンポジション・リユース原則(Composite Reuse Principle, CRP)

継承よりもコンポジション(「has-a」関係)を使用して再利用を行うべきです。継承は強い結合を生む一方、コンポジションは柔軟性を高めます。

車のクラスが起動と停止の機能を持っているとします。今、警報システムを追加したいとします。

class Alarm:
    def start_alarm(self):
        print("警報開始")

    def stop_alarm(self):
        print("警報終了")

class Car:
    def __init__(self):
        self.alarm = Alarm()

    def start(self):
        self.alarm.start_alarm()
        print("車を起動")

    def stop(self):
        self.alarm.stop_alarm()
        print("車を停止")

コンポジションにより、既存のクラスに新しい機能を追加することが容易になります。

7. ディミテルの法則(Law of Demeter, LoD)

オブジェクトは他のオブジェクトとの関係をできるだけ少なく保つべきです。つまり、直接関係を持つオブジェクトとのみ通信し、間接的な依存を避けるべきです。

書店クラスが本の著者名を表示する場合を例にします。

class Author:
    def __init__(self, name):
        self.name = name

class Book:
    def __init__(self, author):
        self.author = author

class Bookstore:
    def print_author_name(self, book):
        print(book.author.name)

この場合、BookstoreがBookの内部構造を知らなければならないため、原則に違反します。

解決策として、Book側で著者名を取得するメソッドを提供します。

class Author:
    def __init__(self, name):
        self.name = name

class Book:
    def __init__(self, author):
        self.author = author

    def get_author_name(self):
        return self.author.name

class Bookstore:
    def print_author_name(self, book):
        print(book.get_author_name())

このように、BookstoreはAuthorとの直接的な関係を避けて、Book経由でのみ情報を取得します。

タグ: 設計原則 オブジェクト指向 ソフトウェア設計 プログラミング アーキテクチャ

10月5日 08:04 投稿