リファクタリング。プログラミングにおける、コードの内部構造を改善するプロセスです。
大規模な変更、新機能の追加、そんなときにリファクタリングは効果的です。ただ、リファクタリングは、大きなイベントとして行うものではありません。
日常から、継続的なリファクタリングで、コードを改善する。大切です。
そんな日常のリファクタリングで、すぐに使えるテクニックをご紹介します。
リファクタリングとは?
書籍「リファクタリング」
リファクタリングという概念は、書籍「リファクタリング」が世に広めました。マーティン・ファウラー氏の名著です。
初版1999年、第2版2018年に発行(日本語版は2019年発行)。
その考え方から具体的なテクニックの解説まで、リファクタリングの教科書といえる書籍です。
マーティン・ファウラー氏は、Webでもリファクタリングの情報発信を行っています。書籍には収まりきらない、更に深い情報が得られます。(注:全編英語です)
リファクタリングの定義
書籍「リファクタリング」では、こう定義されています。
「振る舞いを保ちつつ」が、ポイント。
リファクタリングを行っても、外からみれば何も変化がないように見えなければいけません。ほかの工学分野ではあまり見られない、ソフトウェア開発独自の考えです。
日常に活かすリファクタリング
リファクタリングは、日常からこまめに行うことが重要です。大事な考え方は、この2つ。
- それは分かりやすいコードか?
- それは直しやすいコードか?
コーディング中に頭に置いておきましょう。
すぐに使えるリファクタリングカタログ12選
書籍「リファクタリング」から、すぐにコーディングで役立つテクニック12個をご紹介します。
コードはすべてJavaScriptで書かれています。が、他の言語でも意味するところは同じです。
PART1:はじめの一歩
まずは、比較的手軽にできるリファクタリングから。
- 変数の使いまわしをやめる
- オブジェクト型に置き換える
- 値オブジェクトに置き換える
- コレクションの参照はコピーを返す
これだけでも、プログラムの見通しは、だいぶ良くなります。
変数の使いまわしをやめる
まずは変数の使い方です。変数を使いまわすのを止めましょう。
// (これはなに?)
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で損することは何もありません。
オブジェクト型に置き換える
大抵のプログラミング言語には、次の2種類の型を持っています。
プリミティブ型 | int、double、stringといった、言語であらかじめ準備している型 |
---|---|
オブジェクト型 | structやclassなど、ユーザーが定義できる型 |
プリミティブ型で定義されている変数を、オブジェクト型に置き換えましょう。
サンプルを見てみます。
// 優先度(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")
)
);
priority
は、優先度を表現する、文字列プリミティブ型です。
単純な文字列変数ですが、「優先度を比較する」など、特殊な振る舞いが必要になります。そのようなロジックは、コードのあちこちに、あっという間に増殖していきます。
そういった時は、プリミティブで定義していたデータを、新たなクラスで置き換えます。
ここでは、Priority
クラスを定義し、特殊な振る舞いを付与しています。
なんのための修正か、はじめはピンとこないかも知れません。
そのデータが単なる数値や文字列ではなく、意味のあるデータに見える。これがコードの見通しに、絶大な効果をもたらすようになります。
もちろん、何から何までオブジェクト型にするのもナンセンス。重要なデータを選定して適用するのが良いでしょう。
値オブジェクトに置き換える
オブジェクト型定義のポリシーのひとつに、値オブジェクト(Value Object)というものがあります。
- プロパティの変更は不可
- オブジェクト生成時のみ、内部プロパティを設定できる
変更可能なオブジェクトを、変更ができないオブジェクトに置き換えよう、というものです。
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
);
}
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種類。
- 内部コレクションをそのまま返す
- 内部コレクションを返さず、フィールド操作メソッドを用意する
- 内部コレクションを読み取り専用で返す
- 内部コレクションのコピーを返す
- そのまま返す。簡単ですが推奨されない方法。外部から意図しない操作で書き換えられる可能性があります。
- コレクションを返さず、特定のメソッドを介してしか操作させない。カプセル化には効果的。ただ言語標準の便利なListやVectorの機能を存分に活かせません。
- 読み取り専用で返す。外部で更新される危険はなくなるので有用ですが、本当の読み取り専用にできるのかは言語仕様にも依存します。
- コピーを返す。これが最も問題のない方法です。コピーなので、それがどうされようが、元のコレクションには影響が及びません。
コピーを返すとした場合、心配するのは、コピーによるパフォーマンスの低下。ただ、現在の高性能なマシンスペックを持つコンピューターなら、まずパフォーマンスへの影響はありません。
PART2:ループと条件分岐の単純化
プログラムの複雑さの多くは、ループと条件分岐に起因します。
- ループで複数のことをしない
- ガード節で入れ子条件をなくす
- 特殊ケースオブジェクトを導入する
構造が理解しやすくなるよう、見直しましょう。
ループで複数のことをしない
ループ処理でついやりがちなのが、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を、分かりやすく使いやすくしましょう。
問い合わせと更新のインターフェースを分離する
関数には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));
}
副作用のない関数とは、情報を返すだけの関数です。これは何回呼び出しても問題は起こりません。未払い総額を何回取得したって、同じ結果を返すだけです。
副作用のある関数とは、情報を加工する関数です。これは呼び出すたびに状態が変わるので、何回も呼び出すわけにきません。請求書がたくさん送られても困りますね。
これら2つを明確に分離しましょう。値を返す関数は、副作用を持たせないようにします。情報を取ることだけに特化させるのです。
この考えは「コマンドクエリ分離原則(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:継承の取り扱い
継承は、オブジェクト指向プログラミングでよく知られる機能です。
継承は、有用なメカニズムである反面、誤った利用もしやすく、そして誤っていることに気づきにくいのです。
- 委譲を使ってサブクラスを置き換える
- 委譲を使ってスーパークラスを置き換える
委譲を使ってサブクラスを置き換える
継承は、場合によって特別な振る舞いをするようなオブジェクト、を実装します。
スーパークラスで共通の振る舞いを実装、サブクラスで特別な振る舞いを追加します。
継承は強力ですが、取り扱いを誤ると混乱を招きます。継承関係を委譲によって置き換えることも検討しましょう。
// 注文クラス
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選をご紹介しました。
結局のところどのテクニックも、考えるべきはつぎの2つです。
- それは分かりやすいコードか?
- それは直しやすいコードか?
書籍では、ここに紹介した以外のリファクタリングテクニックや、そもそものリファクタリングの考え方など様々な情報が掲載されています。
慣れてきたら、ぜひ、本格的なリファクタリングも学んでみてください。
コメント