
目次
はじめに:崩れた家の物語
5年前、あるスタートアップ企業のシステムリニューアル案件を担当しました。
前任者が構築したPHP製のECサイトは、見た目は立派でした。管理画面も充実している。決済機能も実装されている。しかし、そのシステムには致命的な問題がありました。
少しの修正が、システム全体を壊す。
「商品画像のサイズを変更したい」という単純な要望が、なぜかカート機能に影響を与える。「会員登録フォームに項目を追加したい」という変更が、注文履歴の表示を崩す。
コードを読み解いていくと、原因が見えてきました。
- すべての機能が1つの巨大なファイルに詰め込まれている
- データベース設計に一貫性がない
- 処理の依存関係が複雑に絡み合っている
つまり、「設計」という土台が存在しなかったのです。
優秀なエンジニアが書いたコードでした。動作も問題なかった。でも、設計がなかったために、メンテナンスも拡張も困難な「レガシーシステム」になっていました。
この経験から、私は学びました。
どんなに完璧なコードも、設計なしでは、システム開発段階において大きな見直しが発生するリスクがある。
AI補助で開発効率は上がっても、設計の責任は人にある
ChatGPTは「大工」であって「建築家」ではない
GitHub CopilotやChatGPTは、確かに驚くべき速さでコードを生成します。
「ログイン機能を作って」と頼めば、数秒で実装コードが完成する。「このAPIを叩いてデータを取得して」と言えば、エラーハンドリングまで含めて書いてくれる。
でも、AIには答えられない問いがあります。
- このシステム全体を、どんなアーキテクチャで構成すべきか?
- 将来の拡張性を考えると、どんな設計パターンを採用すべきか?
- パフォーマンスとメンテナンス性のバランスをどう取るか?
- このプロジェクトの制約条件下で、最適な技術スタックは何か?
これらは、建築家(アーキテクト)の仕事です。
AIは優秀な大工です。でも、家全体の設計図を描くのは、人間の役割なのです。
実例:Flutter製の社内ツールで学んだこと
先日、Flutter製の社内タスク管理ツールを開発する機会がありました。
最初、若手メンバーがGitHub Copilotを使って、驚くべき速さで機能を実装していきました。タスク一覧、タスク作成、タスク編集、タスク削除——すべてが1週間で完成。
しかし、品質管理の観点からコードレビューをすると、大きな問題が見えてきました。
// 各画面で直接APIを呼び出している
class TaskListScreen extends StatefulWidget {
@override
_TaskListScreenState createState() => _TaskListScreenState();
}
class _TaskListScreenState extends State<TaskListScreen> {
Future<void> loadTasks() async {
final response = await http.get('https://api.example.com/tasks');
// レスポンスをパース...
}
}
一見問題なさそうですが、このままでは:
- API仕様が変わったら、すべての画面を修正する必要がある
- オフライン対応を追加したくなったら、大規模なリファクタが必要
- テストが書きにくい
- 状態管理が各画面にバラバラに存在する
つまり、「設計」が欠けていたのです。
私はチームと議論し、以下のような設計方針を立てました。
// Repository層でAPI通信を抽象化
abstract class TaskRepository {
Future<List<Task>> getTasks();
Future<Task> createTask(Task task);
// ...
}
// Provider/Riverpodで状態管理を集中化
final taskProvider = StateNotifierProvider<TaskNotifier, AsyncValue<List<Task>>>((ref) {
return TaskNotifier(ref.read(taskRepositoryProvider));
});
// UIはビジネスロジックから分離
class TaskListScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final tasks = ref.watch(taskProvider);
// UIの構築...
}
}
この設計により:
✅ API仕様変更は1箇所の修正で対応可能
✅ オフライン対応も、Repository層を差し替えるだけ
✅ 単体テストが書きやすい
✅ 状態管理が一元化され、バグが減る
AIは個々のコードは書けますが、このアーキテクチャ全体の設計は、人間が考える必要があります。
「設計」と「コーディング」の違い
家づくりに例えると
システム開発を家づくりに例えてみましょう。

