🫠

DRYにしすぎるとコードは干からびる

に公開

前置き

手前味噌ながら、弊社は高い開発生産性を評価され、Findy Team+ Award 2024, 2025 を2年連続で受賞した。華やかな受賞理由の裏側には様々な要因があるが、その中でも技術的な側面としてひっそりと、開発者が直接触れる作業領域に対して強く作用させていた力学のひとつが、本記事にて紹介する 「DRY との向き合い方(=境界設計)」 である。

「最短距離」で走るつもりが、最速で消耗戦へ

早期のプロダクト開発では「まずはスピードを最優先」という判断がしばしば下される。
だが、短期効率だけを信じて場当たり的な共通化、とりわけ 「誤った DRY」 を積み重ねると、ほんの1ヶ月後には、その“短期効率”こそが最大の足かせになって返ってくる。

初速を上げたつもりが、気づけば境界が溶けたコードベースのメンテに追われ、「本当にやりたい開発」に時間を使えなくなっていく。開発効率は時間とともに自然減衰するものだが、誤った共通化はその減衰曲線を自ら急角度に傾ける。

「アーリーだから多少雑でもいい」
これは、最も危険な自己暗示の一つである。

最短距離で走るつもりが、気づけば最速で消耗戦へ。そんな入り口になりかねない。だからこそ、短期の小手先より、境界の明確さとルールだけは最初から守っておく。その投資対効果は、想像以上に高い。

この「早期の過剰な共通化(境界の消失)」に対する課題感は、決して弊社だけのものではない。実際、Google 社も2024年に Don't DRY your code prematurely と題した記事を公開し、話題を呼んだ。

また、先日開催された アーキテクチャカンファレンス 2025 においても、多くの登壇者が 「変更容易性(Modifiability)」 こそが持続可能な開発の最重要資産であると語り、AI時代だからこそ 「Adaptable AI(AIの変化に適応し続ける設計)」 が必要であるという議論が白熱していた。

AIはあくまで増幅器であり、人間が「目的」や「境界」を正しく定義しなければ、生産性は上がらない

会場で共有されたこの問題意識も、まさに本記事のテーマと直結するものである。

共通化するべきは何か?

ソフトウェア開発において「DRY原則」は、最も基本的で単純ながらも強力なガイドラインとして広く知られている。しかし、時にはこの原則が過剰に適用され、近視眼的な効率化を追うあまり、本来は別々に扱うべきユースケースや概念が、1つの抽象に押し込められてしまうことがある。

その結果として生じるのは、

  • 想定外のパフォーマンス問題(N+1 など)
  • 1つの関数が知るべきことが多すぎる(単一責任の崩壊)
  • 境界があいまいで壊れやすいビジネスロジック

といった、中長期的な開発を阻害する問題である。
そして皮肉なことに、“初速を上げようとして導入した共通化” が、ほんの数週間後には開発速度を奪う側に回る、ということも珍しくない。

本記事ではまず DRY と OAOO の違いを整理したうえで、「何を共通化してはいけないのか」という観点から具体例を通して問題を分析し、そのうえで 「守るべきデータ整合性のルール」を軸にした抽象化の原則 について考える。

DRY と OAOO の違い

