LoginSignup
18
9

More than 1 year has passed since last update.

色んな思想に触れて、自分の中で納得できるように整理したリファクタリングの指針

Last updated at Posted at 2021-12-02

前述

こちらの記事は、
【マイスター・ギルド】本物の Advent Calendar 2021」の2日目の記事になります。

はじめましての方は、はじめまして。
お久しぶりの方は、お久しぶりです。
私、みたむーと申します。

しばらく技術記事らしい記事を書いていなかったため、
今回は久方ぶりの技術記事になります。

学んだことは数多くありましたが、
その中でも私が初学者のときに、
こういう記事を読んでみたかったな
と考えた記事を
自ら執筆しているという感じです。

言語は折角ですから、初心にかえって
PHPを選出しようと思います。

環境

  • OS: Windows
  • PHP: 8.0

リファクタリング -Refactoring-

ところで、あなたはリファクタリングはご存知ですか?

まずは、リファクタリングとはそもそも何かを知りましょう。

リファクタリング (refactoring) とは、コンピュータプログラミングにおいて、プログラムの外部から見た動作を変えずにソースコードの内部構造を整理することである。また、いくつかのリファクタリング手法の総称としても使われる。ただし、十分に確立された技術とはいえず、また「リファクタリング」という言葉に厳密な定義があるわけではない。
Wikipediaより引用

引用元: https://ja.wikipedia.org/wiki/%E3%83%AA%E3%83%95%E3%82%A1%E3%82%AF%E3%82%BF%E3%83%AA%E3%83%B3%E3%82%B0_(%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0)

ここに書かれていることそのままの意味なのですが、
曲者であると私が感じた一番の所以が
「また「リファクタリング」という言葉に厳密な定義があるわけではない。」
という表記です。

image001.jpg

マジで頭を抱えました。
私は理系ですし、定義を重要視する節が少なからずあるのですが、
厳密な定義がないということは、「おおまかにしか決まりがない」ということで、
すなわち、「あなたが考えるリファクタリングが定義である」と言っているのとほぼ同値です。

これについては、ふっざけんなッッ!!!と言わざるを得ませんでした。

・・・え?なんで怒っているのか分からないって?

この厄介さを今から説明しますので、
あなたも一緒に考えてみてください。

定義の重要性

「リンゴ:apple:」というものは、赤くて丸い果物である。
ただし、「リンゴ」という言葉に厳密な定義があるわけではない。

何も変わらないと思うかもしれませんが、
:cherries:2つの赤くて丸い果物 ⇒ 「リンゴ」
って言ってもいいんですよ。

間違ってると思いましたね?
そうです、本当は「サクランボ:cherries:」です。

では、これはどうですか?

🍊 ⇒ 赤い要素の入った丸い果物 ⇒ 「リンゴ」

はいはい、分かっていますよ。
違う、「みかん🍊」だと言いたいんですよね。

では、あなたに問いましょう!
「赤い」とは、赤色ですか?朱色ですか?紅色ですか?

気付きましたか?
ここまで書かれた赤色の文字・・・全部違う色なんです。
でも、全て「赤色」って言っちゃうんですよね。

さて、定義の「赤い」は何%くらいの赤さなんでしょう?
「みかん🍊」の色を赤色だと判断する人がいないと言えますか?

さて、個数の定義は書いていないのですが、
複数あるかもと判断する人がいないと言えますか?

細かい話になってしまいましたが、
こういうことを許容してしまうのが定義の曖昧さの怖いところなんですよね・・・。

もっと分かりやすい例

さて、少し意地悪をしたのですが(笑)

もっと分かりやすい例を挙げましょう。

signal.png

さて、何色?

青色?そんな色ないですよ?

・・・ということです。

ちなみに青色(青信号)だと言われるのは、 昔は今と違って色の区分が少なく、 緑色が青色の区分に含まれていたこと に由来しています。

何が言いたいのか

今までつらつらと語ってきましたが、
こう考えるという人がひとりでも存在するのならそれは定義となるんですよ!

だから、定義はしっかりしろとあれだけ・・・。

つまり、「リファクタリング」の定義は星の数ほどあるってことです。

あなたの「リファクタリング」はあなたの中にある!!!

リファクタリングの大原則

早くリファクタリングの指針について教えてよ!

と思われるかもしれませんが、とても大切なことなので
もう少しだけお付き合いください。

その大切なことの1つが、
これからリファクタリングを語る上で、
絶対に間違えてはいけない大原則です。

それは、「プログラムの外部から見た動作を変えない」ということです。

何を当たり前なことをと思うかもしれませんが、
Git-flowでいうfeaturefix(hotfixes)refactorをしっかりと区分できていますか?

  • 新たに動作を加える
  • 動作の順番を入れ替える
  • 受け取る引数、返却する値(型を含め)を変える
  • エラーを解消する

これらはすべてrefactorではありませんので、ご注意を!!

