【コード改善】今日から使えるリファクタリングテクニック12選

refactoring-techniques

リファクタリング。プログラミングにおいて、コードの内部構造を改善するプロセスです。

大規模変更や、新機能追加のとき、リファクタリングを行うことが効果的です。ただ、リファクタリングは、そのような大きなイベントのときだけ行うものではありません。

日常のコーディングにおいても、継続的なリファクタリングでコードを改善する。大切です。そんな日常のリファクタリングで、すぐに使えるテクニックをご紹介します。

目次

リファクタリングとは?

書籍「リファクタリング」

リファクタリングという概念は、マーティン・ファウラー氏の書籍「リファクタリング」が世に広めたと言われます。

初版1999年、その後第2版が2018年に発行(日本語版は2019年発行)。その考え方から具体的なテクニックの解説まで、リファクタリングの教科書といえる書籍です。

マーティン・ファウラー氏は、Webでもリファクタリングの情報発信を行っています。書籍には収まりきらない、更に深い情報が得られます。(注:全編英語です)

リファクタリングの定義

書籍「リファクタリング」では、このように定義されています。

リファクタリング:外部から見たときの振る舞いを保ちつつ、理解や修正が簡単になるように、ソフトウェアの内部構造を変化させること

振る舞いを保ちつつ」がポイント。

リファクタリングを行っても、外からみれば何も変化がないように見えなければいけません。ほかの工学分野ではあまり見られない、ソフトウェア開発独自の考えです。

日常に活かすリファクタリング

リファクタリングは、日常からこまめに行うことが重要です。大事な考え方は、この2つ。

  • それは分かりやすいコードか?
  • それは直しやすいコードか?

この考えを、コーディング中に頭に置いておきましょう。

すぐに使えるリファクタリングカタログ12選

書籍「リファクタリング」のカタログから、すぐにコーディングで役立つテクニック12個をご紹介します。

コードはすべてJavaScriptで書かれています。が、他の言語でも意味あいは同じです。

サンプルコードは、書籍およびWebサイトで紹介されているものに、管理人が必要に応じてコメント等を追加しています。

PART1:はじめの一歩

まずは、比較的手軽にできるリファクタリングから始めましょう。

はじめの一歩
  1. 変数の使いまわしをやめる
  2. オブジェクト型に置き換える
  3. 値オブジェクトに置き換える
  4. コレクションの参照はコピーを返す

これをするでも、プログラムの見通しはだいぶ良くなります。

変数の使いまわしをやめる

まずは変数の使い方から。変数を使いまわすのを止めましょう

変数の分離
// ???
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);

変数は、途中の結果を参照するときに使います。これに同じものを使いまわすと、変数の意味がぶれ、コードが分かりにくくなります

使いまわしてしまう理由はこれでしょう。『名前を考えるのが面倒くさい』から。

ダメです。良い名前をつけなければ、コードの整理もままなりません。意地でも正しい名前をつけましょう

とにかく変数にconstをつけまくる(不変にする)ことから意識してみましょう。constで損することは何もありません。

オブジェクト型に置き換える

プリミティブ型で定義されている変数を、オブジェクト型に置き換えましょう

オブジェクトによるプリミティブの置き換え
// 優先度(priority)が、高(high)または急ぎ(rush)の、注文リスト(order)を取得
orders.filter(o => "high" === o.priority
                || "rush" === o.priority);