まず前提として、本記事で扱う二つの原則を明確に区別しておく。

  • OAOO原則(Once and Only Once)
    同じロジックやデータ操作をプログラム中で2回以上定義しないことを目指す考え方。
    → 主に「コードの重複」を対象とする。

  • DRY原則(Don't Repeat Yourself)
    「同じ知識・ビジネスルールが複数箇所にバラバラに埋め込まれている状態」を避け、単一の表現に集約する考え方。
    → 主に「知識の重複」を対象とする。

ここで重要なのは、DRY は単なる「コピペ排除」ではないという点である。

  • 同じコードでも、背負っている「意味」や「前提」が違うなら、必ずしも DRY の対象とは言えない
  • 逆に、コードとしては違っていても、同じビジネスルールを複数箇所に書いているなら DRY 違反になり得る

本記事で扱う問題は、本来は異なる意味を持つ操作やユースケースを「同じ知識」と見なしてしまい、1つの関数に押し込めてしまった結果の失敗である。

どこで DRY が「誤用」されるのか

DRY の誤用がよく現れるのは、次のような場面である。

  • 粒度の大きい一連の手続き(ワークフロー)に対して
  • 「同じ処理がありそうだから」という理由だけで
  • 背景にあるユースケースの違いを無視して
  • 無理に共通化の抽象を導入する

このとき本質的には、

異なるユースケース・異なる概念を、
「見た目のコードが似ている」というだけで一つに lumping(ひとまとめ)してしまう

という問題が起きている。
これは DRY の本来の目的である「知識を一意に保つ」こととは異なる。

具体例: ユーザ作成ユースケースの「悪い共通化」

以下では、一般的な Web アプリケーションの API サーバを題材に、時系列で少しずつ設計が悪化していく様子を見ていく。ここで扱うテーマは「ユーザを作成する一連の機能」である。

ステップ1: 素朴な手続き(トランザクションスクリプト)

はじめに、ごく素朴なユーザ作成のユースケースがあるものとする。※ Python 注意

from dataclasses import dataclass

# データアクセスや認可処理のための外部パーツ
from my_app import UserRepoModel, is_writable, repo, request_ctx


@dataclass
class CreateUserInput:
    name: str
    age: int
    email: str


def usecase_create_user(dto: CreateUserInput) -> None:
    if not is_writable(request_ctx):
        raise PermissionError("permission denied")

    if dto.age < 5:
        raise ValueError("too young")
    if len(dto.name) > 20:
        raise ValueError("too long name")

    identifier = repo.new_id()
    repo_model = UserRepoModel(
        identifier=identifier,
        name=dto.name,
        age=dto.age,
        email=dto.email,
    )

    repo.save(repo_model)

ステップ2: 別ユースケースでの「ほぼ同じ処理」の登場

次に、新しい機能として usecase_setup_department を追加する。
これは、department(部署)の初期構築の一環としてユーザを1人作成するユースケースである。

@dataclass
class SetupDepartmentInput:
    # ~~ ユーザ以外の入力値 ~~
    user_name: str
    user_age: int
    user_email: str


def usecase_setup_department(dto: SetupDepartmentInput) -> None:
    if not is_writable(request_ctx):
        raise PermissionError("permission denied")

    # ~~ ユーザ以外の要素に対する手続き ~~

    if dto.user_age < 5:
        raise ValueError("too young")
    if len(dto.user_name) > 20:
        raise ValueError("too long name")

    identifier = repo.new_id()
    repo_model = UserRepoModel(
        identifier=identifier,
        name=dto.user_name,
        age=dto.user_age,
        email=dto.user_email,
    )

    repo.save(repo_model)

ステップ3: 「重複しているから」という理由だけの共通化

ここで「ユーザ作成処理が重複している」と感じたエンジニアは、service_add_user という共通関数(サービス)にロジックをまとめることにした。

from dataclasses import dataclass

from my_app import UserRepoModel, is_writable, repo, request_ctx


@dataclass
class AddUserInput:
    name: str
    age: int
    email: str


def service_add_user(dto: AddUserInput) -> None:
    if dto.age < 5:
        raise ValueError("too young")
    if len(dto.name) > 20:
        raise ValueError("too long name")

    identifier = repo.new_id()
    repo_model = UserRepoModel(
        identifier=identifier,
        name=dto.name,
        age=dto.age,
        email=dto.email,
    )

    repo.save(repo_model)


@dataclass
class CreateUserInput:
    name: str
    age: int
    email: str


def usecase_create_user(dto: CreateUserInput) -> None:
    if not is_writable(request_ctx):
        raise PermissionError("permission denied")

    service_add_user(
        AddUserInput(
            name=dto.name,
            age=dto.age,
            email=dto.email,
        )
    )


@dataclass
class SetupDepartmentInput:
    # ~~ ユーザ以外の要素に関する入力値 ~~
    user_name: str
    user_age: int
    user_email: str


def usecase_setup_department(dto: SetupDepartmentInput) -> None:
    if not is_writable(request_ctx):
        raise PermissionError("permission denied")

    # ~~ ユーザ以外の要素に対する手続き ~~

    service_add_user(
        AddUserInput(
            name=dto.user_name,
            age=dto.user_age,
            email=dto.user_email,
        )
    )

ステップ4: 再利用志向が N+1 を生む

次に、新たなユースケース usecase_construct_all が追加される。
ここでは「複数ユーザを一括作成する」要件が加わる。

怠惰なエンジニアは、既存の service_add_user を再利用したくなる。

@dataclass
class ConstructAllInputUser:
    name: str
    age: int
    email: str


@dataclass
class ConstructAllInput:
    # ~~ ユーザ以外の要素に関する入力値 ~~
    users: list[ConstructAllInputUser]


def usecase_construct_all(dto: ConstructAllInput) -> None:
    if not is_writable(request_ctx):
        raise PermissionError("permission denied")

    # ~~ ユーザ以外の要素に対する手続き ~~

    for user in dto.users:
        service_add_user(
            AddUserInput(
                name=user.name,
                age=user.age,
                email=user.email,
            )
        )  #! ここでユーザ数分の I/O が発生する

元々 service_add_user は「1人のユーザを作る」ための手続きとして書かれたものであり、「多数ユーザを一括作成する」というユースケースを考慮して設計された抽象ではない。

しかし、共通化欲求が強すぎると、このような「抽象の再利用によるパフォーマンス劣化」を招きやすい。

ステップ5: オプショナルパラメータ地獄と責務の膨張

さらに新しい要件として、「メールアドレスだけで仮登録できる usecase_reserve_user」が追加される。

ここでもまた、「既存の service_add_user を拡張するだけで対応できるのでは?」という誘惑が生じる。

- @dataclass
- class AddUserInput:
-     name: str
-     age: int
-     email: str
-
-
- def service_add_user(dto: AddUserInput) -> None:
-     if dto.age < 5:
-         raise ValueError("too young")
-     if len(dto.name) > 20:
-         raise ValueError("too long name")
-
-     identifier = repo.new_id()
-     repo_model = UserRepoModel(
-         identifier=identifier,
-         name=dto.name,
-         age=dto.age,
-         email=dto.email,
-     )
-
-     repo.save(repo_model)

+ @dataclass
+ class AddUserInput:
+     name: str | None
+     age: int | None
+     email: str
+
+
+ def service_add_user(dto: AddUserInput) -> None:
+     #! 振る舞いが「パラメータの組み合わせ」に依存し始める
+
+     if dto.age is not None and dto.age < 5:
+         raise ValueError("too young")
+     if dto.name is not None and len(dto.name) > 20:
+         raise ValueError("too long name")
+
+     identifier = repo.new_id()
+     repo_model = UserRepoModel(
+         identifier=identifier,
+         name=dto.name if dto.name is not None else "",
+         age=dto.age if dto.age is not None else 20,
+         email=dto.email,
+     )
+
+     repo.save(repo_model)


@dataclass
class ReserveUserInput:
    email: str


def usecase_reserve_user(dto: ReserveUserInput) -> None:
    if not is_writable(request_ctx):
        raise PermissionError("permission denied")

    # どういう初期値で補完するかは「ドメインロジック」と見做して委譲する
    service_add_user(
        AddUserInput(
            name=None,
            age=None,
            email=dto.email,
        )
    )

この時点での問題を整理すると、次の通りとなる。

  • 「通常登録」と「仮登録」という 別ユースケース が、どちらも AddUserInput に押し込められている
  • name / age は必須なのか任意なのか、型定義からは読み取れない
  • 実際の振る舞いは 「パラメータの組み合わせ × 条件分岐 × デフォルト値」 に依存し、関数内部に隠蔽される

このまま新しいユースケースが増えていくと、

  • 既存のフラグやオプショナル引数を「まだ使えそう」と再利用しがちになる
  • そのたびに条件分岐が増え、挙動はさらに不透明化
  • 「触っていい範囲」と「触ると壊れる範囲」の境界が誰にも分からなくなる

という悪循環に陥る。

問題の本質は何か?

ここまでの例で起きていることをまとめると、次の通りである。

  • service_add_user守るべきルールを表す抽象にはなっていない

  • 代わりに、複数のユースケースをまとめた “なんでも入り処理関数” になっている

  • つまり、DRY の名のもとにやっているのは

    • ✅ ビジネスルールの重複をなくすことではなく
    • ❌ ユースケースの違いを無視して1箇所に押し込めること(lumping)

本来 DRY を適用すべきなのは、

  • 「ユーザ名は20文字以内」
  • 「年齢は5歳以上」
  • 「予約ユーザと通常ユーザの両方で共通する、データとしての整合性」

といった 守るべきルール(不変条件)そのもの であって、

  • 「どのユースケースからどの順序で呼ばれるか」
  • 「不要なときも含めて処理を1つにまとめるか」

といった 手続き・段取り(オーケストレーション) に対してではない。

どう設計し直すべきか? ─ ルールと手順の分離

では、何をどこまで共通化すべきか。

ここでは、

  • データの整合性を担保する構造体(ドメインモデル)
  • ユースケースごとの手順(ワークフロー)

を分離することで、設計を立て直してみる。

ルールを集約した User クラスの導入

from dataclasses import dataclass
from operator import xor

from my_app import is_writable, repo, request_ctx


class User:
    """
    ユーザに関する「守るべきルール(不変条件)」を集約した構造体。
    このインスタンスが存在する限り、データは常に正しい状態であることを保証する。
    """
    DEFAULT_NAME = ""
    DEFAULT_AGE = 20

    def __init__(
        self,
        identifier: int,
        name: str | None,
        age: int | None,
        email: str,
    ):
        # name/age は「両方ある」か「両方ない」かのどちらか
        # ※ バリデーションロジックはここに集める
        if xor(name is None, age is None):
            raise ValueError("entries must include both name and age or omit both")

        self._is_reserved: bool = (name is None) and (age is None)

        if age is not None and age < 5:
            raise ValueError("too young")
        if name is not None and len(name) > 20:
            raise ValueError("too long name")

        self._identifier: int = identifier
        self._name: str = name if name is not None else self.DEFAULT_NAME
        self._age: int = age if age is not None else self.DEFAULT_AGE
        self._email: str = email

    # --- 生成メソッド(ファクトリ) ---
    # コンストラクタを直接呼ぶ代わりに、目的別の生成メソッドを用意する

    @classmethod
    def create_regular(cls, identifier: int, name: str, age: int, email: str) -> "User":
        """通常登録ルート: 名前と年齢が必須"""
        return cls(identifier=identifier, name=name, age=age, email=email)

    @classmethod
    def create_reserved(cls, identifier: int, email: str) -> "User":
        """仮登録ルート: メールアドレスのみで作成"""
        return cls(identifier=identifier, name=None, age=None, email=email)

    # --- プロパティ ---

    @property
    def is_reserved(self) -> bool:
        return self._is_reserved

    @property
    def identifier(self) -> int:
        return self._identifier

    @property
    def name(self) -> str:
        return self._name

    @property
    def age(self) -> int:
        return self._age

    @property
    def email(self) -> str:
        return self._email

ここでは、

  • 「通常登録」「仮登録」の違いを 生成メソッドで明示
  • どのパスから作られても、構造として不正な状態にならない ことをコンストラクタで保証

している。ここに DRY を適用するのは妥当であり、知識(ユーザの整合性ルール)の唯一の表現として機能する。

ユースケースは「手順の管理」に徹する

一方、ユースケース層では、共通関数を作ろうと頑張るのではなく、ユースケースごとの手順を素直にフラットに書くことに徹する。

@dataclass
class CreateUserInput:
    name: str
    age: int
    email: str


def usecase_create_user(dto: CreateUserInput) -> None:
    if not is_writable(request_ctx):
        raise PermissionError("permission denied")

    user_id = repo.new_id()
    # 通常登録用のメソッドを使う
    user = User.create_regular(
        identifier=user_id,
        name=dto.name,
        age=dto.age,
        email=dto.email,
    )
    repo.save(user)
@dataclass
class SetupDepartmentInput:
    # ~~ ユーザ以外の要素に関する入力値 ~~
    user_name: str
    user_age: int
    user_email: str


def usecase_setup_department(dto: SetupDepartmentInput) -> None:
    if not is_writable(request_ctx):
        raise PermissionError("permission denied")

    # ~~ ユーザ以外の要素に対する手続き ~~

    user_id = repo.new_id()
    # ここでも通常登録用のメソッドを使う
    user = User.create_regular(
        identifier=user_id,
        name=dto.user_name,
        age=dto.user_age,
        email=dto.user_email,
    )
    repo.save(user)
@dataclass
class ConstructAllInputUser:
    name: str
    age: int
    email: str


@dataclass
class ConstructAllInput:
    # ~~ ユーザ以外の要素に関する入力値 ~~
    users: list[ConstructAllInputUser]


def usecase_construct_all(dto: ConstructAllInput) -> None:
    if not is_writable(request_ctx):
        raise PermissionError("permission denied")

    # ~~ ユーザ以外の要素に対する手続き ~~

    user_ids = repo.new_ids(len(dto.users))
    users: list[User] = [
        # ループ内でも安全に生成できる
        User.create_regular(
            identifier=user_ids[i],
            name=d.name,
            age=d.age,
            email=d.email,
        )
        for i, d in enumerate(dto.users)
    ]
    repo.bulk_save(users)  # ユースケース固有の I/O 最適化が可能
@dataclass
class ReserveUserInput:
    email: str


def usecase_reserve_user(dto: ReserveUserInput) -> None:
    if not is_writable(request_ctx):
        raise PermissionError("permission denied")

    user_id = repo.new_id()
    # 仮登録用のメソッドを使う
    user = User.create_reserved(
        identifier=user_id,
        email=dto.email,
    )
    repo.save(user)

ここで行っていることは次の通りである。

  • ルールのチェック・状態の正当性は User クラスに集中
  • ユースケース層では「どのルートで、何人分、どのような粒度で I/O するか」を素直に書く
  • 一括作成ユースケースでは、必要に応じて bulk_save などの最適化を行える

結果として、

  • DRY の対象は「ユーザの整合性ルール」
  • 共通化しない(あえてボイラープレートを許容する)のは「ユースケースごとの手順」

という分割が実現される。

抽象化を導入してよいかを判断するチェックリスト

実務で「これ共通化したほうがよくない?」となったときに役立つよう、簡単なチェックリストを挙げておく。

1. その共通化は「知識」を一意にするか?

  • 単にコードが似ているだけでなく、
  • ドメインルールや整合性チェックを一箇所に集約しているか?

もし「ユースケースの順番や粒度」が似ているだけなら、それは DRY の対象ではない可能性が高い。

2. その関数の責務はユースケースに引きずられていないか?

  • 「通常登録」「仮登録」「一括登録」など、
    明らかにユースケースが違うものを同じ関数に押し込んでいないか?
  • 新しいユースケースを追加したときに、その関数が壊れそうにならないか?

3. 「データの整合性を守る構造体」として切り出せるか?

  • その共通処理は、データの正しさを保証するためのものか?
  • もしそうなら、手続きの一部として書くのではなく、ルールを持った構造体 として独立させられないか?

4. 共通化によってパフォーマンス制約を固定化していないか?

  • 「1件処理する関数」を共通化した結果、バルク処理の最適化余地を消していないか?
  • 逆に、バルク処理の都合を強制したせいで、単発ユースケースで過剰なオーバーヘッドが発生していないか?

5. 変更頻度が高い部分を無理に一箇所に寄せていないか?

  • ビジネスルールとして安定していそうな部分だけを共通化できているか?
  • 仕様が変わりやすいユースケース単位の手順は、しばらくは重複を許容したほうが楽ではないか?

まとめ

DRY は「同じコードを 1 箇所にまとめる」ための原則ではなく、同じ知識・ビジネスルールを多重化しないための指針である。したがって、本来は別々に進化すべきユースケースを、見た目の類似だけでひとつの関数に押し込めてしまうと、

  • パフォーマンス最適化の余地が消える
  • 関数の責務が肥大化する
  • オプショナル引数や条件分岐が増え、API が不透明になる

といった問題に直結する。

共通化の対象にすべきなのは、整合性ルールを一意に担保するための純粋な構造体 であり、

  • どの操作をどの順序で呼ぶか
  • どのユースケースからどう使うか

といった 手順(オーケストレーション) は、当面は多少の重複を許容して個別に書く方が安全なことが多い。

特に、未知の多い初期フェーズでは、恣意的にすぐリファクタリングに走るのではなく、まずユースケースごとの手順を素直に記述しながらフラットなコードベースを育てることに徹し、あとで俯瞰することで初めて得られる「より尤もらしい洞察」 をもとに、

  • 何が純粋かつ不変なルールなのか
  • 何がユースケース依存で変わりやすいのか

を見極めてから、ボトムアップ的に共通化を進めていく戦略を取る方が、結果的に設計の健全性と開発効率の両方を最大化しやすくなると言える。

Appendix: AI-assisted な開発においては、型安全な境界設計はより重要である

※本節で述べる内容は、特定の学術論文に直接依拠するものではなく、Copilot をはじめとする LLM ベースの開発支援ツールを実務で利用するなかで広く共有されつつある観測と経験則に基づくものである。

近年の AI-assisted な開発環境では、コード生成・補完・変換が従来より圧倒的に高速化し、「とりあえず動くコードを書く」こと自体のコストはほぼゼロに近づきつつある。しかしその一方で、抽象境界が曖昧なコードベースは、AI による支援との相性が著しく悪い という特性も認知されつつある。この解釈は、特に型安全性と契約駆動を重視する弊社においても概ね一致するものだった。これは単に「AI に読みにくいコードはダメ」という話ではなく、もっと構造的・本質的な問題なのである。

以下では、この記事で扱ったような「DRY の誤用」「曖昧なインターフェース」「意味の異なるユースケースの lumping」が、AI-assisted 開発においてどのように作用するかについて考察する。

1. AI のコード生成は「型と境界」を手がかりとして推論する

大規模言語モデル(LLM)は、次のような情報を手がかりにコードの意味を推論している。

  • 関数やメソッドの引数の型
  • データ構造の形状
  • インターフェースの粒度
  • 責務の境界(命名・構造)
  • 例外条件・検証ロジック
  • 関連するユースケースのパターン

このため、

  • ❌ オプショナルだらけの入力データ定義
  • ❌ ユースケースを無理やりまとめた巨大な関数
  • ❌ 前提が暗黙化した共通化

などは、AI が step by step で推論するための手掛かりを失わせる

結果として、

  • LLM が誤った前提でコードを生成する
  • リファクタリング提案の品質が著しく落ちる
  • 新機能追加時に一貫性のないパッチが提示される

といった問題が頻発する。

これは LLM の性能ではなく、設計の品質が AI 推論可能性に直結していることを示している。

2. AI は「明確なルール」を理解しやすい。逆に「条件分岐の寄せ集め」は理解しにくい

前述の User クラスのように、ルールとデータをひとまとまりの構造体として閉じ込めた設計は、AI と非常に相性が良い。

  • User.create_regular() → 通常登録
  • User.create_reserved() → 仮登録
  • コンストラクタがどの状態を許容し、どれを許容しないか

これらが構造として明示されていると、AI は次のような推論を非常に正確に行える。

  • 新しいユースケースでどの生成メソッドを使えばよいか
  • バリデーションをどこに書けばよいか
  • 保存前にどんな状態遷移が必要か
  • どの境界をまたぐと意味が壊れるか

逆に、以下のような関数は AI にとって「解釈不能な塊(opaque blob)」となる。

  • オプション引数の組み合わせ次第で挙動が変わる
  • 暗黙のデフォルトが多数ある
  • モードフラグにより複数ユースケースを兼ねてしまう

結果として、AI の生成物が破綻しやすくなる。

3. AI-assisted なリファクタリングは「型の境界」単位で作用する

AI はリファクタリングや構造変換が得意だが、その作用単位は次のような“境界”である。

  • 型(クラス、データ構造)
  • メソッドの定義(シグネチャ)
  • レイヤー境界(手続きとデータ)
  • 依存の方向性

これらが明瞭であるほど、AI のリファクタリング精度は劇的に上がる。

逆に境界が曖昧だと、AI は次のような誤解をする。

  • 本来別ユースケースなのに「統合した方がよい」と誤提案
  • 逆に、実際は不変条件なのに「ユースケースごとに分離した方がよい」と誤提案
  • バリデーションの場所を誤る
  • デフォルト値を推論できず壊れたコードを生成する

つまり、設計の曖昧さは、そのまま「AI が壊すリスク」になる

4. 境界が明確だと、AI との協調作業が「加速」する

  • 構造体が明確なルールを持っている
  • ユースケースが分離されている
  • 入力データ定義がユースケース粒度ごとに分かれている
  • 共通化が最小限に収まっている

こうしたコードベースでは、AI は次のように働く。

  • 既存構造に沿った拡張提案が極めて正確
  • 新ユースケースを自動生成しやすい
  • I/O 最適化の提案がしやすい
  • シリアライザ・API スキーマを自動派生しやすい
  • 変更の影響箇所を極めて正確に推論する

すなわち AI の性能が最大化される

抽象化のミスや境界の曖昧さは、“人間にとっての可読性” を損なうだけでなく、
AI にとっての推論可能性を奪うため、開発速度そのものに影響を与える。

5. 「AI にも読みやすい設計」がより重要に

AI とペアプロする時代では、

  • DRY の誤用を避けること
  • ユースケースを安易に lumping しないこと
  • ルールを構造体として表現すること
  • 型を境界として扱うこと

これらが人間・マシンの両方にとっての開発効率を守る設計原則になる

すでに GitHub Copilot や他の LLM-based IDE に触れている人なら分かるはずだが、

これはもはや理論ではなく、実感として広く共有されつつある。

型安全な境界設計は、AI 時代の「可読性」であり「最適化点」である

DRY の誤用による「曖昧な抽象」は、
人間にとっても AI にとっても可読性・推論可能性を下げる。

逆に、ルールを軸にした明確な境界設計は、

  • AIのコード生成を正確にし
  • 自動リファクタリングの成功率を上げ
  • 安全な変更を容易にし
  • チームの開発速度を安定化させる

という明確なメリットがある。

この記事で扱ったような「DRY の誤用を避けるための原則」は、
AI-assisted な開発でも威力を発揮する普遍的な設計原則 のひとつと言えるだろう。

最後に: AI時代の「速さ」とは、コードを書く速度ではなく、変わり続ける力のこと

ソースコードを書くコストは劇的に下がった。事業を構成する要素のうち「ソフトウェアがカバーする領域」は日々拡大を続け、求められる要件の水準が高度になっていく一方、その開発サイクルも爆発的に加速し、機能リリースのスピード競争は激化の一途をたどっている。

この「加速」には罠がある。 AI を使えば、構造の崩れたコードも、誤った共通化も、これまでとは比較にならない速度で量産できてしまう。それは言わば、技術的負債をマッハで積み上げられる時代の到来とも言える。

そんな、事業環境がめまぐるしく変化する現在において、真に競争力を持つのは「最初にコードを書き殴ったチーム」ではない。市場のフィードバックを受け、仕様変更に耐え、システムを破壊することなく安全に進化させ続けられる(Modifiability を持つ)チームである。

今回紹介した「ルールを守る構造体」や「責務の分離」といった設計は、ドメイン駆動設計(DDD) における 戦術的設計 の一部ではあるが、これらは決して高尚な理論のためにあるのではない。これらは、AI による爆発的なコード増殖のなかで、「変更容易性」という最大の資産を死守するための、極めて実利的な防衛手段である。

ビジネスのコアをコードに反映させ、変更に強い構造を作る「戦略」を持つこと。これこそが、AI という強力な増幅器を積んだ我々が、壁に激突せずにトップスピードで走り続けるための唯一の条件なのかもしれないと思う今日この頃。

Discussion