My Favorite Life

Go there and write simply with golang. Such a person I want to be.

【本メモ】『リファクタリング』

リファクタリング』の本メモを公開。

新装版 リファクタリング 既存のコードを安全に改善する

多くの場合、使用するデータを持つオブジェクトにメソッドは定義されるものです。

リファクタリングを開始するとき、最初にすることは常に同じです。
対象となるコードについてきちんとしたテスト群を作り上げることです。
リファクタリングは非常に秩序だっていて、新たなバグを生み出しにくくなっていますが、
人間が作業する以上、間違いを犯す可能性があります。
このためテストは大切で、きちっとした一連のテストを用意するべきなのです。

「メソッドの抽出」
「メソッドの異動」
「ポリモーフィズムによる条件記述の置き換え」
これらはどれも、クラスの責務を分割させ、保守しやすいコードを書くのに役立ちます。

テスト、少しの変更、テスト、少しの変更、と繰り返していくのです。
このリズムによって、リファクタリングはよりはやく安全に行えるようになります。

リファクタリングはソフトウェア設計を改善する
リファクタリングなしでは、プログラムの設計は劣化していきます。
開発者によるコードの変更が短期的なゴールの実現のために行われたり、
設計全体を理解しないままに行われたりすると、往々にしてコードは構造を崩すことになります。
こうなるとコードを読んで設計を把握することも難しくなります。
これに対して、リファクタリングは、むしろコードをきちんと整えていく作業です。
それによって、適切でない部分を取り除きます。
コードが構造を失うと、累積的に悪影響が及びます。
設計が把握しづらくなるにつれて、それを維持するのが困難になり、
急速に劣化していきます。
リファクタリングを定期的に行うことで、コードの状態をしっかりと保つことができます。
設計のまずいコードは、良いものに比べ、同じ処理をするのに余計なコードを書くことになります。
これは文字どおり同じようなコードがあちこちに散らばっているからです。
重複したコードを排除することは、設計を良くする際の重要事項と言えます。
これは将来的にコードを修正することを考えた場合に、見逃せないものになります。
コード量を減らしても、システムがより速く動作するようになるわけではありません。
占有メモリ量に対する節約効果も、大してありません。
しかしコード量は修正時には重要な意味を持ちます。
コード量が増えるほど、正しく修正するのが難しくなります。
ます、理解しておかなければならないコード量が増えます。
その場合、コードの特定部分を変えても期待どおりに動作しないかもしれません。
なぜなら他の箇所でも少し違った文脈で同じようなことをしており、
その部分を直し忘れるものだからです。
重複部分を排除することで、開発者はコードがただの1回のみ書かれ、
そこですべてが処理されることを保証できます。
これは優れた設計のポイントです。

リファクタリングと設計
「設計時には順調に思考が進みますが、実際は小さな穴だらけなのです」
一方で、リファクタリングは事前設計の代わりになるという意見があります。
この考えでは、事前の設計をいっさい行いません。
最初に思いつくままにコーディングして、ともかく動作させ、
その後リファクタリングで形を整えるのです。
実際、この方式もうまくいきます。

確かに、リファクタリングだけでもきちんとしたソフトウェアを作ることは可能ですが、
それが最も効率的なやり方ではありません。
事前設計をある程度行うのが普通です。
さまざまなアイデアを洗い出し、妥当性を考えます。
そして実現可能性のある最初の解決策について見当をつけます。
この実現可能な解決策が浮かぶまでは、コーディングもリファクタリングもしません。
重要なのは、リファクタリングは、事前設計の役割を変えるということです。
リファクタリングをしない場合、開発者は事前に行うのは、非常に工数がかかるかもしれないという不安です。
こうなると、変更の必要を避けるため、事前設計に対して時間を労力をさらにつぎ込むようになります。

リファクタリングを採用すると、力点の置き方が変わってきます。
事前設計は行うのですが、そこで唯一無二の、完璧な解決策を見出す必要はありません。
ここで必要なのは、妥当な解決策なのです。
解決策をコードとして作り込んでいくにつれて、
やがて問題の本質がもう少し見えてきます。
真の解決策は、事前に考えていたものでないことが判明するかもしれません。
しかし、リファクタリングではこれは問題にはなりません。
なぜなら変更にさほどコストがかからないからです。

