⛓️

初心者がSOLIDを学ぶ:単一責任の原則編

に公開

はじめに

そろそろSOLID原則について完全に理解したい!と思い、SOLID原則について書いていきます。同じく学び始めの人の参考になれば嬉しいです。何か誤りなどご指摘があればいただけると嬉しいです。この記事ではSOLID原則の中の単一責任の原則(Single Responsibility Principle)について書きます。

単一責任の原則とは?

クラスを変更する理由は1つだけであるべきである
ーロバート・C・マーティン

単一責任の原則は、「1つのクラスは1つの責任だけを持つべきである」という原則です。別の言い方をすれば、「クラスを変更する理由は1つだけであるべき」ということになります。この原則を最初に聞いたとき、「責任って何?」と疑問に思いました。調べてみると、ここでいう「責任」とは「変更の理由」と考えることができるそうです。例えば以下のような変更理由があるとします

  1. データの処理ロジックが変わる
  2. データの保存方法が変わる
  3. ユーザーインターフェースの表示方法が変わる

単一責任の原則(以降はSRPと書きます)に従うと、1つのクラスはこれらのうち1つだけの理由で変更されるべきで、複数の理由で変更される場合は、そのクラスは複数の責任を持っているということになります。

SRPに違反する例

SRPに違反している例は下のようなコードです。

import re
import sqlite3
import json

class User:
    def __init__(self, name, email):
        self.name = name
        self.email = email
    
    # メールアドレスを検証する
    def validate_email(self):
        import re
        pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
        return re.match(pattern, self.email) is not None
    
    # データベースに保存する
    def save_to_database(self):
        import sqlite3
        conn = sqlite3.connect('users.db')
        cursor = conn.cursor()
        
        # テーブル作成
        cursor.execute('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)')
        
        # データ挿入
        cursor.execute('INSERT INTO users (name, email) VALUES (?, ?)', (self.name, self.email))
        conn.commit()
        conn.close()
    
    # ユーザー情報を表示する
    def display(self):
        return f"Name: {self.name}\nEmail: {self.email}"
    
    # JSON形式に変換する
    def to_json(self):
        import json
        return json.dumps({"name": self.name, "email": self.email})

このコードには以下の問題があります:

  1. Userクラスが少なくとも4つの責任を持っている:
  • ユーザーデータの保持
  • メールアドレスの検証
  • データベースへの保存
  • 異なる形式での表示(テキスト表示とJSON変換)
  1. なぜ問題か:
  • データベース構造が変わると、Userクラスを変更する必要がある
  • 表示形式が変わっても、Userクラスを変更する必要がある
  • テストが難しい(データベース接続をモックする必要がある)

SRPに従った設計例

SRPに従って書き直すと、下のようになります:

# 1. ユーザーデータのみを保持するクラス
class User:
    def __init__(self, name, email):
        self.name = name
        self.email = email

# 2. メールアドレスの検証を担当するクラス
class EmailValidator:
    @staticmethod
    def is_valid(email):
        import re
        pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
        return re.match(pattern, email) is not None

# 3. データベースとのやり取りを担当するクラス
class UserRepository:
    def __init__(self, db_name='users.db'):
        self.db_name = db_name
    
    def save(self, user):
        import sqlite3
        conn = sqlite3.connect(self.db_name)
        cursor = conn.cursor()
        
        # テーブル作成
        cursor.execute('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)')
        
        # データ挿入
        cursor.execute('INSERT INTO users (name, email) VALUES (?, ?)', (user.name, user.email))
        conn.commit()
        conn.close()
    
    def find_by_email(self, email):
        import sqlite3
        conn = sqlite3.connect(self.db_name)
        cursor = conn.cursor()
        cursor.execute('SELECT name, email FROM users WHERE email = ?', (email,))
        result = cursor.fetchone()
        conn.close()
        
        if result:
            return User(result[0], result[1])
        return None

# 4. ユーザー情報の表示を担当するクラス
class UserFormatter:
    @staticmethod
    def format_as_text(user):
        return f"Name: {user.name}\nEmail: {user.email}"
    
    @staticmethod
    def format_as_json(user):
        import json
        return json.dumps({"name": user.name, "email": user.email})

使用例

# ユーザー作成
user = User("山田太郎", "taro@example.com")

# メールアドレスを検証
if EmailValidator.is_valid(user.email):
    # データベースに保存
    repo = UserRepository()
    repo.save(user)
    
    # 表示
    print(UserFormatter.format_as_text(user))
    print(UserFormatter.format_as_json(user))
else:
    print("メールアドレスが不正です")

SRPのメリット

SRPに従うことで、以下のメリットがあります

  1. 変更が簡単:表示形式だけを変更したい場合は、UseFormatterだけを修正すれば良い
  2. テストが容易:各クラスを個別にテストできる。例えばEmailValidatorのテストではデータベース接続が不要
  3. コードの再利用:例えばEmailValidatorは別のフォームでも再利用できる
  4. 理解しやすい:クラス名を見るだけで何をするクラスなのかが分かる

まとめ

単一の責任の原則は「1つのクラスは1つの責任だけを持つべき」という考え方です。この原則に従うことで、コードがより保守しやすく、テストしやすくなります。

Discussion