Open22

リファクタリング「既存のコードを安全に改善する」を読んだ備忘録

naoki matsuzakinaoki matsuzaki

リファクタリングの最初の例として長い関数を示し、その関数でどこを改善できるかを例を示しながら解説してくれる。

解説はJavaScriptで書かれたコードで、statement関数として定義されている一例を使っている。

関数の抽出

全体として長い関数はいつくつかに分割できる箇所を探し、分割していく。

理解したことをコードに埋め込んでいくとあるが、これは意味のあるコードの塊を関数にして、何をしているかを端的に示す名前をつけることを指す。

この時にスコープ外になる変数がないかを見定める必要がある。

そしてリファクタリングをしたらテストをすることがとても大切。

また、理解するための変数変更は積極的にやるべきと書いてある。

問い合わせによる一時変数の置き換え

ローカル変数は削除するべし。

長い関数を分割していく時には、いつでも取り出せるような値を引数で渡す必要はない。
一時変数があるとローカルスコープの変数がふえてしまい、抽出が面倒になるから。

ローカル変数が削除されたことで問い合わせが複数回になることもあるが、それはパフォーマンスを見てから改善すれば良い。

【ローカル変数の削除のメリット】
→メソッドの抽出が楽になる

名前つけは重要で、かつ難しいこと。

naoki matsuzakinaoki matsuzaki

リファクタリングと機能実装は区別して考えなければならない。
機能実装しながらリファクタを検討するのはやめた方がいいということ。
二つの帽子と例えられ、機能実装している時の帽子からリファクタリングの帽子に切り替えるように区別して考えること。

リファクタリングをする理由

  • ソフトウェア設計を改善する
  • ソフトウェアを理解しやすくする
  • バグの発見を助ける
  • 機能開発の速度を上げる
    • 機能が増えていくと時間がかかると思われるが、内部の品質が上がると把握しやすくなるから

いつリファクタリングするべきか

  • 3回同じコードを書いているなと気づいた時
    • 3つのコードを修正するより、ひとつにまとまっているコードを修正するのが早くなる

リファクタリングは将来の誰かを助けることになるが、それが自分自身であるかもしれない。

リファクタリングは少しずつやっていけばよい。
決してリファクタリングで壊れることはないので、少しずつでも修正していけばいい。
やがて綺麗になる。

naoki matsuzakinaoki matsuzaki

各リファクタリングの手法について説明がされている。

関数の抽出

よく使われる手法で、できるだけ関数の行数を短くしていく。

分ける基準としては

  • 何をしているのか理解した上で、独立した関数として抽出し、目的に相応しい名前をつける
  • 意図と実装の分離
    • 何をしているのか分からない関数なら、分かる関数として抽出するべきと言うこと

手順

  • 新たな関数を作り、その意図に沿って命名
  • 抽出したいコードを新たな関数に移動
  • 変数がスコープから外れる場合はパラメータとして渡すか新たに定義
    • 関数の中でしか使われないものは関数内で定義し、パラメータとして渡さない(変数が参照でしか使われないときなど)
    • ローカル変数が呼び出し元から使われる場合は、代入する形で関数を呼び出す
      • 関数の返り値はそのローカル変数に入れるべき値

関数に置き換えた際、戻り値が複数必要なとき
他のコードを抽出することを選ぶ。
理由としては、関数の戻り値は1つであることが望ましく、複数値が必要なら複数の関数を用意するのがよい。

naoki matsuzakinaoki matsuzaki

メソッドのインライン化

関数の本体自体がわかりやすければ、中に記載されている関数は除外し、インラインで記述した方が良いと言う手法。

うまく分割できていない関数群があるときに使われる。

そう言う無駄な関数化が行われてるのであれば一度全てインライン化して見直すのが良いと書かれている。

間接化しすぎると、ただの関数移譲にしか見えない状態。

手順

関数がポリモーフィックでないメソッドでないことを確認する。
つまり、クラスメソッドでサブクラスでオーバーライドされている場合はできない

  • 関数の呼び出し元をすべてみつける
  • 呼び出し元を関数の内容でインライン化させる
  • テストする

変更は常に細かく実施し、都度テストを実行するのが良いと書かれている。
テストが失敗するとインライン化によってバグが起きていると言うことにもなるため

変数の抽出

変数の抽出は、一つの式でコンテキストでローカル変数を定義するものである。

