2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

第2話 UIに責任を詰め込むな──設計を“読める”形に変えるまで

Last updated at Posted at 2025-05-06

前回までの記事

とりあえずコードを整理してみたつもりだった

AIからもらったコードをそのまま使っていた若手は、
前回の反省を踏まえて、自力でリファクタリングに取り組んだ。

変数名を見直し、関数を分け、処理の順番も少し変えた。
とくにUIが煩雑だったので、「画面の中で状態も処理も完結させた」つもりだった。

師匠に画面を見せながら、少し誇らしげに言う。

若手:「とりあえず、整理してみました。UIの中でひと通り完結するようにしてあります」

修正前コード

import { useState } from 'react';

function App() {
  const [text, setText] = useState('');
  const [todos, setTodos] = useState<string[]>([]);

  const handleAdd = () => {
    if (text.trim() === '') return;
    setTodos([...todos, text]);
    setText('');
  };

  return (
    <div style={{ padding: '2rem' }}>
      <h1>Todoアプリ</h1>
      <input
        type="text"
        value={text}
        onChange={(e) => setText(e.target.value)}
        placeholder="やることを入力"
      />
      <button onClick={handleAdd}>追加</button>
      <ul>
        {todos.map((todo, idx) => (
          <li key={idx}>{todo}</li>
        ))}
      </ul>
    </div>
  );
}

export default App;

修正後コード

import { useState } from 'react';

function App() {
  // 状態(state)がUIコンポーネント内部に集中している
  const [text, setText] = useState('');
  const [todos, setTodos] = useState<string[]>([]);

  // 入力チェックもこの中に定義
  const isInputValid = () => text.trim() !== '';

  // Todo追加処理も直接ここで完結している
  const handleAdd = () => {
    if (!isInputValid()) return;
    setTodos([...todos, text]);
    setText('');
  };

  return (
    <div style={{ padding: '2rem' }}>
      <h1>Todoアプリ</h1>

      <div style={{ display: 'flex', gap: '0.5rem' }}>
        <input
          type="text"
          value={text}
          onChange={(e) => setText(e.target.value)}
          placeholder="やることを入力"
        />
        {/* 入力チェックの判定もここで直接実行 */}
        <button onClick={handleAdd} disabled={!isInputValid()}>
          追加
        </button>
      </div>

      <ul>
        {/* todos の表示も UI 内に直接展開 */}
        {todos.map((todo, idx) => (
          <li key={idx}>{todo}</li>
        ))}
      </ul>
    </div>
  );
}

export default App;

師匠はしばらくコードを眺めていたが、手を組んだまま、静かにこう言った。

師匠:「……まぁ、見た目は多少は整ったけど」

若手は「よかった」と思いかけたが、その言い回しに、どこかモヤっとしたものが残った。

“整ったけど”──そのあとに続く言葉が、少し怖かった。

師匠の静かなひと言が刺さった

師匠は黙ったままコードを見ていた。
若手が自信をもって見せた、整理されたUIコンポーネントの中身。

それでも師匠は、顔色ひとつ変えずにぽつりとつぶやいた。

師匠:「……表示の場所で“判断”までしてるのは、まずいんだよ」
若手:「え?判断ですか?」
師匠:「……表示する責任の場所で、“これを追加していいか”みたいな判断の責任までやるのは、まずいんだよ」

若手は一瞬止まり、少し自信ありげに答える。

若手:「でも、UIの中でまとめた方が分かりやすくないですか?」

師匠は視線をコードから外さず、静かに言葉を続けた。

師匠:「それ、“処理のフロー”なんだよ。動かす順番を並べただけ。
フローチャートと同じで、それは“動的な設計”。悪くはないけど、それだけじゃ足りないんだよ。
本来の設計ってのは、“意味のある構造”をつくること。つまり、静的な設計なんだよ。
まず構造を決める。それから動きを組み合わせていく──順番が逆なんだよ」

若手は何も言い返せなかった。

「動かせるように並べただけ」。
言われてみれば、自分の設計は、まさにそれだった。

構造がない。意味も責任も、バラバラのままだった──

設計の“順番”と“責任”がバラバラだったと気づく

師匠の言葉が頭から離れなかった。
「処理のフロー」と「意味のある構造」──それは、自分がこれまで考えたことのない視点だった。

