
こんにちは。Webフロントエンドエンジニアの飯塚です。
最近では、AIを使った自動コーディングが一般的になりつつあります。
弊社でも積極的に取り入れており、私自身利用するシーンがますます増えてきました。
この技術は開発スピードを飛躍的に向上させますが、AIが生成したコードが、常に人間が意図したものになるとは限りません。
AIコーディングの肝は、いかに適切にコンテキスト(情報)を与えるかにあると私は考えます。
AIの力を最大限に引き出すため、正確なコンテキストを効果的に与えるには、疎結合な設計が不可欠です。
疎結合な設計は、高凝集度(High Cohesion)を伴い、クラスの責任を単一で明確にします。高凝集度とは、一つのクラスが「あれもこれも」ではなく、一つの明確な責任だけを持つことを意味します。この「単一責任の原則」を徹底することで、クラスの機能がシンプルになり、他の部分への影響範囲が狭くなるため、AIに指示を出す際に必要なコンテキストもよりピンポイントで済み、正確なコード生成につながります。
AIの有無に関わらず、この原則は変わりません。今回は、私が直近の案件で遭遇したコードを基に、結合度を下げるための基本的なポイントをまとめました。これらのポイントに配慮することで、将来的なメンテナンスコストを削減できるはずです。
1. インターフェースで「誰がどこをやるか」を明確に
複数の開発者が一つのプロジェクトに取り組む際、互いのコードが密接に連携することで、ちょっとした変更が予期せぬ影響(バグ)につながることがあります。
このリスクを軽減し、モジュールごとの独立性を高め、開発効率とメンテナンス性を向上させるために、開発担当が分かれるところなどの要所でインターフェースの活用を推奨します。
インターフェースを定義することで、「このモジュールは、この約束事(契約)に従って動作する」というルールをあらかじめ明確にできます。これにより、モジュールの内部実装の詳細や、担当外のソースコードを読み解く労力を省き、インターフェースという共通の窓口を通じて連携できるため、モジュール間の関係を疎結合に保てます。
インターフェースがなく、機能の担当範囲が曖昧な場合、他の開発者が作成したコードの挙動を推測しながら機能や使い方を理解する必要が生じます。特にモジュールが大きい場合、これはコードを読み解く(認知する)上でも開発する上でも不必要な時間と労力の浪費につながりかねません。
したがって、担当が分かれているモジュール間では、インターフェースを定義し、モジュール間の依存度を低く保つことが、予期せぬ不具合を防ぎ、開発効率を維持するための重要なプラクティスとなります。
2. Singletonは依存性注入(DI)で管理する
プロジェクト全体で共有したいインスタンスを扱う際、Singletonパターンは便利です。しかし、以下のようにファイル内で直接インスタンスを生成してimportする方法は避けるべきです。
// A.js
import { singletonInstance } from './B.js';
// B.js
export const singletonInstance = new SingletonClass();
この方法だと、A.jsはB.jsに強く依存してしまい、クラスの再利用やテストが難しくなります。代わりに、依存性注入(Dependency Injection: DI)を使いましょう。
// A.js
class MyComponent {
constructor(singletonInstance) {
this.instance = singletonInstance;
}
}
DIは、必要なインスタンスを外部から与える設計手法です。これにより、MyComponentはSingletonClassに直接依存しなくなり、再利用時には別のインスタンスを渡したり、テスト時にはモックインスタンスを渡すことも簡単にできます。
3. クラスをexportし、再利用性とテスト容易性を高める
Singletonの管理と関連しますが、インスタンスを直接exportするのも同様に問題があります。
// MyLogger.js
export const logger = new MyLogger(); // インスタンスを直接export
// どこかのファイル
import { logger } from './MyLogger.js';
logger.log('...');
この書き方では、loggerインスタンスをテストの際に差し替えるのが難しくなります。また、インスタンスをexportするファイルが複数あり、それらが相互に依存し合うと一方のモジュールが初期化される前に他方からアクセスされ(循環import)、予期せぬエラーにつながる可能性があります。
このような問題を避けるには、クラスをexportし、必要な箇所でインスタンス化するようにしましょう。
// MyLogger.js
export class MyLogger {
log(message) { ... }
}
// どこかのファイル
import { MyLogger } from './MyLogger.js';
const logger = new MyLogger();
logger.log('...');
こうすることで、モジュール間の結合度が下がり、コードの再利用性やテスト容易性が向上します。また、各ファイルでインスタンス化のタイミングを自由に制御できるため、循環importの問題が回避できます。
4. EventBusは使い方を考えて
EventBusは、発行/購読(Pub/Sub)モデルを使ってモジュール間の結合度を低く保つための有効な手段です。しかし、安易に導入すると、逆にコードの全体像が把握しにくくなることがあります。
特に、密接な関係にあるモジュール群の中でEventBusを使うのは避けるべきです。直接的なメソッド呼び出しがイベント通信に置き換わり、見かけ上は疎結合に見えるかもしれませんが、実際はモジュール間の処理の流れがEventBusによって隠蔽されてしまい、後からコードを読んだ人がデバッグに苦労することになります。また、EventBusによって、モジュール間の直接的な依存関係は見えなくなりますが、特定のイベントを購読・発行するという形で、依然として強く依存している状態です。この隠された依存関係は、コードの変更やリファクタリングを困難にします。
EventBusは、「直接的な依存関係を持つべきではない完全に切り分けられたモジュール間で、非同期に通信したい場合」など、特定の課題を解決するために限定して使うのがおすすめです。
まとめ
直近の比較的大規模な案件を引き継いだ際、コードの読み解きや機能追加に想定以上の時間を費やしました。
この経験を通じて、インターフェース定義をはじめとした疎結合な設計が、開発効率の向上だけでなく、引継ぎ時の読解時間短縮にも重要であることを改めて認識しました。この設計の基本原則の重要性を、社内に啓蒙していきたいと考えています。
特にAIコーディングの時代において、疎結合な設計はこれまで以上に不可欠です。
今回解説したインターフェース、依存性注入(DI)、クラスのエクスポートといった設計原則は、AIが効率的かつ正確にコードを生成するための基本となります。これらの設計を意識することで、AIへの指示に必要なコンテキストを最小化でき、AIとの協業がスムーズになり、長期的なメンテナンスコストを削減できます。
これらの基本を一つひとつ積み重ね、開発の品質とスピードを向上させていきましょう。