<CodeLearn/>
設計パターン レッスン1

設計の基本原則

良いコードとは何か? DRY、KISS、YAGNIなど普遍的な原則を学ぼう

DRY原則 - Don't Repeat Yourself

DRY(Don't Repeat Yourself)は「同じことを繰り返すな」という原則です。 同じロジックやデータが複数箇所に存在すると、変更時にすべての箇所を修正する必要があり、バグの温床になります。

共通のロジックは関数やクラスに抽出し、一箇所で管理しましょう。

// 悪い例: 同じバリデーションロジックが重複
function validateEmail(email: string): boolean {
  const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return regex.test(email);
}

function validateContactForm(email: string) {
  // 同じ正規表現が重複している!
  const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  if (!regex.test(email)) {
    throw new Error("Invalid email");
  }
}

// 良い例: 共通ロジックを一箇所にまとめる
function isValidEmail(email: string): boolean {
  const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return regex.test(email);
}

function validateContactForm(email: string) {
  if (!isValidEmail(email)) {
    throw new Error("Invalid email");
  }
}

注意

DRYは「同じコード」ではなく「同じ知識」の重複を避ける原則です。 見た目が似ているだけで本質的に異なるロジックを無理にまとめると、かえって複雑になります。

KISS原則 - Keep It Simple, Stupid

KISSは「シンプルに保て」という原則です。 不必要に複雑なコードは理解しにくく、バグが混入しやすく、保守コストが高くなります。 最もシンプルな解決策を選びましょう。

// 悪い例: 過度に複雑
function isEven(n: number): boolean {
  return n.toString(2).split("").reverse()[0] === "0";
}

// 良い例: シンプルで明快
function isEven(n: number): boolean {
  return n % 2 === 0;
}

// 悪い例: 不必要な抽象化
class NumberCheckerFactory {
  createChecker(type: string) {
    if (type === "even") return new EvenChecker();
    if (type === "odd") return new OddChecker();
  }
}

// 良い例: 必要十分なシンプルさ
const isEven = (n: number) => n % 2 === 0;
const isOdd = (n: number) => n % 2 !== 0;

YAGNI原則 - You Aren't Gonna Need It

YAGNIは「今必要ないものは作るな」という原則です。 将来必要になるかもしれない機能を先回りして実装すると、使われないコードが増え、複雑性だけが上がります。

// 悪い例: "将来使うかも"で不要な機能を実装
interface UserService {
  getUser(id: string): User;
  createUser(data: CreateUserDto): User;
  updateUser(id: string, data: UpdateUserDto): User;
  deleteUser(id: string): void;
  exportUsersToCSV(): string;      // まだ要件にない
  importUsersFromCSV(): void;       // まだ要件にない
  generateUserReport(): Report;     // まだ要件にない
  bulkUpdateUsers(): void;          // まだ要件にない
}

// 良い例: 今必要な機能だけを実装
interface UserService {
  getUser(id: string): User;
  createUser(data: CreateUserDto): User;
  updateUser(id: string, data: UpdateUserDto): User;
  deleteUser(id: string): void;
}

必要になったときに追加すればよいのです。アジャイル開発では「必要最小限」を素早く実装し、 フィードバックに基づいて改善するアプローチが重視されます。

関心の分離 (Separation of Concerns)

関心の分離とは、プログラムを「それぞれ異なる関心事を扱う部分」に分割することです。 UI、ビジネスロジック、データアクセスなど、異なる責務を持つコードは分けて管理しましょう。

// 悪い例: すべてが混在
async function handleSubmit(formData: FormData) {
  // バリデーション(ビジネスロジック)
  if (!formData.get("email")) throw new Error("Email required");
  // データベース操作(データアクセス)
  const db = await connectDB();
  await db.collection("users").insertOne({
    email: formData.get("email"),
    name: formData.get("name"),
  });
  // メール送信(外部サービス)
  await sendEmail(formData.get("email") as string, "Welcome!");
  // HTMLレスポンス(UI)
  return "<h1>登録完了</h1>";
}

// 良い例: 関心ごとに分離
// validator.ts
function validateUser(data: CreateUserDto): ValidationResult {
  if (!data.email) return { valid: false, error: "Email required" };
  return { valid: true };
}

// userRepository.ts
async function saveUser(data: CreateUserDto): Promise<User> {
  return await db.collection("users").insertOne(data);
}

// emailService.ts
async function sendWelcomeEmail(email: string): Promise<void> {
  await sendEmail(email, "Welcome!");
}

// userController.ts
async function handleSubmit(formData: FormData) {
  const data = parseFormData(formData);
  const validation = validateUser(data);
  if (!validation.valid) throw new Error(validation.error);
  const user = await saveUser(data);
  await sendWelcomeEmail(user.email);
  return user;
}

結合度と凝集度 (Coupling vs Cohesion)

良い設計は低結合・高凝集を目指します。

結合度 (Coupling) - 低いほど良い

モジュール間の依存の強さ。結合度が高いと、一つの変更が他の多くの箇所に波及します。

  • • インターフェースを介して通信する
  • • 具象クラスではなく抽象に依存する
  • • グローバル変数を避ける

凝集度 (Cohesion) - 高いほど良い

モジュール内の要素がどれだけ関連し合っているか。凝集度が高いと、モジュールの目的が明確になります。

  • • 一つのモジュールは一つの目的
  • • 関連する機能をまとめる
  • • 無関係な機能を混ぜない
// 悪い例: 高結合 - UserServiceがDBの内部実装に直接依存
class UserService {
  async getUser(id: string) {
    const client = new MongoClient("mongodb://localhost:27017");
    await client.connect();
    const db = client.db("myapp");
    return await db.collection("users").findOne({ _id: id });
  }
}

// 良い例: 低結合 - インターフェースで依存を抽象化
interface UserRepository {
  findById(id: string): Promise<User | null>;
}

class UserService {
  constructor(private repo: UserRepository) {}

  async getUser(id: string): Promise<User | null> {
    return this.repo.findById(id);
  }
}

// MongoDBでもPostgreSQLでもテスト用モックでも差し替え可能

コードスメル (Code Smells)

コードスメルとは、コードに問題がある可能性を示すサインです。 バグではありませんが、放置すると将来的に問題を引き起こします。

長すぎる関数 (Long Method)

50行以上の関数は分割を検討。一つの関数は一つのことだけ行う。

神クラス (God Class)

何でもできる巨大なクラス。責務を分割して複数のクラスにする。

マジックナンバー (Magic Number)

意味のない数値リテラル。定数として名前を付ける。

過剰なコメント (Excessive Comments)

コメントが必要なほど複雑なコードは、コード自体を改善すべき。

フィーチャーエンヴィー (Feature Envy)

他のクラスのデータに頻繁にアクセスする関数。そのクラスに移動すべき。

// 悪い例: マジックナンバー
if (user.age >= 18 && order.total >= 5000) {
  applyDiscount(order, 0.1);
}

// 良い例: 名前付き定数
const MINIMUM_AGE = 18;
const DISCOUNT_THRESHOLD = 5000;
const DISCOUNT_RATE = 0.1;

if (user.age >= MINIMUM_AGE && order.total >= DISCOUNT_THRESHOLD) {
  applyDiscount(order, DISCOUNT_RATE);
}

確認クイズ

1 / 3

DRY原則の意味として正しいのはどれ?

まとめ

  • DRY - 同じ知識を繰り返さない。共通ロジックは一箇所にまとめる
  • KISS - シンプルに保つ。不必要な複雑さを避ける
  • YAGNI - 今必要ないものは作らない。必要になったら追加する
  • 関心の分離 - 異なる責務のコードは別のモジュールに分ける
  • 低結合・高凝集 - モジュール間の依存は最小限に、モジュール内の関連性は最大限に
  • コードスメル - 問題のサインを見逃さず、早めにリファクタリングする