前回までの記事
入力・処理・出力だけの責務で見る設計の観点
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を使う時は、こういう風に指示するとどうだ?」
-
具体的な構造を指定する
- 「DDDで作って」ではなく、「Source(入力)、Transform(処理)、Sink(出力)の3つの役割に分けて」のように、具体的な構造を指定する
- 例えば「Sourceは入力フォーム、Transformはタスクの作成ロジック、Sinkはタスクリストの表示」のように、各役割の責務を明確に示す
-
各役割の責務を明確にする
- Sourceは入力の受け付けだけ、Transformはビジネスロジックだけ、Sinkは表示だけ
- 各役割が混ざらないように、明確な境界を設ける
-
テストのしやすさを考慮する
- 「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で見る
- 技術の基礎からやり直すために、なぜ一歩勇気を振り絞れたのかのノンフィクション作品です。