23
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【初心者〜中級者向け】関心の分離・カプセル化・インターフェイス設計をコードで理解する

23
Last updated at Posted at 2026-04-14

本記事の内容

📚 目次

  • 関心の分離の基本
  • 関心の分離とカプセル化
  • インターフェイスと実装の分離

1. 関心の分離の基本

1-1. 関心の分離がされていない状態とは?

👉 一つのクラスに複数の責務が混在している状態

例:

  • 予約処理(予約IDを受け取り、予約処理を実施)
  • 画面設定変更(設定値を受け取り、現在の設定を変更)
  • メール送信(メルマガを一斉送信)

関心の分離が考慮されていない設計

3f1b677e-61fa-4e28-b4e5-634a1831f53a.jpeg

関心の分離を考慮した設計

2ac20e41-7e29-448e-997e-9228a544f530.jpeg


❌ なぜダメなのか?

  • 1つの変更が他に影響する(デグレしやすい)
  • どこに何があるか分かりづらい(属人化)
  • 再利用しづらい
  • テストしづらい
  • コンフリクトが起きやすい

👉 「1つの処理で複数のことをしない」


1-2. 悪い例(責務が混在)

class CookingService {
  private ovenTemperature: number = 180;
  private platingStyle: string = "standard";
  private tag: string = "ご飯";

  cookDish(dishName: string): void {
    console.log(`${dishName}${this.ovenTemperature}℃で調理しました`);
  }

  changePlating(style: string): void {
    this.platingStyle = style;
    console.log(`盛り付けスタイルを ${style} に変更しました`);
  }

  postToSNS(message: string): void {
    console.log(`SNSに投稿しました: ${message} / タグ:${this.tag}`);
  }
}

1-3. 良い例(責務ごとに分離)

class CookingService {
  constructor(private ovenTemperature: number) {}

  cookDish(dishName: string): void {
    console.log(`${dishName}${this.ovenTemperature}℃で調理しました`);
  }
}

class PlatingService {
  constructor(private platingStyle: string) {}

  changePlating(style: string): void {
    this.platingStyle = style;
    console.log(`盛り付けスタイルを ${style} に変更しました`);
  }
}

class SNSService {
  constructor(private tag: string) {}

  post(message: string): void {
    console.log(`SNSに投稿しました: ${message} / タグ:${this.tag}`);
  }
}

👉 インスタンス変数単位で分割するのがコツ


1-4. 解決アプローチ

  • インスタンス変数ごとにクラスを分割
  • 依存関係を図式化すると理解しやすい

依存関係を図式化

dd6e3e35-2005-4ff9-98ea-eb477a58c805.jpeg


悪い例(関心が混ざっている)

class CookingService {
  // オーブンの温度設定
  private ovenTemperature: number = 180;
  // 盛り付けスタイル
  private platingStyle: string = "standard";
  // タグの設定
  private tag: string = "ご飯";

  cookDish(dishName: string): void {
    console.log(`${dishName}${this.ovenTemperature}℃で調理しました`);
  }

  changePlating(style: string): void {
    this.platingStyle = style;
    console.log(`盛り付けスタイルを ${style} に変更しました`);
  }

  postToSNS(message: string): void {
    console.log(`SNSに投稿しました: ${message} / タグ:${this.tag}`);
  }
}

改善例(状態ごと分離)

class CookingService {
  private ovenTemperature: number;

  constructor(temperature: number) {
    this.ovenTemperature = temperature;
  }

  cookDish(dishName: string): void {
    console.log(`${dishName}${this.ovenTemperature}℃で調理しました`);
  }
}

class PlatingService {
  private platingStyle: string;

  constructor(style: string) {
    this.platingStyle = style;
  }

  changePlating(style: string): void {
    this.platingStyle = style;
    console.log(`盛り付けスタイルを ${style} に変更しました`);
  }
}

class SNSService {
  private tag: string = "ご飯";

  constructor(tag: string) {
    this.tag = tag;
  }

  post(message: string): void {
    console.log(`SNSに投稿しました: ${message} / タグ:${this.tag}`);
  }
}

2. 関心の分離とカプセル化

2-1. なぜ難しいのか?

👉 開発が進むと「つい同じクラスにロジックを追加したくなる」

❌ 悪い例(肥大化するクラス)

class SellingPrice {
  public readonly amount: number;

