そのクラス、「将来使うかも」で作ったやつ、1年後に誰も触れてなくない?😶

そのクラス、「将来使うかも」で作ったやつ、1年後に誰も触れてなくない?😶

これ絶対あるじゃん??これ絶対あるじゃん??

  • PRを開いたら、今のチケットと関係ない汎用クラスが3つ追加されてた
  • AbstractBaseNotificationHandlerFactoryみたいな名前のクラスが、リリースから一度もインスタンス化されてない
  • 「将来複数の〇〇に対応するかもしれないから」で、設計が2段階ネストした記憶がある
  • 新メンバーに「このクラス、どこで使ってるんですか?」って聞かれて「えっと…」ってなった

これがPremature Abstraction(早すぎる抽象化)、以降「先読み設計」って呼ぶね、が原因なんだよね。


てかシンプルに言うとねてかシンプルに言うとね

先読み設計とは、まだ誰にも頼まれてないのに、「いつか使うかも」で今のコードを複雑にしちゃうことなんだよね。

それの何がまずいの?ってなる気持ち、わかる!「設計力があって先見の明がある証拠じゃん」って思うよね。でも実際には、予測した未来が来ないまま「読めない・触れない・消せない」謎コードだけが残るっつーこと。消そうとしても「何かに使ってたらどうしよう」って怖くて消せないし、テストを書こうとしても対象が曖昧でどこから手をつければいいかわからなくなる。良かれと思って作ったやつが、静かな地雷になるんだよね。


ちょっと待って、先に全然関係ない話していい?🏠ちょっと待って、先に全然関係ない話していい?🏠

2年前の話。

社会人3年目の結菜さんは、将来のことを真剣に考えてたんだよね。「いつか子供が3人できたらいいな」「親の介護もするかもしれない」「友達が泊まりにくることもある」——そう考えた結菜さん、まだ結婚もしてないのに都内6LDK、月35万の物件を契約したの。マジで。

最初の1年、6部屋のうち使ってるのはベッドルームとリビングだけ。残り4部屋には段ボールが積まれた。

「ゆうと、この部屋どうしようかな〜」
「将来子供部屋にするんじゃなかったの?」
「でもまだじゃん……」

「将来のためって言ったけど、今この家賃、正直しんどい」

半年後、彼氏と別れた。3年後、転勤が決まった。6LDKは使いきれないまま引っ越し。払い続けた家賃の累計、約1,260万円。

使わなかった4部屋のために、結菜さんは何百万も払い続けてたんだよね。えぐくない?

これが本番で起きてるやつが、先読み設計ってこと。


じゃあコードで見てみよっか👀じゃあコードで見てみよっか👀

最初の要件は「ユーザーにウェルカムメールを送る」だけだった。でも実装者の松本くんは考えた。「将来SMSも送るかもしれない」「Slackにも通知するかも」「海外展開したら多言語も…」

# 「将来使うかもしれない」を詰め込んだ先読み設計の例

from abc import ABC, abstractmethod


class AbstractNotificationChannel(ABC):
    """将来の拡張を見越した汎用通知チャネル基底クラス"""

    @abstractmethod
    def send(self, recipient: str, content: str, locale: str = "ja") -> bool:
        pass

    @abstractmethod
    def validate_recipient(self, recipient: str) -> bool:
        pass

    @abstractmethod
    def format_content(self, content: str, locale: str) -> str:
        pass


class NotificationTemplateEngine:
    """将来の多言語対応を見越したテンプレートエンジン(現在はjaのみ)"""

    def __init__(self, locale: str = "ja"):
        self.locale = locale
        self.templates = {
            "ja": {"welcome": "ようこそ、{name}さん!"},
            "en": {"welcome": "TODO"},  # 未実装
            "zh": {"welcome": "TODO"},  # 未実装
        }

    def render(self, template_key: str, **kwargs) -> str:
        template = self.templates.get(self.locale, {}).get(template_key, "")
        return template.format(**kwargs)