一行で実装されている時より、一度変数として置き換えた方がわかりやすい時もあります。
また、何度も繰り返しその値が参照されているときは、一度一時変数に置くことでコードが短くなり、理解しやすくなる時がある。

クラスでのコンテキスト

クラスの中で変数と置く場合、クラス全体で使えるかどうかを見極める。
もし使えると判断した場合は、変数ではなくメソッドとして抽出するのも良い

class Order {
    constructor(aRecord) {
        this._data = aRecord;
    }

   get quantity() {
       return this._data.quantity;
    }

   get price() {
       return this._data.price;
    }

  // このように追加する
   get item() {
        return this.quantity + this.price;
    }
}

naoki matsuzakinaoki matsuzaki

変数のインライン化

変数は名前から何を表しているのか明確になるものですが、不用意に変数を作るものではなく、あまり意味を持たない変数は削除し、インライン化した方が良いのではないか?と言う考え方。

手順

  • 変数が変更不可と宣言されていなければ、変更不可にしてテストすること


これは、代入が一度しか行われていないことを確認するため、とても重要である

naoki matsuzakinaoki matsuzaki

関数宣言の変更

いわゆる関数名の変更です。
ただ、この関数名の変更には使われている影響箇所に注意しながら変更しないとバグの原因ともなりやすい。(そのためのテストがあるのだが。。。)

リファクタリングの手法として2つの方法を紹介してある

  • 簡易な手順
  • 移行的手順

簡易的手順

パラメータ削除は内部で利用していないかをチェックして削除。

関数名変更は、すべての参照を探し、新しいものに更新。

テストをする

という簡単な方法

移行的手順

  • 必要なら関数本体をリファクタしとく
  • 同じ関数を別名で作成する
  • 古い関数に関数のインライン化をする
  • 新しく作った関数名を元の名前に戻す

ここで関数宣言変更のポイントとして、関数名の変更とパラメータの変更は同時にするなと書かれている。

片方を実施し、テストし、問題ないことを確認した上でもう片方を施すというやりかた。

移行的手順のメリットは、新旧二つの関数を残すこと。
だとすべての関数名を変更しなくても影響を与えずにリファクタリングできるのはとても大きいと感じた。

function circum(radius) {
    return 2 * Math.PI * radius;
}

これを下記のようにすることで段階的に移行できる

function circum(radius) {
    return circumference(radius);
}

function circumference(radius) {
    return 2 * Math.PI * radius;
}

これによって、circumとcircumferenceどちらも使える。

naoki matsuzakinaoki matsuzaki

変数のカプセル化

関数のように古い関数を転送用関数として残しながらリファクタリングができないので、データのリファクタリングは厄介。

データの再構成を関数の再構成というやり方でリファクタリングしていく。

変数のカプセル化とは、変数を参照・更新するためのカプセル化関数を作り、変数参照をそのカプセル化関数に変える。

let defaultValue = { name: 'tanaka', age: 10 };

const getDefaultUser = () => defaultValue;
const setUser = (arg) => defaultValue = arg;

パラメータオブジェクトの導入

これは、引数が多かったり、引数として渡すデータ項目が他の関数と同じでバケツリレーのように渡されている時に有効となる。

いわゆるこういったデータの塊を「データの群れ」としており、単一のデータ構造に置き換えるのが有効とされている。

※データをまとめるにはデータ項目間の関係を明示することができるようになる

手順

  1. クラスにする
  2. ひとまとめにする関数の関数宣言の変更で、そのパラメータを追加する
  3. 新たな構造体の正しいインスタンスを渡すように各呼び出し側を修正
  4. 元のパラメータを削除

const schoolName = "test高校";
const user = [
  { name: "tanaka", age: 10 },
  { name: "sato", age: 10 },
  { name: "yamada", age: 10 },
  { name: "kato", age: 10 }
];
const prefecture = 'tokyo'

function getInfo(schoolName, user, prefecture) {
  return `${schoolName}${prefecture}にあります`;
}

function getUser(schoolName, user) {
  return `${schoolName}${user.length}人います`;
}

class School {
  constructor(name, user, prefecture) {
    this.name = name;
    this.user = user;
    this.prefecture = prefecture;
  }
}

const school = new School(schoolName, user, prefecture);
function getInfo(schoolName, user, prefecture, school) {
  return `${schoolName}${prefecture}にあります`;
}

