Open2

DRY原則/SOLID原則などのプログラミングでの考え方について📝

まさぴょん🐱まさぴょん🐱

DRY原則とSOLID原則について

それぞれがプログラミングにおける重要な設計思想であり、コードの品質や保守性を高めるための指針です。

DRY原則

DRYは「Don't Repeat Yourself(自分を繰り返さない)」の略で、ソフトウェア開発における基本的な原則の一つです。この原則は、コードやロジックの重複を避けることを目的としています。

概要

  • 目的: コードの重複を減らし、再利用性を高めることで、保守性や可読性を向上させる。
  • 意味: 同じ機能や情報を複数の場所で繰り返し記述するのではなく、一箇所にまとめて管理する。
  • : 例えば、同じ計算ロジックが複数の関数で使われている場合、そのロジックを1つの関数にまとめて呼び出すようにする。

具体例

# DRYに違反する例
def print_user1():
    print("名前: 山田太郎, 年齢: 30")

def print_user2():
    print("名前: 佐藤花子, 年齢: 25")

# DRYを守る例
def print_user(name, age):
    print(f"名前: {name}, 年齢: {age}")

print_user("山田太郎", 30)
print_user("佐藤花子", 25)

上の例では、重複したprint文を1つの関数にまとめることでDRYを実現しています。

メリット

  • 修正が容易(1箇所を直せば済む)。
  • コード量が減り、可読性が上がる。
  • バグの発生リスクが減少。

注意点

  • 過剰にDRYを追求すると、逆にコードが複雑になる場合がある(「抽象化のしすぎ」に注意)。
  • 類似しているが異なる要件を持つコードを無理やり統合しない。

SOLID原則

SOLIDは、オブジェクト指向設計における5つの原則の頭文字を取ったもので、ロバート・C・マーティン(Uncle Bob)が提唱しました。これらは、柔軟で拡張性が高く、保守しやすいコードを書くためのガイドラインです。それぞれ詳しく見ていきましょう。

1. S - 単一責任の原則 (Single Responsibility Principle)

  • 定義: 「1つのクラスやモジュールは、1つの責任だけを持つべきである」。
  • 目的: クラスが複数の役割を持つと、変更が必要になったときに影響範囲が広がりやすくなる。それを防ぐ。
  • :
    # 単一責任に違反する例
    class User:
        def save_to_database(self):
            print("データベースに保存")
        def send_email(self):
            print("メール送信")
    
    # 改善例
    class User:
        def save(self):
            UserRepository().save(self)
    
    class UserRepository:
        def save(self, user):
            print("データベースに保存")
    
    class EmailService:
        def send_email(self, user):
            print("メール送信")
    
    • 修正版では、データベース保存とメール送信を別のクラスに分離。

2. O - オープン・クローズドの原則 (Open/Closed Principle)

  • 定義: 「クラスは拡張に対して開いており、修正に対して閉じているべき」。
  • 目的: 既存のコードを変更せずに新しい機能を追加できるようにする。
  • :
    # オープン・クローズドに違反する例
    class Payment:
        def process(self, payment_type):
            if payment_type == "credit":
                print("クレジットカード処理")
            elif payment_type == "paypal":
                print("PayPal処理")
    
    # 改善例(ポリモーフィズムを使用)
    from abc import ABC, abstractmethod
    
    class PaymentMethod(ABC):
        @abstractmethod
        def process(self):
            pass
    
    class CreditCard(PaymentMethod):
        def process(self):
            print("クレジットカード処理")
    
    class PayPal(PaymentMethod):
        def process(self):
            print("PayPal処理")
    
    class Payment:
        def process(self, method: PaymentMethod):
            method.process()
    
    • 新しい支払い方法を追加する際、Paymentクラスを修正せず、新しいクラスを追加するだけ。

3. L - リスコフの置換原則 (Liskov Substitution Principle)

  • 定義: 「派生クラスは基底クラスと置換可能でなければならない」。
  • 目的: 継承を使用する際に、基底クラスの機能を壊さないようにする。
  • :
    # リスコフに違反する例
    class Bird:
        def fly(self):
            print("飛ぶ")
    
    class Penguin(Bird):
        def fly(self):
            raise Exception("ペンギンは飛べません")  # 基底クラスの期待を裏切る
    
    # 改善例
    class Bird:
        pass
    
    class FlyingBird(Bird):
        def fly(self):
            print("飛ぶ")
    
    class Penguin(Bird):
        pass
    
    • ペンギンが飛べないことを考慮し、飛ぶ機能を持つクラスを分離。