家を建てる前に、必ず設計図を描きますよね。
- どこに柱を立てるか
- 水回りをどこに配置するか
- 将来、部屋を増築できるような構造にするか
- 地震に耐えられる構造にするか
これらを考えずに、いきなり釘を打ち始める大工はいません。
でも、システム開発では、設計なしにコードを書き始めるエンジニアは珍しくありません。
特にAIツールの登場で、この傾向は強まっています。「すぐコードが書ける」からこそ、設計をスキップしてしまうのです。
設計の有無で変わる、システムの未来
設計なしで作った場合
初期: 速い!すぐ動く!
3ヶ月後: 新機能を追加しづらい...
6ヶ月後: バグが多発。修正が怖い。
1年後: 作り直した方が早い状態に。
設計してから作った場合
初期: 時間がかかる...
3ヶ月後: 新機能がスムーズに追加できる
6ヶ月後: バグが少ない。影響範囲が予測しやすい。
1年後: 継続的に成長し続けるシステムに。
時間をかけて設計する = 未来への投資なのです。
設計力を鍛えるための3つの習慣
習慣1:「コードを書く前に図を描く」
私が新しいシステムを作る前に、必ずやることがあります。
ホワイトボードに図を描くこと。
紙でも、iPadでも、なんでも構いません。とにかく、視覚化するのです。
最低限描くべき図
1. システム構成図
[ ユーザー ] → [ Webアプリ(Flutter Web) ]
↓
[ API Server(PHP) ]
↓
[ データベース(MySQL) ]
↓
[ 外部API(決済サービス等) ]
2. データモデル図(ER図)
[ User ] 1 ─── ∞ [ Task ]
| |
| 1 | ∞
| |
∞ 1
| |
[ Team ] ─────── [ Project ]
3. 画面遷移図
ログイン画面
↓
ホーム画面 → タスク一覧 → タスク詳細
↓ ↓ ↓
設定画面 新規作成 編集画面
これらの図を描くことで、全体像が見えるようになります。
そして重要なのは、コードを書く前にチームでこの図を共有し、議論することです。
「この処理、こっちの方が効率的じゃない?」
「将来的にこの機能を追加する可能性があるから、ここは拡張しやすく設計しよう」
こうした議論を、コードを書く前にすることで、後戻りを防げます。
習慣2:「設計パターンを学び、使いこなす」
設計パターンは、先人たちの知恵の結晶です。
よくある問題に対する、実証済みの解決策がパターン化されています。
私がよく使う設計パターン(Flutter/PHP共通)
1. Repository パターン
- データの取得元(API、ローカルDB、キャッシュ)を抽象化
- データソースが変わっても、ビジネスロジックに影響しない
2. Factory パターン
- オブジェクトの生成ロジックを分離
- 環境(開発/本番)に応じて、適切なインスタンスを生成
3. Observer パターン(Pub/Sub)
- イベント駆動の仕組みを実現
- モジュール間の疎結合を保つ
4. Strategy パターン
- アルゴリズムを切り替え可能にする
- 例:決済方法(クレカ/PayPal/Apple Pay)の切り替え
実例:Swift製iOSアプリでのRepository パターン
// プロトコルで抽象化
protocol UserRepository {
func getUser(id: String) async throws -> User
func saveUser(_ user: User) async throws
}
// 本番環境用の実装
class RemoteUserRepository: UserRepository {
func getUser(id: String) async throws -> User {
// API通信でユーザー情報を取得
}
}
// テスト用の実装(モック)
class MockUserRepository: UserRepository {
func getUser(id: String) async throws -> User {
// 固定のテストデータを返す
}
}
// 使う側は、どちらの実装かを意識しない
class UserService {
let repository: UserRepository
init(repository: UserRepository) {
self.repository = repository
}
func loadUser(id: String) async throws -> User {
return try await repository.getUser(id: id)
}
}
この設計により、テストが書きやすく、変更に強いコードが実現できます。
習慣3:「リファクタリングを習慣化する」
良い設計は、最初から完璧には作れません。
作りながら、改善し続けることが重要です。
私のリファクタリングルール
1. 3回同じコードを書いたら、共通化する
DRY原則(Don't Repeat Yourself)ですね。
悪い例:
// 各画面で同じバリデーションを書いている
if (email.isEmpty || !email.contains('@')) {
return 'メールアドレスが不正です';
}
良い例:
// バリデーションを共通化
class Validators {
static String? validateEmail(String? value) {
if (value == null || value.isEmpty) {
return 'メールアドレスを入力してください';
}
if (!RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(value)) {
return 'メールアドレスの形式が正しくありません';
}
return null;
}
}
2. 1つの関数/メソッドは20行以内を目指す
長い関数は、責任が多すぎるサイン。小さく分割しましょう。
3. 週に1回、「技術的負債の返済日」を設ける
新機能開発ばかりではなく、既存コードの改善に時間を割く日を作ります。
品質管理の観点からも、この習慣は非常に重要です。
システム構築を「家づくり」で理解する
設計の重要性を、もう少し深く「家づくり」の比喩で考えてみましょう。
ケース1:拡張性を考えた設計
悪い設計:
全ての部屋の壁が構造壁(取り外せない)
→ 将来、間取り変更ができない
良い設計:
必要最小限の構造壁で、あとは間仕切り壁
→ ライフスタイルの変化に対応できる
システムで言うと:
- すべての機能が密結合している vs モジュール分割されている
- PHPの巨大なモノリシックアプリ vs マイクロサービス的な設計
ケース2:メンテナンス性を考えた設計
悪い設計:
配管や配線がコンクリートに埋め込まれている
→ 修理するとき、壁を壊す必要がある
良い設計:
配管・配線を点検口からアクセスできるように設計
→ メンテナンスが容易
システムで言うと:
- コードが複雑に絡み合っている vs 責任が明確に分離されている
- グローバル変数だらけ vs 適切な依存性注入(DI)
ケース3:パフォーマンスを考えた設計
悪い設計:
水回りが家の四隅にバラバラに配置
→ 配管が長くなり、お湯が出るまで時間がかかる
良い設計:
水回りを近くに集約
→ 配管が短く、効率的
システムで言うと:
- 毎回データベースに問い合わせる vs 適切なキャッシング戦略
- N+1問題が発生 vs JOINやEager Loadingで最適化
実践:設計レビューの重要性
設計力を鍛える最も効果的な方法は、他者の設計をレビューすること、自分の設計をレビューしてもらうことです。
私がチームで実践している設計レビュー
設計レビューの流れ
1. 設計書の作成(実装前)
- システム構成図
- データベース設計(ER図)
- API設計(エンドポイント一覧)
- 画面遷移図
2. レビュー会の開催
- 設計者がプレゼンテーション(15分)
- 質疑応答(30分)
- 改善案の議論(15分)
3. 設計書の修正
- レビューで出た指摘を反映
- 再度簡易レビュー
4. 実装開始
この流れで、実装後の大規模な手戻りを防ぐことができます。
設計レビューで聞くべき質問
拡張性:
- この設計で、将来的に〇〇機能を追加できますか?
- ユーザー数が10倍になっても対応できますか?
保守性:
- 新しいメンバーが参加したとき、理解しやすい構造ですか?
- バグが発生したとき、原因箇所を特定しやすいですか?
パフォーマンス:
- ボトルネックになりそうな箇所はありますか?
- データ量が増えても、レスポンス速度を維持できますか?
セキュリティ:
- 個人情報の扱いは適切ですか?
- 不正なアクセスを防げていますか?
こうした質問を通じて、設計の穴を事前に見つけることができます。
AIと共存する設計者としての心構え
AIは「提案者」、判断は人間が行う
ChatGPTに「このシステムの設計案を考えて」と聞くことは可能です。
実際、私も設計の壁打ち相手としてAIを活用しています。
私:「PHPで在庫管理システムを作ります。複数の倉庫、複数の商品、
入出庫の履歴管理が必要です。データベース設計を提案してください」
ChatGPT: 「以下のようなテーブル構成を提案します...」
AIは、素晴らしい提案をしてくれます。
しかし、最終的な判断は人間が行う必要があります。
- このプロジェクトの制約条件(予算、期間、チームのスキル)を考慮して実現可能か?
- 将来の拡張計画を踏まえて、最適な設計か?
- パフォーマンス要件を満たせるか?
これらの判断には、プロジェクト全体を俯瞰する視点が必要です。
「なぜその設計にしたのか」を説明できるか
良い設計者は、設計の意図を明確に説明できます。
「なぜこのアーキテクチャを選んだのですか?」
この質問に、きちんと答えられるかどうか。
- 「ChatGPTがそう言ったから」は、答えになりません
- 「このプロジェクトの制約条件を考慮し、拡張性と保守性のバランスを取った結果、この設計が最適だと判断しました」というように、論理的に説明できることが重要です
まとめ:AI時代だからこそ、設計思考が差別化要因になる
技術トレンドは、猛スピードで変化します。
- 5年前、Flutterはまだ登場していませんでした
- 10年前、Swiftはまだありませんでした
- 15年前、iPhoneすら存在していませんでした
しかし、「設計の原則」は変わりません。
- 責任の分離(Single Responsibility Principle)
- 開放/閉鎖原則(Open/Closed Principle)
- 依存性の逆転(Dependency Inversion Principle)
- 疎結合・高凝集
これらの原則は、何十年も前から存在し、今も有効です。
AIがコードを書く時代だからこそ、「なぜその設計なのか」を考え、説明できるエンジニアの価値が高まっています。
あなたがこれからできること
✅ 次の開発から、コードを書く前に図を描く習慣をつける
✅ 設計パターンの本を1冊読む(例:『増補改訂版 Java言語で学ぶデザインパターン入門』は言語を超えて役立ちます)
✅ チームで設計レビューの文化を作る
✅ AIを「壁打ち相手」として活用し、設計案を議論する
設計は、一朝一夕では身につきません。
でも、意識して取り組めば、確実に成長します。
そして、その積み重ねが、AI時代に生き残る、いや、活躍するエンジニアへの道なのです。
次回予告
第4回では、「AIと共存する時代の"チーム開発力"」をテーマにお届けします。
生成AIが進化しても、システム開発はチームで行うもの。
むしろ、AIツールが普及したからこそ、人間同士のコミュニケーションが重要になってきました。
- GitHub Copilotを使ったチーム開発の変化
- AIがいても必要な「伝わるドキュメント」の書き方
- ベテランと若手、AIが交わる現場のリアル
実際のプロジェクト事例を交えて、お伝えします。
【AI時代を生き抜くシステムエンジニア論 - 連載目次】
- AI時代の到来と、SEの役割の変化
- AIに代替されないSEの基本「課題定義力」と「論理的思考」
- 変わる技術トレンド、変わらない「設計思考」(今回)
- AIと共存する時代の"チーム開発力"
- AIを味方につけるためのスキルマップ
- これからも変わらない"システムエンジニアの本質"