function getUser(schoolName, user, school) {
  return `${schoolName}${user.length}人います`;
}

function getInfo(school) {
  return `${school.name}${school.prefecture}にあります`;
}

function getUser(school) {
  return `${school.name}${school.user.length}人います`;
}

こうすることによって、Schoolクラスに関することはメソッドを集約することも可能となる。

naoki matsuzakinaoki matsuzaki

関数郡の変換への集約

一つのデータから、様々な派生情報値を計算するのはさまざまな場所で利用される。
そういった派生値計算はまとめておく方が良いという考え。

関数郡を集約したい理由

  • 重複記述を避ける
  • データと関数を近い位置に置いておかないと関数を見つけるのが難しくなる

やることは複数の関数を一つの関数に集約させ、データと関数を同じオブジェクトに入れること

手順

  1. 変換されるレコード(データ)を入力とし、同じ値を返す変換関数を作る
  2. その変換関数に入れ込みたい関数を追加する
  3. レコードには、その入れ込んだ関数フィールドを設ける

const reading = {
  customer: "ivan", quantity:10, month: 5, year: 2017
};

あるところでは
client1

const baseChange = baseRate(reading.month, reading.year) * reading.quantity;

別の箇所では
client2

const base = (baseRate(reading.month, reading.year) * reading.quantity);
const taxableCharge = Math.max(0, base - taxThreshold(reading.year));

と色々な箇所で利用されている。

一旦関数で置き換えます。

function calculateBaseCharge(reading) {
  return baseRate(reading.month, reading.year) * reading.quantity;
}

client1

const baseChange = calculateBaseCharge(reading);

client2

const taxableCharge = Math.max(0, calculateBaseCharge(reading) - taxThreshold(reading.year));

となります。

ここから関数郡に変換していきます。

function enrichReading(original) {
  const result = _.cloneDeep(original);
  result.baseCharge = calculateBaseCharge(result);
  return result;
}

こうすることで下記はこう変換できます。

client1

const result = enrichReading(reading);
const baseChange = result.baseCharge;

client2

const result = enrichReading(reading);
const taxableCharge = Math.max(0, result.baseCharge - taxThreshold(reading.year));
naoki matsuzakinaoki matsuzaki

フェーズの分離

こちらはすっと頭に入りませんでした。

一つのコードが複数の処理を行っている場合は、別々のモジュールに分離しろというお話しです。

処理をフェーズで分けるのは一般的なので、関数の中で複数処理が行われていないかを注視する必要があります。

手順

  1. フェーズの異なるコードを関数として抽出する
  2. 抽出した関数に渡すための中間データ構造を導入
  3. 抽出した関数のパラメータを見て、前半のフェーズでも使われているようだったら中間データ構造へ移す
  4. 前半フェーズのコードを関数の抽出で追い出し、中間データ構造を返すようにする

中間データ構造に入れるかどうかの見極めについて

  • 前半のフェーズで作られているかどうか
  • 前半のフェーズで使われているかどうか

つまり、大元の関数で引数として渡しているものを後半のフェーズで使うだけなら、中間データ構造に入れる必要がないということ

naoki matsuzakinaoki matsuzaki

オブジェクトによるプリミティブの置き換え

通常、値は数値や文字列そのままで使うことが多いが、色々な処理が入ってくること、プリミティブそのもので扱わない方が良い。

データを新たにクラスとして作ることを検討する。

手順

  1. 変数のカプセル化を実施
  2. データ値のための単純な値クラスを生成
  3. 値クラスのインスタンスを作るように大元のクラスのsetterを変更
  4. インスタンスのフィールドに値を格納するようにする
  5. 大元のクラスのgetterを変更し、値クラスのgetterに変更する

すこしややこいが、たとえばOrderクラスに渡すデータの中にpriorityという値がある。
このpriorityが色々な処理によって変換して扱うことが多くなる可能性もあるため、priorityの値自体をクラス化してしまおうという考え方である。

Orderクラスにpriorityの値のgetterとsetterを用意

class Order{
  constructor(data){
    this.priority = data.priority
  }

  get priority() {return this._priority};

  set priority(aString) {this._priority = aString};
}

そのあとにPriorityクラスを作成

class Priority {
  constructor(value) {
    this._value = value;
  }

