公開日 2026-04-16

「動いた」で終わるコードと、「また触れる」コードの違い

「動いた」で終わるコードと「また触れる」コードの差を、命名・副作用・失敗時の見え方から整理する。

目次

  1. 1. 前提と結論
  2. 2. 「動いた」が意味する範囲と限界
  3. 3. 名前と役割が見えるか
  4. 4. 入力と出力、副作用の場所が見えるか
  5. 5. 失敗したときの見え方があるか
  6. 6. テスト・静的解析・CI はどこを助けるのか
  7. 7. 「また触れるコードか」の確認リスト
  8. 8. まとめ

対象読者: コードを最後まで動かした経験はあるが、数日後に見返すと触りにくさを感じる人、AI 生成コードやサンプルコードをつなぎながら実装している人

「動いたコード」と「また触れるコード」の差を整理する。ここで言う「また触れる」は、きれいとか高度な設計という意味ではなく、数日後の自分や別の人が次の変更でどこを見ればよいか分かる状態を指す。

この記事で扱わないもの:

  • DDD や設計パターンの詳説
  • フレームワーク別ベストプラクティス
  • 大規模リファクタリング手順
  • 包括的なテスト入門

1. 前提と結論

「また触れるコード」は、次の変更でどこを見ればよいかが見えるコード。 「動いたコード」は、その時点で期待した結果が出たことしか保証していない。

「動いた」こと自体は悪くない。最初の 1 本を最後まで作った経験には大きな意味がある。

ただ、実務や継続開発では、その次で困りやすい。

  • どこを直せばよいか分からない
  • 何を壊しそうか想像しにくい
  • エラー時の挙動が読めない
  • 保存や送信の順序が見えない

ここで見たいのは、「コードを難しくする話」ではなく「次の変更を軽くする話」だ。

2. 「動いた」が意味する範囲と限界

「動いた」が意味するのは、ひとまず次のようなことだ。

  • いまの入力では期待どおりに見えた
  • ひとまず画面に結果が出た
  • その場ではエラーにならなかった

ここには、次の保証は入っていない。

  • 数週間後に仕様が変わっても直しやすいこと
  • 失敗時に何が起こるかが分かること
  • DB 保存や通知送信の順序が安全なこと
  • 壊したときにすぐ気づけること

学習用コードなら、その場で動くことに意味がある。 ただ、少しでも育てる前提があるなら、「動いた」の先に何が必要かを知っておくと楽になりやすい。

3. 名前と役割が見えるか

まず差が出やすいのは、名前と役割の見え方。 お問い合わせフォーム送信処理を例にすると、「動いた」だけのコードはこうなりやすい。 例を読みやすくするために declare(strict_types=1) や interface は残しているが、問題にしたいのは見た目の整い方ではなく、役割の混ざり方にある。

<?php

declare(strict_types=1);

interface InquiryRepository
{
    public function save(array $payload): void;
}

interface Mailer
{
    public function send(string $to, string $subject, string $body): void;
}

final class InquirySubmitActionBefore
{
    public function __construct(
        private InquiryRepository $repository,
        private Mailer $mailer,
    ) {
    }

    public function __invoke(array $request): array
    {
        $name = trim((string) ($request['name'] ?? ''));
        $email = trim((string) ($request['email'] ?? ''));
        $body = trim((string) ($request['body'] ?? ''));

        if ($name === '' || $email === '' || $body === '') {
            return ['ok' => false, 'message' => 'invalid'];
        }

        $payload = [
            'name' => $name,
            'email' => $email,
            'body' => $body,
        ];

        $this->repository->save($payload);
        $this->mailer->send($email, 'received', $body);

        return ['ok' => true, 'message' => 'saved'];
    }
}

このコードは、たしかに動きそうだ。 でも、次の変更では困りやすくなる。

  • バリデーションをどこで変えるか分かりにくい
  • 保存とメール送信が同じ場所に混ざっている
  • message の文字列だけで失敗理由を扱っている
  • __invoke() が何を担当しているのか広すぎる

長いから悪いのではない。 「どこを直せばよいか」が名前から見えないことが、つらさの中心にある。

4. 入力と出力、副作用の場所が見えるか

同じ題材でも、役割と副作用の場所を少し分けるだけで、次に触るときの見通しが変わる。

<?php

declare(strict_types=1);

final class InquirySubmissionInput
{
    public function __construct(
        public readonly string $name,
        public readonly string $email,
        public readonly string $body,
    ) {
    }

    public static function fromArray(array $request): self
    {
        return new self(
            name: trim((string) ($request['name'] ?? '')),
            email: trim((string) ($request['email'] ?? '')),
            body: trim((string) ($request['body'] ?? '')),
        );
    }

    public function isValid(): bool
    {
        return $this->name !== '' && $this->email !== '' && $this->body !== '';
    }
}

final class InquirySubmissionResult
{
    private function __construct(
        public readonly bool $ok,
        public readonly string $message,
    ) {
    }

    public static function success(): self
    {
        return new self(true, 'saved');
    }

    public static function invalid(): self
    {
        return new self(false, 'invalid');
    }
}

