🦁

【50日目】『プリンシプル オブ プログラミング』を意識してJavaScriptをリファクタリングしてみた

に公開

技術ブログ50日目。
本日は、JavaScriptの読みやすさを追求します。

〇課題  注文配送システムにおける「知りすぎている」関数
注文(Order)オブジェクトを受け取り、配送先の国に基づいて配送料を計算する関数です。しかし、この関数は注文オブジェクトの内部構造(顧客、そのプロフィール、さらにその下の住所など)を深く知りすぎています。いわゆる 「列車事故(連鎖するメソッド呼び出し)」 が発生しており、内部構造が少し変わるだけでコード全体が壊れてしまう、非常に脆い状態です。

〇修正前コード(動くけれど読みにくいコード)

// 課題コード
function calculateShippingFee(order) {
  // 1. デメテルの法則(最小知識の原則)違反
  // 注文から顧客、プロフィール、住所、国へと深すぎるアクセスが発生している
  const country = order.customer.profile.address.country;

  // 2. 外部へのロジック漏洩(カプセル化の欠如)
  // 本来は「住所」や「注文」自身が知っておくべき判定ロジックが外に漏れ出している
  if (country === "Japan") {
    return 500;
  } else if (country === "USA") {
    return 2000;
  }

  return 3000;
}

// 実行例
const myOrder = {
  id: "ORD123",
  customer: {
    name: "山田",
    profile: {
      address: {
        country: "Japan",
        city: "Tokyo"
      }
    }
  }
};

const fee = calculateShippingFee(myOrder);
console.log(`配送料: ${fee}円`);

〇リファクタリングの指針
名著『プリンシプル オブ プログラミング』に基づき、以下の点を意識しました。
デメテルの法則:最小知識の原則(Law of Demeter)
「直接の友達とだけ話す」を徹底。order から遠くのデータを引き抜くのではなく、必要な情報を「尋ねる」形に整理しました。

カプセル化(Encapsulation)
データとそれを扱うロジックを一つに集約。外部からはシンプルなインターフェースで呼び出せるようにしました。

情報隠蔽(Information Hiding)
オブジェクトの内部構造を隠蔽。将来的に住所の持ち方が変わっても、配送料計算ロジックを修正する必要がない状態を作りました。

〇修正後コード

/**
 * 住所情報を管理するクラス(最小単位のカプセル化)
 */
class Address {
  constructor(country, city) {
    this.country = country;
    this.city = city;
  }

  // 外部に内部構造を意識させないためのメソッド
  isDomestic() {
    return this.country === "Japan";
  }

  getCountry() {
    return this.country;
  }
}

/**
 * 注文情報を管理するクラス
 */
class Order {
  constructor(id, customer) {
    this.id = id;
    this.customer = customer;
  }

  /**
   * 配送料を計算する(ロジックを自身のクラス内にカプセル化)
   * デメテルの法則に基づき、配送先の国を自身が知っている形にする
   */
  calculateShippingFee() {
    const country = this.customer.getAddress().getCountry();

    const feeTable = {
      "Japan": 500,
      "USA": 2000
    };

    return feeTable[country] || 3000;
  }
}

/**
 * 顧客情報を管理するクラス
 */
class Customer {
  constructor(name, address) {
    this.name = name;
    this.address = address;
  }

  getAddress() {
    return this.address;
  }
}

// 実行例:呼び出し側は「計算の詳細」を知らなくて済む
const myAddress = new Address("Japan", "Tokyo");
const myCustomer = new Customer("山田", myAddress);
const myOrder = new Order("ORD123", myCustomer);

const fee = myOrder.calculateShippingFee();
console.log(`配送料: ${fee}円`);

〇主な修正ポイント

  1. デメテルの法則の適用
    修正前の order.customer.profile.address.country という連鎖を断ち切りました。各クラスが隣接するオブジェクトとだけやり取りすることで、構造の変化に対する耐性が高まりました。

  2. カプセル化による「責任」の移動
    「どの国がいくらか」という配送料の知識を Order クラスの中に閉じ込めました。これにより、__データとロジックがセット__になり、仕様変更時に見るべき場所が明確になります。

  3. 「Tell, Don't Ask(命令せよ、尋ねるな)」の実践
    外からデータを取り出して(Ask)加工するのではなく、オブジェクトに「配送料を計算して」と依頼(Tell)する形にしました。

  4. 疎結合なオブジェクト構造
    Address や Customer が独立しているため、住所の形式が変わっても、配送料計算のインターフェースを変えずに内部の実装だけを差し替えることが可能になりました。

〇参照先
▼公式ドキュメント
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Functions/get

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Classes/Private_elements

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Classes#メヽッドの定ぉ

▼書籍
プリンシプル オブ プログラミング 3年目までに身につけたい 一生役立つ101の原理原則

以上

Discussion