本記事の内容
- 下記の書籍の「6章:関心の分離」についての内容となります。
- https://amzn.asia/d/03bmKUcd
📚 目次
- 関心の分離の基本
- 関心の分離とカプセル化
- インターフェイスと実装の分離
1. 関心の分離の基本
1-1. 関心の分離がされていない状態とは?
👉 一つのクラスに複数の責務が混在している状態
例:
- 予約処理(予約IDを受け取り、予約処理を実施)
- 画面設定変更(設定値を受け取り、現在の設定を変更)
- メール送信(メルマガを一斉送信)
関心の分離が考慮されていない設計
関心の分離を考慮した設計
❌ なぜダメなのか?
- 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. 解決アプローチ
- インスタンス変数ごとにクラスを分割
- 依存関係を図式化すると理解しやすい
依存関係を図式化
悪い例(関心が混ざっている)
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. 設計手順
- 獲得したい結果を定義
- 必要な入力を定義
- インターフェイスを定義
- 実装する
例: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人程度の実務未経験の方が応募し技術面接を受けます。
その経験を通し、実務未経験者の方にぜひ身につけて欲しい技術力(文法)をここでは紹介していきます。


