0
1

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話「React×DDDの設計、師匠から学んだシンプルな設計の考え方」

Posted at

前回までの記事

入力・処理・出力だけの責務で見る設計の観点

1ファイル(App.tsx)肥大化の理由を探る

「うーん...」

私は、画面の前で唸っていた。前回、AIの力を借りて作ったTodoアプリのコードを見つめながら。

App.tsxに全部書いちゃってるんですよね...」

確かにアプリは動いている。でも、このコードの構造には何か問題がある。そんな漠然とした不安が私の心を離れなかった。

「おや、まだコードと睨めっこしているのか?」

突然、後ろから声が聞こえた。振り返ると、そこには穏やかな笑みを浮かべた師匠の姿があった。

「はい...前回お約束した通り、コードを整理しようと思って...でも、どこから手をつければいいのか...」

「関心の分離」という言葉は聞いたことがある。でも、実際にコードを前にすると、何をどう分離すればいいのか、さっぱり分からない。

師匠は、ゆっくりと私の横に座った。

「コードの整理か...」

師匠は画面を見つめながら、しばらく黙っていた。

「お前、コードの整理ってどう思う?」

「えっと...」私は少し考えて答えた。「変数をきれいに並べたり、関数を分けたり...そういうことですか?」

「あ、最近フロントエンドの勉強会で『コンポーネント設計』っていう話を聞いたんですけど...」

師匠は首をかしげながら言った。「コンポーネント設計...?最新の設計手法は知らん!でもな、時代が変わっても変わらない設計原則があるんだ」

「設計原則...ですか?」

「うむ」師匠は頷いた。「どんなシステムにも共通する、シンプルな考え方がある」

そう言って、師匠は画面に表示されたApp.tsxを指さした。

「そうだ、俺が知ってる『STS』という設計の考え方を教えてやろう。これは、どんなプロダクトやサービスにも使える基本的な考え方なんだ」

STS設計という考え方

「STS...ですか?」私は首を傾げながら聞き返した。

師匠は穏やかな表情で、ゆっくりと説明を始めた。

「STS設計とはな、Source(ソース)、Transform(トランスフォーム)、Sink(シンク)の頭文字を取ったものだ。どんなプログラムも、この3つの役割に分けて考えることができるんだよ」

師匠は、次のSTSの説明をしてくれた。

役割 説明 Todoアプリでの例 ECサイトでの例 バッチ処理での例
Source
(入力)
データや操作の発生源
・ユーザーの入力
・システムイベント
・外部からのデータ
・新規タスクの入力
・完了チェックボックスのクリック
・タスク削除ボタンの押下
・商品検索フォーム
・カートへの追加
・注文ボタンのクリック
・CSVファイルの読み込み
・DBからのデータ取得
・APIからのデータ受信
Transform
(処理)
データの検証や加工
・バリデーション
・ビジネスロジック
・データ変換
・タスク文字数の検証
・重複タスクのチェック
・タスクの優先度付け
・在庫数のチェック
・価格計算
・配送料の計算
・データの形式変換
・集計処理
・エラーチェック
Sink
(出力)
処理結果の反映先
・画面表示
・データ保存
・外部システム連携
・タスクリストの更新
・LocalStorageへの保存
・完了済みの表示更新
・商品一覧の表示
・カート情報の更新
・注文データの保存
・DBへの保存
・ログ出力
・メール送信

「どんなシステムでも、この3つの役割に分類できるだろう?」

「3つの役割...」

「そうだな。例えば、お前が作ったTodoアプリで考えてみよう」師匠は画面に表示されたApp.tsxを指さした。「今、このコードには何が書かれている?」

「えっと...」私は画面をスクロールしながら答えた。「新しいタスクの入力を受け付けて、バリデーションをして、リストに追加して、画面を更新して...」

「そうだ」師匠は満足げに頷いた。「実は、今お前が言ったことが、まさにSTSの流れなんだ」

私は思わず身を乗り出した。

「まず、『新しいタスクの入力を受け付ける』。これが Source だ。ユーザーの入力やシステムのイベントなど、何かが『始まる』ところだな」

「なるほど...」

「次に、『バリデーションをする』。これが Transform だ。入力されたデータをチェックしたり、加工したり。ビジネスロジックの中心となる部分だな」

「そして最後に、『リストに追加して画面を更新する』。これが Sink だ。処理の結果を保存したり、画面に表示したり。つまり、どこかに『流し込む』わけだ」

私は少しずつ理解し始めていた。「でも、今のコードは...」

「そう」師匠は私の言葉を受けて続けた。「今のApp.tsxは、この3つの役割が全部ごちゃ混ぜになっているんだ。これじゃあ、テストを書くのも難しいし、機能を追加するのも大変だろう?」

確かにその通りだった。タスクの文字数制限を変更しようと思っても、UIのコードの中からロジックを探し出さなければならない。テストを書こうにも、表示のための処理と本質的なロジックが分離されていないため、どこをテストすればいいのかも分かりにくい。

「じゃあ、具体的にどうやって分ければいいんでしょうか?」