interface InquiryRepository
{
    public function save(InquirySubmissionInput $input): void;
}

interface Mailer
{
    public function send(string $to, string $subject, string $body): void;
}

final class InquirySubmissionService
{
    public function __construct(
        private InquiryRepository $repository,
        private Mailer $mailer,
    ) {
    }

    public function submit(InquirySubmissionInput $input): InquirySubmissionResult
    {
        if (! $input->isValid()) {
            return InquirySubmissionResult::invalid();
        }

        $this->repository->save($input);
        $this->mailer->send($input->email, 'received', $input->body);

        return InquirySubmissionResult::success();
    }
}

この形でも複雑な設計はしていない。 それでも、次の変更で見る場所ははっきりする。

  • 入力の組み立ては InquirySubmissionInput
  • バリデーションの入口は isValid()
  • 保存と通知は InquirySubmissionService
  • 呼び出し結果は InquirySubmissionResult

「また触れる」コードとは、こういうふうに「どこで何が起こるか」が見える状態を指す。

5. 失敗したときの見え方があるか

次に見たいのは、失敗時の見え方。 成功する経路だけなら、多少雑でも動いて見える。

実際に困りやすいのはこういう場面だ。

  • 入力が足りない
  • 保存には成功したが通知に失敗した
  • 同じ送信が二重に実行された
  • 呼び出し側が失敗理由を分からない

失敗時の見え方を少しだけ足すと、たとえば次のようになる。

<?php

declare(strict_types=1);

final class InquirySubmissionResult
{
    private function __construct(
        public readonly bool $ok,
        public readonly string $message,
    ) {
    }

    public static function success(): self
    {
        return new self(true, 'saved');
    }

    public static function invalid(): self
    {
        return new self(false, 'invalid');
    }

    public static function deliveryFailed(): self
    {
        return new self(false, 'delivery_failed');
    }
}

interface InquiryRepository
{
    public function save(InquirySubmissionInput $input): void;
}

interface Mailer
{
    public function send(string $to, string $subject, string $body): void;
}

final class InquirySubmissionServiceWithFailureVisibility
{
    public function __construct(
        private InquiryRepository $repository,
        private Mailer $mailer,
    ) {
    }

    public function submit(InquirySubmissionInput $input): InquirySubmissionResult
    {
        if (! $input->isValid()) {
            return InquirySubmissionResult::invalid();
        }

        $this->repository->save($input);

        try {
            $this->mailer->send($input->email, 'received', $input->body);
        } catch (\Throwable) {
            return InquirySubmissionResult::deliveryFailed();
        }

        return InquirySubmissionResult::success();
    }
}

この例でも完璧ではないが、少なくとも「失敗しても saved しか返ってこない」状態は避けられる。 呼び出し側から見て、どこで何が起きたかを追えるのは大きな差になる。

前の InquirySubmitActionBefore だと、失敗の情報は invalid という文字列だけだった。 入力が足りないのか、保存に失敗したのか、通知に失敗したのかが見えない。

「また触れる」状態では、少なくとも次が見えたほうが楽になる。

  • どこで失敗する可能性があるか
  • 失敗したら何が返るか
  • 呼び出し側が次に何を判断できるか

ここが見えないコードは、動いていても次の修正で重くなりやすい。

6. テスト・静的解析・CI はどこを助けるのか

「また触れる」状態を支えるのは設計だけではない。 壊したときに気づける仕組みも要る。

仕組み助けること
PHPUnit期待している振る舞いが崩れたときに気づける
PHPStan明らかな型のずれや危ない値の扱いに気づける
GitHub Actions手元では通っていた前提が CI でも崩れていないか確認できる

ツールが「また触れる状態そのもの」を作るわけではない。 名前、責務、入出力、副作用、失敗時の見え方がまずあり、そのうえでツールが「壊したら気づける」部分を支える。

この先を深掘りするなら、次の記事が参考になる。

7. 「また触れるコードか」の確認リスト

最後に、自己確認用の短いリストを置いておく。 全部を一気に直す前提ではなく、次の 1 歩を決めるための質問になる。

質問Yes なら見えているもの
名前から役割が分かるかどこを直すか見つけやすい
入力と出力が追えるか値の流れを追いやすい
副作用の場所が分かるか保存、送信、外部呼び出しの影響範囲が見える
失敗したときの見え方があるか呼び出し側で次の判断がしやすい
壊したときに気づけるかテストや解析の意味が出てくる

1 つでも No があるなら、その観点が次の改善候補になる。 「全部だめ」と考える必要はない。 いちばん困っているところから直すだけでも、次の変更は軽くなる。

8. まとめ

「動いた」コードは、その時点で価値がある。 ただ、少しでも育てる前提があるなら、そこだけでは足りなくなる。

「また触れる」コードとは、難しい設計を入れたコードではない。次の変更でどこを見ればよいかが分かるコードを指す。 名前、責務、入出力、副作用、失敗時の見え方が見えるようになると、コードは触りやすくなる。

そのうえで、テストや静的解析、CI が「壊したときに気づける状態」を支える。 完璧な構成より、まず次の変更で困らない状態を目指せばいい。