// 優先度クラス
class Priority {
  higherThan(order) {...}

// 優先度(priority)が、通常より高い注文リスト(order)を取得
orders.filter(o => o.priority.higherThan(new Priority("normal")))

大抵のプログラミング言語には、次の2種類の型を持っています。

プログラミング言語の2種類の型
  • プリミティブ型 int、double、stringといった、言語であらかじめ準備している型
  • オブジェクト型 structやclassなど、ユーザーが定義できる型

サンプルにある、priority(優先度)という変数。普通は文字列プリミティブ型で定義するでしょう。

単純そうに見えるそのデータも、「優先度を比較する」など、特殊な振る舞いが必要になります。そのようなロジックは、コードのあちこちに、あっという間に増殖していきます。

そういった時は、プリミティブで定義していたデータを、新たなクラスで置き換えます。そして特殊な振る舞いをクラスに付与します。

なんのための修正か、はじめはピンとこないかも。しかし、そのデータが単なる数値や文字列ではなく、意味のあるデータに見える。これがコードの見通しに、絶大な効果をもたらすようになります。

もちろん、何から何までオブジェクト型にするのもナンセンス。重要なデータを選定して適用するのが良いでしょう。

値オブジェクトに置き換える

変更可能なオブジェクトを、変更ができないオブジェクトに置き換えよう、というもの。

参照から値への変更
class Product {
  // 割引を適用する
  applyDiscount(arg) {
    // priceオブジェクトの価格プロパティを更新する
    this._price.amount -= arg;
  }

class Product {
  // 割引を適用する
  applyDiscount(arg) {
    // 割引価格を適用した新しいpriceオブジェクトを生成する
    this._price = new Money(this._price.amount - arg, this._price.currency);
  }

オブジェクト型定義のポリシーのひとつに、値オブジェクト(Value Object)というものがあります。

値オブジェクト(Value Object)
  • プロパティの変更は不可
  • オブジェクト生成時のみ内部プロパティを設定できる

priceオブジェクトを考えましょう。オブジェクトを更新するなら、普通はこう。

  • priceオブジェクトのプロパティを更新する

問題はなさそう。ただやっかいなのは、どこかで知らぬ間に値が更新されてしまう可能性があること。

そこで、priceオブジェクトを値オブジェクトにします。すると更新はこうなります。

  • プロパティを変更した)新しいpriceオブジェクトを生成する

これなら、他でいつのまにか更新されることはありません。

この性質、特に分散システムや並行システムにおいて有効です。あるオブジェクトを複数処理で同時に参照するようなことが起こっても、値オブジェクトであれば不整合が起こりません

コレクションの参照はコピーを返す

「コレクション」とは、オブジェクトの集合を扱うオブジェクト。Array、List、Vector、などがそうですね。

オブジェクトが保有するコレクションを、外部から参照する。よくあるシチュエーションです。

この話は、外部から参照するときに、コレクションをそのまま返さずにコピーを返す、という話です。

コレクションのカプセル化
// 人クラス
class Person {
  // 授業リスト取得(内部で保持するリストをそのまま返却してしまう)
  get courses() {return this._courses;}
  // 授業リストを設定
  set courses(aList) {this._courses = aList;}

// 人クラス
class Person {
  // 授業リスト取得(コピーを作ってそれを返却する)
  get courses() {return this._courses.slice();}
  // 授業を追加
  addCourse(aCourse)    { ... }
  // 授業を削除
  removeCourse(aCourse) { ... }

クラス内部のコレクションの公開方針は、大きく以下の4種類。

コレクション返却方針
  1. 内部コレクションをそのまま返す
  2. 内部コレクションを返さず、フィールド操作メソッドを用意する
  3. 内部コレクションを読み取り専用で返す
  4. 内部コレクションのコピーを返す
  • そのまま返す。簡単ですが推奨されない方法。外部から意図しない操作で書き換えられる可能性があります。
  • コレクションを返さず、特定のメソッドを介してしか操作させない。カプセル化には効果的。ただ言語標準の便利なListやVectorの機能を存分に活かせません
  • 読み取り専用で返す。外部で更新される危険はなくなるので有用ですが、本当の読み取り専用にできるのかは言語仕様にも依存します。
  • コピーを返す。これが最も問題のない方法です。コピーなので、それがどうされようが、元のコレクションには影響が及びません。

コピーを返す、とした場合、コピーによるパフォーマンス低下が起こるのでは?大丈夫です。現在の高性能なマシンスペックからすれば、まずパフォーマンスへの影響はありません。

PART2:ループと条件分岐の単純化

プログラムの複雑さの多くは、ループと条件分岐に起因します。

ループと条件分岐の単純化
  1. ループで複数のことをしない
  2. ガード節で入れ子条件をなくす
  3. 特殊ケースオブジェクトを導入する

構造が理解しやすくなるよう、見直しましょう。

ループで複数のことをしない

ループ処理でついやりがちなのが、1回のループ中で複数のことを同時に行うこと。ひとつのループでやることはひとつにしましょう。

ループの分離
// 平均年齢
let averageAge = 0;
// 給与総額
let totalSalary = 0;
// 従業員数分ループ
for (const p of people) {
  averageAge += p.age;
  totalSalary += p.salary;
}
// 平均年齢を求める
averageAge = averageAge / people.length;

// 給与総額を求める
let totalSalary = 0;
for (const p of people) {
  totalSalary += p.salary;
}

// 平均年齢を求める
let averageAge = 0;
for (const p of people) {
  averageAge += p.age;
}
averageAge = averageAge / people.length;

ひとつのループにまとめてしまう心理は、以下のようなもの。