こうした力点の変化は、設計の簡素化という重大な効果をもたらします。
私はリファクタリングを始める前には、常に柔軟性に富む解決策ばかりを求めてきました。
要求のヒアリングでも、ソフトウェアが使われるにつれて、それらがどのように変化するということを常に気にしていました。
設計の変更は高くつくものだったので、予想できるすべての変更に耐える設計を追求しました。
しかし、柔軟性を求めた解決策は複雑になりがちです。
その結果できたソフトウェアは、予想していた変更に簡単に対処できるものの、
通常は保守しにくくなります。
また対処する場合にも、設計を柔軟にするための工夫をどのように行なっているかを理解する必要が出てきます。
1つや2つではこれは大した問題になりませんが、
変更というものはソフトウェア全体に起こり得るものです。
すべてにおいて柔軟性を実現しようとすると、
ソフトウェアが過度に複雑化し、保守が難しくなってきます。
こうしたやり方では、当然のことですが、
いろいろと工夫した部分が実際には必要とされないという挫折感を味わうことにもなります。
いくつかは役立つこともあるでしょうが、どれが役立つかを予測することはできません。
柔軟性を実現するには、必要以上に大きな範囲での作り込みを強いられるのです。

次は、教訓です。
システムが行なっていることを十分にわかっているつもりでも、
推測はやめて、まず実際に計測をすること。
そこで本質が何かをつかむこと。
十中八九、あなたの推測は間違っています。

パフォーマンスチューニングに、前述の90%の法則を使います。
このやり方では、まずプログラムを整理された形で作り上げます。
そして、チューニングの段階に来て、初めてパフォーマンスを考慮します。
チューニング段階では、一定の手順に従い最適化を行なっていきます。

まず、該当プログラムが、実際に時間とメモリをどのように消費しているかを計測するために、
プロファイラを実行します。
こうすることで、パフォーマンス上のホットスポットになっているプログラムの場所が検出されます。
その後、ホットスポットに集中し、その部分についてのみ、最適化を行います。
こうすればホットスポットに集中できるため、少ない労力でより多くの効果を上げることができます。
ただし、この作業でも注意しなければならないことがあります。
リファクタリングと同様に、変更を小さなステップの積み重ねで行うことです。
各ステップで、コンパイル、テスト、プロファイラによる実行を繰り返します。
もしパフォーマンスが改善しなかった場合は、変更を元に戻します。
満足する水準に達するまで、ホットスポットを見つけては退治していきます。

プログラムの設計が優れていると、パフォーマンス解析の際、より細かな単位へ集中することが可能になります。
プロファイラにより、ボトルネックとしてコードの限定された箇所が示され、
そこを簡単にチューニングできます。コードが非常に明確に書かれているので、どのような選択肢があり、
どのようなチューニグが有効かをより深く理解できるのです。
リファクタリングが速いソフトウェアを作るのに重要な技術
リファクタリングを行なっている段階では、短期的にソフトウェアの動作を遅くしますが、
最適化の段階に入ったときには、チューニング作業を行いやすくします。
最終的には得をすることになるのです。

変更の偏り
私たちは変更がなるべく容易になるようにソフトウェアの構造を考えます。
結局のところソフトウェアはソフトであるべきなのです。
変更しなければならない時には、変更箇所を1つに特定して、そこだけを変えるようにしたいのです。
これがうまくいかないと、密接に関係する2つの不吉な臭いを味わうことになります。
1つのクラスが別々の理由で何度も変更される状況では「変更の偏り」が起こっています。
「DBが新しくなるたびに、いつものこの3つのメソッドを変更しなければならないし、
金融商品を追加するたびに毎回この4つのメソッドを修正している」などということが同一クラスで見られる場合、
クラスを2つに分けた方がすっきりするでしょう。
こうすれば、ある変更要求に対して、対応するクラスが1つだけ修正されることになります。
もちろん、DBや金融商品を実際に追加する要求が生じたあとで、
初めてこの必要性に気づくこともあります。
変更要求に対処するためにはクラスを1つだけ変更し、
バリエーションが増えた時は新しいクラスを追加するというのが望ましい姿です。
「クラスの抽出」で変更の理由ごとにクラスをまとめていくことが重要です。

「変更の偏り」は、1つのクラスがさまざまな変更要求の影響を被る現象であり、
「変更の分散」は、1つの変更がさまざまなクラスの変更を引き起こす現象です。
いずれにせよ、変更とクラスが1対1になるようにするのが理想的です。

### メソッドオブジェクトによるメソッドの置き換え
長いメソッドで、「メソッドの抽出」を適用できないようなローカル変数の使い方をしている
メソッド自身をオブジェクトとし、すべてのローカル変数をそのオブジェクトのフィールドとする。
そうすれば、そのメソッドを同じオブジェクト中のメソッド群に分解できる

