はじめに
表題のリファクタリング手法を学んだので、今回はこれを記事にしたいと思います。
活用前
まずは以下のコードをみてください。
// A*B + A*C の状態を表すコード
class Order {
bool isPremiumMember;
int cartTotal;
bool hasCoupon;
Order(this.isPremiumMember, this.cartTotal, this.hasCoupon);
bool isFreeShipping() {
// A*B + A*C の状態
if ((isPremiumMember && cartTotal >= 2000) || (isPremiumMember && hasCoupon)) {
return true;
}
return false;
}
}
ECサイトなどでの送料無料判定を模した処理です。
if文の条件式(括弧の中の式)もよく見る構造をしていますね。
活用後
では次に以下のコードをみてください。
// リファクタリング前のコード
// A*B + A*C を整理して、A * (B + C) の状態を表すコード
class Order {
bool isPremiumMember;
int cartTotal;
bool hasCoupon;
Order(this.isPremiumMember, this.cartTotal, this.hasCoupon);
bool isFreeShipping() {
// 算術演算の論理で整理: A * (B + C)
return isPremiumMember && (cartTotal >= 2000 || hasCoupon);
}
}
先述の判定式を組み替えています。
これが成り立つのは、
- 算術演算の分配則 $A \times B + A \times C = A \times (B + C)$
こういった法則によるものです。
効能
一見するとこのような変換をする必要を感じにくいですよね。
文量も減ってスッキリしたとはいえ、
コードの意図も活用前と比較して掴み取りにくくなったようにも感じられます。
しかし、このような変換をする意図は実は別にあります。
それは、
算術演算のロジックを適用して隠れた条件式を見つけ出し、その意味の纏まりを活かしてリファクタリングしていく
こういったリファクタリングをしようとしていたのです。
詰まる所、算術演算の活用はリファクタリングのための布石であり、前座です。
その真意はプロダクトコード内のif文が抱える問題点をより分析しやすくし、
より綺麗なコードへと纏め上げることにありました。
どういうことかというと...
// A*B + A*C の状態を表すコード
class Order {
bool isPremiumMember;
int cartTotal;
bool hasCoupon;
Order(this.isPremiumMember, this.cartTotal, this.hasCoupon);
bool isFreeShipping() {
// A*B + A*C の状態
if ((isPremiumMember && cartTotal >= 2000) || (isPremiumMember && hasCoupon)) {
return true;
}
return false;
}
}
元々のこのコードを、
// リファクタリング前のコード
// A*B + A*C を整理して、A * (B + C) の状態を表すコード
class Order {
bool isPremiumMember;
int cartTotal;
bool hasCoupon;
Order(this.isPremiumMember, this.cartTotal, this.hasCoupon);
bool isFreeShipping() {
// 算術演算の論理で整理: A * (B + C)
return isPremiumMember && (cartTotal >= 2000 || hasCoupon);
}
}
こういった変形を行い、
それを契機に、
// リファクタリング後のコード
// A*B + A*C を整理して、A * (B + C) の状態を表すコード
class Order {
bool isPremiumMember;
int cartTotal;
bool hasCoupon;
Order(this.isPremiumMember, this.cartTotal, this.hasCoupon);
bool isFreeShipping() {
// 算術演算の論理で整理: A * (B + C)
// 整理されたことで「isEligibleForDiscount」という関数の抽出が明確に!
return isPremiumMember && _isEligibleForDiscount();
}
// 抽出した関数
bool _isEligibleForDiscount() {
return cartTotal >= 2000 || hasCoupon;
}
}
こういったリファクタリングを行える、ということです。
今回の場合は関数抽出のきっかけを作っています。
応用
では今度は少し複雑な条件式をケースにしてみましょう。
例1
活用前
まずはこちらから。
// !A && !Bの状態
class Article {
bool isDraft;
bool isDeleted;
String content;
Article(this.isDraft, this.isDeleted, this.content);
void publish() {
// !A && !B の状態。否定が連続していて直感的でない
if (!isDraft && !isDeleted) {
print('記事を公開: $content');
} else {
print('公開できません');
}
}
}
否定かつ否定、といった条件式になっています。
人間の脳は否定形、つまり「〜ではない」といった内容を処理するのが苦手であり、
それが複数重なると認知負荷が跳ね上がってしまいます。
現状のコードはそれほどでもないかもしれませんが、少し脳のメモリを使いますし、直感的ではありません。
条件が増えるとさらにカオスになっていくでしょう。
活用後
では同じように、これも算術演算の論理を適用してみましょう。
// !A && !B の状態を整理して、!(A || B) の状態を表すコード
// ド・モルガンの法則を適用
class Article {
bool isDraft;
bool isDeleted;
String content;
Article(this.isDraft, this.isDeleted, this.content);
void publish() {
// !(A || B) の状態。
// 「公開不可な状態(isUnpublishable)」の【否定】として読めるようになる
if (!_isUnpublishable()) {
print('記事を公開: $content');
} else {
print('公開できません');
}
}
// 抽出した関数:肯定的な条件(OR)にまとまったことでロジックが明快に
bool _isUnpublishable() {
return isDraft || isDeleted;
}
}
活用前の条件式、「!A && !B」を「!(A || B)」に変形しています。
これは、
-
!A && !Bは!(A || B)と等しい -
!A || !Bは!(A && B)と等しい
といったド・モルガンの法則を適用できるからです。
この法則を使うことで、「複数の否定」を「1つの肯定的な条件」へとまとめることができ、
それを足がかりに関数抽出などに繋げることができるのです。
今回の場合ですと、!(isDraft || isDeleted) に変形しています。
すると、 (isDraft || isDeleted) の部分が「公開できない状態」というひとまとまりの意味を持つようになり、
関数として綺麗に抽出できるのです。
例2
上記ド・モルガンの法則の二つ目も試してみましょう。
活用前
まずは適用前から。
// !A || !B の状態
class User {
bool hasValidCard;
bool hasSufficientBalance;
User(this.hasValidCard, this.hasSufficientBalance);
}
class PaymentProcessor {
void process(User user) {
// !A || !B の状態。条件が複雑に感じる
if (!user.hasValidCard || !user.hasSufficientBalance) {
throw Exception('決済に失敗しました。カード情報または残高を確認してください。');
}
// 以降、決済の正常系ロジック...
print('決済完了');
}
}
今回は否定または否定、といった条件式です。
否定かつ否定よりも分かりにくく、非常に読み間違えやすく、誤解を起こしやすくなっています。
活用後
ではこれもド・モルガンの法則を適用してみましょう。
// !A || !B の状態を整理して、!(A && B) の状態を表すコード
// ド・モルガンの法則を適用
class User {
bool hasValidCard;
bool hasSufficientBalance;
User(this.hasValidCard, this.hasSufficientBalance);
// 抽出した関数:オブジェクト自身に「決済可能か?」を問う形にする(カプセル化)
bool canMakePayment() {
return hasValidCard && hasSufficientBalance; // A && B の部分
}
}
class PaymentProcessor {
void process(User user) {
// !(A && B) の状態。
// 「決済可能(canMakePayment)ではない(!)」と直感的に読める
if (!user.canMakePayment()) {
throw Exception('決済に失敗しました。カード情報または残高を確認してください。');
}
// 以降、決済の正常系ロジック...
print('決済完了');
}
}
今回の場合は、!(hasValidCard && hasSufficientBalance) に変形します。
括弧の中身である (hasValidCard && hasSufficientBalance) は「決済可能である(支払い能力がある)」という肯定的な概念になっています。
しかも、これをUserクラス側にメソッドとして抽出することで、
コードが「英語の文章」のように自然に読めるようになっています。
関数抽出だけでなく、オブジェクト指向のクラス設計にも応用することができるのです。
この法則を適用することで、
人間にとって理解しやすい肯定的な概念(&& や || の肯定形の塊)を見つけ出し、名前をつけて切り出せるようになります。
そうすることで、コードの意図が圧倒的に伝わりやすくなるのです。
if文のリファクタリグに関しては他にも色々とありますので、
そちらは僭越ながら、こちらの記事をご参照ください。
参考