Git-flowについては過去に記事にしていますので、
興味がある方はこちらをどうぞ。

図解すると

「A ⇒ B ⇒ C」という流れで処理されるプログラムをリファクタリングするとして、

image002.png

今回は上図のような、Bの処理を分割するようなリファクタリングだとしますね。

image003.png

そうすると、今回触る赤色の部分以外に変化を与えてはいけないということです。

つまり、今回のリファクタリングで考えるべき部分は一部で、
全体の流れを考える必要がなくなります。

image004.png

結局考えるべき範囲は絞られることになり、
同じ入力(INPUT)に対して、同じ出力(OUTPUT)ができればいいということになります。

ということで、この分割するリファクタリングを行ったとすると、
「B1 ⇒ B2」の処理自身が「B」であると、言うことができるかもしれませんね。

image005.png

こういった処理の同値変換を行うことがリファクタリングです。

リファクタリングの指針

ここまでの話に則って考えると、
あなたも自然と同じ指針になると思います。

というのも、この同値変換の考えをするならば、
リファクタリングする方法が
「分割」「置換」の指針しか存在しないからです。

そのため、私がリファクタリングするときには
この指針に則って考えるようにしています。

キーワード

そして、
先の指針に加えて今から説明するキーワードを知っておくと
どうリファクタリングをすればよいのか、という良いヒントになることでしょう。

複雑なものを簡単にするコツは、パターン化することです。
それは、意図行動を言語化するようなものです。

作業フローをよく作成する人ならば、案外簡単にパターン化できるかもしれませんね。

さて、
そんなキーワードが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で処理を抜けるという特徴があります。

視覚的に説明するのであれば、以下のように考えると分かりやすいかもしれません。

$xが偶数のとき
function sample(int $x): int {
  // true
  if ($x % 2 === 0) {
    // $xが偶数だったら、そのまま
    return $x; // 処理終了、これ以降のコードは通ることがない
  }



}
$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を残すこともあります。

あくまで、可読性を高めるという目的があるからなので、
なんでも省略すればいいと、勘違いしないようにしましょう。

ちなみに、今の私なら以下のようなリファクタリングを考えます。

サンプル(案1)
function sample(int $x): int {
  $isEvenNumber = $x % 2 === 0;

  if ($isEvenNumber) {
    // $xが偶数だったら、そのまま
    return $x;
  }

  // $xが奇数だったら、2倍する
  return $x * 2;
}
サンプル(案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;
}

微妙な感じかもしれませんが、これで少しリファクタリングができました。
これは値を作る部分和を考える部分「分割」したのと同義です。

image006.png

ちなみに、今の私なら以下のようなリファクタリングを考えます。

サンプル(案)
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つ目は、「ネスト」です。

ネストとは、入れ子のことで
同じ形や同じ種類のものが入り込んでいる形のことを言います。

具体例を挙げるならば、
マトリョーシカのような状態をイメージしてもらえれば
分かりやすいのかなと思います。

image007.png

以下のコードを見ていきましょう。

サンプル
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」ですね!

サンプル(早期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であるのは、ここだけと判明します。

そのため、まず以下のようにコードが書けますね。

サンプル(出力値が3のとき)
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であることはすぐに分かりますね。

そのため、まず以下のようにコードが書けますね。

サンプル(出力値が2のとき)
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;
}

どちらも同様の処理は可能ですが、
パフォーマンスのことや、条件の簡潔さなどの理由から
個人的には「メジャー法」をオススメします。

そして、これら条件の整理は「分割」「置換」の応用です。

image008.png

image009.png

image010.png

こう見ると、
出力値によって「分割」し、条件を整理して「置換」していることが分かりやすいですね。

指針とキーワード

ということで、
指針に則ってリファクタリングを考える上で
3つのキーワードを紹介しました。

  • 1.else、早期return
  • 2.重複、類似
  • 3.ネスト

これらは、あくまで私が考えるものですから、
あなたが考えるキーワードを作っても良いと思います。

そして、ここまで読んだあなたは
それぞれのキーワードで行ったリファクタリングが
すべて「指針の領域内」であることに気付いていることだと思います。

まとめ

さて、具体例を挙げながら
私が考えるリファクタリングの指針について、
述べてきた訳なのですが、どうだったでしょうか。

もちろん違う思想、指針を持つ方もいらっしゃると思いますし、
それが誤りであるということを言いたい訳ではありません。

結局、
あなたが考えるリファクタリング私が考えるリファクタリングが異なっていても
どちらが正しいとかそういうことではなく、何の問題もないんです。

まだ何も知らないあなた、初学者であるあなた、異なる思想を持つあなたにとって、
この記事が何かのヒントになったり、あなたの糧になったりすれば、
それだけで私がこの記事を執筆した価値があると思っています。

最後に改めて、
私が考えるリファクタリングの指針を挙げることで
この筆を止めることにします。

私が考えるリファクタリングの指針は、
「分割」「置換」です。

18
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
9