  toString() {return this._value};
}

OrderクラスがこのPriorityクラスを使うように修正します。

class Order{
  constructor(data){
    this.priority = data.priority
  }

  get priority() {return this._priority};

  get priorityString() {
    return this._priority.toString()
  };

  set priority(aString) {
    this._priority = new Priority(aString)
  };
}

こうすることで、下記のように利用できます。

const data = {priority: 10, name: 'tanaka'};
const order = new Order(data);

order.priority.toString()

priorityへのアクセスは、Priorityクラスを使うようにしています。

メモ

constructorのthis.priority = data.priorityで値をセットしているが、これは、setterが呼ばれる。
この時にsetterではPriorityクラスインスタンスを生成し_priorityにセットしているため、getterではPriorityインスタンスが取得できるようになっている

naoki matsuzakinaoki matsuzaki

問い合わせによる一時変数の置き換え

一時変数は関数にするとよい。

  • パラメータとして渡さなくて良くなる
  • 似通った関数で計算ロジックが重複するのを避けれる

このリファクタリングはクラス内で行うことを推奨している。

対象は、一時変数として役割を持つもの。あらかじめ計算された結果を参照するためだけの変数に限る。

class Order{
  constructor(quantity, item){
    this.quantity = quantity
    this.item = item
  }

  get price() {
    const basePrice = this.quantity * this.item;
    let discountFactor = 0.98;
    if (basePrice > 1000) discountFactor -= 0.03;
    return basePrice * discountFactor;
  }
}

このbasePriceとdiscountFactorをgetter関数にしていく

class Order{
  constructor(quantity, item){
    this.quantity = quantity
    this.item = item
  }

  get price() {
    return this.basePrice * this.discountFactor;
  }

  get basePrice() {return this.quantity * this.item};

  get discountFactor() {
    return this.basePrice > 1000 ? 0.95 : 0.98;
  }
}

naoki matsuzakinaoki matsuzaki

クラスの抽出

大きくなりすぎたクラスを抽出する(分ける)方法

  • データとメソッドの一部をまとめて別のクラスにできないか
  • 互いに強く依存してる関係のもの

手法

  1. どのように切り出すか?を検討
  2. 切り出した新たなクラスを作成
  3. 元のクラス生成時に新たな子クラスも作るように修正し、アクセスできるようにしておく
  4. getterで新たなクラスのgetterにアクセスするようにする

class Person {
  constructor(data) {
    this._name = data.name;
    this._officeAreaCode = data.officeAreaCode;
    this._officeNumber = data.officeNumber;
  }

  get name() {return this._name}
  set name(args) {this._name = args}

  get telephoneNumber() {return `${this.officeAreaCode} ${this.officeNumber}`}

  get officeAreaCode() {return this._officeAreaCode}
  set officeAreaCode(args) {this._officeAreaCode = args}

  get officeNumber() {return this._officeNumber}
  set officeNumber(args) {this._officeNumber = args}
}

この中で、電話番号の振る舞いを切り出す。

class Person {
  constructor(data) {
    this._name = data.name;
    this._officeAreaCode = data.officeAreaCode;
    this._officeNumber = data.officeNumber;
    this._telephoneNumber = new TelephoneNumber(data);
  }

  get name() {return this._name}
  set name(args) {this._name = args}

  get telephoneNumber() {return this._telephoneNumber.telephoneNumber}

  get officeAreaCode() {return this._telephoneNumber.areaCode}
  set officeAreaCode(args) {this._telephoneNumber._officareaCodeeAreaCode = args}

  get officeNumber() {return this._telephoneNumber.number}
  set officeNumber(args) {this._telephoneNumber.number = args}
}

class TelephoneNumber {
  constructor(data) {
    this._officeAreaCode = data.officeAreaCode;
    this._officeNumber = data.officeNumber;
  }
  get telephoneNumber() {return `${this.areaCode} ${this.number}`}
  get areaCode() {return this._officeAreaCode}
  set areaCode(args) {this._officeAreaCode = args}
  get number() {return this._officeNumber}
  set number(args) {this._officeNumber = args}
}

こうすることによって、officeAreaCodeやofficeNumberの関数名は変わらず参照先がTelephoneNumberのareaCodeやnumberとなっているため、影響を受けない。

