1. はじめに
「継承はアンチパターン」、オブジェクト指向の文脈ではよく耳にする言葉だと思います。
私自身もうまく継承を扱いきれずにコードを複雑にしてしまったことがあります。
ただ、継承を絶対悪とみなすことには違和感があり、文脈を限定すれば有効活用できるのではと考えてきました。
先日、API リクエストを受け取る DTO(Data Transfer Object)を設計する中で、「機械的な継承ならむしろ有効では?」 という整理に至りました。
使用言語は Python、データ構造は Pydantic を利用しています。
本記事では、その思考プロセスと最終的に採択した実装パターンを共有します。
TL;DR
- 使い方目線の継承は避ける:ドメインでの継承は責務が崩れやすい
- 作り方目線の継承は活かせる:DTO(入力境界)の継承は「機械的用途」であり、アンチパターンではない
2. 継承が嫌われる理由とその背景
「継承は危険だ」という理解は、多くの設計書籍で強調されています。
理由は主に次の三点と理解しています。
-
抽象と実装の境界が難しい
必要十分な設計を伴わないまま実装へ進むと、抽象/実装の境界が揺らぎ、やがて瓦解する。 -
機能追加で継承ツリーが腐る
当初はシンプルでも、機能追加を繰り返す中でスパゲッティ化しやすい。 -
事業の変遷で親子の責務が変動する
要求の変化により初期の抽象境界そのものが変わり、継承構造が合わなくなる。
これらの理由により、「継承=避けるべきもの」と捉えていました。
一方で、プロダクトには「使い方」を強く意識せずに済む領域もあります。
その領域における継承もアンチパターンとして扱うべきか検討の余地があると考えました。
3. 継承の目的別分類:「使い方」vs「作り方」
そこで一度、継承を「どういう目的で使うか」に整理しました。
-
使い方の継承
- 親の振る舞いを再利用しつつ、子で振る舞いを変える
- → ドメインモデルで採用すると破綻する。
-
作り方の継承
- 大きな仕組みを前提に共通の仕立てを持たせる
- → 設計が固定されている範囲に限定すると壊れにくい。
「使い方」に継承を当てると破綻するが、「作り方」に限れば有効と整理しました。
4. Pydanticで実感した「作り方」継承の有効性
作り方の継承の有効性を実感したのは Pydantic の利用でした。
Pydanticの採用については別途Zennの記事にしています。
(参照:AWS Lambda + API Gateway構成でPydanticを導入して型安全性と可読性を改善した話)
Pydantic は BaseModel
の継承を前提に設計されています。
この継承は「フレームワークに従うための機械的な継承」であり、ドメイン意味論を分けるための継承ではありません。
本記事では“作り方”の一環として、共通基底に最小のファクトリ(from_event()
)を置き、Pydanticのmodel_validate()
に委譲する方針を採ります。
PydanticはBaseModel
継承とmodel_validate()
を前提とするため、基底で辞書に整えるだけに留め、代入・変換・検証はPydanticに任せます。
5. 実装パターン
方針の要点:
- RequestParamsModel は「入力境界ポリシー」と「最小の共通ファクトリ」だけを持つ
- 各APIのDTO はフィールド宣言だけ
-
代入・型変換・検証 は Pydantic に任せる(=
model_validate()
)
この設計では、DTOの責務は「構造定義」に限定されており、振る舞いの追加やドメインロジックの混入が起こらないため、継承による破綻リスクが極めて低いと考えています。
5.1 共通基底と“最小”ファクトリ
# dto_base.py
from typing import Any, Mapping
from pydantic import BaseModel, ConfigDict
class RequestParamsModel(BaseModel):
# 入力境界ポリシー:未知キー禁止/不変/name/alias両対応
model_config = ConfigDict(extra="forbid", frozen=True, populate_by_name=True)
@classmethod
def from_event(cls, event: Mapping[str, Any]) -> "RequestParamsModel":
# API Gateway (REST/HTTP) の queryStringParameters だけを見る最小実装
qs = event.get("queryStringParameters") or {}
return cls.model_validate(qs) # ← 変換・検証・インスタンス化はPydanticに任せる
5.2 APIごとのDTO(例:/orders)
# dto.py
from datetime import date
from pydantic import Field, AliasChoices
from dto_base import RequestParamsModel
class OrdersQuery(RequestParamsModel):
page: int = Field(1, ge=1)
per_page: int = Field(20, ge=1, le=200)
# 異表記(sortBy 等)はDTO側で吸収する
sort_by: str | None
sort_asc: bool = True
status: str | None = None
date_from: date | None = None
date_to: date | None = None
5.3 DTO → ハンドラ
# handler.py (AWS Lambda の例)
from dto import OrdersQuery
from app.commands import FindOrders
from app.handlers import FindOrdersHandler
handler_instance = FindOrdersHandler(...)
def handler(event, context):
params: OrdersQuery = OrdersQuery.from_event(event) # ← 共通化ポイント
6. まとめ
- 使い方の継承は避ける:ドメインでは責務変化で破綻しやすい。
- 作り方の継承は活かす:DTO(入力境界)の機械的継承は有効。
- DTO は API ごとに直書きし、必要箇所のみ関数化で再利用するのが現実的で安全。
- 継承を 「使い方」ではなく「作り方」に限定すると、破綻を避けつつフレームワークの利便性を享受できる。
この記事が「継承どうする問題」に悩む方の一助になれば幸いです。