前述
こちらの記事は、
「【マイスター・ギルド】本物の Advent Calendar 2021」の2日目の記事になります。
はじめましての方は、はじめまして。
お久しぶりの方は、お久しぶりです。
私、みたむーと申します。
しばらく技術記事らしい記事を書いていなかったため、
今回は久方ぶりの技術記事になります。
学んだことは数多くありましたが、
その中でも__私が初学者のときに、
こういう記事を読んでみたかったな__と考えた記事を
自ら執筆しているという感じです。
言語は折角ですから、初心にかえって
PHPを選出しようと思います。
環境
- OS: Windows
- PHP: 8.0
リファクタリング -Refactoring-
ところで、あなたは__リファクタリング__はご存知ですか?
まずは、リファクタリングとはそもそも何かを知りましょう。
リファクタリング (refactoring) とは、コンピュータプログラミングにおいて、プログラムの外部から見た動作を変えずにソースコードの内部構造を整理することである。また、いくつかのリファクタリング手法の総称としても使われる。ただし、十分に確立された技術とはいえず、また「リファクタリング」という言葉に厳密な定義があるわけではない。
Wikipediaより引用
ここに書かれていることそのままの意味なのですが、
曲者であると私が感じた一番の所以が
「また「リファクタリング」という言葉に厳密な定義があるわけではない。」
という表記です。
マジで頭を抱えました。
私は理系ですし、定義を重要視する節が少なからずあるのですが、
厳密な定義がないということは、__「おおまかにしか決まりがない」ということで、
すなわち、「あなたが考えるリファクタリングが定義である」__と言っているのとほぼ同値です。
これについては、__ふっざけんなッッ!!!__と言わざるを得ませんでした。
・・・え?なんで怒っているのか分からないって?
この厄介さを今から説明しますので、
あなたも一緒に考えてみてください。
定義の重要性
「リンゴ」というものは、赤くて丸い果物である。
ただし、「リンゴ」という言葉に厳密な定義があるわけではない。
何も変わらないと思うかもしれませんが、
⇒ 2つの赤くて丸い果物 ⇒ 「リンゴ」
って言ってもいいんですよ。
間違ってると思いましたね?
そうです、本当は「サクランボ」です。
では、これはどうですか?
🍊 ⇒ 赤い要素の入った丸い果物 ⇒ 「リンゴ」
はいはい、分かっていますよ。
違う、「みかん🍊」だと言いたいんですよね。
では、あなたに問いましょう!
__「赤い」__とは、赤色ですか?朱色ですか?紅色ですか?
気付きましたか?
ここまで書かれた赤色の文字・・・全部違う色なんです。
でも、全て__「赤色」__って言っちゃうんですよね。
さて、定義の__「赤い」__は何%くらいの赤さなんでしょう?
「みかん🍊」の色を赤色だと判断する人がいないと言えますか?
さて、個数の定義は書いていないのですが、
複数あるかもと判断する人がいないと言えますか?
細かい話になってしまいましたが、
こういうことを許容してしまうのが定義の曖昧さの怖いところなんですよね・・・。
もっと分かりやすい例
さて、少し意地悪をしたのですが(笑)
もっと分かりやすい例を挙げましょう。
さて、何色?
青色?そんな色ないですよ?
・・・ということです。
ちなみに青色(青信号)だと言われるのは、
昔は今と違って色の区分が少なく、
緑色が青色の区分に含まれていたこと
に由来しています。
何が言いたいのか
今までつらつらと語ってきましたが、
__こう考えるという人がひとりでも存在するのなら__それは定義となるんですよ!
だから、定義はしっかりしろとあれだけ・・・。
つまり、「リファクタリング」の定義は星の数ほどあるってことです。
あなたの「リファクタリング」はあなたの中にある!!!
リファクタリングの大原則
早くリファクタリングの指針について教えてよ!
と思われるかもしれませんが、とても大切なことなので
もう少しだけお付き合いください。
その大切なことの1つが、
これからリファクタリングを語る上で、
__絶対に間違えてはいけない大原則__です。
それは、__「プログラムの外部から見た動作を変えない」__ということです。
何を当たり前なことをと思うかもしれませんが、
Git-flowでいう__feature__、__fix(hotfixes)__と__refactor__をしっかりと区分できていますか?
- 新たに動作を加える
- 動作の順番を入れ替える
- 受け取る引数、返却する値(型を含め)を変える
- エラーを解消する
これらはすべて__refactorではありません__ので、ご注意を!!
Git-flowについては過去に記事にしていますので、
興味がある方はこちらをどうぞ。
図解すると
__「A ⇒ B ⇒ C」__という流れで処理されるプログラムをリファクタリングするとして、
今回は上図のような、Bの処理を分割するようなリファクタリングだとしますね。
そうすると、今回触る赤色の部分以外に変化を与えてはいけないということです。
つまり、今回のリファクタリングで考えるべき部分は一部で、
全体の流れを考える必要がなくなります。
結局考えるべき範囲は絞られることになり、
同じ入力(INPUT)に対して、同じ出力(OUTPUT)ができればいいということになります。
ということで、この分割するリファクタリングを行ったとすると、
__「B1 ⇒ B2」の処理自身が「B」である__と、言うことができるかもしれませんね。
こういった処理の同値変換を行うことがリファクタリングです。
リファクタリングの指針
ここまでの話に則って考えると、
あなたも自然と同じ指針になると思います。
というのも、この同値変換の考えをするならば、
リファクタリングする方法が
「分割」と「置換」の指針しか存在しないからです。
そのため、私がリファクタリングするときには
この指針に則って考えるようにしています。
キーワード
そして、
先の指針に加えて今から説明するキーワードを知っておくと
どうリファクタリングをすればよいのか、という良いヒントになることでしょう。
複雑なものを簡単にするコツは、パターン化することです。
それは、__意図__と__行動__を言語化するようなものです。
作業フローをよく作成する人ならば、案外簡単にパターン化できるかもしれませんね。
さて、
そんなキーワードが3つありますので、
それぞれのキーワードについて、具体例を用いて
紹介と説明をしていくことにしましょう。
1.else、早期return
1つ目は、__「else、早期return」__です。
以下のコードを見てみましょう。
function sample(int $x): int {
if ($x % 2 === 0) {
// $xが偶数だったら、そのまま
return $x;
} else {
// $xが奇数だったら、2倍する
return $x * 2;
}
}
ある数値($x
)を与えたときに、偶数か奇数かで処理が変化する関数(function
)です。
コード内に__「else」が存在していますね。
ここで使用するのが、「早期return」__というわけです。
関数(functon
)は、return
で処理を抜けるという特徴があります。
視覚的に説明するのであれば、以下のように考えると分かりやすいかもしれません。
function sample(int $x): int {
// true
if ($x % 2 === 0) {
// $xが偶数だったら、そのまま
return $x; // 処理終了、これ以降のコードは通ることがない
}
}
function sample(int $x): int {
// false
if ($x % 2 === 0) {
} else {
// $xが奇数だったら、2倍する
return $x * 2; // 処理終了
}
}
この__「早期return」__の仕組みを利用することで、
以下のようにリファクタリングすることができます。
function sample(int $x): int {
if ($x % 2 === 0) {
// $xが偶数だったら、そのまま
return $x; // 偶数はここで早期return
}
// ここに到達するのは、奇数のときだけ
// $xが奇数だったら、2倍する
return $x * 2;
}
こうしてリファクタリングすることで、中括弧({}
)を減らして少し見やすくすることができます。
また、今回はありませんでしたが、else if
なども省略することができます。
※ただし、省略できるのはあくまで早期returnができるからです。
また、条件をハッキリさせたいという理由からあえてelseを残すこともあります。
あくまで、__可読性を高めるという目的__があるからなので、
なんでも省略すればいいと、勘違いしないようにしましょう。
ちなみに、__今の私__なら以下のようなリファクタリングを考えます。
function sample(int $x): int {
$isEvenNumber = $x % 2 === 0;
if ($isEvenNumber) {
// $xが偶数だったら、そのまま
return $x;
}
// $xが奇数だったら、2倍する
return $x * 2;
}
function sample(int $x): int {
$isEvenNumber = $x % 2 === 0;
// $xが偶数だったら、そのまま
// $xが奇数だったら、2倍する
return $isEvenNumber ? $x : $x * 2;
}
- 条件をパッと見て分かりやすいようにしたいな
- 複雑でない分岐なら、条件演算子(
$a ? $b : $c
)を使用してもいいかな
と、考えた結果のリファクタリングになります。
やったことは、条件の__「置換」__ですね。
2.重複、類似
2つ目は、__「重複、類似」__です。
重複は、繰り返しやループという意味も含めています。
同じことを繰り返すという意味では、重複ですからね。
以下のコードを見てみましょう。
function sample(int $x): int {
if ($x % 3 === 0) {
$a = $x * $x;
return $x + $a;
} else if ($x % 3 === 1) {
$b = $x * 2;
return $x + $b;
} else {
$c = $x - 1;
return $x + $c;
}
}
めちゃくちゃになってきましたね!!
どうでしょう、あなたはリファクタリングした姿をイメージできますか?
よく見ると、先ほどの__「else、早期return」__のリファクタリングを利用できそうです。
パパッとやってみましょう。
function sample(int $x): int {
if ($x % 3 === 0) {
$a = $x * $x;
return $x + $a;
}
if ($x % 3 === 1) {
$b = $x * 2;
return $x + $b;
}
$c = $x - 1;
return $x + $c;
}
ここまでは簡単にリファクタリングできそうですね。
さて、今回のキーワードは__「重複、類似」ですから、
重複や類似を探すわけなのですが、重複はなさそうですね。
となれば、まずは「類似」__の出番です。
function sample(int $x): int {
if ($x % 3 === 0) {
// 値を作る
return // 作った値と$xの和
}
if ($x % 3 === 1) {
// 値を作る
return // 作った値と$xの和
}
// 値を作る
return // 作った値と$xの和
}
こうして抽象的な言葉に置き換えてあげると、分かりやすくなりますかね?
※こういった抽象化は、経験も必要になるとは思います・・・。
そうすると、条件によって異なるのは、
__「値の作り方だけ」__であることに気付けると思います。
function sample(int $x): int {
if ($x % 3 === 0) {
// 値を作る
}
if ($x % 3 === 1) {
// 値を作る
}
if ($x % 3 === 2) {
// 値を作る
}
return // 作った値と$xの和
}
こんな風に重複している箇所を共通の処理にしてしまえば、
これもリファクタリングになります。
しかし、このままだとよくないので、きちんと意味が通るようなコードに戻します。
function sample(int $x): int {
if ($x % 3 === 0) {
$addNumber = $x * $x;
}
if ($x % 3 === 1) {
$addNumber = $x * 2;
}
if ($x % 3 === 2) {
$addNumber = $x - 1;
}
return $x + $addNumber;
}
微妙な感じかもしれませんが、これで少しリファクタリングができました。
これは値を作る部分と和を考える部分に__「分割」__したのと同義です。
ちなみに、__今の私__なら以下のようなリファクタリングを考えます。
function sample(int $x): int {
$addNumber = getAddNumber(number: $x);
return $x + $addNumber;
}
function getAddNumber(int $number): int {
if ($number % 3 === 0) {
return $number * $number;
}
if ($number % 3 === 1) {
return $number * 2;
}
return $number - 1;
}
- 条件だけが異なり、抽象的な処理は同じだな
- ある数値(
$x
)に紐づいて、和を考える数値($addNumber
)が決まるな(関数の関係)
と、考えた結果のリファクタリングになります。
やったことは、値を作る部分全体の__「置換」__ですね。
3.ネスト
3つ目は、__「ネスト」__です。
ネストとは、入れ子のことで
同じ形や同じ種類のものが入り込んでいる形のことを言います。
具体例を挙げるならば、
マトリョーシカのような状態をイメージしてもらえれば
分かりやすいのかなと思います。
以下のコードを見ていきましょう。
function sample(bool $x, bool $y, bool $z): int {
if ($x) {
if ($y && $z) {
return 1;
} else if ($y) {
return 2;
} else {
return 3;
}
} else {
if ($y) {
return 1;
} else {
return 3;
}
}
}
こんなコードは存在しなさそうですが・・・、
もし存在したら見るのも嫌になりそうなコードです(笑)
まずは、__「else、早期return」__ですね!
function sample(bool $x, bool $y, bool $z): int {
if ($x) {
if ($y && $z) {
return 1;
}
if ($y) {
return 2;
}
return 3;
}
if ($y) {
return 1;
}
return 3;
}
こう見ると、if
がネストしていますね。
これは個人的な思想ですが、if
のネストは根絶したい派です。。。
とはいえ、if
のネストを消すのは意外と簡単です。
条件を合体させて&&
で結んでやるだけでサクッと消えます。
※ただし、それにより条件が複雑になることもありますので、
可読性の観点には十分に注意しましょう。
function sample(bool $x, bool $y, bool $z): int {
if ($x && ($y && $z)) {
return 1;
}
if ($x && $y) {
return 2;
}
if ($x) {
return 3;
}
if ($y) {
return 1;
}
return 3;
}
function sample(bool $x, bool $y, bool $z): int {
if ($x && $y && $z) {
return 1;
}
if ($x && $y) {
return 2;
}
if ($x) {
return 3;
}
if ($y) {
return 1;
}
return 3;
}
これで、__「ネスト」__が削除できました。
ただ・・・あんまりすっきりしないんですよね~。
そんなときは、条件の整理をしてあげるといいと私は考えています。
では、コードの条件を整理してみましょう。
このときに私はよく真偽表を使用するようにしています。
※何が入力されたときに、何が出力されるのかが分かれば、他の方法でも大丈夫です。
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
---|---|---|---|---|---|---|---|---|
$x | T | T | T | T | F | F | F | F |
$y | T | T | F | F | T | T | F | F |
$z | T | F | T | F | T | F | T | F |
出力値 | 1 | 2 | 3 | 3 | 1 | 1 | 3 | 3 |
注:T・・・True(真)
F・・・False(偽)
ここで2通りの考え方で、説明します。
※方法は、私が勝手に名前を付けていますので、ご容赦を・・・。
メジャー法
メジャー法は、一番多いパターンから考えていく方法です。
つまり、先ほどの真偽表で見ると、
__「3 ⇒ 1 ⇒ 2」__の順に考えていくことになります。
3 | 4 | 7 | 8 | |||||
---|---|---|---|---|---|---|---|---|
$x | T | T | F | F | ||||
$y | F | F | F | F | ||||
$z | T | F | T | F | ||||
出力値 | 3 | 3 | 3 | 3 |
1 | 5 | 6 | ||||||
---|---|---|---|---|---|---|---|---|
$x | T | F | F | |||||
$y | T | T | T | |||||
$z | T | T | F | |||||
出力値 | 1 | 1 | 1 |
2 | ||||||||
---|---|---|---|---|---|---|---|---|
$x | T | |||||||
$y | T | |||||||
$z | F | |||||||
出力値 | 2 |
こうして分類した後に、それぞれの共通点を探します。
そうすると、出力値が3のときは、すべて$yがFである__ことに気付きますか?
そのとき、他を見てみると$yがFである__のは、ここだけと判明します。
そのため、まず以下のようにコードが書けますね。
function sample(bool $x, bool $y, bool $z): int {
if (!$y) {
return 3;
}
// 分からない
}
そうすると、もう出力値が3のときは考える必要がなくなりますから、
残りは__「1 ⇒ 2」__ですね。
1 | 5 | 6 | ||||||
---|---|---|---|---|---|---|---|---|
$x | T | F | F | |||||
$y | T | T | T | |||||
$z | T | T | F | |||||
出力値 | 1 | 1 | 1 |
2 | ||||||||
---|---|---|---|---|---|---|---|---|
$x | T | |||||||
$y | T | |||||||
$z | F | |||||||
出力値 | 2 |
さらに__$yがTのとき__しか残っていないので、$y
の情報はもう必要ありません。
$y
を削除してあげると、以下のようになります。
1 | 5 | 6 | ||||||
---|---|---|---|---|---|---|---|---|
$x | T | F | F | |||||
$z | T | T | F | |||||
出力値 | 1 | 1 | 1 |
2 | ||||||||
---|---|---|---|---|---|---|---|---|
$x | T | |||||||
$z | F | |||||||
出力値 | 2 |
次は、__出力値が1のとき__を考えましょう。
・・・共通点が見当たらないですね。
またはでもなく、かつでもない。
かといって全部繋げるのは・・・ってなりますよね。
こういう場合、あるテクニックを使用すると状況が一変します。
そのテクニックというのが、「反転」です。
どちらかの真偽を「反転」してしまうんです。
つまり、「$x ⇒ !$x」のような感じで考えるんです。
そうすると・・・
1 | 5 | 6 | ||||||
---|---|---|---|---|---|---|---|---|
!$x | F | T | T | |||||
$z | T | T | F | |||||
出力値 | 1 | 1 | 1 |
2 | ||||||||
---|---|---|---|---|---|---|---|---|
!$x | F | |||||||
$z | F | |||||||
出力値 | 2 |
これなら出力値が1のときは、__!$xまたは$zが真である__ことに楽に気付くことができますね。
ということで、それ以外が__出力値が2のとき__ですから、以下のようにすれば完成ですね。
function sample(bool $x, bool $y, bool $z): int {
if (!$y) {
return 3;
}
if (!$x || $z) {
return 1;
}
return 2;
}
マイナー法
マイナー法は、一番少ないパターンから考えていく方法です。
つまり、先ほどの真偽表で見ると、
__「2 ⇒ 1 ⇒ 3」__の順に考えていくことになります。
2 | ||||||||
---|---|---|---|---|---|---|---|---|
$x | T | |||||||
$y | T | |||||||
$z | F | |||||||
出力値 | 2 |
1 | 5 | 6 | ||||||
---|---|---|---|---|---|---|---|---|
$x | T | F | F | |||||
$y | T | T | T | |||||
$z | T | T | F | |||||
出力値 | 1 | 1 | 1 |
3 | 4 | 7 | 8 | |||||
---|---|---|---|---|---|---|---|---|
$x | T | T | F | F | ||||
$y | F | F | F | F | ||||
$z | T | F | T | F | ||||
出力値 | 3 | 3 | 3 | 3 |
そうすると、出力値が2のときは、__$xと$yがTかつ$zがFである__ことはすぐに分かりますね。
そのため、まず以下のようにコードが書けますね。
function sample(bool $x, bool $y, bool $z): int {
if ($x && $y && !$z) {
return 2;
}
// 分からない
}
そうすると、もう出力値が2のときは考える必要がなくなりますから、
残りは__「1 ⇒ 3」__ですね。
1 | 5 | 6 | ||||||
---|---|---|---|---|---|---|---|---|
$x | T | F | F | |||||
$y | T | T | T | |||||
$z | T | T | F | |||||
出力値 | 1 | 1 | 1 |
3 | 4 | 7 | 8 | |||||
---|---|---|---|---|---|---|---|---|
$x | T | T | F | F | ||||
$y | F | F | F | F | ||||
$z | T | F | T | F | ||||
出力値 | 3 | 3 | 3 | 3 |
ここで共通点を考えてみましょう。
そうすると、出力値が1のときは、__すべて$yがTである__ことに気付くことができそうです。
となれば、それ以外が__出力値が3のとき__ですから、以下のようにすれば完成ですね。
function sample(bool $x, bool $y, bool $z): int {
if ($x && $y && !$z) {
return 2;
}
if ($y) {
return 1;
}
return 3;
}
条件を整理した結果
function sample(bool $x, bool $y, bool $z): int {
if ($x && $y && $z) {
return 1;
}
if ($x && $y) {
return 2;
}
if ($x) {
return 3;
}
if ($y) {
return 1;
}
return 3;
}
ということで、上記のコードが
条件を整理してみた結果、以下のような結果となりました。
function sample(bool $x, bool $y, bool $z): int {
if (!$y) {
return 3;
}
if (!$x || $z) {
return 1;
}
return 2;
}
function sample(bool $x, bool $y, bool $z): int {
if ($x && $y && !$z) {
return 2;
}
if ($y) {
return 1;
}
return 3;
}
どちらも同様の処理は可能ですが、
パフォーマンスのことや、条件の簡潔さなどの理由から
個人的には__「メジャー法」__をオススメします。
そして、これら条件の整理は__「分割」と「置換」__の応用です。
こう見ると、
出力値によって__「分割」し、条件を整理して「置換」__していることが分かりやすいですね。
指針とキーワード
ということで、
指針に則ってリファクタリングを考える上で
3つのキーワードを紹介しました。
- 1.else、早期return
- 2.重複、類似
- 3.ネスト
これらは、あくまで私が考えるものですから、
あなたが考えるキーワードを作っても良いと思います。
そして、ここまで読んだあなたは
それぞれのキーワードで行ったリファクタリングが
すべて__「指針の領域内」__であることに気付いていることだと思います。
まとめ
さて、具体例を挙げながら
私が考えるリファクタリングの指針について、
述べてきた訳なのですが、どうだったでしょうか。
もちろん違う思想、指針を持つ方もいらっしゃると思いますし、
それが誤りであるということを言いたい訳ではありません。
結局、
__あなたが考えるリファクタリング__と__私が考えるリファクタリング__が異なっていても
どちらが正しいとかそういうことではなく、何の問題もないんです。
まだ何も知らないあなた、初学者であるあなた、異なる思想を持つあなたにとって、
この記事が__何かのヒント__になったり、__あなたの糧__になったりすれば、
それだけで私がこの記事を執筆した価値があると思っています。
最後に改めて、
私が考えるリファクタリングの指針を挙げることで
この筆を止めることにします。
私が考えるリファクタリングの指針は、
「分割」と「置換」です。