また、telephoneNumberに関してもTelephoneNumberクラスのメソッドを参照しているため、影響受けずにクラスの分離が可能となる。

この方法を取ると、Personクラスを利用している箇所の修正が不要となる。

naoki matsuzakinaoki matsuzaki

委譲の隠蔽

クライアントがクラスが別のクラスの値を取得するとなった場合、委譲先のクラスのことについても知っておかなければならない状態となる。

クライアントは最初のアクセスしたクラスのみを知るだけにするようにする。

手順

  1. 委譲先のオブジェクトの各メソッドに対応するメソッドを大元のクラスに追加する
  2. クライアントは大元のクラスを呼ぶようにする
  3. 委譲先へのアクセスがなくなったら、委譲先のオブジェクトへのアクセサを取り除く

委譲 の隠蔽の反対が仲介人の削除です。

naoki matsuzakinaoki matsuzaki

ステートメントの関数内への移動と呼び出し側への移動

関数内への移動

特定の関数を呼び出すたびに同じコードが実行されている時は、反復コードを関数に組み込むことを検討する。

反復コードの変更が生じても1箇所で済む。

ステートメントの移動は、関数の一部とみなす方が理解しやすい場合

呼び出し側への移動

集約されたものが、異質なものが混じり合った状態になることもある。
その他、複数箇所で利用していた共通の振る舞いを一部の呼び出しに対してだけ変更する必要が出てきた場合。

手順としては、呼び出し側に移動しないもので関数を別名で作成し、呼び出し側移動ステートメントを呼び出し側に記載する。

テストし、問題なければ関数名を戻す。

関数呼び出しによるインラインコードの置き換え

let appliesToMass = false;
for(const of states) {
     if (s === "MA") appliesToMass = true;
}
↓
appliesToMass = states.includes("MA");

関数を利用することで、複数の振る舞いをまとめることができる。
良い関数名ならインラインコードと置き換えた時に意味をなすはず。

使える関数を知っておくことが重要

ステートメントのスライド

const pricingPlan = retrievePricingPlan();
const order = retreiveOrder();
let charge;
const chargePerUnit = pricingPlan.unit;const pricingPlan = retrievePricingPlan();
const chargePerUnit = pricingPlan.unit;
const order = retreiveOrder();
let charge;

互いに関係する処理が並ぶ様にするとコードが理解しやすくなる。

同じデータ構造にアクセスするのが複数ある場合は、それらだけをまとめるべき。

ステートメントのスライドでひとまとめにすることで、関数の抽出も可能となる。

naoki matsuzakinaoki matsuzaki

ループの分離

これは私も勘違いしていたが、ループの回数はできるだけひとまとめにする様にしていた。

しかし、ループは分離するべきと述べてある。
その理由

  • 最適化は別の話でコードを綺麗にした後に行えば良い
    • リファクタリングと最適化は分離する
  • ループの修正時、必ず全ての処理内容を追っかけないといけない(例えば二つのことをしていたらどちらも把握しなければならない)
    • 分離することで変更すべき処理だけを理解すれば良くなる
let averageAge = 0;
let totalSalary = 0;
for (const p of people) {
    averageAge += p.age;
    totalSalary += p.salary;
}

averageAge = averageAge / people.length;

これを分離する

let averageAge = 0;
for (const p of people) {
    averageAge += p.age;
}

let totalSalary = 0;
for (const p of people) {
    totalSalary += p.salary;
}

ある程度ループが塊で存在していたら、関数の抽出をし、関数化する。

naoki matsuzakinaoki matsuzaki

パイプラインによるループの置き換え

反復処理でループを使う方法と、コレクションのパイプラインをつ勝つ方法がある。
パイプラインを使うことで各処理を一連の操作として記述可能となる。

JavaScriptでは、「map」や「filter」など。

数としては結構あるので、確認して損はなさそう。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array

const names = [];

for (const i of input) {
    if(i.job === 'programer'){
        names.push(i).name;
    }
}

↓
input.filter(i => i.job === 'programer').map(i => i.name);

デッドコードの削除

いつか使うかもしれないということで、コードを残すことがあるが、辞める。

未使用コードはソフトウェアの動作を把握しようとする際の大きな足かせになる。

naoki matsuzakinaoki matsuzaki

変数の分離

変数に何度も代入することがある。
何度も代入設定することは、そのメソッドにおいて、変数が複数の責務を持っていることと同意となる。

