命名のプロセス
#翻訳
http://arlobelshee.com/good-naming-is-a-process-not-a-single-step/
Creative Commons Attribution 3.0 Unported License.
多くの人が、1回で最高の命名をしようとする。これは難しく、うまく行くことなんて滅多にない。問題はネーミングというのは設計であるということだ。あらゆるものに収まりの良い場所を与え、正しい抽象化をしなくてはならない。これを最初の1回で完璧にこなせる可能性は低い。だから進化的ネーミングについて話をしよう。
良い名前を見つけるためには、一連の手順にそってやるのが最も簡単なアプローチだと、今のところは考えている。ステップは各々、
https://gyazo.com/4c01cacc124cfd76f01d7b1bd3d29158
1. 名前が無い(Missing)
2. 無意味 (Nonsense)
3. 実直 (Honest)
4. 実直かつ完全 (Honest and Complete)
5. 正しいことをする (Does the Right Thing)
6. 意図を表す (Intent)
7. ドメイン抽象 (Domain Abstraction)
それぞれの手順では、コードの一部を見て、発生していることの種類を理解し、何かを感じ取り、それを書き留める。このリストに沿ってこれを繰り返す。私の目的にあう十分良い名前になるまでやり続ける。
負債のあるコードに効率的に作用する
これを読んだ人の中には、なぜそんなに名前に固執するのか不思議に思うものもいるだろう。名前をつけるのは面倒で多分これで簡単になるかもしれない。でもそれは本当に問題なんだろうか?
この問いに対する答えは、技術的負債を理解したり、負債化を防いだり、返済したりすることの中心にある。
すべての技術的負債の元
負債のあるコードとは、追うのが難しいすべてのコードである。技術的負債とは、コードを読むのを難しくしているすべてのものである。
私がコードを読むことにフォーカスしていることに、違和感を覚える人も多いだろう。所詮、私たちはプログラマなのだ。複雑なコードを読むのは得意だし、そんなコードを修正するのが私たちの仕事だ。技術的負債の定義が、コードの変更コストやリスクについてのものじゃダメなことってあるだろうか?
私はそうであるべきだと思う。開発者が費やす時間の大部分はコードを読むことなのは明らかだ。設計よりも、コードを書くことよりも、検査するよりも、(たぶんだけど😜)ミーティングの時間よりも多い。私が見たEclipseデータの分析によると、プログラマはプログラミング全体の時間の60-70%をコードを読むことに費やしている。
だからプログラマがより効率的に働けるようにするには、コードを読む力を改善する必要があるんだ。
さらには、新しいコードにバグが埋め込まれる確率は、循環的複雑度に関して超線形になる。長いメソッドや深いネストのメソッドがあると、修正時のバグが増えるということだ。興味深いことに、構文的な可読性とも関係している。インデントが統一されてないと、プログラマはより多くのバグを書いてしまう。コードをより複雑にしたとき以上にね。
これは、バグが不完全な理解から発生するために起こる。私たちが一度に頭に入れておけること以上に理解するのが難しいシステムだと、不完全な理解が発生する。どれだけ頭に入れておけるかを決める大きな要素が、どれだけ読みやすいかということだ。詳細を記憶して扱う必要がないように、それが何をしているか、ラベル付けされたものを、どれだけ早く特定できるか。
どんなコードにも効率的に作用する
どんなコードにも情報が含まれる。この情報の一部は、あなたに向けられている。これはクイックスキャンで見ることができ、読む必要すらない。情報の中には、それを探すのに注意深く読む必要があるものもある。間にあるものもある。読む必要があるが、ほとんどの読み手は一度見れば情報を発見できるだろう。
コードをもっとスキャンしやすくしたければ、関連する情報の割合を増やす必要がある。これはまた、無関係な情報を隠すことを意味する。
負債を減らすプロセスは単純だ。
1. なにか見る。
2. 気付きをえる。
3. 書きとめる。
4. チェックインする。
なにか見る 調査するものを選ぶ。すべてを理解しようとはしないことだ。これは長すぎる道のりかもしれないし、あなたの脳がオーバーフローしてしまうかもしれない。そう、あなたの脳でさえも。
気付きをえる 何でも構わない。完璧な気付きを求めてはいけない。まだ明白ではない有用そうなものを見つけるのだ。
書き下す ある名前。プログラミング言語において、何でも書ける場所はごくわずかしかない。名前は最高だ。
コメントが使えるかもしれないが、コメントはコードの一部ではない。コードと重複するので、重複にまつわる問題の原因となりうる。
コメント以外で任意のものを記録できる場所は、名前とアサーションだけだ。もし気付きが構造的なものであれば、それは名前に従属したものだ。これが実行時のものであれば、アサーションを使う。
アサーションは見つけやすくなくてはならない。だからコアコードの周りにアサーションをまき散らさないようにしよう。気付きを用例として表現し、それをテストに書こう。そして、気付きに関するテストに名前を付けるのだ(どんなコードが実行されるのかではなく)。
だから、実行時の気付きですら(テスト名として)名付けられて保存される。特定の用例と評価は、テストのボディやアサーションに保存される。気付きは名前に従属するのだ。
チェックインする 今すぐチェックインしよう。コードが良くなったはずだ。完璧ではない。だが、レガシーコードのマントラを思い出そう。「Goodは金がかかりすぎる、やりたいこと全てがより(早く)良くなった」。世界を少しでもより良くした今、利益を確定させよう。チェックインだ。
気付きのループがここにある全て
ところでこのループが、現代ソフトウェア開発の全てだ。
レガシーコードをリファクタリングすると、このループが実行され、名前がつけられる。
レガシーコードを理解することは、このループを実行し、テストの用例として何かを書くことだ。
TDDはこのループを3回繰り返す。
最初に、顧客インタビューを確認し、それをテストの一例として記録する。
次に、テストをよく見て、コードの中に名前を記述する。
3番めは、(新しい)レガシーコードをリファクタリングする。
設計とは、自分が見ている場所が「このテストを書くのがどれほど大変だったか」で、名前を変えることでその気付きを書きとめるループである。(通常これは「正しいことをする」名前に修正するステップである)
このループのどこにいるかを常に把握し、それを素早くおこなう方法を習得することで、マスタークラスのソフトウェアエンジニアになれる。
問題はもちろん、それを素早くやる方法を知ることにある。マスターは、このループが本当に速い。2~45秒の間で定常的にこれを実行できる。顧客インタビュー、既存のコード、既存のテストコードなど、気付きを求めている場所に関係なく、このスピードを実現できる。そして、新しい名前、名前の変更、抽象化の変更、新しいテスト、テストの変更など、どこに気付きを書く必要があるかに関わらず、それができる。さらに速い人も知っている。
名前の話に戻ろう
名前は私たちの気付きを記録する場所だ。そして有用に気付きをえる可能性が最も高いのはどこか、を知る良い手段が必要だ。面白いことに、既に名付けられたものの質を見ることが、どこを見るべきか知るための優れた方法であるように思う。これが、この記事の冒頭で述べた、進化的ネーミングにつながる。
要するに、ソフトウェア開発のマスターとは、これを素早く繰り返すことを意味する。コアループを柔軟に使って。
1. なにか見る。現在の名前付けの質をもとに、どこをより見るべきかを決める。
2. 気付きをえる。通常は名前付けを見ることで行う。
3. 書き下す。リファクタリングツールを使って、気付きを名前に表現する。または可読性の高いアサーションを使って、テストにアサーションとして表現する。
4. チェックインする。メッセージを使用してコミットに名前を付けることで、意図を表す。
MissingからNonsenseへ
まずMissingからNonsenseへの名前の最初の推移を詳しく見てみよう。
(とあるクラスやメソッド、変数、パラメータ、フィールドを) 理解するために、ある程度の大きさから始めることもあれば、そうでないこともある。このステップでは大きなコードの塊があって、それを断片的に理解しなければならないのだ。
見るべきところ: 手始めに見るのは「長い」ものだ。長いメソッド、長いクラス、長いファイル、長い引数リスト、長い式。
私は、一緒くたになってそうなものを探す。例えば、
ひとまとまり文
明確でない式 (通常計算や論理式を含む)
よく一緒に渡されるパラメータの集合
メソッドで一緒に使われるパラメータの集合 (別の変数が検証済みかどうかを示すbool値のようなもの)
私は、上記のような一緒くたになっているコード片を選ぶ。どれだけ良い部分を見つけたかは問題ではない。よりよいコード片を見つけて、理解を早めるテクニックはいくつかあるが、どのコード片を選んでも一歩前進はするだろう。あとで洗練することはできる。
私たちの気付き: 一緒になっている1つの塊
私たちはこのコード片を理解したいのだ。最初のステップは、名前を付けれるものを作ることだ。よくある言語では、異なる5つのものいずれかに名前を付けることができるので、どれか1つを作る必要がある。そのためには以下のリファクタリングのどれかを行う。
メソッド抽出(Extract method): 文の塊に名前を付ける
変数、パラメータ、フィールドの導入: 式に名前を付ける
パラメータオブジェクトの導入: 一緒に渡されるパラメータの集合に名前を付ける
名前は何でも構わない。無意味な名前でも構わない。ここでは、塊を抽出して名前が付けられるようにするのが目的だ。
チェックインするか否か
通常、各ステップの終わりにはコードをチェックインする。だが、今回はそうしなくてもよい。このステップでは実際にはコードをより良くしてはいないからだ。名前はまだ無意味なものだ。以前はコードを読むには、すべてが一緒くたになっており、全部を見なければならなかったのが、今回もすべてを見なければならず、しかもその名前が無意味なものなので、コードはその名前が付けられたものの詳細まで見なくてはならないからだ。
だから、MissingからNonsenseが速く実行できる言語では、チェックインはせず、Honestなネーミングができるまで待つ。
だが、よいリファクタリングツールが存在しない言語も多い。その場合、単に分割するだけでも大変なことがある。動的型付け言語でパラメータオブジェクトを導入するには、影響箇所の探索と変更にかなりの時間がかかる。そんなときは、無意味な名前の段階でも進んでチェックインしよう。少しでも後戻りの可能性があるなら保存しておいた方が良い。
良い塊の見つけ方
長いメソッドは次のようなものから構成される傾向がある。
1. ガード条件
2. メソッド引数から、メソッドが実際に必要とするデータをローカル変数に読み込む
3. 何らかの処理
4. 結果の計算
5. なにかに計算を書き込んだり、戻り値にしたりする
これは、より重要なものがメソッドの終わり近くにあることを意味する。メソッドの上からでなく下から塊を探すようにしよう。
また多くの言語では、関数は複数のパラメータを受け付けるが、戻り値は1つしか返せないので、抽出は簡単になる。またくさんのローカル変数のような複雑な情報は、関数の中に書くほうが、関数の外に受け渡すよりも簡単だ。これはパラメータリストと戻り値が同じカージナリティを持てる言語では問題にならない。
ふつうコードを読む目的は、読むことそのものではなく、何かを達成したいことがあるから読むのである。これを念頭において、どのコードを読むのが大変かを判断しよう。
関連性の高いかたまりを素早く見つけるには、二分探索するとよい。探しているものが何も含まれていない大きなかたまり、小さなかたまりを探す。どちらでも、探索スペースを早く減らすことができる。
関連がないのが分かっている大量のコードを抽出する場合は、名前をHonestにしてから先に進む。
長いメソッドは、単一の制御構造(ガード条件とローカル変数へのデータのセット)によって占められていることが多い。制御構造の本体はたいていは、適切なターゲットとなる。複数の制御構造が連続している場合は、最後の制御構造を使う。
例えば、BecomeFroglike()メソッドに巨大なforeachループが含まれている場合は、ループの中身を抽出し、MakeOneBecomeFroglike()という名前を付ける。
例外処理はちょっと特別だ。tryブロックの中身は、抽出対象として適していることが多い。私は通常、結果の関数に*_Impl()という名前を付ける。複雑なcatchブロックは、抽出する価値がないことが多いので、他に抽出する価値のあるところを探そう。
NonsenseからHonestへ
私たちは"何か"に名前を付けたが、意味のない名前であった。私のFAAシステムの例では、メソッドはpreLoad()と名付けられた。システムがこの名前でメソッドを呼び出すことは分かるが、メソッドが何をするのか、なぜそれを呼ばなきゃいけないのかは分からない。
そこで、実直な名前をつけることにしよう。完全である必要はない。その名前が、とある事実を教えてくれさえすればよいのだ。メソッドをみてその実行の要点が分かれば良い。メソッドの中身を読み、何が中心に見えるかを探ろう。
どこを見るか: メソッドの中身を見よう。メソッド内部や他のメソッドとの間で繰り返されているパターンを見つけよう
("database", "screen", "network", "service"といった)システムコンポーネントである変数を探すとよい。戻り値がどこから来てるか、何度も使われているものがないかを見よう。
例えば、多く使われているグローバルな名前dbに気づいたとしよう。いくつかの箇所で、レコードセットのオブジェクトが生成され、値をセットし、dbを使ってそれらを読み書きしている。読み書きが一緒になっているので、メソッドには doSomethingEvilToTheDatabase() と名付ける。
私たちの気付き: コードが行うことの1つ。また、作業がどれだけ容易か、どれだけ安全化、その他の有用な特性を判断することもできる。
名前は完全ではないが、実直なものだ。
実直にはいくつかの方法がある。実際、doSomethingEvilToTheDatabaseにも複数の気付きが含まれている。
1. メソッドの主な作用は、データベースに関することのようだ。
2. データベースに対して何をしているかはまだ分からない。
3. 貧弱なデータベースをどう扱っているかについて、あらゆる批判を受けている。気に食わない。
2つめと3つめの気付きは、1つめと同じくらい重要だ。たとえ私がこの時点でどこかへ行ってしまったとしても、コードの未来の読者にある程度の気付きを与えられる。このメソッドが何をするのかわからなくても、このメソッドはヤバそうだと警戒させることができる。
それが何をするのか誰も正確に知らないこと、中央のデータベースに何かしているということ、最後に触った人がそのメソッドが何か悪さをしていると感じたことが分かる。よろしい。私の持っている全ての知識を伝達できている。
書き留めておくこと: より良い名前、それは実直なことを言い表し、まだ知らないことについてもハッキリしていること。
気付きを書き出すには、もう1つリファクタリングを実施する。「リネーム」だ。
より限定的に!
このフェーズで失敗しがちなのは、名前を汎用的なものにしてしまうことだ。限定的なものが良いよ!
命名の目的は、読み手がコードの詳細を見ることなく、それに付けられた名前を見るだけで、それが何をしているか分かるようにすることだ。具体的で詳細な知識が読み手には必要だ。したがって、名前はより限定的なものでなくてはならない。
例えば、関数名をhandleFlightInfoSomehow()にすることもできる。実直なものかもしれない。だが、コードが何をしているのかを正確に把握することはできない。
少し先に目を向けると、コードが実行しているすべてのことを名前が伝えるようにすることが次のステップである(Honest and Complete)。ここで限定的にしておくと、完全性への道は、名前に付け加えていくことになる。
私があまりにも一般的で不正確な人間だったら、関数が行うことのほとんどまたは全ては、すでに名前で大まかに記述できると考える。Honestにしておけば、名前の中に含まれていないものを見るのがずっと難しくなり、名前の後にあるコードを読めないような名前を付けるのが難しくなる。
関数だけでなない
このステップはクラスや変数に名前を付けるのにも適用される。一般的にクラスのNonsenseな名前というのは、-Managerや-Operationで終わるものだ。Nonsense変数には通常、型と同じ名前が付けられる。
いずれにしても、このことについて、1つの気付きをもつ必要がある。クラスの重要な責務は何か? 変数の場合は、このFooインスタンスは、他のFooインスタンスと何が違うのか? 変数を使ったコードが、この1つをどう考えるのか? 気づいたことは何であれ、その名前として書き留めておこう。
HonestからHonest and Completeへ
このメソッドがしている全てを全部表す名前にする必要がある。より多くの気付きを含むように名前を付けて、次に読む人がより簡単にコードを読めるようにしておくのだ。
このレベルは次に読む人がコードを簡単に読めるようにするためには、本当は必要ない。メソッドの中身を読まなくても信頼できる名前で中身が分かるようにすることが、このステップでの目的である。
見るべきところ: 名前を付けるものの中身
手順は以下のとおりだ。
1. コードがしている1つのこと(作用、計算、結果、状態の変更)を探す。
2. 既に付いてる名前に含まれていないかを確認する。
3. 含まれていなければ、その名前を追加する。
注意: 名前の一部を広くしすぎないように。目的はメソッドがやっていること全てを表現できるような1つの抽象概念を見つけることではない。目的は正確さだ。読み手はメソッド名を見て、メソッドがしていることを正確に知れるようにするべきだ。
メソッド名を誰かにメールで送ることを考えてみよう。あくまでも名前だけだ。メソッドの中でやっている全てのことを正確に生成できるだろうか? 何かを付け加えても欠いてもいけない。
気付き: 名前でまだ明示的に示されていない、何か特別なこと
命名規約に関して教わったことはみな忘れよう。このステップでは長い名前は良いことだ。接続詞を使ってもよいし、繰り返しがあってもよい。名前に全てのことが表現されることが目的なのだから。したがって、誰もが名前を信頼できるようになる。
書き留めておくこと: 名前に「節」を追加する
本当に大きなこと
場合によっては、サブコンポーネントを理解するために、サブコンポーネントを切り出して、より適切な名前を付ける必要がある。これは特に長いメソッドでよく行われる。これをやる際には、メインの名前に集中するようにしよう。メインのやっていることに名前を付けることが重要かどうか分かるまで、塊を抽出し、気付きを記録する。
ガード条件は良い例だ。通常は簡単に識別できるが、スペースを大量に消費する。それを名前で使うことはしないが、後の部分が読みにくくなることがある。
ガード条件と思われるものをいくつか抽出して、抽出されたコンポーネントをHonest and Completeなものにして、本当に全てがガード条件かどうかを確認してみよう。もしそうであったら、それらはそのままにしておいて、元のメソッドに戻ろう。
ガード条件以外になるまで、抽出を続ける。残ったメソッドの中をより深く追って、すべてをそのメソッドの名前に入れるようにする。作業が完了したら、ガード条件をインライン化するか、_ValidateAllInputs()のような名前で抽出したメソッドとして残しておくかを決めよう。
この例では、メソッドが行っているすべてのことを見つけるために、いくつかのステップを踏む必要があった。私は最終的に4つの主要な機能を特定したが、それぞれが多くのステップで行われていた。そのため私のメソッドは、parseXmlAndStoreFlightToDatabaseAndLocalCacheAndBeginBackgroundProcessing()という名前になった。
メソッドではないもの
このステップは変数やクラス名にも適用される。
クラスはすべての個々の責務を名前に含まなくてはならない。
変数はすべての変数の使用用途を名前に含まなくてはならない。
レガシーコードでは、変数は驚くほど色んな使い方をされているかもしれない。私はかつてイン・アウトに使われ過ぎているパラメータに、
searchHintThenBestMatchSoFarUntilItBecomesReturnValueOrReasonNoMatchWasFound
と名付けたこともある。
余談: 「何であるか」や「何をしているか」によるネーミング
このステップでは、名付けようとしているものの重要な特徴をあらわす全てが、名前に含まれる。それが何であるかではなく、それが何をするのかによって命名している。この違いは神クラスと長いメソッドを排除するために重要だ。
あるものが、それが何であるかによって命名するとき、それは同一性になんとなく関連のありそうなものを全部含みたくなる。一方、それが何をするかによって命名するとき、短い名前を好む私たちは、命名するものがやることを少なくするモチベーションが産まれる。
もし機能を集めて大きなものが欲しいのであれば、「それが何であるか」で命名しよう。分割し小さくしたいのであれば、「それが何をするか」で命名しよう。
これはWhole Valueパターンの反対になる。コードにPrimitive Obsessionがある場合、すなわちコードが行うことのすべての部分が、コードベースの周りに散らばっていて、それを収集したい場合にはWhole Valueを使う。何かを作り、それに基づく名前を付けると、プログラマは自然とそれら全てを集めてしまう。神クラスや長いメソッドは、それを分割したいと思う。それが何をしてるかで命名すれば、プログラマは自然とクラスやメソッドを分割するようになるものだ。
Honest and CompleteからDoes the Right Thingへ
名前を完全なものにすると、その実装の詳細をみることなく、クラスやメソッド、変数の責務を理解できるようになる。これによって責務の変更もできるようになる。
このステップでは、作業対象の名前のみに着目し、サイトと本体の両方を無視する。「この名前には意味がある?」ということだ。
私たちが消したい「節(clause)」には通常2種類ある。
この節は名前に含まれる他の節と無関係だろうか?
この節はカプセル化したい関心事だろうか?
見るべきところ: 名前、それ自体
気付きを書きとめ、他のものを見つけよう
気付き:公開したくない1つの節
書きとめることは、構造的なリファクタリングを要求する。Honest and Completeな名前をキープする必要がある。もし名前が気に入らないなら、それが何をしているかを表す異なる名前を付けなければならない。
書きとめること: ふるまいの1つを切り出したり、カプセル化する
これをやるには「インライン」のリファクタリングを使う。
メソッドから責務を安全に移動する
メソッドから、ある責務を分離したいとき、私は次の手順にしたがってやるようにしている。
1. 切り出したい部分の前にある全てをメソッドとして抽出する。
2. 切り出したい部分をメソッドとして抽出する。
3. 切り出したい部分の後ろにある全てをメソッドとして抽出する。
4. メソッドの名前を、抽出した3つのメソッドそれぞれに分配する。
5. 名前を完全なものにするために必要な、欠けている節を追加する。例えば、分割によって、ある計算をどこかに書き出す部分から切り離せるかもしれない。外部のメソッドは、...AndRecordYield...()と名付けられるし、内部の名前は、...AndCalculateYield...()と...AndRecordYieldToService()と名付けることができる。メソッドが小さくなるので、名前を限定的にする方法がわかりやすくなる。
6. これですべての呼び出し元が3つのメソッドを直接使うようになり、機能が分割された。
カプセル化
もう1つのよくあるのは、何かカプセル化したいケースである。このとき、名前から節を消すだけでなく、実際には呼び出し側が影響を受けないように、動作をカプセル化する必要がある。問題はふるまいがパラメータによって評価されることだ。
その一例がparseXmlAndStoreFlightToDatabaseAndLocalCacheAndBeginBackgroundProcessing()でのローカルキャッシュの扱いだ。このキャッシュをカプセル化して、性能のためだけにリードスルーキャッシュとして使いたいとしよう。
この問題を解決するには、リファクタリングできそうな一連の処理を見つけて、パラメータをフィールドに移すことだ。場合によっては、新たにクラスを作成したり、クラスを分割したり、使っているメソッドを別のクラスに移動したりする必要がある。また、オブジェクトの生存期間を変更したり、メソッドのすべての呼び出し元からの値への参照を消したりするのを含むこともある。
こうするためには、メソッドがパラメータではなく、フィールドを介して全ての値を受け取るようにリファクタリングし、オートフィクスを使って、未使用のパラメータを削除し、コールスタックを登って、出来る限り未使用のパラメータを削除し続けることが、もっとも簡単な方法となる。
ここに、staticなメソッドから始めるときに使う一般的な手順を示す。
1. パラメータオブジェクトを導入する。カプセル化したい1つのパラメータを選び、クラスFoo
2. staticメソッドをインスタンスメソッドに変える。導入したパラメータを選択する。
3. クラス名Fooを、少なくともHonestレベルの名前に改善する。
4. そのメソッドを呼び出している箇所に行き、新しい型の生成をする。そのパラメータを呼び出し元の呼び出し元にまで押し上げる。
5. カプセル化するパラメータの使い方を、新しいクラスのフィールドを使うように変更する。
6. 使っているところが無くなったら、未使用のパラメータを削除するためにオートフィクスを使って、今カプセル化されたフィールドを削除する。
7. 呼び出し元のメソッドでpublicフィールドを使っているところを探し、関連する文や式をメソッドに切り出す。
8. 抽出したメソッドをstaticに変換し、それを新しい型に移動するためにインスタンスメソッドに変換する。
9. 7〜8をカプセル化したフィールドを使う呼び出しメソッドがなくなるまで繰り返す。
10. このメソッドの呼び出し元へとスタックを登り、ステップ4から繰り返す。値の初期化まで到達したら止める。
11. フェッチや生成を新しい型のファクトリメソッドに移す。(ExtractやMake Static, Convert to Instanceを使う)
12. 元のメソッドの他の呼び出し元に対して、ステップ4から繰り返す。
13. この時点で、カプセル化する値への参照は、新しいクラス内だけになる。この値をとるコンストラクタの利用者は、ファクトリメソッドまたは他のコンストラクタだけである。
14. コンストラクタをprivateにする。
15. publicプロパティをインライン化して、カプセル化した値にアクセスするようにする。これですべてのクラスメソッドが、フィールドを使うようになる。
この手順は長いが、適切なツールを使えば各ステップを1〜2秒でこなせる。自分でコードを編集する必要はない。よく使われている値でも、練習すれば2〜10分でカプセル化できるようになる。
複数の関連する値を一度にカプセル化することもできる。またクラスを分割したり、似たケースを扱いたいときも、この応用でいけるようになる。
クラスと変数
クラスをDoes the Right Thingへリファクタリングするには、1回以上のSplit Classのリファクタリングを実行することになる。それぞれの分割で1つの責務を切り出す。
変数のリファクタリングでは、新しいローカル変数の導入しどれか1つをメソッドで使うように変更する。
Does the Right ThingからIntentへ
クラスやメソッド、変数がしていることにフォーカスできるところまで、私たちのネーミングは改善されてきた。実直な名前にし、正確にやっていることを表すようになった。名前は正しく明瞭なものになった。
だが、野暮ったい。
この名前を使って呼び出しているメソッドを理解したいのだが、その目的をまだ果たせては居ない。名前は「やっていること」を表していて、なぜ注意しなきゃいけないのかは分からない。
これでは呼び出し元のコードを読むときにつまずいてしまう。各サブコンポーネントの目的を示す名前が必要だ。だが現時点では、名前によって何をしているかは分かる。
呼び出し元のコードを読むときはいつも、その実装が意図したものであるかチェックしなければならない。これには精神的なエネルギーを要する。
私たちは気付きを得るための対象を見極めるところまで来た。そのコンテキストからの気付きを含める必要がある。
見るべきところ: 呼び出しているメソッドや使っている場所
使われている場所全部を読もう。他のクラスやメソッド呼び出しの流れでその果たす役割を理解しよう。メソッドの前後で何が起きるのか?
おそらく、この名前の周辺にある他の名前もイケてないと思われる。気付きを得る前に、それらも改善する必要がある。他の部分がHonestであれば気付きが得られることもあるが、少なくともHonest and Completeであった方が良い。
Honest and Completeレベルで、コンテキストのすべての項目を見ようとすると、その対象が実はDoes the Right Thingでないと分かるかもしれない。あまりないケースかもしれないが、そうだったとしたら修正しよう。
使っているコンテキストを調べると、ある流れが見えてくる。その対象は大きなオーケストレーションの中の1ステップだろう。そのステップには目的がある。この目的は、より抽象度の高いレベルにあることが多い。
気付き: コードの存在の目的。大きなオーケストレーションで使われているのはなぜ?
「やっていること」から「目的」を言い表すように名前を変えよう。よくある目的とは、以下のようなものだ。
メソッド
最初の状態やプロセスに関係なく、それが何を達成するのか (事後条件)
始めや終わりの状態に関係なく、それがやっている変換
それを置き換えるビジネスプロセス
メソッドの中心となる責務
クラス
メソッドたちに共通の責務
現実世界におけるクラスとはなんであるか (Whole Value)
呼び出すコンテキストがクラスに委譲し、クラスが果たす責務。呼び出し側が無視したいものは何か?
変数
このインスタンスが同じ型の他のインスタンスと違うのは何か?
このインスタンスでコンテキストが持つ目的とは何か? それはどう使われるのか?
書きとめておくこと: 目的に表す新しい名前
私たちの例では、storeFlightToDatabaseAndStartProcessing() は、 beginTrackingFlight() になる。最初の名前は、メソッドが何をするかが分かる。後の名前は、そのメソッドの目的が分かるようになっている。
私たちはフライトの追跡に何が必要かを知る必要はない。私たちはこのメソッドが追跡プロセスを始めることになる、ということを知る必要はある。
近くにstopTrackingFlight()があることを期待するだろうし、追跡されているフライトについての情報を得るためにいくつか検索クエリのメソッドもあるかもしれない。
別のところでもやってみる
別の呼び出しコンテキストでこのステップを繰り返してみよう。同じ気付きを得るかもしれないし、別のコンテキストだと付けた名前がしっくりこないかもしれない。複数の呼び出し元で同じコードを、目的が何かではなく、そのメソッドを呼ぶと何が起こるかとして使っているかもしれない。
こうなると、実装を共有する2つのメソッドを作ったほうがよい。異なるものを意図した表現は、異なるものとして外側の世界に見えたほうが良い。コンピュータに対しては、「今のところ」同じことをしているかもしれないけど。
実装を共有するメソッドのリファクタリング
次の手順を行う。
1. メソッドの全体を別のメソッドとして切り出す。
2. 切り出した(内部用になる)メソッドを、以前使っていたDoes the Right Thingな名前に変更する。
3. 外側のメソッドをコピーし、その名前を確実に一意になるように編集する。(ここではNonsenseでよい)
4. Nonsenseメソッドの名前を新しい目的に変更する。
5. 新たな目的でメソッドを使うように、呼び出し側を修正する。
一旦Nonsenseにしてから、手作業で名前を変更するのは、ツールがエラーをキャッチできるようにするためだ。普段は手動でコードを編集するのは危険なので避けるようにしているのだが。
名前が被ったり、別の名前を付けてしまったりすることがある。特にインポートされたネームスペースを持つ継承された関数やグローバル関数があると、その可能性が高まる。だが、これらは全部リネームリファクタリングツールが検出すべき問題だ。独自のNonsenseなものに手作業で変えることで、間違いを犯す可能性を減らしている。その後、名前を最終的なものに変えることによって、その一時的な名前が残ったままにならないことを保証している。
実装を共有するクラスのリファクタリング
クラスの場合は、使っているIDEがSplit Classをどうやるかに依存する。
もし、Split Classが委譲をサポートするのであれば、以下のようになる。
1. Split Classして、すべてを選択する。古い型を使っている
2. 内部の型の名前を、Does the Right Thingの名前に変更する。
3. 外部の型のためにファイルをコピーする (ファイル単位のクラスを想定)
4. コピーしたファイルのクラス名とコンストラクタを、一意性が保たれるように手動で変更する。ここではNonsenseなものが良い。
5. コピーしたクラスの名前を新たに目的を表すものに変更する。
6. コンストラクタを使っている箇所を、新しいクラスの方をインスタンス生成するように修正する。
7. コンパイルエラーに従い、毎回オートフィクスを適用して、変数の型を変更する。
Split Classが移譲をサポートしなければ以下の手順だ。
1. 新しいクラスをNonsenseな一意な名前で作る。古い型のprivateな1つのフィールドと古い型のコンストラクタを持つ。以上するメッンバを作るために、auto generateを使う。
2. コンフリクトを起こさないために、古いクラスをDo the Right Thingな名前にリネームする。
3. リネームをundoする。
4. 外側のクラスの名前を内部クラスの名前に手動で編集する。
5. 内部クラスをDoes the Right Thingの名前に手動で編集する。
6. 正しい型を使うように、外側のクラスのフィールドの型と構造を更新する。
7. この時点で、すべての呼び出し側は、目的に基づく名前を介して外部クラスを使うようになる。
8. 移譲をサポートするSplit Classを前提とした手順の3を参照。
切り出したものに新しい名前を導入したいが、すべての利用者には外部用のものを使わせたい。内部用のものを切り出せない場合は、shim(くさび)として新らに外向けのものを作り、名前を手動で編集して配置することで同じ結果を得ることができる。
また、一般的なリファクタリングのテスト戦略も使う。リファクタリングの最も役に立つ部分は、コードを変更することではなく、どのようなコードの変更が安全であるかを示す子だ。場合によっては、リファクタリングが意図したとおりに実行されないことがあるが、その分析を利用して、実行したいことが安全であることが確認できる。
Renameリファクタリングを実行し、Unduすると、名前の変更を手動で実行して”くさび”を配置できる場合でも、新しい名前でコンフリクトが起こるかどうかをテストできる。
注意: Nonsenseに逆戻りしないように
このステップは失敗しやすいところだ。失敗するとNonsenseな名前になってしまう。そして、それに気付かない。コンテキストが整っている名付け直後であれば、意味のある名前だと感じるかもしれないが、次に使おうとしたときにはNonsenseかもしれない。
問題はこのステップが意図的に情報を減らすことに起因する。何をするかについての情報の一部を、なぜそれを使うのか、その目的に置き換えているからだ。私たちはそのクラス/メソッドの潜在的な利用者に、信頼してほしいとお願いしていることになる。信頼を得なければならないのだ。それには、一貫して明確な名前とDoes the Right Thingsなコードが必要だ。
Nonsenseになる方法
なぜ使われるかではなく、いつ使われるかを表す名前を付けるとNonsenseなものになりやすい。初期条件で名前を付けても同じくNonsenseになりやすい。メソッド名では初期条件を無視しよう。そうしないと抽象概念が壊れて終わる。
他では決して使われない新しい概念を作り出すと、Nonsenseになりやすい。単発の抽象化はノイズでしかない。多くの名付けられたエンティティを参照し、抽象概念を作り一貫してそれを使わなければならない。
コンピュータ・サイエンスの用語を使うのはNonsenseなものになる最良の名前付けだ。プログラミング言語を書かない人には、ソフトウェアエンジニアの領域の名前は馴染みがない。だがソフトウェアエンジニアとしても、ドメイン固有の言語の方が読みやすいはずだ。
Nonsenseを避けるためには、コンテキストに特有なものになるようにしなくてはならない。そのクラス/メソッドがやることが、なぜそれをやりたいと思う人がいるのか、その理由を明確に名前に表現する。Honest and Completeの名前のうち、より明確にしてくれるものがあれば残しておいてもよい。残りは捨てよう。
IntentからDomain Abstractionへ
ようやく、ネーミングにおける最重要なステップへの準備が整った。進化的設計のすべてが「良い名前を付けよう。継続的に。」ということに行き着く理由が、このステップにある。ドメイン抽象化を正しくし、名前付けを調整する時がきた。
今、私たちは一緒に使われている名前の集合を手にしている。それぞれ個別に意図を表現している。責務が正しい場所に配置されている。だが、これらの名前を総合すると、アイデアの寄せ集めにすぎない。それぞれが自分自身とそのコンテキストを、うまく表現しているが、共有コンテキストは形成されない。
ドメイン抽象化とは、あるコードの集合のための共有コンテキストにすぎない
この時点では、私たちの探し求めている気付きは、全体の価値である。生成ドメインパターンを見つけたい。一度設定したら、すでに書かれているコード同様に、足りないコードが明らかになる概念である。
足りないコードはまだ書く必要はない(ドメイン抽象化を誤るかもしれないので)が、このコードをどこにどのように追加すればよいのかは、明確な状態のままにしておく。
このステップで行う基本的なプロセスは、「基本データ型への執着」を見つけ、欠けているValue Object(Whole Value)に関する気付きをえて、それを記録することである。これを機械的に行えば、統一されたコンセプトがまだ見えてないコードについても、気付きをえることができるようになる。
機械的に「基本データ型への執着」を探す
見るべきところ: いくつか共通点をもつメソッドやクラスの集合
探すべき共通のパターンを示す。
クラスの集合に関して:
クラスの外との相互依存している
1つのクラスでいくつかのメソッド
似た名前を持つクラス
Thing / ThingIsValid
接頭辞/接尾辞が繰り返されている。〜Transform,〜Traversal,Tree〜, Tail〜
寄せ集めの名前のクラス
〜Manager
〜Transform
名前が〜erで終わる。結果やオブジェクトよりもアクションを意味している。
Feature Envy: 自分よりも他のオブジェクトのメソッドやプロパティに多くアクセスしている
メソッドの集合に関して:
複数のメソッドに対して、一緒に渡される複数のパラメータ
毎度同じ組のオブジェクトのフィールドを使うメソッドの集合
似た名前をもつメソッド
begin / end / see progress / poll for data
create / read / update /destroy
source / sink / transform
類義語を含む名前
同じデータに対する多数の操作。
一緒に呼ばれるメソッド (特にエラー処理や生成ロジックで一般的)
システム内で一致する名詞がない名前の定型句
フィールドとパラメータ
変数の型名ではなく、変数の名前でそれが何であるかを狭める
firstName: String
lastName: String
socialSecurityNumber: String
許容される値を狭めたり解釈が必要となる名前
rangeInMeters: int
hardwareModeFlags: int
これらはそれぞれ、ドメイン抽象化の集合に何かが欠けていることを示している。他のコードは抽象化が欠落しているのを甘やかしている。
これらは、より明確で単一の目的を持てるものに、行き過ぎた一般化概念を適用しているコードがあることを示している。
それに関してやること
上記の1つを見れば、何かが欠けていることが分かるだろう。目的は何が欠けているかを理解し、それを作ることだ。これは2つのステップからなる。1つめは、プリミティブに名前を付け、代わりに使うべきものに名前を付けることだ。
気付き: 置き換えられるプリミティブとドメインの概念
コードにおけるプリミティブの名前を変える必要はない。それは置き換えてしまうからだ。代わりにノートやホワイトボードに記録しよう。見つけたプリミティブと、コードがそれに対して固執しているふるまいを正確に記述しよう。
気付きをコードに表現したいことだろう。それには、次のようにして行う必要がある。
1. 固有の概念を一般的な概念から切り離す
2. 甘やかしているコードのそれぞれの塊を調べる
3. そのコンテキストから新しい概念に関する部分を切り離す
4. 新しい概念に関連する部分を移動する
変更を実施する
記録すること: 私たちのドメインへ導入する新たなValueObject(Whole Value)と、それを使うための既存のコードの修正
最初の3ステップは、前のフェーズで使ったリファクタリングを利用する。
1. コンセプトをMissingからNonsenseへと変え、必要に応じて命名レベルをあげる。
1. メソッド: メソッド抽出を使う
2. クラス: クラス抽出を使う
3. パラメータおよびフィールド: パラメータオブジェクトを使う(問題は型だ)
2. Find usagesを使う
3. また分離する。上記参照。
ステップ4では、以下を使う。
メソッド/クラスの移動
インスタンスメソッドへの変換
staticへの変換
(もしC#だったら) 拡張メソッドへの変換
リネーム
リネームの最も一般的な形式は、意図的にコード片を結合することだ。プリミティブなものがシステムでしばらく動き続けていると、複数のコードがそのプリミティブなものに対して、同じ操作を実行したいと考えるようになる。
ステップ3では、これら重複したコード片を小さなメソッドに切り出した。それぞれのコンテキストに基づき、適切な名前(Intentレベルの名前)を付けたのだ。
4では私たちは、
1. 新しいクラスに全てを移動する。
2. 重複しない名前を使い、ふるまいの変更が無いようにする。
3. 同じ実装が近くになるように並べ替える
4. 同じ実装のペアを調べて、意図を共有しているかどうか確認する。(特に欠けた抽象概念を加える場合、すべての重複が同じ目的を持つわけではない)
5. 同じ実装と意図を持つペアを見つけたら、どちらか一方のあまり目立たない方の名前を、もう一方の名前と同じものに変更する。
6. 次に片方を削除し、呼び出し元はコードを共有したままにする。
7. ジソすを共有する各ペアが、意図を共有しなくなるまで繰り返す。
を行う。
実装ではなく、意図を共有するペアを見つけることが共通している。これらはバグに繋がるかもしれないし、機能になるかもしれない。
同じ目的を持っているが実装が異なる2つのコード片を見つけたら、以下の手順を実施する。
1. 2つよく似た名前にリネームする
共通のプレフィクスに違いを表す何かを付け加える
違いは実装であって、意図やドメインに関するものではない。それはスメルであり、以前よりはよい。
後でもう一度名前を付けることで、そのスメルに対処できる。まずこの手順を完了させよう。
2. 2つのそれぞれについてテストを書く。アサーションメソッドを共有した共通部分と、それぞれ個別のアサーションを使った違いの部分の両方。
3. (オプション) 区分値を使って2つのメソッドを1つにまとめる
最後のステップは、多かれ少なかれ何かを明確にする。コードスメルを隠し修正の可能性を減らすかもしれないが、妥当なドメイン抽象概念へと成長しうる新しい概念を作り出すことになるかもしれない。
やり方の詳細は、私のgist Combine2methods into one with a discriminator variable を見てほしい。(ここでは、少し異なる2つのCar.Drive()の実装を組み合わせている)
gistの履歴には、安全性を損なうことなく再設計するための一連(17ステップ)のリファクタリングがある。
私は呼び出し側がどうなるかを示すためだけにテストを書いている。各ステップを検証するためにテストに依存しているのではない。
呼び出し側の何千のコードを完全にはテストされてないコードでこれを実行するのも、(安全で)同じくらい快適である。
そして、それは…
私たちはドメイン抽象概念を識別し、書きとめた。これで命名は完了だ。
現実の世界では、私はこのプロセス全体を完了するのに一週間ほどかかった(トータル時間は30分ほど)。Missingの名前を1つずつ見つけて、それらに名前を付け、Honestレベルにあげていく。それを1つの領域で4〜6回ほどやった後、それぞれの名前をDoes the Right Thingにアップグレードする。それを2〜3回繰り返すと、目的がハッキリするので名前をさらにアップグレードできるようになる。
そして、同じ変数を操作するステートメントの基本要素や重複した手続きを頻繁に目にするようになる。あたりの全ての名前がその意図を表すようになると、欠けているWhole Valueが明らかになる。多くの場合、領域全体の意図が明らかになる前に、Intentな命名を2〜5回行うことになるだろう。
新しいドメイン抽象概念を導入する。実際、私はいつも3〜5個の抽象概念を一気に導入している。適用可能なケースの約60%で、新しい抽象概念を使うために、コードはプロジェクト全体で急速に変化する。その後、抽象概念が重要であることが分かってくると、1〜2週間かけてゆっくりと変わっていく。それは新しいオペレーションを引き起こし、他の適用可能なケースをキレイにするのだ。