class NotificationFactory:
    """将来複数チャネルを切り替えられるようにしたファクトリ(現在はEmailのみ)"""

    @staticmethod
    def create(channel_type: str) -> AbstractNotificationChannel:
        if channel_type == "email":
            return EmailNotificationChannel()
        # "sms", "slack", "push" は未実装
        raise NotImplementedError(f"{channel_type} は未実装")


class EmailNotificationChannel(AbstractNotificationChannel):

    def validate_recipient(self, recipient: str) -> bool:
        return "@" in recipient

    def format_content(self, content: str, locale: str) -> str:
        return content

    def send(self, recipient: str, content: str, locale: str = "ja") -> bool:
        if not self.validate_recipient(recipient):
            return False
        print(f"メール送信: {recipient} -> {content}")
        return True


# 実際に呼び出す箇所
def send_welcome_email(user_email: str, user_name: str):
    engine = NotificationTemplateEngine(locale="ja")
    content = engine.render("welcome", name=user_name)
    channel = NotificationFactory.create("email")
    channel.send(user_email, content)

新メンバーの小林さんが入社して、ウェルカムメール機能の修正を頼まれた。

「え、メール1本送るだけなのに……なんでファクトリが要るの?」
AbstractNotificationChannelって何?」
enzh、TODOのままだけど、これどこかで使ってる?」

「ドキュメントもないし、何を消したら壊れるかわからん」 と小林さんは頭を抱えた。

松本くんに聞いたら「将来使うかもって思って」——SMSもSlackも多言語も、2年経った今もまだ来てない。

わたしも昔これやったから言えるんだけど、「将来のための設計」って書いてるとき、めちゃくちゃ気持ちいいんだよね。その気持ちよさ、1年後に後任が味わう混乱で全部返ってくる 💔


やばいのまだあんだけど、バグに気づけない問題😭やばいのまだあんだけど、バグに気づけない問題😭

「コードが複雑なだけじゃん」って思うかもしれないけど、先読み設計のほんとにやばいところは、バグがあっても気づけない構造になること。

結菜さんの話で言うと——6LDKの部屋が6つあって、どの部屋も「将来使う予定」で放置してたら、どっかでカビが生えても気づかないじゃん?「使ってない部屋だから」って確認もしないし、見に行く人もいない。

コードも同じ。

# テストしようとすると、こうなる

def test_send_welcome_email():
    # どのチャネルのテスト?EmailのみなのにFactoryを経由する必要ある?
    # localeはja固定?それともja/en/zh全部テストすべき?
    # validate_recipient と format_content と send、それぞれ独立してテストするの?
    # AbstractNotificationChannelの実装クラスが増えたらテストは何個になる?

    # channel_type × locale × template_key × recipient_pattern の組み合わせ数:
    # 今は  1 × 1 × 1 × 2  =  2通り
    # SMS追加 → 2 × 3 × 2 × 2 = 24通り
    # Slack追加 → 3 × 3 × 2 × 2 = 36通り

    pass

「将来使うかも」のコードは、テストの組み合わせだけじゃなくてテストを書く気力まで奪う。「この抽象クラス、今のテストで本当にカバーできてる?」ってわからないまま、テストが薄いコードが積まれていく。

先読み設計は壊れやすいだけじゃなく、バグを隠す設計でもある。


分けてた会社の話もするね〜✨分けてた会社の話もするね〜✨

同じ時期、別の会社にいた大輔さんチームも同じ要件を受けた。「ユーザーにウェルカムメールを送る」。

「今の要件、ウェルカムメールだけだよね?」
「そうそう、それだけ」
「じゃあそれだけ作ろうか」

# 今必要なことだけを書いたコード

import smtplib
from email.message import EmailMessage


def send_welcome_email(user_email: str, user_name: str) -> None:
    """ウェルカムメールを送信する"""
    msg = EmailMessage()
    msg["Subject"] = "ようこそ!"
    msg["From"] = "noreply@example.com"
    msg["To"] = user_email
    msg.set_content(f"ようこそ、{user_name}さん!")

    with smtplib.SMTP("smtp.example.com") as smtp:
        smtp.send_message(msg)

これだけ。抽象クラスもファクトリもテンプレートエンジンもない。

テストも一瞬で書ける。