可能ならば変数を分離した後の新しい変数は変更不可とするのが良い。

let temp = 2 * (height + width);
console.log(temp);
temp = height * width;
console.log(temp);const perimeter = 2 * (height + width);
console.log(perimeter);
const area = height * width;
console.log(area);

フィールド名の変更

データ構造は、何が起こっているかを理解するための鍵となる、それほどデータ構造は重要なものとなる。

手順

  1. レコードがカプセル化していない場合は、レコードのカプセル化をする
  2. オブジェクト内の非公開フィールドの名前を変更し、内部メソッドを調整
const organization = {name: "America", country: "USA"}

これをカプセル化しておく

class Organization {
    constructor(name, country) {
        this._name = name;
        this._country = country;
    }

    get name() { return this._name};
    get country() { return this._country};

    set name(aString) { return this._name = aString}
    set country(aCountryCode) { return this._country = aCountryCode}
}

const organization = new Organization("America", "USA")

その後、フィールド名を変更する

class Organization {
    constructor(name, country) {
        this._title = name;
        this._country = country;
    }

    get title() { return this._title};
    get country() { return this._country};

    set title(aString) { return this._title = aString}
    set country(aCountryCode) { return this._country = aCountryCode}
}

こうすることによって、変更する箇所を一つに集約できる。

naoki matsuzakinaoki matsuzaki

プログラムを複雑にしている原因になる一般的なものとして「条件分岐」の複雑化です。

ここで覚えておくべきこととして、

  • 条件記述は難しさを増大させる
  • 大きなブロックのコードは常にコードを分解し、それぞれの意図に沿って名付けた関数の呼び出しに置き換える

つまり、条件文で複数条件を確認する様な場合は、必ず一つの関数に置き換えて意味を持たせた関数をif文に渡す様にする。

条件記述の統合

例えば、複数の条件判定があり、それぞれの条件は異なるのに、結果のアクションが同じ場合
→その時は、「and」や「or」を使って単一の結果を返す一つの条件判定に統合するべきということです。

理由

  • まとめることで行なっている判定が実は一つだという意図を明示できる
  • 関数の抽出の準備になる

しかしこれは一概にも言えず、行わない理由にもなる。
複数の判定が本当に別々で、単一の判定と考えるべきならするべきではない。

if (anEmployee.seniority < 2) return 0;
if (anEmployee.monthsDisabled > 12) return 0;
if (anEmployee.isPartTime) return 0;

↓改善後

if (isNotEligibleForDisability()) return 0;

function isNotEligibleForDisability() {
  if (anEmployee.seniority < 2
      || anEmployee.monthsDisabled > 12
      || anEmployee.isPartTime) return 0;
}

いずれの条件判定にも副作用がないことを確認しておく。
ある場合は、「問い合わせと更新の分離」を行う。

問い合わせと更新の分離はこのあと出てきますが、「コマンドとクエリの分離」という概念があるよう、何かを取得する処理と何かを更新する処理が同じ関数にある場合、その関数は実行されることで副作用として別の値が更新されてしまいます。

コマンドとクエリの分離に則り、取得と更新を別関数に分けることが大切となります。

naoki matsuzakinaoki matsuzaki

ポリモーフィズムによる条件記述の置き換え

複雑な条件ロジックは特に難解です。

ここで、クラスとポリモーフィズムを用いると、この分離をより明快に表現可能となる。

よくあるのは、一揃いの方を作り、それぞれの型で同じ関数名ではあるものの、やっていることは別であるということをさせます。

手順

少し難しいので、例を示します。

例えば、鳥を例に挙げるとこの様に鳥のタイプによって処理が異なる場合があります。

function plumage(bird) {
  switch (bird.type) {
    case 'EuropeanSwallow':
      return 'average';
    case 'AfricanSwallow':
      return (bird.numberOfCoconuts > 2) ? 'tried' : 'average';
    case 'NorwegianBlueParrot':
      return (bird.voltage > 100) ? 'scorched' : 'beautiful';
    default:
      return 'unknown';
  }
}

function airSpeedVelocity(bird) {
  switch (bird.type) {
    case 'EuropeanSwallow':
      return 35;
    case 'AfricanSwallow':
      return 40 - 2 * bird.numberOfCoconuts;
    case 'NorwegianBlueParrot':
      return (bird.isNailed) ? 0 : 10 + bird.voltage /10;
    default:
      return null;
  }
}