師匠は、にっこりと笑った。「そうだな。実際のコードを見ながら、一つずつ整理していこうか」

AIとの実践と評価

AIへの具体的な指示

「じゃあ、実際にコードを整理していこうか」師匠は、AIのプロンプト画面を見ながら言った。

「お前、AIにどうやって指示してたんだ?」

「えっと...」私は少し考えてから答えた。「『DDDを意識したTodoアプリを作ってください』って言ったんです」

「ふーん。じゃあ、AIにこう聞いてみろ」

師匠は、画面に表示されたプロンプト欄に指を置きながら言った。

「じゃあ、このSTSの考え方を使って、AIに指示してみよう」

以下の要件で、Todoアプリのコードを整理してください:

1. 3つの役割に分ける
   - Source: ユーザーの入力を受け付ける部分
   - Transform: タスクの作成や状態変更のロジック
   - Sink: タスクの表示や保存

2. 各役割の責務
   - Sourceは入力の受け付けだけ
   - Transformはビジネスロジックだけ
   - Sinkは表示や保存だけ

3. テストがしやすい構造にする
   - 各役割が独立していること
   - ビジネスロジックがUIから分離されていること
   - 各役割間のデータの受け渡しが明確であること

「この指示の仕方、どう思う?」師匠は私に尋ねた。

「え...でも、AIはSTSの考え方を理解できるんでしょうか...」

「やってみないとわからんが」師匠は頷いた。「でも、STSの考え方は、すべてのアーキテクチャや設計の考え方の基本だ。DDDはよくわからんけど、そういう技術の根本にある考え方だ。データの流れを整理する、責務を分けるといった基本的な考え方だ」

AIが生成したコードの評価

「じゃあ、実際にAIに聞いてみようか」

師匠は、AIのプロンプトを送信した。しばらくすると、AIがコードを生成し始めた。

「おお、なかなか良い感じだな」師匠は画面を見つめながら言った。「Source、Transform、Sinkの役割が、きれいに分かれている」

AIは、以下のような構成でコードを生成していた:

src/
  ├── components/
  │   ├── TodoInput.tsx    # Source: 入力を受け付ける
  │   └── TodoList.tsx     # Sink: タスクの表示
  ├── domain/
  │   └── Todo.ts          # Transform: タスクのビジネスロジック
  └── App.tsx              # 全体の組み立て

そして、各ファイルの役割は明確に分かれていた:

// components/TodoInput.tsx(Source)
export const TodoInput: React.FC<{
  onAdd: (text: string) => void;
}> = ({ onAdd }) => {
  const [text, setText] = useState('');
  
  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (text.trim()) {
      onAdd(text.trim());  // 入力の受け付けだけ
      setText('');
    }
  };
  // ... 入力フォームのUI
};

// domain/Todo.ts(Transform)
export class Todo {
  constructor(
    public readonly id: string,
    public readonly text: string,
    public readonly completed: boolean = false
  ) {}

  static create(text: string): Result<Todo, string> {
    // バリデーションやビジネスロジック
    if (text.length === 0) {
      return Result.failure('タスクは空にできません');
    }
    return Result.success(new Todo(crypto.randomUUID(), text));
  }

  toggle(): Todo {
    return new Todo(this.id, this.text, !this.completed);
  }
}

// components/TodoList.tsx(Sink)
export const TodoList: React.FC<{
  todos: Todo[];
  onToggle: (id: string) => void;
  onDelete: (id: string) => void;
}> = ({ todos, onToggle, onDelete }) => {
  // 表示のロジックだけ
  return (
    <ul>
      {todos.map(todo => (
        <li key={todo.id}>
          {/* タスクの表示UI */}
        </li>
      ))}
    </ul>
  );
};

「え?でも、これって...」私は画面をスクロールしながら言った。「ファイル構成が、先ほどの指示と違うんですけど...」

「ああ、そうだな」師匠は笑った。「お前が指示したのは『components、domain、infrastructureの3つのディレクトリに分ける』だったな。でも、AIは、STSの役割に基づいて、より適切な構成を提案してくれたんだ」

「本質...ですか?」

「うむ」師匠は頷いた。「お前が指示したのは『3つの役割に分ける』ということだ。AIは、それを実現するために、より適切なファイル構成を考えてくれたんだ」

「なるほど...」私は、画面を見つめながら言った。「AIは、指示の本質を理解して、より良い方法を提案してくれるんですね」

「そうだ」師匠は満足げに笑った。「でも、その判断は、お前がするんだ。AIの出力を評価するには、設計の本質を理解している必要がある」

AIとの向き合い方

「そういえば」師匠は続けた。「お前、AIに指示する時、何か気をつけてることはあるか?」

「えっと...」私は考え込んだ。「特に意識してないです...」

