python-design-patterns
KISS・関心の分離・単一責任・継承より合成といったPythonの設計原則を適用するスキル。新規サービスやコンポーネントをゼロから設計する際、肥大化したGodクラスや巨大な関数をリファクタリングする際、継承と合成の選択・密結合や内部型の露出といった構造的問題のレビュー・I/Oとビジネスロジックが混在してテストしづらいコードの改善など、クラス設計や責務の分割に関わる場面で活用してください。
description の原文を見る
Python design patterns including KISS, Separation of Concerns, Single Responsibility, and composition over inheritance. Use this skill when designing a new service or component from scratch and choosing how to layer responsibilities, when refactoring a God class or monolithic function that has grown too large, when deciding whether to add a new abstraction or live with duplication, when evaluating a pull request for structural issues like tight coupling or leaking internal types, when choosing between inheritance and composition for a new class hierarchy, or when a codebase is becoming hard to test because of entangled I/O and business logic.
SKILL.md 本文
Python デザインパターン
基本的なデザイン原則を用いて、保守しやすい Python コードを書きます。これらのパターンは、理解しやすく、テストしやすく、変更しやすいシステムを構築するのに役立ちます。
このスキルを使うとき
- 新しいコンポーネントまたはサービスの設計
- 複雑または絡み合ったコードのリファクタリング
- 新しい抽象化を作成するかどうかの判断
- 継承と合成の選択
- コードの複雑さと結合度の評価
- モジュール型アーキテクチャの計画
核となる概念
1. KISS(シンプルに保つ)
動作する最もシンプルなソリューションを選びます。複雑さは具体的な要件によって正当化される必要があります。
2. Single Responsibility(単一責任原則)
各ユニットは 1 つの変更理由を持つべきです。関心事を焦点の絞られたコンポーネントに分離します。
3. Composition Over Inheritance(継承よりも合成)
クラスを拡張するのではなく、オブジェクトを組み合わせることで動作を構築します。
4. Rule of Three(3 つの法則)
抽象化する前に 3 つのインスタンスがあるまで待ちます。重複は時に過度な抽象化よりも優れています。
クイックスタート
# シンプルさは賢さに勝る
# ファクトリ/レジストリパターンではなく:
FORMATTERS = {"json": JsonFormatter, "csv": CsvFormatter}
def get_formatter(name: str) -> Formatter:
return FORMATTERS[name]()
基本パターン
パターン 1: KISS - シンプルに保つ
複雑さを加える前に、「より単純なソリューションが機能するか」を問い掛けます。
# 過度なエンジニアリング: 登録機構付きファクトリ
class OutputFormatterFactory:
_formatters: dict[str, type[Formatter]] = {}
@classmethod
def register(cls, name: str):
def decorator(formatter_cls):
cls._formatters[name] = formatter_cls
return formatter_cls
return decorator
@classmethod
def create(cls, name: str) -> Formatter:
return cls._formatters[name]()
@OutputFormatterFactory.register("json")
class JsonFormatter(Formatter):
...
# シンプル: 単に辞書を使う
FORMATTERS = {
"json": JsonFormatter,
"csv": CsvFormatter,
"xml": XmlFormatter,
}
def get_formatter(name: str) -> Formatter:
"""名前でフォーマッターを取得します。"""
if name not in FORMATTERS:
raise ValueError(f"Unknown format: {name}")
return FORMATTERS[name]()
ファクトリパターンはここではコードを追加する以上の価値を提供しません。実際の問題を解決するときのためにパターンを取っておきます。
パターン 2: 単一責任原則
各クラスまたは関数は 1 つの変更理由を持つべきです。
# 悪い例: ハンドラーがすべてを行う
class UserHandler:
async def create_user(self, request: Request) -> Response:
# HTTP解析
data = await request.json()
# バリデーション
if not data.get("email"):
return Response({"error": "email required"}, status=400)
# データベースアクセス
user = await db.execute(
"INSERT INTO users (email, name) VALUES ($1, $2) RETURNING *",
data["email"], data["name"]
)
# レスポンスフォーマット
return Response({"id": user.id, "email": user.email}, status=201)
# 良い例: 関心事を分離
class UserService:
"""ビジネスロジックのみ。"""
def __init__(self, repo: UserRepository) -> None:
self._repo = repo
async def create_user(self, data: CreateUserInput) -> User:
# ビジネスルールのみ
user = User(email=data.email, name=data.name)
return await self._repo.save(user)
class UserHandler:
"""HTTP関連のみ。"""
def __init__(self, service: UserService) -> None:
self._service = service
async def create_user(self, request: Request) -> Response:
data = CreateUserInput(**(await request.json()))
user = await self._service.create_user(data)
return Response(user.to_dict(), status=201)
これで HTTP の変更がビジネスロジックに影響しません。また、その逆も同様です。
パターン 3: 関心事の分離
コードを、明確な責任を持つ別個のレイヤーに構成します。
┌─────────────────────────────────────────────────────┐
│ APIレイヤー(ハンドラー) │
│ - リクエストをパース │
│ - サービスを呼び出す │
│ - レスポンスをフォーマット │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ サービスレイヤー(ビジネスロジック) │
│ - ドメインルールとバリデーション │
│ - オペレーションをオーケストレート │
│ - 可能な限り純粋関数 │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ リポジトリレイヤー(データアクセス) │
│ - SQLクエリ │
│ - 外部API呼び出し │
│ - キャッシュオペレーション │
└─────────────────────────────────────────────────────┘
各レイヤーはその下のレイヤーのみに依存します:
# リポジトリ: データアクセス
class UserRepository:
async def get_by_id(self, user_id: str) -> User | None:
row = await self._db.fetchrow(
"SELECT * FROM users WHERE id = $1", user_id
)
return User(**row) if row else None
# サービス: ビジネスロジック
class UserService:
def __init__(self, repo: UserRepository) -> None:
self._repo = repo
async def get_user(self, user_id: str) -> User:
user = await self._repo.get_by_id(user_id)
if user is None:
raise UserNotFoundError(user_id)
return user
# ハンドラー: HTTP関連
@app.get("/users/{user_id}")
async def get_user(user_id: str) -> UserResponse:
user = await user_service.get_user(user_id)
return UserResponse.from_user(user)
パターン 4: 継承よりも合成
クラス階層を拡張するのではなく、オブジェクトを組み合わせることで動作を構築します。
# 継承: 硬く、テストが困難
class EmailNotificationService(NotificationService):
def __init__(self):
super().__init__()
self._smtp = SmtpClient() # モックするのが困難
def notify(self, user: User, message: str) -> None:
self._smtp.send(user.email, message)
# 合成: 柔軟でテスト可能
class NotificationService:
"""複数のチャネルを通じて通知を送信します。"""
def __init__(
self,
email_sender: EmailSender,
sms_sender: SmsSender | None = None,
push_sender: PushSender | None = None,
) -> None:
self._email = email_sender
self._sms = sms_sender
self._push = push_sender
async def notify(
self,
user: User,
message: str,
channels: set[str] | None = None,
) -> None:
channels = channels or {"email"}
if "email" in channels:
await self._email.send(user.email, message)
if "sms" in channels and self._sms and user.phone:
await self._sms.send(user.phone, message)
if "push" in channels and self._push and user.device_token:
await self._push.send(user.device_token, message)
# フェイクで簡単にテスト
service = NotificationService(
email_sender=FakeEmailSender(),
sms_sender=FakeSmsSender(),
)
応用パターン
パターン 5: Rule of Three(3 つの法則)
抽象化する前に 3 つのインスタンスがあるまで待ちます。
# 2 つの似た関数? まだ抽象化しない
def process_orders(orders: list[Order]) -> list[Result]:
results = []
for order in orders:
validated = validate_order(order)
result = process_validated_order(validated)
results.append(result)
return results
def process_returns(returns: list[Return]) -> list[Result]:
results = []
for ret in returns:
validated = validate_return(ret)
result = process_validated_return(validated)
results.append(result)
return results
# これらは似ているように見えますが、待ってください!実は同じですか?
# 異なるバリデーション、異なる処理、異なるエラー...
# 重複は時に間違った抽象化よりも優れています
# 3 番目のケースの後に初めて、本当のパターンがあるかどうかを検討します
# しかし、その場合でも、時に明示的であることがより抽象的であることより優れています
パターン 6: 関数サイズのガイドライン
関数を焦点を絞った状態に保ちます。関数が以下の場合、抽出します:
- 20~50 行を超える(複雑さによって異なる)
- 複数の異なる目的を果たす
- 深くネストされたロジックがある(3 レベル以上)
# 長すぎる、複数の関心事が混在
def process_order(order: Order) -> Result:
# 50行のバリデーション...
# 30行の在庫確認...
# 40行の支払い処理...
# 20行の通知...
pass
# より良い: 焦点を絞った関数で構成
def process_order(order: Order) -> Result:
"""顧客注文を完全なワークフローで処理します。"""
validate_order(order)
reserve_inventory(order)
payment_result = charge_payment(order)
send_confirmation(order, payment_result)
return Result(success=True, order_id=order.id)
パターン 7: 依存性注入
テスト可能性のために、コンストラクタを通じて依存性を渡します。
from typing import Protocol
class Logger(Protocol):
def info(self, msg: str, **kwargs) -> None: ...
def error(self, msg: str, **kwargs) -> None: ...
class Cache(Protocol):
async def get(self, key: str) -> str | None: ...
async def set(self, key: str, value: str, ttl: int) -> None: ...
class UserService:
"""注入された依存性を持つサービス。"""
def __init__(
self,
repository: UserRepository,
cache: Cache,
logger: Logger,
) -> None:
self._repo = repository
self._cache = cache
self._logger = logger
async def get_user(self, user_id: str) -> User:
# キャッシュを最初にチェック
cached = await self._cache.get(f"user:{user_id}")
if cached:
self._logger.info("Cache hit", user_id=user_id)
return User.from_json(cached)
# データベースからフェッチ
user = await self._repo.get_by_id(user_id)
if user:
await self._cache.set(f"user:{user_id}", user.to_json(), ttl=300)
return user
# 本番環境
service = UserService(
repository=PostgresUserRepository(db),
cache=RedisCache(redis),
logger=StructlogLogger(),
)
# テスト
service = UserService(
repository=InMemoryUserRepository(),
cache=FakeCache(),
logger=NullLogger(),
)
パターン 8: よくある反パターンを避ける
内部型を公開しない:
# 悪い例: ORM モデルを API に漏らす
@app.get("/users/{id}")
def get_user(id: str) -> UserModel: # SQLAlchemy モデル
return db.query(UserModel).get(id)
# 良い例: レスポンススキーマを使用
@app.get("/users/{id}")
def get_user(id: str) -> UserResponse:
user = db.query(UserModel).get(id)
return UserResponse.from_orm(user)
I/O とビジネスロジックを混在させない:
# 悪い例: SQL がビジネスロジックに埋め込まれている
def calculate_discount(user_id: str) -> float:
user = db.query("SELECT * FROM users WHERE id = ?", user_id)
orders = db.query("SELECT * FROM orders WHERE user_id = ?", user_id)
# ビジネスロジックがデータアクセスと混在
# 良い例: リポジトリパターン
def calculate_discount(user: User, order_history: list[Order]) -> float:
# 純粋なビジネスロジック、簡単にテスト可能
if len(order_history) > 10:
return 0.15
return 0.0
ベストプラクティスの要約
- シンプルに保つ - 機能する最もシンプルなソリューションを選ぶ
- 単一責任 - 各ユニットは 1 つの変更理由を持つ
- 関心事を分離 - 明確な目的を持つ別個のレイヤー
- 継承せず、合成する - 柔軟性のためにオブジェクトを組み合わせる
- Rule of Three - 抽象化する前に待つ
- 関数をシンプルに保つ - 20~50 行(複雑さによって異なる)、1 つの目的
- 依存性を注入する - テスト可能性のためにコンストラクタ注入を使用
- 抽象化する前に削除 - デッドコードを削除し、次にパターンを検討
- 各レイヤーをテストする - 各関心事の分離テスト
- 明示的さは賢さに勝る - 読みやすいコードはエレガントなコードに勝る
トラブルシューティング
クラスが成長し、複数の責任があるように見えますが、分割がおかしく感じます。 「変更理由」テストを適用します: このクラスの編集が必要になるすべての変更をリストアップします。リストに異なるドメインからの項目がある場合(例えば、HTTP 解析とビジネスルールとフォーマット)、分割します。すべての変更が同じドメイン関心から発生する場合、クラスは適切なサイズかもしれません。
すべての依存性をコンストラクタを通じて注入すると、7 個以上のパラメータを持つコンストラクタが生成されます。 これは依存性注入の問題ではなく、1 つのクラスが多くの責任を持ちすぎていることの兆候です。まず、クラスをより小さなユニットに分割し、その後、各コンストラクタは自然により小さくなります。
合成が深くネストされた複雑なラッパーオブジェクトを生成し、追跡が困難です。 合成を浅く保ちます(2~3 レベル)。ラッピングが唯一のメカニズムの場合、Protocol ベースのアプローチまたはシンプルな関数合成がデコレータオブジェクトのチェーンよりもクリーンかどうかを検討します。
Rule of Three は、まだ抽象化しないと言っていますが、重複は 1 つのコピーが更新されたときに他のコピーが更新されないため、バグを引き起こしています。 危険な方法で異なる重複は、より早く抽象化すべきです。Rule of Three はヒューリスティックであり、法律ではありません。コピーがすでに正しく異なる場合は、すぐに抽出し、共有動作を実行するテストを追加します。
サービスレイヤーが API レイヤーからインポートしており、依存性方向を破ります。 これはレイヤー化の違反です。サービスレイヤーはハンドラーからインポートしてはいけません。両方がインポートできる共有型/モデルレイヤーを導入し、依存性矢印が下向きを指しているままにします(API → Service → Repository)。
関連スキル
python-testing-patterns— ここで確立された依存性注入構造を使用して、各レイヤーを分離してテストしますpython-project-setup— 最初からレイヤー境界を強制するプロジェクト構造とツーリングを設定します
ライセンス: MIT(寛容ライセンスのため全文を引用しています) · 原本リポジトリ
詳細情報
- 作者
- wshobson
- リポジトリ
- wshobson/agents
- ライセンス
- MIT
- 最終更新
- 不明
Source: https://github.com/wshobson/agents / ライセンス: MIT
関連スキル
doubt-driven-development
重要な判断はすべて、本番環境への展開前に新しい視点から対抗的レビューを実施します。速度より正確性が重要な場合、不慣れなコードを扱う場合、本番環境・セキュリティに関わるロジック・取り消し不可の操作など影響度が高い場合、または後でバグを修正するよりも今検証する方が効率的な場合に活用してください。
apprun-skills
TypeScriptを使用したAppRunアプリケーションのMVU設計に関する総合的なガイダンスが得られます。コンポーネントパターン、イベントハンドリング、状態管理(非同期ジェネレータを含む)、パラメータと保護機能を備えたルーティング・ナビゲーション、vistestを使用したテストに対応しています。AppRunコンポーネントの設計・レビュー、ルートの配線、状態フローの管理、AppRunテストの作成時に活用してください。
desloppify
コードベースのヘルスチェックと技術負債の追跡ツールです。コード品質、技術負債、デッドコード、大規模ファイル、ゴッドクラス、重複関数、コードスメル、命名規則の問題、インポートサイクル、結合度の問題についてユーザーが質問した場合に使用してください。また、ヘルススコアの確認、次の改善項目の提案、クリーンアップ計画の作成をリクエストされた際にも対応します。29言語に対応しています。
debugging-and-error-recovery
テストが失敗したり、ビルドが壊れたり、動作が期待と異なったり、予期しないエラーが発生したりした場合に、体系的な根本原因デバッグをガイドします。推測ではなく、根本原因を見つけて修正するための体系的なアプローチが必要な場合に使用してください。
test-driven-development
テスト駆動開発により実装を進めます。ロジックの実装、バグの修正、動作の変更など、あらゆる場面で活用できます。コードが正常に動作することを証明する必要がある場合、バグ報告を受けた場合、既存機能を修正する予定がある場合に使用してください。
incremental-implementation
変更を段階的に実施します。複数のファイルに影響する機能や変更を実装する場合に使用してください。大量のコードを一度に書こうとしている場合や、タスクが一度では完結できないほど大きい場合に活用します。