from unittest.mock import patch, MagicMock


def test_send_welcome_email():
    with patch("smtplib.SMTP") as mock_smtp_class:
        mock_smtp = MagicMock()
        mock_smtp_class.return_value.__enter__.return_value = mock_smtp

        send_welcome_email("user@example.com", "田中")

        # 送信されたか確認するだけ、それだけでいい
        mock_smtp.send_message.assert_called_once()

半年後、「SMS通知も追加したい」という要件が来た。

「あー、じゃあsend_sms_notification関数を別で作ろうか。メールと分けて管理したほうがシンプルじゃん」
「え、共通化しなくていいの?」
「今の時点では共通化する理由ないじゃん。必要になったらやろう」

大輔さんのチームは定時で対応完了した。松本くんのチームは「抽象クラスの設計ごと見直しが必要で〜」って1週間かかった。


ちゃんとした名前もあるから一応言うね📚ちゃんとした名前もあるから一応言うね📚

YAGNI(You Aren't Gonna Need It)とは、「今必要でないものは実装するな」っつーこと

1990年代にKent Beckって人が言い出した考え方で、「将来使いそう」は作る理由にならない——今の要件だけが根拠、っていうスタンスなんだよね。

Martin Fowlerも「YAGNI破ると4つのコストがかかる」って言ってて。作るコスト、遅延コスト(他を先に作れなかった分)、維持するコスト、修正コスト。全部ムダになるやつ。

難しそうに聞こえるけど、要は「それ、今必要?」って問い続けるだけなんだよね。


これだけ覚えて帰って👌これだけ覚えて帰って👌

合言葉はこれ。smm(先読みマジムダ)

コードを書くとき・レビューするとき、この問いを使って。

この抽象化が必要になる「出来事」って、具体的に何?

「将来SMSにも対応するかもしれない」は出来事じゃない。

出来事は「来週のスプリントでSMS通知のチケットが積まれた」「プロダクトオーナーから多言語対応の要件が正式に出た」——これ。

「かもしれない」が3個並んだとき、ほぼ先読み設計が始まってるサインなんだよね。確率10%の3つの未来のために、今のコードが確実に複雑になる。結菜さんが「子供が3人できるかも・親の介護かも・友達が泊まるかも」の3つのために6LDKを借りたのと、まったく同じことが起きてるってこと。

「出来事」が言葉にできないなら、その抽象化はまだ作るタイミングじゃないっつーこと。


まとめるとこういうことねまとめるとこういうことね

先読み設計(アンチパターン)YAGNIに従った設計
実装の判断基準「将来使うかもしれない」「今この要件が存在する」
コードの複雑さ使われない抽象化・ファクトリが増える今の要件に必要な分だけ書く
新メンバーの理解コスト「これ何に使うんですか?」が発生するコードと要件が1対1で対応している
変更への対応速度既存の設計ごと見直しが必要になる必要になったタイミングでシンプルに追加できる
バグの発見しやすさテスト対象が曖昧で網羅が難しいテスト対象が明確で迷わず書ける

「設計力がある人ほど、今必要なことだけを書く」——それがYAGNIなんだよね。


沼りたい人はこっちも読んで📖沼りたい人はこっちも読んで📖

権威ある書籍

  • Kent Beck『エクストリームプログラミング』(初版1999年 / 第2版2004年)——YAGNIの提唱元。XPのシンプルデザイン原則としてYAGNIが定義されている原著。

一次資料

  • Martin Fowler「Yagni」https://martinfowler.com/bliki/Yagni.html ——YAGNIに違反したときの4種類のコスト(cost of build・cost of delay・cost of carry・cost of repair)を論理的に整理した記事。記事内の概念の直接の出典。

コミュニティ資料

  • 「YouArentGonnaNeedIt」- C2 Wiki https://c2.com/xp/YouArentGonnaNeedIt.html ——Ward CunninghamのXPコミュニティウィキ。YAGNIの定義「Always implement things when you actually need them, never when you just foresee that you need them.」が最初に記録されている場所。

Dev-Here「ギャルでも分かる設計の勘ドコロ!」シリーズ 第7回