  private static readonly SELLING_COMMISSION_RATE = 0.05;
  private static readonly DELIVERY_FREE_MIN = 5000;
  private static readonly SHOPPING_POINT_RATE = 0.01;

  constructor(amount: number) {
    if (amount < 0) {
      throw new Error('金額は0以上である必要があります');
    }
    this.amount = amount;
  }

  calculateCommission(): number {
    return Math.floor(this.amount * SellingPrice.SELLING_COMMISSION_RATE);
  }

  calcDeliveryCharge(): number {
    return this.amount >= SellingPrice.DELIVERY_FREE_MIN ? 0 : 500;
  }

  calcShoppingPoint(): number {
    return Math.floor(this.amount * SellingPrice.SHOPPING_POINT_RATE);
  }
}

✅ 良い例(責務ごとに分離)

金額クラス

class SellingPrice {
  public readonly amount: number;

  constructor(amount: number) {
    if (amount < 0) {
      throw new Error('金額は0以上である必要があります');
    }
    this.amount = amount;
  }
}

販売手数料

class SellingCommission {
  private static readonly RATE = 0.05;
  public readonly amount: number;

  constructor(sellingPrice: SellingPrice) {
    this.amount = Math.floor(sellingPrice.amount * SellingCommission.RATE);
  }
}

配送料

class DeliveryCharge {
  private static readonly FREE_MIN = 5000;
  private static readonly DEFAULT_FEE = 500;
  public readonly amount: number;

  constructor(sellingPrice: SellingPrice) {
    this.amount =
      sellingPrice.amount >= DeliveryCharge.FREE_MIN
        ? 0
        : DeliveryCharge.DEFAULT_FEE;
  }
}

ポイント

class ShoppingPoint {
  private static readonly RATE = 0.01;
  public readonly amount: number;

  constructor(sellingPrice: SellingPrice) {
    this.amount = Math.floor(sellingPrice.amount * ShoppingPoint.RATE);
  }
}

使用例

const price = new SellingPrice(10000);

const commission = new SellingCommission(price);
const deliveryCharge = new DeliveryCharge(price);
const shoppingPoint = new ShoppingPoint(price);

console.log(price.amount); // 10000
console.log(commission.amount); // 500
console.log(deliveryCharge.amount); // 0
console.log(shoppingPoint.amount); // 100

2-2. クラス分割の判断基準

✅ そのロジックは「そのクラスの性質」か?

OK例:

バリデーション
フォーマット
同値比較

NG例:

手数料
配送料
ポイント


✅ 変更理由が同じか?

OK:

金額の仕様変更

NG:

キャンペーンでポイント倍率変更
会員ランク変更


✅ その他

クラス名から自然に読めるか?
将来ロジックが増えそうか?

3. インターフェイスと実装の分離

3-1. 設計手順

  1. 獲得したい結果を定義
  2. 必要な入力を定義
  3. インターフェイスを定義
  4. 実装する

例:BMI計算

  • 獲得したい結果 = BMI
  • 必要な入力 = 身長・体重

※ BMIの計算式は「(BMI = 体重 ÷ (身長 × 身長))」


インターフェイス

/**
 * @param heightMeter 身長(m)
 * @param weightKg 体重(kg)
 * @returns BMI
 */
function bmi(heightMeter: number, weightKg: number): number {}

実装

function bmi(heightMeter: number, weightKg: number): number {
  return weightKg / (heightMeter * heightMeter);
}

3-2. インターフェイス設計の考え方

定義するのはこの2つだけ👇

獲得したい結果
必要な入力

👉 中身の実装は考えない


💡 例:エアコン

  • 結果:部屋を涼しくする
  • 入力:冷房ボタンを押す

👉 ユーザーは内部構造を知らなくていい

まとめ

  • 関心の分離 = 責務ごとに分ける
  • カプセル化 = 内部を隠す
  • インターフェイス = 外からの使い方を定義する

👉 この3つを意識するとコードの保守性が一気に上がります

株式会社シンシア

株式会社xincereでは、実務未経験のエンジニアの方や学生エンジニアインターンを採用し一緒に働いています。
※ シンシアにおける働き方の様子はこちら

シンシアでは、年間100人程度の実務未経験の方が応募し技術面接を受けます。
その経験を通し、実務未経験者の方にぜひ身につけて欲しい技術力(文法)をここでは紹介していきます。

23
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?