### メソッドの移動
あるクラスでメソッドが定義されているが、現在または将来において、
そのクラスの特性よりも他クラスの特性の方が、そのメソッドを使ったり、
そのメソッドから使われたりすることが多い。
同様の本体を持つ新たなメソッドを、それを最も多用するクラスに作成する。
元のメソッドは、単純な委譲とするか、またはまるごと取り除く。
メソッドの移動は、リファクタリングに欠かせないものです。
私は、クラスの振る舞いが多すぎる場合や、クラス間でのやり取りが多く、結合度が高すぎる場合にメソッドを移動します。
メソッドを移動することで、クラスは単純になり、結果として、ひとまとまりの責務をすっきりとした実装に収めることができます。

### フィールドの移動
あるクラスに定義されているフィールドが、
現在または将来において、定義しているクラスよりも、他のクラスから使われることの方が多い。
移動先のクラスに新たなフィールドを作って、その利用側をすべて変更する。
クラス間で状態や振る舞いを移動するのは、リファクタリングの本質です。
システムが発展していくにつれて、新たなクラスや責務の振り直しが必要になります。
ある週では正しく適正であった設計判断も、次の週にはそうでなくなります。
それが問題なのではなく、それについて何もしないことが問題なのです。

### クラスの抽出
2つのクラスでなされるべき作業を1つのクラスで行なっている。
クラスを新たに作って、適切なフィールドとメソッドを元のクラスからそこに移動する。
「クラスは、きっちり抽象化されたものであり、少数の明確な責務を担うべきである」
といったガイドラインを耳にしたことがあるでしょう。
実際には、クラスは成長します。
そうしたクラスには、大量のメソッドとデータがあります。
大きすぎて簡単に理解できないクラスです。
切り出せるところがどこかを考えて、実際に切り出す必要があります。
データとメソッドの一部をまとめて別のクラスにできそうであれば、
それは良い目安です。
他にも、データの一部がよく同時に変更されたり、特に互いに依存する関係にあれば、
それも良い目安になります。

### オブジェクトによる配列の置き換え
配列の各要素が、それぞれ異なる意味を持っている
その配列を、要素ごとに対応したフィールドを持つオブジェクトに置き換える

### フィールドのカプセル化
公開フィールドがある。
それを非公開にして、そのアクセサを用意する。
オブジェクト指向の主要な教義の1つは、カプセル化、あるいはデータの隠蔽です。
すなわち、自分のデータを公開してはならないということです。
データを公開すると、それを所有しているオブジェクトが知らないうちに、
他のオブジェクトから値を変更されたりアクセスされたりします。
これによって、データが振る舞いから分離されてしまいます。
これは、プログラムのモジュール度を下げるので良くないこととされています。
データをそれを使う振る舞いをいっしょにまとめておけば、コードの変更は容易です。
コードの変更箇所がプログラム全体に散らばらず、1ヶ所にあるからです。
「フィールドのカプセル化」の手順は、データを隠してアクセサを追加することから始まりますが、
アクセサしか持たないクラスは、オブジェクト指向の利点を真に活かしていない無意味なクラスであり、
オブジェクト指向技術は単に浪費するだけの恐ろしいものになってしまいます。
「フィールドのカプセル化」が終われば、次に、この新しいメソッドを利用するメソッドを探して、
「メソッドの移動」を適用し、それらのメソッドが荷物をまとめて新しいオブジェクトに引っ越しできないかを調べます。

### サブクラスによるタイプコードの置き換え
クラスの振る舞いに影響を与える不変のタイプコードがある
そのタイプコードをサブクラスに置き換える
タイプコードが振る舞いに関係しないならば、
「クラスによるタイプコードの置き換え」が適用できます。
しかし、タイプコードが振る舞いに影響する場合、
取るべき最善の手は、異なる振る舞いを扱うためにポリモーフィズムを利用することです。
これを行うためには、タイプコードを、ポリモーフィックな振る舞いを実現する継承構造で置き換える必要があります。
この継承構造には、タイプコードに対応するサブクラスを持つクラスが存在します。

### 条件記述の分解
複雑な条件記述がある
その条件記述部と then 部およびelse 部から、メソッドを抽出する
プログラム中で最も複雑なところといえば、複雑な条件ロジックでしょう。
条件判定をして、その結果によって異なる処理を行うコードを書くとき、
よく考えないで長々としたメソッドを書いてしまうことがあります。
メソッドの長さも、コードを読みにくくする要因の1つですが、
条件記述があるとその難しさも増大します。この問題は、コードには、
条件記述部にしろそのアクション部にしろ、何が起きるかは書いてあっても、
それがなぜ起きるのかについてわかるように書いておくのは容易でないという事実から来ています。
どんなに長いコードブロックでも、それを分解すること、そしてそのブロックにふさわしい名前を持ったメソッドの呼び出しで一群のコードを置き換えることによって、
プログラマの意図を明確化できます。