  • 処理を、ループまで含めたブロックとして捉えられていない
  • 何回もループを回すのは、効率が悪そう

処理の捉え方は、ループブロックのコメントに、処理の目的をつけるようにすると、見えやすくなります。効率に関しては、現在のコンピューターの処理能力からすれば、まず問題になりません。

ループの分離がうまくできると、さらに関数型コレクション操作にも発展させやすくなります。

ガード節で入れ子条件をなくす

条件付きロジックは、プログラムをいとも簡単に複雑にします。

条件分岐が多段階の入れ子構造になっていませんか?ガード節で入れ子を解消しましょう

ガード節による入れ子の条件記述の置き換え
// 支払額計算
function getPayAmount() {
  let result;
  // 他界の場合
  if (isDead)
    result = deadAmount();
  else {
    // 離職の場合
    if (isSeparated)
      result = separatedAmount();
    else {
      // 退職の場合
      if (isRetired)
        result = retiredAmount();
      // 在籍の場合
      else
        result = normalPayAmount();
    }
  }
  return result;
}

// 支払額計算
function getPayAmount() {
  // 他界の場合
  if (isDead) return deadAmount();
  // 離職の場合
  if (isSeparated) return separatedAmount();
  // 退職の場合
  if (isRetired) return retiredAmount();
  // 在籍の場合
  return normalPayAmount();
}

条件付きロジックには、大きく2つのスタイルがあります。

条件記述のスタイル
  • thenとelseの、いずれもが正常動作
  • thenとelseの、どちらかが正常動作でどちらかが例外操作

ガード節が登場するのは、2番目のパターン、どちらかが例外操作の場合です。

リファクタリング前も後も、やっていることは変わりません。違いはこれ。

  • リファクタリング前の場合 リターンは最後にひとつ
  • リファクタリング後の場合 例外的な場合に即時リターン

例外的になったらさっさと脱出。これが、ガード節です。

ガード節を導入すると、コードのネスト(段落)が浅くなり、見た目で分かりやすくなります。

このテクニックは、「早期リターン」(Early Return)という呼び方で知られています。

特殊ケースオブジェクトを導入する

データが、ある特殊なパターンの時だけ、特殊なことをしたい。よくあるケースです。

あちこちに判定を入れるのではなく、特殊な動きをするオブジェクトを導入すると、スッキリします。

特殊ケースの導入
// 顧客クラス
class Customer {
  get name() {...}
};

// 場所クラス
class Site {
  get customer() { return this_.customer; }
};


// 利用側
// 顧客名を取得する(顧客が未知なら名前を"居住者とする")
if (aCustomer === "unknown") {
  customerName = "居住者";
} else {
  customerName = aCustomer.name;
}

// 顧客クラス
class Customer {
  get name() {...}
};

// 未知顧客クラス
class UnknownCustomer {
    get name() {return "居住者";}
};

// 場所クラス
class Site {
  get customer() {
    // 顧客が未知なら未知顧客オブジェクトを、既知なら保持中の顧客オブジェクトを返す
    return (this_.customer === "unknown") ? new UnknownCustomer() : this._costomer;
  }
};


// 利用側
// 顧客名を取得する
const customerName = aCustomer.name;

こんな場合を考えます。