UIの中で状態を管理し、入力チェックをして、処理を分岐し、さらにそのまま画面に反映している。

それは、「何を表示するか」だけではなく、
「どれが正しい入力か」や「追加すべきかどうか」まで判断していた。

若手:「……僕、画面の中で全部やろうとしてました」

師匠はうなずきながら答える。

師匠:「それじゃ“構造”が見えないんだよ。UIに責任を背負わせすぎなんだ」

画面に責任を詰め込んだ結果、「何をどこで決めているのか」が読みにくくなり、
設計としても、他の人には伝わらないものになっていた。

若手はようやく、「処理の順番」ではなく、「責任の並び」として設計を見ることの大切さに気づき始めた。

責任を分けることで、設計が“読める”ようになる

若手は、コードの中に詰め込んでいた処理を一つひとつ外に出し始めた。
入力の妥当性チェック、状態の更新、そして表示――
これまでUIコンポーネントの中でまとめていたものを、責任ごとに分けていく。

UIは「言われたとおりに見せるだけ」にし、
「何を見せるべきか」「どんなときに追加するか」は、UIの外で決めるように変えた。

若手:「……これ、どこで何やってるか、ひと目で分かるようになりました」

再修正後のコード

import { useState } from 'react';

// UIの外にロジックを分離(責任を分ける)
function isInputValid(text: string): boolean {
  return text.trim() !== '';
}

function addTodo(todos: string[], text: string): string[] {
  return [...todos, text];
}

function App() {
  const [text, setText] = useState('');
  const [todos, setTodos] = useState<string[]>([]);

  const handleAdd = () => {
    if (!isInputValid(text)) return;
    const updated = addTodo(todos, text);
    setTodos(updated);
    setText('');
  };

  return (
    <div style={{ padding: '2rem' }}>
      <h1>Todoアプリ</h1>
      <div style={{ display: 'flex', gap: '0.5rem' }}>
        <input
          type="text"
          value={text}
          onChange={(e) => setText(e.target.value)}
          placeholder="やることを入力"
        />
        <button onClick={handleAdd} disabled={!isInputValid(text)}>
          追加
        </button>
      </div>
      <ul>
        {todos.map((todo, idx) => (
          <li key={idx}>{todo}</li>
        ))}
      </ul>
    </div>
  );
}

export default App;

そこで、ふと過去を思い出す。
以前のプロジェクトで、仕様の認識違いや不具合が繰り返されたこと。
レビューでも「この処理、なんでここにあるの?」と何度も聞かれたこと。
どれも「UIの中に全部詰めていたから」だったのかもしれない。

師匠:「出力は、“言われたとおりに見せるだけ”でいいんだよ。
「何をどう判断するかは、ちゃんと外に出しておかないと──伝わらないんだよ。

“責任を分ける”という設計の原則が、少しずつ自分の中に染み込んでいく。

コードの意味が見えるようになる。
誰かに説明できるようになる。
それが「読める設計」ということなんだ──

次回、「構造を並べ直す」という新たなステップへ。
“変化に強い構造”を目指して、設計の再構築が始まる──

次回の記事予告

ここまでお読みいただき、ありがとうございました。
本記事では、AIを使って開発を進める中で直面した「設計不在」の怖さと、それを乗り越えるために必要な“伝わる設計”の視点について紹介しました。

次回は、師匠から教わった設計の基本――「STS設計(入力・変換・出力)」というシンプルで強力な考え方を軸に、実際にReactアプリの構造を改善していくプロセスを紹介します。

もし、AIの出力をそのまま使ってコードが混乱してきた方、自分の設計に不安を感じている方には、きっと役立つ内容になるはずです。ぜひご期待ください!


また、今回紹介した内容をより実践的に学びたい方には、以下のUdemy講座もおすすめです。

Udemyコース(8,800円 → クーポンで割引中)

▶️ AIとC#で極める!クリーンコードの技法(限定クーポン付き)

  • C#でクリーンコードと設計力を身につける実践講座
  • ChatGPTの活用方法や、伝わるコードの考え方を解説

出版書籍『あきらめない者たち』

▶️ Amazonで見る

  • 技術の基礎からやり直すために、なぜ一歩勇気を振り絞れたのかのノンフィクション作品です。
2
2
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
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?