多くのプログラマは、このようなときに条件部を抽出しません。
条件部はとても短くて、そうすることに大して価値があるように見えないからです。
しかし条件部は短くても、そのコードの意図と実体とのギャップは大きいのです。
上のような小さな例でさえ、
元のコードよりは、notSummer(date)と書いてある方が、明確なメッセージとして伝わります。
元のコードでは、それをよく読んで、やっていることを理解しなければなりません。
この例では、さほど難しくないかもしれませんが、それでも、
抽出されたメソッドの方がコメントのように楽に読めます。

### ポリモーフィズムによる条件記述の置き換え
オブジェクトのタイプによって異なる振る舞いを選択する条件記述がある
条件記述の各アクション部をサブクラスでオーバーライドするメソッドに移動する。
元のメソッドはabstractにする。
ポリモーフィズムの真髄は、オブジェクトの振る舞いがその型によって変わるとき、
明示的な条件記述を書かなくてもいいようにすることです。

### ヌルオブジェクトの導入
null 値のチェックが繰り返し現れる
その null値をヌルオブジェクトで置き換える。
ポリモーフィズムの真髄は、オブジェクトのタイプを尋ねてその答えに基づいて振る舞いを呼び出すのではなく、
タイプを尋ねずにその振る舞いをただ呼ぶだけですませようというものです。
そのときオブジェクトは、そのタイプにふさわしい動作を行います。

### オブジェクトそのものの受け渡し
あるオブジェクトから複数の値を取得し、それらの値をメソッド呼び出しのパラメータとして渡している
代わりにオブジェクトそのものを渡す。
パラメータリストを変更に対してより頑強にするだけでなく、
しばしばコードを読みやすくします。
長いパラメータリストは、呼び出す側も呼び出された側も、どこにどの値があるのかを覚えておかなければならないため、
扱いづらいものです。
またコードの重複も招きやすいものです。なぜなら呼び出されたオブジェクトは、
渡されたオブジェクトで定義されているメソッドを使って中間的な値を計算する恩恵を享受できないからです。
しかし問題点もあります。値をじかに渡す場合、呼び出されたオブジェクトは、
その値に対する依存関係を持ちますが、その値を提供したオブジェクトに対する依存関係はありません。
オブジェクトを渡すと、渡されたオブジェクトと呼び出されたオブジェクトとの間で依存関係が発生します。
これが、依存関係の構造を乱すならば、
「オブジェクトそのものの受け渡し」を適用すべきではありません。

呼び出されたメソッドが他のオブジェクトから取得できる多くの値を使用しているということは、
そのメソッドが本来はそれらの値を持っているオブジェクトに定義されるべきだった可能性を示唆しています。
「オブジェクトそのものの受け渡し」を検討するときは、
代替案として「メソッドの移動」も検討すべきです。

### Template Methodの形成
異なるサブクラスの2つのメソッドが、類似の処理を同じ順序で実行しているが、
各処理は異なっている。
元のメソッドが同一になるように、各処理を同じシグニチャのメソッドにする。
そしてそれらを引き上げる

### 階層の抽出
仕事をし過ぎているクラスがある。少なくとも、多くの条件分岐を持つ部分がある。
クラス階層を作成して、サブクラスがそれぞれ特殊ケースの1つを表現するようにする
進化的な設計の立場では、当初はあるクラスが1つのアイデアを実装すると考えていたのものの、
あとになって実際にはそれが2つ3つ、時には10個のアイデアを実装していることに気づくのはよくあることです。
最初はそのクラスも単純に作成されます。
数日後、数週間後には、
1つのフラグといくつかの判定を追加しさえすれば、そのクラスを新しい条件でも使えることに気づきます。
1ヶ月後には、また別の利用方法を追加するかもしれません。
1年後には、フラグと条件分岐があちこちに散りばめられた、ひどい状態になってしまいます。
もし、スイス・アーミー・ナイフのようなクラスに出会ったならば、
物事を単純化して、種々の要素に分解する戦略が必要となります。
ここでの戦略は、条件に対応するロジックがオブジェクトの生存期間を通じて変化しない場合にのみ有効となります。
そうでない場合には、各条件を分離する前に「クラスの抽出」を適用する必要があるかもしれません。