  • サービスを提供する「場所」がある
  • その場所にいる「顧客」を管理する
  • 通常は顧客が存在する。でも、まだ顧客がいない場合もある
  • 顧客がいない場合、名称として「居住者」と表示する

顧客がいなければ…という条件をあちこちで処理するのは大変です。

そのような場合に、「顧客がいないという状態の顧客オブジェクト(UnknownCustomer)」を導入すると、使う側は分かりやすくなります。

「オブジェクトがない」という状態、JavaScriptではunknownと表現しますが。Javaなどの他の言語ではNullと表現するものもあります。なのでこのパターンを「Nullオブジェクトパターン」と呼んだりもします。

PART3:APIのリファクタリング

APIは、モジュールや関数の接続部です。

APIのリファクタリング
  1. 問い合わせと更新のインターフェースを分離する
  2. インターフェースからフラグパラメータを削除する
  3. ファクトリ関数でオブジェクト生成する

APIを、分かりやすく使いやすくしましょう。

問い合わせと更新のインターフェースを分離する

関数には2種類あります。副作用のある関数とない関数です。副作用とは、その関数を呼ぶことで、どこかのなにかしらの情報が変更される、というもの。

副作用のある関数と、副作用のない関数は、明確に分けましょう

問い合わせと更新の分離
// 未払い総額を取得して請求書を送信する
function getTotalOutstandingAndSendBill() {
  const result = customer.invoices.reduce((total, each) => each.amount + total, 0);
  sendBill();
  return result;
}

// 未払い総額を取得する
function totalOutstanding() {
  return customer.invoices.reduce((total, each) => each.amount + total, 0);  
}
// 請求書を送信する
function sendBill() {
  emailGateway.send(formatBill(customer));
}

副作用のない関数は、何回呼び出しても問題は起こりません。なんの情報も変化しないからですね。こういう関数は、好きなだけ何度も呼び出して使える安心感が生まれます。

特に、値を返す関数は、副作用を持たせないほうがよいです。情報と取ることだけに特化させるのです。

この考えは「コマンドクエリ分離原則(CQS:Command Query Separation)」と呼ばれます。 

インターフェースからフラグパラメータを削除する

関数のインターフェースにある、何かしらの機能をON/OFFするようなパラメータ。フラグ引数と呼びます。

フラグ引数は止めましょう。それをどう呼び出せばいいのか、理解がとても難しくなります。

フラグパラメータの削除
// 寸法を設定
function setDimension(name, value) {
  // 縦の寸法なら
  if (name === "height") {
    this._height = value;
    return;
  }
  // 横の寸法なら
  if (name === "width") {
    this._width = value;
    return;
  }
}

// 縦の寸法を設定
function setHeight(value) {this._height = value;}
// 横の寸法を設定
function setWidth (value) {this._width = value;}

フラグ引数を止めて、それぞれの役割をもつ関数を別々に定義すると、混乱がなくなります。

ファクトリ関数でオブジェクト生成する

オブジェクトが生成されるとき、コンストラクタという特殊関数が呼ばれます(たいていのオブジェクト指向言語では)。

コンストラクタで、プロパティの初期値を与えたりなど、オブジェクトの初期準備をします。このとき、場合によっては、条件に応じて様々なパターンのオブジェクトを作りたいときがあります。

こういうときに、ファクトリ関数を準備すると融通が利きます。ファクトリ=工場、オブジェクトを作り出すためにある関数です。

ファクトリ関数によるコンストラクタの置き換え
// 従業員オブジェクトを、'E"(エンジニア)を指定して生成
leadEngineer = new Employee(document.leadEngineer, 'E');

// ファクトリ関数でエンジニアの従業員オブジェクトを生成
leadEngineer = createEngineer(document.leadEngineer);

コンストラクタで条件をつけようとすると、フラグ引数のようなものが必要になってしまいます。

ファクトリ関数ならが、その生成内容に応じた名称をつけられるので、その生成の意図も明確になります。

PART4:継承の取り扱い

継承は、オブジェクト指向プログラミングでよく知られる機能です。

継承の取り扱い
  1. 委譲を使ってサブクラスを置き換える
  2. 委譲を使ってスーパークラスを置き換える

継承は、有用なメカニズムである反面、誤用もしやすく、そして誤用に気づきにくいのです。

委譲を使ってサブクラスを置き換える

場合によって特別な振る舞いをするオブジェクト。その実装方法として知られるのが、継承です。スーパークラスで共通の振る舞いを実装し、サブクラスで特別な振る舞いを追加します。

継承は強力な仕掛けですが、取り扱いを誤ると混乱を招きます。継承関係を委譲によって置き換えることも検討しましょう、。

委譲によるサブクラスの置き換え
// 注文クラス
class Order {
  // 発送までの日数
  get daysToShip() {
    // 倉庫からの発送日数を返す
    return this._warehouse.daysToShip;
  }
}

// 優先注文クラス(注文クラスを継承)
class PriorityOrder extends Order {
  // 発送までの日数
  get daysToShip() {
    // 優先プランで定義する発送日数を返す
    return this._priorityPlan.daysToShip;
  }
}

// 注文クラス
class Order {
  // 発送までの日数
  get daysToShip() {
    // 優先注文であれば優先プランの、
    // そうでなければ倉庫の発送日数を返す
    return (this._priorityDelegate)
      ? this._priorityDelegate.daysToShip
      : this._warehouse.daysToShip;
  }
}

// 優先注文クラス(注文クラスからの委譲)
class PriorityOrderDelegate {
  // 発送までの日数
  get daysToShip() {
    // 優先プランで定義する発送日数を返す
    return this._priorityPlan.daysToShip
  }
}

継承の欠点と知られるのは、以下の2点。

継承の欠点
  • 一度しか使えない バリエーションを複数持たせたい場合でも、継承は1つのバリエーションしか実現できません。
  • クラス間が密接に関連してしまう スーパークラスの内部構造も含めてサブクラスが引き継ぐため、スーパークラスの変更が簡単にサブクラスを壊してしまう。

この欠点を解消するのが、委譲です。継承を使わずに、個別の振る舞いをする別のクラス(上記の例でいうPriorityOrderDelegate)を導入します。直接の継承関係がなくなるので、独立性が高まります。

この考えは、GoFデザインパターンにおける「クラス継承よりもオブジェクトのコンポジションを優先せよ」という原則で知られています。

更に考えを発展させると、

