TypeScriptのモジュール分離と単一責任原則(SRP)に基づく設計を初心者向けに徹底解説
生徒
「TypeScriptでコードが長くなってきて、どこに何があるかわからなくなってきました。どうすれば整理できますか?」
先生
「それは、モジュール分離と単一責任原則を使うことで解決できますよ。コードを役割ごとに分けて整理する方法です。」
生徒
「役割ごとに分けるって、具体的にどうやるんですか?」
先生
「それでは、基本的な考え方から実際のコードまで、順番に見ていきましょう!」
1. モジュール分離とは?プログラムを整理する基本
TypeScriptのモジュール分離とは、プログラムを機能ごとに別々のファイルに分けて管理することです。例えば、お部屋の整理整頓を想像してください。洋服はクローゼット、本は本棚、文房具は引き出しに分けて収納しますよね。プログラムも同じように、役割ごとにファイルを分けることで、どこに何があるかがすぐにわかるようになります。
モジュール分離をすることで、次のようなメリットがあります。プログラムが読みやすくなり、どこに何が書いてあるかがすぐにわかります。また、バグを見つけやすくなり、修正も簡単になります。さらに、他の人と一緒に開発するときも、ファイルを分けることで作業が分担しやすくなります。
2. 単一責任原則(SRP)とは?一つのことだけをする考え方
単一責任原則(Single Responsibility Principle、略してSRP)とは、一つのモジュールやクラスは、一つの責任だけを持つべきという考え方です。これは英語でシングル・レスポンシビリティ・プリンシプルと言います。
わかりやすく例えると、コンビニの店員さんを想像してください。もし一人の店員さんが、レジ打ち、品出し、清掃、調理、配達まで全部やっていたら、とても大変ですよね。それよりも、レジ担当、品出し担当、清掃担当と分けた方が、それぞれが自分の仕事に集中できて効率的です。
プログラムでも同じです。一つのファイルやクラスに色々な機能を詰め込むのではなく、それぞれに一つの役割だけを持たせることで、コードがシンプルで理解しやすくなります。この原則を守ることで、プログラムの保守性が高まり、変更が必要になったときも影響範囲が限定されます。
3. 実践:ユーザー管理システムをモジュール分離してみよう
それでは、実際にユーザー管理システムを例に、モジュール分離と単一責任原則を使った設計を見ていきましょう。まず、すべてを一つのファイルに書いた悪い例から見てみます。
悪い例:すべてが一つのファイルに詰め込まれている
// main.ts - すべてが一つのファイルに書かれている
class User {
constructor(public name: string, public email: string) {}
validate(): boolean {
if (this.name.length === 0) return false;
if (!this.email.includes('@')) return false;
return true;
}
saveToDatabase(): void {
console.log(`データベースに保存: ${this.name}`);
}
sendEmail(message: string): void {
console.log(`${this.email}にメール送信: ${message}`);
}
generateReport(): string {
return `ユーザー名: ${this.name}, メール: ${this.email}`;
}
}
const user = new User('田中太郎', 'tanaka@example.com');
user.saveToDatabase();
user.sendEmail('登録ありがとうございます');
この例では、一つのクラスが、データの保持、バリデーション、データベース保存、メール送信、レポート生成と、たくさんの責任を持っています。これでは、メールの送り方を変えたいときにUserクラスを修正する必要があり、その変更が他の機能に影響を与える可能性があります。
良い例:モジュール分離と単一責任原則を適用
では、同じ機能を単一責任原則に基づいて分割してみましょう。まず、ユーザーのデータを表すモデルを作ります。
// models/User.ts - ユーザーのデータを表すだけ
export class User {
constructor(
public name: string,
public email: string
) {}
}
次に、ユーザーデータが正しいかチェックするバリデーターを作ります。バリデーションとは、データが正しい形式かどうかを検証することです。
// validators/UserValidator.ts - ユーザーデータの検証だけを担当
import { User } from '../models/User';
export class UserValidator {
validate(user: User): boolean {
if (user.name.length === 0) {
console.log('エラー: 名前が空です');
return false;
}
if (!user.email.includes('@')) {
console.log('エラー: メールアドレスが不正です');
return false;
}
return true;
}
}
データベースへの保存を担当するリポジトリを作ります。リポジトリとは、データの保存や取得を行う場所という意味です。
// repositories/UserRepository.ts - データベース操作だけを担当
import { User } from '../models/User';
export class UserRepository {
save(user: User): void {
console.log(`データベースに保存: ${user.name}`);
// 実際のデータベース保存処理がここに入る
}
findByEmail(email: string): User | null {
console.log(`${email}でユーザーを検索`);
// 実際の検索処理がここに入る
return null;
}
}
メール送信を担当するサービスを作ります。
// services/EmailService.ts - メール送信だけを担当
import { User } from '../models/User';
export class EmailService {
send(user: User, message: string): void {
console.log(`${user.email}にメール送信: ${message}`);
// 実際のメール送信処理がここに入る
}
}
レポート生成を担当するサービスを作ります。
// services/ReportService.ts - レポート生成だけを担当
import { User } from '../models/User';
export class ReportService {
generateUserReport(user: User): string {
return `ユーザー名: ${user.name}, メール: ${user.email}`;
}
}
最後に、これらを組み合わせて使うメインファイルを作ります。
// main.ts - 各モジュールを組み合わせて使う
import { User } from './models/User';
import { UserValidator } from './validators/UserValidator';
import { UserRepository } from './repositories/UserRepository';
import { EmailService } from './services/EmailService';
import { ReportService } from './services/ReportService';
const user = new User('田中太郎', 'tanaka@example.com');
const validator = new UserValidator();
if (validator.validate(user)) {
const repository = new UserRepository();
repository.save(user);
const emailService = new EmailService();
emailService.send(user, '登録ありがとうございます');
const reportService = new ReportService();
const report = reportService.generateUserReport(user);
console.log(report);
}
実行結果は次のようになります。
データベースに保存: 田中太郎
tanaka@example.comにメール送信: 登録ありがとうございます
ユーザー名: 田中太郎, メール: tanaka@example.com
4. モジュール分離のメリットを実感しよう
このようにモジュールを分離すると、たくさんのメリットがあります。まず、変更に強くなります。例えば、メールの送信方法を変更したいときは、EmailServiceファイルだけを修正すれば良く、他のファイルには影響しません。
また、テストがしやすくなります。それぞれのモジュールが独立しているので、個別にテストコードを書くことができます。例えば、UserValidatorだけをテストしたいときは、データベースやメール送信のことを気にせずにテストできます。
さらに、再利用性が高まります。EmailServiceは、ユーザー登録以外の場面でも使うことができます。例えば、パスワードリセットのメール送信や、お知らせメールの送信など、色々な場面で同じEmailServiceを使い回すことができます。
そして、チーム開発がしやすくなります。Aさんはバリデーション担当、BさんはデータベースI担当、Cさんはメール送信担当と分けることで、お互いの作業が干渉しにくくなります。
5. モジュール分離の基本ルールと命名規則
モジュールを分離するときは、いくつかの基本ルールを守ると、より読みやすいコードになります。まず、ファイル名とクラス名を一致させることです。UserValidator.tsというファイルには、UserValidatorというクラスを書きます。
次に、フォルダ構成を役割で分けることです。よく使われるフォルダ構成は次のようなものです。modelsフォルダにはデータの構造を定義するクラス、servicesフォルダにはビジネスロジックを処理するクラス、repositoriesフォルダにはデータの保存や取得を行うクラス、validatorsフォルダにはデータの検証を行うクラス、utilsフォルダには汎用的な便利機能を入れます。
また、exportとimportを正しく使うことも大切です。他のファイルから使いたいクラスや関数には、exportキーワードを付けます。そして、他のファイルで使うときは、importで読み込みます。
6. 実践的なアドバイス:最初は完璧を目指さなくてOK
初心者の方は、最初から完璧なモジュール分離を目指す必要はありません。まずは小さく始めて、徐々に改善していくことが大切です。
最初は、関連する機能をまとめることから始めましょう。例えば、ユーザー関連の機能は一つのフォルダにまとめる、商品関連の機能は別のフォルダにまとめる、といった具合です。
そして、一つのファイルが長くなってきたら、分割を考えるタイミングです。目安として、一つのファイルが百行を超えたら、分割できる部分がないか考えてみましょう。
また、同じようなコードを何度も書いていることに気づいたら、それを共通のモジュールとして切り出すチャンスです。繰り返しを避けることで、コードの保守性が向上します。