4. I - インターフェース分離の原則 (Interface Segregation Principle)

  • 定義: 「クライアントが必要としないインターフェースを強制すべきではない」。
  • 目的: 巨大なインターフェースを避け、必要な機能だけを提供する。
  • :
    # インターフェース分離に違反する例
    class Worker:
        def work(self):
            pass
        def eat(self):
            pass
    
    class Robot(Worker):
        def work(self):
            print("働く")
        def eat(self):  # ロボットは食べない
            pass
    
    # 改善例
    class Workable:
        def work(self):
            pass
    
    class Eatable:
        def eat(self):
            pass
    
    class Human(Workable, Eatable):
        def work(self):
            print("働く")
        def eat(self):
            print("食べる")
    
    class Robot(Workable):
        def work(self):
            print("働く")
    
    • ロボットに「食べる」機能を強制しない。

5. D - 依存性逆転の原則 (Dependency Inversion Principle)

  • 定義: 「高レベルのモジュールは低レベルのモジュールに依存せず、両者は抽象に依存すべき」。
  • 目的: 依存関係を逆転させ、柔軟性とテスト容易性を高める。
  • :
    # 依存性逆転に違反する例
    class Database:
        def save(self):
            print("データベースに保存")
    
    class UserService:
        def __init__(self):
            self.db = Database()  # 具体クラスに依存
    
    # 改善例
    from abc import ABC, abstractmethod
    
    class Storage(ABC):
        @abstractmethod
        def save(self):
            pass
    
    class Database(Storage):
        def save(self):
            print("データベースに保存")
    
    class UserService:
        def __init__(self, storage: Storage):
            self.storage = storage  # 抽象に依存
    
    • UserServiceが具体的なDatabaseに依存せず、抽象的なStorageに依存。

DRYとSOLIDの関係

  • DRYはコードの重複を避ける具体的なテクニックに焦点を当てた原則。
  • SOLIDはオブジェクト指向設計における構造的・抽象的な指針で、より広範な設計思想を提供。
  • 両者は補完的で、DRYを守りつつSOLIDを適用することで、効率的かつ柔軟なコードが書けます。
まさぴょん🐱まさぴょん🐱

DRYやSOLID以外の原則やガイドラインについて📝

DRYやSOLID以外にも、コーディングやソフトウェア設計を効率的かつ高品質にするための原則やガイドラインが数多く存在します。
これらは状況や目的に応じて適用されるもので、設計思想や開発哲学を反映しています。

1. KISS原則 (Keep It Simple, Stupid)

  • 概要: 「シンプルに保て、馬鹿者」という意味で、設計や実装を必要以上に複雑にしないことを推奨。
  • 目的: シンプルなコードは理解しやすく、バグが少なく、保守が容易。
  • :
    • 複雑な条件分岐や多層の抽象化を避け、必要最低限のロジックで目的を達成する。
    # 複雑すぎる例
    def calculate_price(item, discount, tax, is_premium):
        return item * (1 - discount) * (1 + tax) if is_premium else item * (1 + tax)
    
    # KISSを適用
    def calculate_price(item, discount, tax):
        price = item * (1 - discount)
        return price * (1 + tax)
    
  • 注意点: シンプルさを追求しすぎて、必要な柔軟性や拡張性を失わないようにする。

2. YAGNI原則 (You Aren’t Gonna Need It)

  • 概要: 「それは必要ないよ」という意味で、現在の要件に必要ない機能やコードを追加しない。
  • 目的: 無駄な開発時間を減らし、コードベースを小さく保つ。
  • :
    • 将来のために「とりあえず」多機能なクラスを作らない。
    # YAGNIに違反
    class UserManager:
        def save_to_db(self): pass
        def save_to_file(self): pass  # 今は使わない
        def save_to_cloud(self): pass  # 今は使わない
    
    # YAGNIを適用
    class UserManager:
        def save_to_db(self): pass  # 今必要な機能だけ
    
  • 関連: アジャイル開発やリーン開発と相性が良い。