  • 振る舞いが変わるところだけ抜き出して
  • その部分に継承関係を持たせる

といったこともできます。GoFデザインパターンにおけるStrategyパターンですね。

委譲を使ってスーパークラスを置き換える

継承から委譲へ、の、もう一つのパターン。今度はスーパークラスの置き換えです。

今度は、継承にふさわしくないクラスからの継承を解消する、という対処です。

委譲によるスーパークラスの置き換え
// リストクラス
class List {...}
// スタッククラス(リストクラスの操作をそのまま継承)
class Stack extends List {...}

// リストクラス
class List {...}
// スタッククラス
class Stack {
  constructor() {
    // 内部にリストを保持
    this._storage = new List();
  }
  push(item) {
    this._storage.push(item);
  }
  pop() {
    this._storage.pop();
  }
}

例で示されるのは、Listクラスを継承してStackクラスを作ろうとしている事例。

  • Listは、要素の追加や削除といった操作を持ちます。
  • Stackは、後入れ先出し(LIFO:Last In First Out)の操作で、これも言ってみれば要素の追加や削除です。

一見、Listから継承してもよさそうに見えます。しかし問題となるのは、Stackが、Listの操作をすべて引き継いでしまっていること

Listの操作を使うと、先頭への挿入や中間の削除など、本来Stackで出来るべきではない操作も出来てしまいます

スーパークラスの操作がサブクラスで意味をなさなくなっているケースです。このような、スーパークラスの実装を使いたいだけの継承は、BaseBeanと呼ばれるアンチパターンで知られます。

対処方法はわりと単純。Listクラスから継承するのでなく、Listを内部に持って、それを使えばよいのです。Listの機能をフルに使いつつ、余計な操作を外部から隠すことが可能です。

まとめ

書籍「リファクタリング」の中から、すぐに使えそうなテクニック12選をご紹介しました。

書籍では、ここに紹介した以外のリファクタリングテクニックや、そもそものリファクタリングの考え方など様々な情報が掲載されています。

慣れてきたら、ぜひ、本格的なリファクタリングも学んでみてください。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次