0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

アンチパターンを避ける「作り方」目線の継承利用

Posted at

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 ごとに直書きし、必要箇所のみ関数化で再利用するのが現実的で安全。
  • 継承を 「使い方」ではなく「作り方」に限定すると、破綻を避けつつフレームワークの利便性を享受できる。

この記事が「継承どうする問題」に悩む方の一助になれば幸いです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?