リファクタリング(第2版) 既存のコードを安全に改善するを読んだので、ざっくりまとめます〜。
著者:Martin Fowler
翻訳:児玉 公信,友野 晶夫,平澤 章,梅澤 真史
出版社:オーム社
発売日:2019/12/01
所感
リファクタリングとはからなぜやるか、どうやるかが一通り載っている本。
前半では、リファクタリングの考え方が広く載っており、後半は手法がまとめてある。
使い方としては、リファクタリングのケースをとりあえず頭に入れておいて、似たような場面に出くわしたら本書で探すというような辞書的な側面が強いと思います。
言語はjsなので特に問題なく読めました、他言語への応用も効くと思います。
自分は駆け出しだが、コードを書く以上リファクタリングからは逃げられない。
それはソフトウェアが常に進化し続けるもので、その時々に応じてコードの最適解[1]も変わるから。自分みたいな駆け出しの園児ニアが書いたコード、優れたエンジニアが書いたコード、全てにおいてリファクタリングは必要になる。
そういう意味で、この本は早めに読んでおいて得しかないのかなと思いながら読みました。理解は浅い
本書を読んで一番得られたものは「リファクタリングは気軽に行って良い!」というマインド。
リファクタリングはソフトウェアの動きを変えずに行われる。つまり、途中でやめてもいいわけです。
ある程度軽量な粒度で行うことができるので、まとまった時間を取ることができずとも現在の機能追加のタスクと同時に少しずつ行って少しづつ良くしていく。もちろんテストコードがある前提だと思うが(本書でもテストについて一章使っている)、、
自分はテストコードがないプロジェクトに参加しているから大胆なことはできないが、結局修正したところはテストするから小さな粒度であれば気軽に行って良いだろう。
もう一つパフォーマンスと可読性のトレードオフ問題に関しても大きな答えを得られた。
結論として可読性をとるべきである。
大体の場面においてリファクタリングによるパフォーマンスの劣化は問題にならない。
さらに、適切なプログラムの中ではパフォーマンスチューニングは容易に行えることができる。
リファクタリングの良さがわかったけど結局どうすれば良いのか。
この本を読んで少しづつ理解していきたい。
What
外部から見た振る舞いを変更させずに内部の構造を変化させること。
常に振る舞いが変わらないので、継続的に行うことが可能。
Why
- 開発速度が速くなるため。
- 機能追加する際でも、リファクタリングを行い整理されたコードに機能追加を行う方が効率的である。
- バグが見つけやすい
- コードリーディングが容易になることでソフトウェアを理解しやすくなる。
When
- 普段の作業の中。
- 修正の必要がないコードのリファクタリングは行う必要はない。
- 不吉な臭いがした時。
不吉な臭いとは
- 名前がおかしい時
- コードリーディングに時間がかかる
- 名前付けは最も難しいが効果も大きい
- 重複コード
2. 重複部分が本当に一致しているか判断するために読み込まなければならない。
2. 修正の際、同様の変更を漏れなく見つけなければならない。 - 長い関数
3. 6行を超えると臭いがし始める。
3. 長いほど理解が難しい
3. 関数を短く切って適切な名前をつけることができれば処理は見なくて良い。
3. 短い関数は委譲を繰り返すだけで計算を行なっていないように見えるかもしれないがそれで大丈夫!1行でも分割する価値はある。 - パラメータが多い
4. 複数の関数で共通のパラメータを使っているならクラスに切り出す。
4. こういうアーキテクチャの話で単一責任の原則突き詰めるとクラス大量にできて逆にわかりづらくないかと思うこともあるが、複数の責任がまとめられているクラスより整理されたクラスの方が使いやすいというマインド。 - グローバル変数
5. いつ値を変更しているかわからない。
5. 変更されないことが保証されているならグローバルでも臭くない時がある。 - 偏向の偏り
6. データベースを変更した時あるメソッドを変更しないといけないなど関係ない処理に影響が及ぶ - 変更の分散
7. ビジネスロジックの変更時に他のモジュールも書き換えないといけない。 - 基本データ型への執着
8. ドメインを対象とする型を自作しようという話。
8. 例えば電話番号の型や、扱う金額の単位の幅を指定した型。
8. なんでもプリミティブ型(特に文字列)で処理を行わない、UI表示時などに表記の揺れを防げる。 - 重複したスイッチ文
9. 新たな分岐を追加したら重複したスイッチ分全てを書き換える必要がある - ループ
10. パイプライン[2]に置き換える。 - 一時的属性
11. インスタンス変数は常に必要とされている想定で考える。 - メッセージの連鎖
12. あるオブジェクトが別のオブジェクトの関数を使ってさらに別のオブジェクトを。。。みたいなパターン
12. 割とやりがちな気が。。「委譲の隠蔽」を使う。 - 巨大なクラス
13. 変数の名前に共通の接頭辞や接尾辞がついていたら抽出できる。 - データクラス
14. ゲッターやセッターしか持たないクラスは危険。色々なクラスから頻繁にアクセスされる可能性がある。
14. 振る舞いを持たせる。 - 相続拒否
15. 親クラスには共通のものだけを保持させる。
15. 親のものは全て使われる前提。 - コメント
17. 丁寧なコメントはむしろ危ない。関数を細かく定義する。
How
本書では丁寧にサンプル付き、想定されるパターン別に解説されている。
6章の初めの一歩と10章の条件記述の単純化がとっつきやすそう。
忘備録用のメモ。
関数の抽出
何をしているかコードを読まないとわからない時、「何」をするかを示す関数として抽出する。
関数のインライン化
何も考えずに関数に切り出せば良いわけではない。
関数の中身が名前よりわかりやすい場合はインライン化を行う。
// 中身が十分わかりやすいので関数にする必要はない。
function moreThanFive(number) {
return number > 5;
}
変数の抽出
式など複雑な計算の際は説明変数を使い、式を分解する。
変数名はローカル変数の時は関数の中で意味のある命名。
より広いコンテキストで使われる際は広い意味の命名。
=>この場合は関数の抽出で良い。
多くの場合はクラス内の処理であるので、説明変数ではなく、関数にすれば良い。
(問い合わせによる一時変数の置き換え)
関数宣言の変更
特に関数の引数のパラメータを変更したい時について
ここでいうパラメータにオブジェクトを渡すか値を渡すかは状況によって異なる。
オブジェクトを渡せばインターフェースに統合され、関数が知るべきことが増える。一方でオブジェクトの他のプロパティにアクセスすることができ、ロジックの変更があってもこの関数だけを変更すれば良いという意味ではカプセル化が促進されている。
記憶力が悪いので、個人的には引数が一つの場合は値を渡す方式をとることになりそう。
変数のカプセル化
データは操作しずらい。
関数は内部で関数を呼び出すことができるので、名前の変更や移動を容易に行うことができる。
データに対してゲッターやセッターを用いデータへのアクセスを関数経由にすることが重要。
*クラス内フィールドへの内部参照は許容される
また、こうすることでいつデータが変更されるか特定できる。
- カプセル化関数を作る
- 変数への参照をゲッター代入をセッターに置き換える
データ構造の内容の変更は制御できない。
内容を制御するには参照時にコピーを返し、元のデータ構造に反映されないようにする手段もある。
パラメータオブジェクトの導入
関数のパラメータ数が多い時、新たにパラメータオブジェクトを導入することでパラメータ数を削減できる。
パラメータオブジェクトはカプセル化された関数であったり、クラスになったりする。
- 構造体を作成する
- 後で振る舞いをまとめやすいのでクラスにすることが多い。
- 関数宣言の変更
- パラメータの呼び出しを修正
- クラスの中で振る舞いを持たせてそもそもの関数を移動する<=すごい
関数群のクラスへの集約
クラスを定義することで関数の引数が減る。
関数を見つけやすくなる。
レコードのカプセル化
レコード:単純なデータ構造
レコードに格納されている値に応じた関数を意識しないといけない。
オブジェクト化することによって、それぞれの値の関数を準備できる。呼び出し側は格納されているデータを意識する必要がない。
- レコードを保持する変数に変数のカプセル化を施す。
- 変数の中身をクラスに置き換える。
- レコードへの参照をオブジェクトを返す関数に置き換える
- オブジェクトを返す部分をアクセサにする。
- オブジェクトのフィールドがレコードの場合はディープコピーやコレクションのカプセル化を行う。
コレクションのカプセル化
フィールドがコレクションの時はクラスを介さずに変更できる。
addやremoveのメソッドを作ることでコレクションに対する変更方法を定義する。
問い合わせによる一時変数の置き換え
変数を関数に置き換えると関数の抽出が容易になる。
クラス外で用いると、パラメータが多くなるのでクラス内で使う。
常に変数の結果が同じ時のみ使える。スナップショット的な変数には使わない。
- 一時変数の値が使われる前に確定していることを確認
- 一時変数を読み取り専用にする(可能なら)
- 変数を関数にする
クラスの抽出
大量のメソッドとデータを持つクラスは容易に理解できない。
Personクラスの電話番号をTelephoneNumberに移すぐらいの粒度で良い。
- クラスの責務を決める
- 切り出した責務のクラスをつっくる
- 親クラスのインスタンス作成時に新たな小クラスのインスタンスを作り、親クラスインスタンスから参照できるようにする。
- メソッドを移す
- 新たな子クラスのインスタンスを公開するか決める
委譲の隠蔽
class Person {
get department() {return this._department;}
}
class Department {
get manager() {return this._manager;}
}
ある人(aPerson)の上司(manager)を知りたい時、clientがaPerson.department.managerを呼び出す。
委譲先(department)を呼び出すために、aPersonがdepartmentを知っていることをclientが知っていなければならない。
departmentのインターフェースが変更された時にclientにも影響が及ぶ。
この依存を断ち切り、clientがaPerson.managerと呼び出せるように変更し、委譲を隠蔽する。
class Person {
get manager() {return this._department.manager;}
}
- 委譲先のオブジェクトのメソッドに対応する委譲用メソッドを中間のオブジェクトに持たせる。
- クライアントが委譲用メソッドを呼び出すようにする。
デッドコードの削除
使わないコードは消すべきである。コメントアウトではなく。
将来必要になるかもしれないという心配はしない。バージョン管理システムでいつでも戻せる。
変数の分離
変数が設定されるのは一度だけ。
何度も設定される時は、複数の責任を持っていることになる。
参照から値への変更
難しい。。
オブジェクトを入れ子にする際、内部オブジェクトは参照もしくは値として扱うことができる。
参照の場合は更新可能なオブジェクトを共有している、値は変更が共有されないオブジェクト。
値は元のオブジェクトに対して変更不可であるので扱いやすい。分散や並行システムで有効。
条件記述の分解
条件記述を関数に抽出する
条件判定と条件ごとの処理に対して行う。
三項演算子まで持っていくと綺麗。
条件記述の統合
複数の条件式で同じ結果を返している時は一つに統合する。
if分のネストなんかもandで抜き出す
ガード節による入れ子による条件記述の置き換え
thenとelseのどちらかが異常系の時、ガード節に置き換える。
then-elseどちらも同じウェイトで読むのしんどいから。
ポリモーフィズムによる条件記述の置き換え
複雑な条件ロジックが複数ある時はそれぞれクラスに切り出し、ポリモーフィズムで解決する。
switch分の時に有効になりやすい。
特殊ケースの導入
特殊ケースの共通処理を特殊オブジェクト切り出す。
よくある例だとnullに対して共通の処理を呼び出せるようにしたい時。
アサーションの導入
アサーションは常に真であることを前提にした条件文のこと。
処理が動く際の必要な前提を理解することができるので、コメントを書くならアサーションを導入する。
アサーションの条件に一致しない場合、即座に処理が終了するのでバグの発見にも役立つ。
*真であると思うところに書くのではなく、真である必要があるところに書く。
Tips的テクニック
-
関数の戻り値の変数名をresultにする
- 戻り値が自明になる
- 何を返しているかは関数名を見ればわかる
-
ローカル変数を出来るだけ少なくする
2. つまり、ローカル変数部分は関数で呼び出すようにする
2. メソッドの抽出が楽になるから -
動的型付け言語では変数名に型を含ませる
3. 例CustomerクラスのインスタンスならaCustomer -
プロパティ名の頭に_つける
4. *言語やコーディング規約によってバラバラ
[1]本書引用
良いコードかどうかは、変更がどれだけ容易なのかで決まる
[2]mapとかfilterなど第一級関数