⛓️
初心者がSOLIDを学ぶ:単一責任の原則編
はじめに
そろそろSOLID原則について完全に理解したい!と思い、SOLID原則について書いていきます。同じく学び始めの人の参考になれば嬉しいです。何か誤りなどご指摘があればいただけると嬉しいです。この記事ではSOLID原則の中の単一責任の原則(Single Responsibility Principle)について書きます。
単一責任の原則とは?
クラスを変更する理由は1つだけであるべきである
ーロバート・C・マーティン
単一責任の原則は、「1つのクラスは1つの責任だけを持つべきである」という原則です。別の言い方をすれば、「クラスを変更する理由は1つだけであるべき」ということになります。この原則を最初に聞いたとき、「責任って何?」と疑問に思いました。調べてみると、ここでいう「責任」とは「変更の理由」と考えることができるそうです。例えば以下のような変更理由があるとします
- データの処理ロジックが変わる
- データの保存方法が変わる
- ユーザーインターフェースの表示方法が変わる
単一責任の原則(以降は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})
このコードには以下の問題があります:
-
User
クラスが少なくとも4つの責任を持っている:
- ユーザーデータの保持
- メールアドレスの検証
- データベースへの保存
- 異なる形式での表示(テキスト表示とJSON変換)
- なぜ問題か:
- データベース構造が変わると、
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に従うことで、以下のメリットがあります
-
変更が簡単:表示形式だけを変更したい場合は、
UseFormatter
だけを修正すれば良い -
テストが容易:各クラスを個別にテストできる。例えば
EmailValidator
のテストではデータベース接続が不要 -
コードの再利用:例えば
EmailValidator
は別のフォームでも再利用できる - 理解しやすい:クラス名を見るだけで何をするクラスなのかが分かる
まとめ
単一の責任の原則は「1つのクラスは1つの責任だけを持つべき」という考え方です。この原則に従うことで、コードがより保守しやすく、テストしやすくなります。
Discussion