「そうか」師匠は言った。「AIを使う時は、こういう風に指示するとどうだ?」

  1. 具体的な構造を指定する

    • 「DDDで作って」ではなく、「Source(入力)、Transform(処理)、Sink(出力)の3つの役割に分けて」のように、具体的な構造を指定する
    • 例えば「Sourceは入力フォーム、Transformはタスクの作成ロジック、Sinkはタスクリストの表示」のように、各役割の責務を明確に示す
  2. 各役割の責務を明確にする

    • Sourceは入力の受け付けだけ、Transformはビジネスロジックだけ、Sinkは表示だけ
    • 各役割が混ざらないように、明確な境界を設ける
  3. テストのしやすさを考慮する

    • 「Source、Transform、Sinkの各役割が独立していること」という要件を入れる
    • 「各役割間のデータの受け渡しが明確であること」という要件も入れる
    • これにより、各役割を個別にテストできる構造と、データの流れが実現できる

「これらを意識して指示すれば、AIはより良いコードを生成してくれるはずだ」

「例えば、お前のApp.tsxは、こんな風に整理できる」

// App.tsx(変更前)
export const App: React.FC = () => {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [text, setText] = useState('');

  // Source: 入力の受け付け
  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (text.trim()) {
      // Transform: タスクの作成
      const newTodo = new Todo(crypto.randomUUID(), text.trim());
      // Sink: 状態の更新と表示
      setTodos([...todos, newTodo]);
      setText('');
    }
  };

  // Transform: タスクの状態変更
  const handleToggle = (id: string) => {
    setTodos(todos.map(todo => 
      todo.id === id ? todo.toggle() : todo
    ));
  };

  // Transform: タスクの削除
  const handleDelete = (id: string) => {
    setTodos(todos.filter(todo => todo.id !== id));
  };

  return (
    <div>
      {/* Source: 入力フォーム */}
      <form onSubmit={handleSubmit}>
        <input
          type="text"
          value={text}
          onChange={(e) => setText(e.target.value)}
          placeholder="新しいタスクを入力"
        />
        <button type="submit">追加</button>
      </form>

      {/* Sink: タスクリストの表示 */}
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => handleToggle(todo.id)}
            />
            <span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
              {todo.text}
            </span>
            <button onClick={() => handleDelete(todo.id)}>削除</button>
          </li>
        ))}
      </ul>
    </div>
  );
};
// App.tsx(変更後)
export const App: React.FC = () => {
  const [todos, setTodos] = useState<Todo[]>([]);

  // Source: 入力の受け付け
  const handleAdd = (text: string) => {
    // Transform: タスクの作成(Todoクラスに移動)
    const result = Todo.create(text);
    if (result.isSuccess) {
      // Sink: 状態の更新
      setTodos([...todos, result.value]);
    }
  };

  // Source: タスクの操作
  const handleToggle = (id: string) => {
    // Transform: タスクの状態変更(Todoクラスに移動)
    setTodos(todos.map(todo => 
      todo.id === id ? todo.toggle() : todo
    ));
  };

  const handleDelete = (id: string) => {
    // Sink: 状態の更新
    setTodos(todos.filter(todo => todo.id !== id));
  };

  return (
    <div>
      {/* Source: 入力コンポーネント */}
      <TodoInput onAdd={handleAdd} />

      {/* Sink: 表示コンポーネント */}
      <TodoList
        todos={todos}
        onToggle={handleToggle}
        onDelete={handleDelete}
      />
    </div>
  );
};

「見てみろ」師匠は言った。「変更前は、Source、Transform、Sinkの役割が全部混ざっている。でも、変更後は...」

「あ!」私は気づいた。「各役割が分かれているんです!」

「そうだ」師匠は頷いた。「Sourceは入力の受け付けだけ、Transformはビジネスロジックだけ、Sinkは表示だけ。これが、STSの考え方だ」

「なるほど...」私は、画面を見つめながら言った。「これなら、テストも書きやすそうですね」

「うむ」師匠は満足げに笑った。「では、次回は実際のコードを見ながら、STSの考え方を実践していこう」

「はい!」私は、期待に胸を膨らませながら答えた。

この日、私はApp.tsx肥大化の本当の理由と、設計をシンプルにするための“役割分担”の大切さを学んだ。

まとめ

  • App.tsx肥大化の原因は「役割の混在」
  • STS(Source/Transform/Sink)で責務を分けると設計がシンプルになる
  • AIに指示する時も「役割」を意識すると良いアウトプットが得られる
  • 設計の本質を理解して、AIの提案を自分で評価することが大切

こうして私は、設計の“分け方”の大切さを実感したのだった。

ふと、師匠が何かを思い出したように、画面のTodoクラスを指さした。

「あ、そうだ」師匠はTodoクラスを指さした。「エンティティの変換の部分に、設計としてダメな部分があるんだ」

「え?でも、これって...」

「うむ」師匠は頷いた。「次回は、そういうエンティティの変換に関する問題について、詳しく話していこう」

(次回に続く)

次回予告

第3話「React×DDDの設計、レイヤーの迷路で見つけた設計の落とし穴」


関連情報

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

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

▶️ AI時代でも生き残れるエンジニアへ!AI×React×クリーンコードでシンプルな設計を武器にする実践入門(限定クーポン付き)

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

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

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

▶️ Amazonで見る

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?