3. Law of Demeter (デメテルの法則)

  • 概要: 「知り合い以外とは話さない」という比喩で、オブジェクトが他のオブジェクトの内部構造に深く依存しないようにする。
  • 目的: 依存関係を減らし、カプセル化を強化。
  • ルール:
    • メソッドは自分自身のフィールドや引数として渡されたオブジェクトのメソッドのみを呼び出す。
    • 「ドット(.)」の連鎖(例: a.b.c.d())を避ける。
  • :
    # デメテルの法則に違反
    class Order:
        def get_customer(self):
            return self.customer
    
    class Customer:
        def get_address(self):
            return self.address
    
    order.get_customer().get_address()  # 深い依存
    
    # 改善例
    class Order:
        def get_customer_address(self):
            return self.customer.get_address()
    
  • メリット: コードの結合度が下がり、変更に強い設計になる。

4. GRASP (General Responsibility Assignment Software Patterns)

  • 概要: オブジェクト指向設計における責任の割り当てに関するパターン集。SOLIDと補完的。
  • 主なパターン:
    • 情報エキスパート (Information Expert): データを持つオブジェクトにそのデータを扱う責任を与える。
    • クリエイター (Creator): オブジェクトを作成する責任を、そのオブジェクトと密接に関連するクラスに与える。
    • 低結合 (Low Coupling): クラス間の依存関係を最小限にする。
    • 高凝集 (High Cohesion): クラスの責任を関連性の高いものに集中させる。
  • :
    • 注文データを扱うOrderクラスが、注文の計算ロジックも担当する(情報エキスパート)。
  • 目的: 責務の割り当てを明確にし、SOLIDを具体的に実践する指針を提供。

5. CoC (Convention over Configuration)

  • 概要: 「設定より規約」という原則で、デフォルトの規約を優先し、明示的な設定を最小限にする。
  • 目的: 開発者が考えるべきことを減らし、生産性を向上。
  • :
    • Ruby on Railsでは、ファイル名やクラス名に規約があり、それに従えば設定不要。
    • UserControllerという名前なら、自動でusersテーブルと紐づく。
  • 関連: 「マジック」と呼ばれる自動化が多いフレームワークで採用される。

6. PoLA (Principle of Least Astonishment)

  • 概要: 「驚き最小の原則」。コードやインターフェースが直感的で、ユーザーの期待を裏切らないようにする。
  • 目的: 使いやすさと予測可能性を高める。
  • :
    • add(2, 3)が5を返すのは当然だが、突然文字列"23"を返すと驚く。
    # 驚く例
    def add(a, b):
        return str(a) + str(b)  # "23"
    
    # 期待通り
    def add(a, b):
        return a + b  # 5
    
  • 適用: API設計やUIデザインにも関連。

7. TDAE (Tell, Don’t Ask)

  • 概要: 「聞かずに命令しろ」。オブジェクトの状態を問い合わせるのではなく、直接命令を送る。
  • 目的: カプセル化を強化し、制御の逆転を防ぐ。
  • :
    # Ask(聞く)
    class Light:
        def is_on(self):
            return self.state
        def turn_on(self):
            self.state = True
    
    if light.is_on():
        pass
    else:
        light.turn_on()
    
    # Tell(命令)
    class Light:
        def ensure_on(self):
            self.state = True
    
    light.ensure_on()
    
  • メリット: 呼び出し側が内部状態を気にする必要がなくなる。

まとめと比較

原則 主な焦点 適用場面
DRY コードの重複排除 実装レベル
SOLID オブジェクト指向設計 設計レベル
KISS シンプルさ 実装・設計全般
YAGNI 必要性に基づく開発 要件定義・実装
Law of Demeter 依存関係の削減 オブジェクト間通信
GRASP 責務の割り当て オブジェクト指向設計
CoC 規約の活用 フレームワーク開発
PoLA 直感性・予測可能性 API/UI設計
TDAE カプセル化 オブジェクト指向実装

これらの原則は、状況やプロジェクトの規模、チームの文化によって使い分けられます。
例えば、小規模なスクリプトではKISSやDRYを重視し、大規模なシステムではSOLIDやGRASPが役立ちます。