ここで鳥の種類によって振る舞いを変えるため、クラスを作成します。

function plumage(bird) {
  return new Bird(bird).plumage;
}

function airSpeedVelocity(bird) {
  return new Bird(bird).airSpeedVelocity;
}

class Bird {
  constructor(birdObject) {
    Object.assign(this, birdObject);
  }

  get plumage() {
    switch (bird.type) {
      case 'EuropeanSwallow':
        return 'average';
      case 'AfricanSwallow':
        return (bird.numberOfCoconuts > 2) ? 'tried' : 'average';
      case 'NorwegianBlueParrot':
        return (bird.voltage > 100) ? 'scorched' : 'beautiful';
      default:
        return 'unknown';
    }
  }

  get airSpeedVelocity() {
    switch (bird.type) {
      case 'EuropeanSwallow':
        return 35;
      case 'AfricanSwallow':
        return 40 - 2 * bird.numberOfCoconuts;
      case 'NorwegianBlueParrot':
        return (bird.isNailed) ? 0 : 10 + bird.voltage /10;
      default:
        return null;
    }
  }
}

ここで、それぞれの鳥の種類に対応するサブクラスと適切なサブクラスをインスタンス化するファクトリ関数も作成します。

function plumage(bird) {
  return createFactory(bird).plumage;
}

function airSpeedVelocity(bird) {
  return createFactory(bird).airSpeedVelocity;
}

function createFactory(bird) {
  switch (bird.type) {
    case 'EuropeanSwallow':
      return new EuropeanSwallow(bird);
    case 'AfricanSwallow':
      return new AfricanSwallow(bird);
    case 'NorwegianBlueParrot':
      return new NorwegianBlueParrot(bird);
    default:
      return new Bird(bird);
  }
}

class EuropeanSwallow extends Bird {
  get plumage() {
    return 'average';
  }
}
naoki matsuzakinaoki matsuzaki

アサーションの導入

アサーションによって前提を明示することができる様になります。

アサーションは、実行時点でプログラムがある状態になっているはずということを読み手に伝えることもできます。

特にエラーの出所の特定が難しい場合に役立つ。

naoki matsuzakinaoki matsuzaki

フラグパラメータの削除

フラグ引数は、呼び出し元が呼び出し先に対してどのロジックを実行して欲しいかを指示するためのパラメータとして使われます。

フラグ引数は好ましくありません。どの関数呼び出しが使えるか、それをどう呼び出せば良いのかを理解するプロセスが煩雑となる。

中でもboolean型は厄介。一体何をしているのかを呼び出し元から理解できない、関数の中を追わなければならない。

この様な場合は、条件記述の分解を適用させ、呼び出し元で分解した関数を呼び出す様にするべきということです。

function deliveryData(order, isRush) {
  if (isRush) {
    // ラッシュ時の注文処理
  } else {
    // 通常の注文処理
  }
}
naoki matsuzakinaoki matsuzaki

setterの削除

setterは外部で値を変えることができますが、オブジェクトを生成した後に値を変えたくない場合は、setterを削除します。

例えば、Personクラスでidは変えたくない場合は、オブジェクト生成時に指定する。

class Person {
  constructor(id) {
    this._id = id;
  }

  get name() {
    return this._name;
  }
  set name(args) {
    this._name = arg;
  }
  get id() {
    return this._id
  }
}

コマンドによる関数の置き換え

function score(candidate, medicalExam, scoringGuide) {
  let result = 0;
  let healthLevel = 0;
  // 長いコード
}

class Score {
  constructor(candidate, medicalExam, scoringGuide) {
    this._candidate = candidate
    this._medicalExam = medicalExam
    this._scoringGuide = scoringGuide
  }

  execute() {
    this._result = 0;
    this._healthLevel = 0;
    // 長いコード
  }
}

関数自身をカプセル化することが有用な場合もある。

その様なオブジェクトを「コマンドオブジェクト」または単に「コマンド」と呼びます。

長い関数がある時、その関数を中で分解することも可能です。

コマンドは複雑な計算を扱うための強力なメカニズムを提供してくれる一方、関数を呼び出して仕事させたいだけで、その関数が複雑でないなら、コマンドオブジェクトにする必要性はない。