リファクタリング
- リファクタリングリスト
- リファクタリング 最初の例
- リファクタリングの原則
- コードの不吉な臭い
- テストの構築
- リファクタリングはじめの一歩
- カプセル化
- 特性の移動
- データの再編成
- 条件記述の単純化
- API のリファクタリング
- 継承の取り扱い
リファクタリングリスト
- setter の削除
- アサーションの導入
- 値から参照への変更
- アルゴリズムの置き換え
- 委譲によるサブクラスの置き換え
- 委譲によるスーパークラスの置き換え
- 委譲の隠蔽
- オブジェクトそのものの受け渡し
- オブジェクトによるプリミティブの置き換え
- ガード節による入れ子の条件記述の置き換え
- 関数群のクラスへの集約
- 関数群の変換への集約
- 関数宣言の変更
- 関数によるコマンドの置き換え
- 関数の移動
- 関数のインライン化
- 関数の抽出
- 関数呼び出しによるインラインコードの置き換え
- クラス階層の平坦化
- クラスの抽出
- コマンドによる関数の置き換え
- コレクションのカプセル化
- コンストラクタ本体の引き上げ
- サブクラスによるタイプコードの置き換え
- サブクラスの削除
- 参照から値への変更
- 条件記述の統合
- 条件記述の分解
- スーパークラスの抽出
- ステートメントの関数内への移動
- ステートメントのスライド
- ステートメントの呼び出し側への移動
- 仲介人の除去
- デッドコードの削除
- 問い合わせと更新の分離
- 問い合わせによる一時変数の置き換え
- 問い合わせによる導出変数の置き換え
- 問い合わせによるパラメータの置き換え
- 特殊ケースの導入
- パイプラインによるループの置き換え
- パラメータオブジェクトの導入
- パラメータによる関数の統合
- パラメータによる問い合わせの置き換え
- ファクトリ関数によるコンストラクタの置き換え
- フィールドの移動フィールドの押し下げ
- フィールドの引き下げフィールド名の変更フェーズの分離
- フラグパラメータの削除変数のインライン化
- 変数のカプセル化
- 変数の抽出変数の分離
- 変数名の変更
- ポリモーフィズムによる条件記述の置き換え
- メソッドの押し下げ
- メソッドの引き上げ
- ループの分離
- レコードのカプセル化
リファクタリング 最初の例
リファクタリングの第一歩
しっかりとした一連のテスト群を用意する。
関数の分割
意味ある単位で分けられないか考える。その部分を関数に切り出して適切な命名をすることで、エイリアスをつけてわかりやすくできる。
フェーズの分離
関数より大きな意味ある単位に分割して、モジュールに分ける。
高度なリファクタ
計算ロジックを、分岐によって実現するのではなく、ポリモーフィズムによって実現するようにする、など。
良いコードとは何なのか?
色々な評価基準はあるが、変更しやすいことは好みを超越した普遍的な良さ。ただしそれは数字で測れないので……
リファクタリングの原則
リファクタリングの定義
リファクタリング(名詞) 外部から見たときの振る舞いを保ちつつ、理解や修正が簡単になるように、ソフトウェアの内部構造を変化させること。
リファクタリングする(動詞) 一連のリファクタリングを適用して、外部から見た振る舞いの変更なしに、ソフトウェアを再構築すること。
動詞の方が一連の作業であって、その中に名詞の方のリファクタリングがいくつも含まれていることがある。また、小さいステップを踏んで常に動作する状態で行うのであって、途中で壊れた状態が一定期間以上続くなら、それはリファクタリングとは言えない。
大きいステップで変更できることでも、あえて小さいステップで行う。この辺は TDD っぽい考え方。
外部からみた振る舞い、とは? -> パフォーマンスが変わるとしても、ユーザーが感知できるレベルでないなら、外部から見た振る舞いは変わっていないと言える。また、リファクタリング前にバグが存在するなら、リファクタリング後にもそのバグは再現されなければならない。直すのはまた後で。いわゆる、「帽子をかぶり変える」というやつ。
いつリファクタリングするか?
三度目の法則
似たようなコードを三回書いたらリファクタリング開始、という、スリーストライクのやつ。
準備のためのリファクタリング 機能追加を容易にするために
理解のためのリファクタリング コードをわかりやすくするために
ゴミ拾いのためのリファクタリング
いわゆるボーイスカウトの原則。ただ、リファクタリング自体を、悪いコード・醜いコードを直す作業であると考えてはいけない。良いコードも、場合によってはリファクタリングが必要だからだ。
リファクタリングを避けるとき
どれだけ内部的なコードが汚くても、それが外側からは単なる API としてみなせる、つまり、上手に隠蔽されている場合、リファクタリングの必要性は薄い。そこを読んでどのように動いているのかを把握しなければならなくなったときに、リファクタリングの利点が出てくる。
リファクタリングは、あくまで生産性を上げる活動、つまり、純粋に経済的な基準で行うべき。美学とかは捨て置け。
コードの不吉な臭い
- 変な命名
- 重複コード
- 特に、重複 switch。ポリモーフィズムで書き換えることを考える。
- 長い関数
- 長いパラメータリスト
- 構造体にまとめられないか考える。
- グローバルなデータ
- mutation
- SRP 違反
- OCP 違反
- LSP 違反(相続拒否)
- デメテルの法則
- 疑わしき一般化
- 一時的属性
- 直和で作るべきものを直積で作るとこうなることが多い。
- 仲介人
- 過剰な delegation が行われているなら、もう直接使った方がいい。
- 巨大なクラス
テストの構築
バグレポートを受け取ったら、まずそれを再現するテストを書く。
リファクタリングはじめの一歩
関数の抽出
- 逆: 関数のインライン化
- 動機: 意図と実装の分離。実装だけでは、その実装が全体として何をしているのかがわかりづらい。実装を切り離してそこに関数名という「意図」を書くことができる。
関数のインライン化
- 逆: 関数の抽出
- 動機: 分割がうまくいってないと考えられる場合、一旦処理を全部インライン化してから再度関数の抽出をすることで読みやすくする。
変数の抽出
- 逆: 変数のインライン化
- 動機: 関数の抽出とほぼ同じ。
変数のインライン化
- 逆: 変数の抽出
- 動機: 変数に格納されている意味がほぼないような変数を見つけた場合、それを除去する。
関数宣言の変更
- 動機: 関数のシグネチャから、その関数の意図がすぐわかるようにする。
変数のカプセル化
- 動機: field の隠蔽。
変数名の変更
- 動機: scope が長い変数とか、動的型付け言語の変数とかにおいて、中身を直観的に推測できるようにする。
パラメータオブジェクトの導入
- 動機: よく一緒に渡されているパラメータを、構造体化することで、パラメータ数を減らしつつ、value object を設計して問題領域を整理する。
関数群のクラスへの集約
- 動機: 共通のデータに対して互いに関わりの深い処理を行う関数群は、同じクラス内に置いて凝集度を高めることができる。
関数群の変換への集約
- 動機: 複数の関数を呼ぶ一連の処理が頻繁に登場する場合、その一連の処理を行う関数を定義してしまう。所謂 Facade pattern ですね。
フェーズの分離
- 動機: フェーズを分離することで、関心の分離が行われ、変更を行うときにトピック毎に分けて対処できる。例えばコンパイラ。ソースコードを入力として受け取る -> トークンに分割する -> 構文木にパースする -> 最適化などの処理を行う -> バイトコードを生成する という風にフェーズ分割でき、分割すると責任分離がされてて変更容易性が高まる。
カプセル化
レコードのカプセル化
- 動機: getter/setter を介して field へのアクセスを行わせることで、無効なアクセスをさせないようにしておく。
コレクションのカプセル化
- 動機: いわゆる first class collection みたいな。primitive なレコードを露出するよりは、それを wrap して有効なアクセサのみを提供するような形にしておいた方が便利。
オブジェクトによるプリミティブの置き換え
- 動機: 例えば、電話番号みたいなデータを
String
型で宣言していたとして、そこに色々処理が入ってきて複雑になってきたときに、PhoneNumber
class を宣言するみたいな。value object で built-in な型を replace する感じの話。
問い合わせによる一時変数の置き換え
- 動機: 一時変数に代入している式が、実は domain model 内でやるべき計算の場合があり、その場合は貧血ドメインモデルを脱する好機となる。
クラスの抽出
- 逆: クラスのインライン化
- 動機: 大きすぎるクラスを分割する。
クラスのインライン化
- 逆: クラスの抽出
- 動機: クラスの担う責任がやせ細りすぎたときに、そのクラスと結合度の高いクラスとマージすることで、整理を行う。場合によっては、その後分割しなおすこともある。
委譲の隠蔽
- 逆: 仲介人の除去
- 動機: 委譲先の API に変更が入っても、仲介人は変更がされないように設計しなおすことで、利用側に変更が波及しないようにする。所謂 Adoptor pattern 的なやつ。
仲介人の除去
- 逆: 委譲の隠蔽
- 動機: 委譲しまくってて、もはや仲介人の存在意義が疑われるような場合、それを除去してソースコードを整理する。
アルゴリズムの置き換え
- 動機: 置き換えたいアルゴリズムの部分を切り離して独立させることによって、アルゴリズムの差し替えを容易にする。Strategy pattern っぽい話。
特性の移動
関数の移動
- 動機: 結合度を下げ、凝集度を上げる。
フィールドの移動
- 動機: 同上。引数の数も減らせる。
ステートメントの関数内への移動
- 逆: ステートメントの呼び出し側への移動
- 動機: 例えば、特定の関数を利用する際、同じような前処理・後処理が色々な場所でされている場合、その前処理・後処理をその関数内に引き込んでしまった方が、コードの重複を防げて嬉しい。
ステートメントの呼び出し側への移動
- 逆: ステートメントの関数内への移動
- 動機: 関数を定義した当時は上手い抽象化であったとしても、時を経てその抽象化の境界線が適当ではなくなってくる可能性がある。その際に、境界線の見直しとして、ある処理を関数の外に追い出して、それを呼び出し側で決めることができるようにすることで、適切な抽象度・柔軟性を持たせることができる。
関数呼び出しによるインラインコードの置き換え
- 動機: 関数の抽出 に同じ。
ステートメントのスライド
- 動機: 処理順を変更して、論理的に近いことをやっているステートメントを、ソースコード上でも近くに配置するようにする。
ループの分離
- 動機: 1回のループでたくさんのことを行うのではなく、1回のループで1個のことだけを行うようにすることで、ソースコードの見通しが立ちやすくなり、ボトルネックの特定にも寄与できるため強力な最適化の誘因になり得る。
パイプラインによるループの置き換え
- 動機:
filter
map
などのコレクション操作を利用することで、より宣言的に記述でき、可読性の向上が見込める。
デッドコードの削除
- 動機: デッドコードは可読性を落とすため。
データの再編成
変数の分離
- 動機: 変数の使いまわしをやめることで、変数とその責務が1:1になり、可読性が上がる。
フィールド名の変更
- 動機: 命名を適切に保つため。
問い合わせによる導出変数の置き換え
- 動機: 単純に算出できる値に関しては、変数や field に格納するのではなく、毎回計算するようにすることで、変数や field の更新し忘れによるバグを防ぐことができる。
参照から値への変更
- 逆: 値から参照への変更
- 動機: 参照を持たせると、mutation が思わぬバタフライエフェクトを生む可能性があるので、値をコピーして持つことで、そうした想定外のバグを防ぐことができる。
値から参照への変更
- 逆: 参照から値への変更
- 動機: 参照から値への変更とは逆に、コピー元と同期を取らなければならないようなデータの場合、同期する分だけソースコードが冗長になるため、参照を持つことで同期を実現した方が読みやすい。
条件記述の単純化
条件記述の分解
- 動機: やってること自体は 関数の抽出 だが、
if
に渡す条件や、その条件のときに行われる処理に関しては、殊冗長になりやすいため、こうした類の処理に関しては特に意識して関数の抽出を検討すべき。
条件記述の統合
- 動機: 同じ処理を行う分岐ならば、複数の
if
を並べるよりも、一つのif
にまとめてしまった方が読みやすいため。ただし、そうすると条件文が長くなることが予想されるため、条件記述の分解 と併せて行うのがよい場合が多い。
ガード節による入れ子の条件記述の置き換え
- 動機: ガード節によって
if
のネストを小さくし、可読性を上げる。
ポリモーフィズムによる条件記述の置き換え
- 動機: 条件分岐が複雑になりすぎたとき、その条件分岐をポリモーフィズムによって解決することで、条件分岐を整理する。ただし、やりすぎには注意をする。継承の semantics から外れない程度に。
特殊ケースの導入
- 動機: 無値を null ではなく、無値を表すための定数 object を用意したり、無値を表すための型を定義したりすることで、特殊パターンを扱いやすくする。所謂 Null Object pattern。
アサーションの導入
- 動機: 所謂 fail fast。documentation 的な役割もある。
API のリファクタリング
問い合わせと更新の分離
- 動機: 所謂 CQRS。副作用のある関数と副作用のない関数を明確に見分けられるようにデザインすることで、可読性が上がり、エンバグしづらくなる。
パラメータによる関数の統合
- 動機: 構造がよく似てる関数が複数ある場合、似ているが違う部分を外から引数として渡すように変更することで、1つの関数にまとめ、メンテナンス性を上げる。
フラグパラメータの削除
- 動機: 引数で渡したフラグによって条件分岐するような関数は避け、呼び出し側で分岐させるようにすることで、結合度を下げる。
オブジェクトそのものの受け渡し
- 動機: あるオブジェクトから色々値を get してからそれらを別の関数に引き渡すのは、デメテルの法則違反の場合がある。オブジェクトそのものを渡すようにすることでわかりやすくなる。そのオブジェクトの同じ部分集合だけが頻繁に使われるようなら、クラスの抽出 を考えてもよい。
問い合わせによるパラメータの置き換え
- 逆: パラメータによる問い合わせの置き換え
- 動機: 引数に渡さなくても、callee 側で算出できたり、callee 側が別の関数を呼び出すことで求められる値は、引数として渡すべきではなく、引数から外すことによってパラメータリストは短くなり、可読性を上げられる。
パラメータによる問い合わせの置き換え
- 逆: 問い合わせによるパラメータの置き換え
- 動機: callee 側が、参照透過でない関数を呼び出してしまうと、その callee も参照透過でなくなってしまう。参照不透過な部分を caller 側に寄せ、値を引数に取ることで、callee 側の参照透過性を守ることができる。
setter の削除
- 動機: mutation の抑制。
ファクトリ関数によるコンストラクタの置き換え
- 動機: 柔軟性が高くなる。詳しくは factory method で調べるとわかる。
コマンドによる関数の置き換え
- 逆: 関数によるコマンドの置き換え
- 動機: 所謂 GoF の Command pattern。柔軟性が上がる。ただし、柔軟性と引き換えにソースコードが複雑になりやすいので、その点は注意して必要性を考える。
関数によるコマンドの置き換え
- 逆: コマンドによる関数の置き換え
- 動機: 単純に関数を呼び出したいだけであって、他の便利機能みたいなのは要らないのであれば、Command pattern をやめて単純な関数として定義した方がわかりやすくなる。
継承の取り扱い
メソッドの引き上げ
- 逆: メソッドの押し下げ
- 動機: サブクラスで似たような関数がある場合、スーパクラスにその関数を移動することでコードの重複を避ける。必要に応じて、パラメータによる関数の統合 を適用してから行う。
フィールドの引き上げ
- 逆: フィールドの押し下げ
- 動機: メソッドの引き上げ とほぼ同じ。コンストラクタ本体の引き上げ とともに行う。
コンストラクタ本体の引き上げ
- 動機: メソッドの引き上げ や フィールドの引き上げ とほぼ同じ。ただし、コンストラクタは特殊な部分があり、簡単に引き上げができない場合がある。これが難しいと感じた場合、ファクトリ関数によるコンストラクタの置き換え でやってみる。
メソッドの押し下げ
- 逆: メソッドの引き上げ
- 動機: あるメソッドがサブクラスのごく一部でのみ使われている場合、利用しているサブクラスにのみ関数を定義するようにすることで、他のその関数が不要なサブクラスから不要な関数が不可視になり、整理される。
フィールドの押し下げ
- 逆: フィールドの引き上げ
- 動機: メソッドの押し下げ に同じ。
サブクラスによるタイプコードの置き換え
- 逆: サブクラスの削除
- 動機: 所謂 State pattern というか、Strategy pattern というか。enum みたいなものでタイプコードをつけておいて分岐するのではなく、ポリモーフィズムで解決できるようにするための準備をする。ポリモーフィズムによる条件記述の置き換え ができるようにする。
サブクラスの削除
- 逆: サブクラスによるタイプコードの置き換え
- 動機: サブクラスが小さすぎて、もはや存在価値がないという場合に、削除することでソースコードの整理を行う。
スーパークラスの抽出
- 動機: 似たクラスがある場合に、スーパークラスを抽出することでコードの重複を避ける。委譲によるスーパークラスの置き換え を後でやって、委譲に書き換えてもよい。
クラス階層の平坦化
- 動機: スーパークラスとサブクラスを区別する必要がなくなる場合があり、そうなった場合不要な継承・サブクラスを削除して単一のクラスにしてしまうことで、ソースコードの整理を行う。
委譲によるサブクラスの置き換え
- 動機: 継承は密結合になってしまうため、その結合度を落としたい場合。
委譲によるスーパークラスの置き換え
- 動機: 継承してみたはいいものの、継承した関数の一部がサブクラスで意味を成していないなら、それはそもそも継承すべきでなかったと認めて委譲による書き換えを行う。所謂 LSP っぽい話。