まえがき
主にWebプログラミングをターゲットとして書いていますが、汎用的に使えることも書いてると思います。
リファクタリングメソッドをメモろうとした経緯
コーディングをしていると、外科医のお医者さんにでもなったような気分になります。現状の病に侵されているプログラムを、みると、どうしてもっと早く処置をしなかったのかという段階や、周りの人には手が負えなくなっていてもこの程度のアプリケーションなら苦もなく直せますよっていうレベルの作りのものまで、色々なフェーズのアプリケーションを見てきました。
ただし、例外なく処置が遅れるほどリファクタリングには痛みを伴わなければなりません。それでもできる苦痛なくリファクタリングを行う方法を体系化していれば、ある程度巨大なプログラムの変更に立ち向かうスキルを得られたことになります。このようなスキルはとても大切であるにも関わらず、あまり体系化されていないように思い、一部の上級者の人々の秘伝のタレ化している気がしているので、実際の実務のケースで出くわしたアンチパターンに対してうまくいったという経験則だけに基づき、僕が考えたさいきょうのリファクタリングメソッドを記録しておこうと思いました。
リファクタリングは必要か
機能の代替コストに寄ります。一般的にコードの規模が大きいほど、また、機能の複雑度に寄って、判断します。
綺麗に設計されたコードに寄せることが目的であるため、リファクタリングするよりも綺麗に設計されたコードで1から綺麗に置き換えた方がはやくコストが低いという場合において、リファクタリングは本来の目的を達成するための手段としては最適ではないかもしれません。
安全な運用を行いながら、最も手っ取り早くコードを誰もができるだけ把握して管理をしやすく状態にするという目的があるのであれば、マイクロサービスアーキテクチャに置き換えるという選択が手っ取り早いこともあります。
どのようにリファクタリング能力を身につけるべきか
冒頭でリファクタリングは外科医に似ていると話しました。これは、チーム全体の技術力や経験値・ノウハウによって取り得る手段が制限されることを意味します。例えば、重度の患者に対して、難しい手術をする際にその手術をそもそも出来る技術力があるのかどうか、自信もってやれるかどうか、医者のレベルで成功率が上がっていくのと同様に、プロジェクトが救われるかどうかは技術者の知見と腕にかかっているからです。そもそもリファクタリングの手段を持っていない段階であれば、その手段を身に着けなければいけません。色々なリファクタリング手段やコードのアンチパターンを改善して手段を検討できる段階になって初めて理想の状態を描けるようになります。逆に言うと、その理想の状態が見えていなければ、リファクタリングにかかる無駄なコストに無限あるいは、必要以上に無駄な工数に付き合わされるはめになり、プロジェクトは失敗します。僕が関わっていた何社かでも、プロダクトを仕切るリーダークラスの技術者が理想の状態を描けず、そうなっていったプロジェクトをいくつか見ました。
リファクタリングは簡単ではないので、今自分たちがいる組織のエンジニアの技術力で出来る範囲、出来ないのであれば、何が最善か、コストをかけてでもやるべきかどうかと言ったような何らかの経営(あるいは技術集団における意思決定)判断が必要になるでしょう。というのも、リファクタリングやサービス設計に踏み込んだとしても、何らかの要因でリファクタリングは、必ずしも思った通りには進まないケースが多いからです。
バグの温床や実装を複雑にするコードのパターンというものは開発者のバックグラウンドやそれを取り巻く状況において無限に存在し(これも患者さんの関係みたいですね)、ケーススタディでしか学べません。僕も実際に自分の脳みそで考えられる範囲で効率的だと思ったリファクタリング手段も実際の現場では、おぞましいほどに想定外のプログラムの挙動によって様々な問題に立ち向かわなければ行けなくなり、大体のケースにおいて、影響範囲を全部把握することを強いられるか、それらの挙動の全容を把握する前にそれらを消し去ることになります。
また、リファクタリングによって返って状況が悪化するというケースはそんなに珍しくありません。リファクタリングは理想の状態の認識が高いレベルで開発メンバーの間で一致していない場合、より良くしていこうと頑張った結果、直せど直せど負債が生まれ、ルールベースのコードが破綻していき、色々な思想が混ざったキメラが産まれ、ますます手に負えないプロジェクトになっていくことさえあります。
つまり、複数人でリファクタリングをする場合は、前より何を良くしているのかという認識とリファクタリングによって達成される目標が高いレベルで揃っていることがリファクタリングの成功条件だと思っています。
一般的に綺麗に設計されていないプログラムのリファクタリングは方針を揃えた少数精鋭で行うのがベストだと思っています。
リファクタリングに関する良書は沢山有りますが、自分の場合はその中身を真に理解するためには、自分がその事例に遭遇して本当に困った経験をして実際に直した経験をするのが一番手っ取り速いと思いました。リファクタリングはやればやるほど成熟していき、他のエンジニアの方が直せなくて困ってたデータ構造でさえ、熟練者の手にかかると瞬殺できたりするので外科医に近いなと思った所以だったりもします。また、実践をするためには巨人の方の上に乗り、真剣に問題を考え、困難を解決するためなら解決方法の模索を惜しまないという気持ちで理論を自分の頭にインプットすることも大切です。すでに問題解決のために有用とされている良書については知っていて損はないですし、その知識によって思いもよらぬリファクタリングのトラブルに巻き込まれた時のとっさの判断力がつくようになるからです。
リファクタリングの原則
僕が思うリファクタリングを行う際の原則を書きます。
- 不要だと分かったものはすぐに消す
- コードを最小元にする
- コードの変更によって影響を受けるファイルやコードの範囲が明確になるようにする(疎結合のスコープを把握しやすくする)
- 機能を置き換える必要があるものは入力と出力の関係を保ちながら変更を行い、入力値・出力値の変更は最後に行う
-
設計の複雑性排除を最初に考え、最もシンプルな動作になってなければ最もシンプルなロジックへと変更する道筋を思い描く
(ロジックが本来より複雑な状態でリファクタリングを行いコードがマシになっていったとしても、出来上がった物は歪なままであり、根本的にその状態で実装を重ねていくと、やはり将来的にも負債になっていく可能性が高い。この状態でテストを作ってしまうと、テストケースが実装の方針に縛られてしまい、更にそのテストすら負債になっていく可能性が高い。実装によってテストが引きずられているため、筆者はこれはTDDと真逆のアンチパターン現象だと思っている。) - **リファクタリングの方針とルールをチームで共有する。**コミュニケーションが失敗するとリファクタリングは困難になる。
- リファクタリング範囲にテストが存在していなければ、テストを作り、アプリケーションに求められている処理が把握できるようにする。
- **コアな機能でなければ、最悪の場合、機能のデグレは覚悟する。**心臓を守るために、手足は切断しなければいけないということはある。名医は最小限の犠牲で最大の効果をもたらす。一般的に全てを守らなければいけないと思いこむほどリファクタリングは遅れる。ただし、**必要がないのに関わらず手足を切断しだしたらそれはヤブ医者ならぬヤブエンジニアに過ぎない。**犠牲にしても大丈夫な機能を見抜けることこそ、リファクタリングの熟練者の真髄である。
僕はリファクタリングは地味でつまらない作業だと思っていますが、そこでどれだけのスピードで正しい形に持っていけるかはエンジニアの能力を測る上で最も能力が加味されるところであると思ってます。
前置きは、これくらいにして、以下からこの記事のタイトルの由来にもなった実際の現場で出くわしたアブノーマルなコード実装を元に、それに対してどの様に対処したか、なぜそれが有効だったか、汎用的な手段を幾つか紹介します。誰もがやりたがらなかった魑魅魍魎どもを退治するアブノーマルアプリケーション・リファクタリングの極意をご覧いただければと思います。
アブノーマル・パターン
DBのカラムが数十・数百に及んでいる
対処法
まずは、それらが何のために使われているのか一つずつ見ていきます。
分離できるものが見つかるたびに、新しく理想の定義状態を明確にしていきます。
この段階であとは、少しずつ使われているアプリケーションに合わせてデータを整形できるように処理します。
原則は以上のような感じになります。テーブルに新しくデータ移行して対処する方法をいくつか紹介します。
NoSQL + Apache Beam
NoSQL DBを利用して、運用中のデータを一旦NoSQL DBに全部突っ込めるようにします。ここから新しくデータ構造が確定したデータをApache Beamにも流すようにし、新しく定義されたRDBに整形されたデータを蓄積させます。この方法の良いところは、リファクタリング中でも本番稼動させ続けられることと、アプリケーション側のコードのリファクタリングが、DBリファクタリングを終えてから行えると言うところになります。また、Apache Beamのパイプラインは自由に付け替え可能であるため、複数パターンのデータ構造の検証が容易です。
アプリケーション側に新しく作ったテーブルに合わせたエンドポイントを設定し、完成したらエンドポイントをすげ替える
おそらく一般的な方法は以下のようになると思います。DBを再設計後、既存のテーブルから新しいテーブルへデータを抽出し、移行が完了したら、新しいアプリケーションに置き換え、既存のテーブルを消すという作業になります。
ただし、この作業、データ量に寄っては、安全移行が難しく、よく失敗する光景を目にすることもあるのではないでしょうか。少しでも、安全に行うために、本番環境のエンドポイントを出来た順に分離し、webサーバーで接続先アプリケーションを分けたり(マイクロサービス化)、接続先データソースを複数にするという手段を用いることが好ましいです。
何でもかんでも変数定義
基本的には、スコープの中で使われる変数は少ないに越したことないのですが、よくあるパターンです。値の再利用で済むところをわざわざ新しい変数を用意して、同じ値を代入して使用していたり、定義されているのに使われていなかったり、あらゆる値を変数に代入してから使っていたりします。
対処法
まずは使われていない変数はIDEなどの機能を利用して全部消しましょう。次に、本当にその値は変数にする必要があるのかどうかを考慮します。例えば、
一度しか使われない
Boolean toggleFlag = true;
toggle(toggleFlag)
のようなデータを変数にする必要があるのかということを考慮します。また、変数にする必要がある場合、次の散らばるGlobal変数パターンに当てはまって、影響範囲がでかくなりすぎないかなどの考慮が必要になります。あるクラスでのみ必要なステータスのような値や、一度しか使わない値をスコープの外やファイル外に切り出すと返って、その値への関心がプロジェクトの中で大きくなってしまい、どこで関連する値なのかが把握しづらくなります。次の項目で、スコープを制限するテクニックについては詳しく述べます。
また、スコープが大きくなるほど、一般的に定義される変数は増えるので、関数切りだしのリファクタリングを並行して行い、一つの処理に関わる変数の量を減らしていきます。
さらに、構造体、クラス等の値に切り出すことによって、変数の管理を他のオブジェクトに委任し、処理内に関わる変数が減らせることがあるので、一度に多くの変数を把握しなければいけない状態であるよりもなるべく関心の範囲を最小限に実装し直すのが良いです。
スコープはみ出しすぎ変数・散らばるGlobal変数
Global変数は悪というのを頭に叩き込まれてる人も多いのではないでしょうか。プログラミングをしているとどうしても避けられない場合を除いて、scopeを制限しないと言うのは悪です。また、scopeも必要最低限度にするのが良いと言うのが原則です。
対処法
静的型付言語かつオブジェクト指向プログラミングをしている方は慣れているかもしれませんが、原則、クラスの値はprivate変数で管理し、どうしてもpublicメソッドで使うパラメータであってもできる限り、外部からの変更をさせないようにクラス内のprivateメソッドの中で隠蔽して完結していることが望ましく、getterアクセサ経由の取得でしか値を取得できないようにするのが原則です。というのもカプセル化した処理内容に副作用を与える外部からのパラメータは増えれば増えるほど、管理が難しくなるからです。Global変数の使用は基本的に論外で、使っていい例外は不変値だけで、使わない事を推奨します。不変値といえど、仕様の変更を受けて複数のコードにまたがる影響を受ける可能性がないとは言い切れないからです。その他は、全て引き数経由、あるいはmixinやtraitと言った共通の処理をクラスに埋め込む方法で対応します。クラスの継承は管理が難しいので、デザインパターンやフレームワークの仕様に沿っていなければなるべく使わない方が良いでしょう。
また、Globalにせざるをえない値は集約場所を作るべきです。それは、値であればfileかDBが適しています。原則に従えば、globalである必要がない値は、特定のスコープの範囲で集約されているので、global変数もよほどの数にならないはずですが、役割でscopeを分けるのは有用です。例えば、localeやエラーメッセージの類は、辞書ファイルなどで設定した方が良かったりするなど、Global変数であっても、全ての集約場所を統一するのは危険です。重要なことは、どこに何の設定ファイルがあるのか把握できるようにチームでルールを決めたり、フレームワークやライブラリを有効に使用することです。
以上を踏まえ、クラスや処理の中からGlobalである必要がないものをファイル検索から見つけてきて、全て必要最低限のスコープに戻していきます。検索は名前重複してると識別量が多いので大変ですが、フィルタなどを掛けて頑張ります。他のリファクタリングで消せそうな物があれば、後回しにしたりしてこのリファクタリングを出来る範囲でやるだけでも構いません。それだけでもやらないよりマシになります。この際オブジェクト指向言語であれば、不変値はprivateのインナークラスや、const変数として定義し変数はstaticな変数として定義するなどして、どこからこの設定値が参照されているのか分かりやすくします。
関数型言語であれば、基本的に値は最大入不可のスコープドな世界で閉じるので、突拍子のない副作用の心配やglobalな値を持つ可能性を制限することが出来るので、immutableかつpureな関数実装を強制するフレームワークを導入して置き換えていくなども視野に入ります。
Magic Number
対処法
一度でも、ある関数に突っ込まれてるパラメータが誤解を招きそうだと思って危険だと感じた際は、Enumratorや一時変数を使って、命名します。全文検索をかけると、該当メソッドが見つかると、この場合のように可読性を上げるために変数を増やすことは、何でもかんでも変数定義の例外で一回しか使わない引数名をつけても許されます。
引き数多過ぎ
引き数の数やタイプの変更は経験上バグを生み出しやすいかつ、致命的なバグを生むことが多くあります。
特にpublicに設定され、至る所で呼び出されている関数の場合、変更を忘れると、もはや、修正が効かなくなるレベルでカオスを生み出す可能性が高いのでリファクタリング時に最も気を使うシチュエーションの一つです。
対処法
引数はできるだけ構造体や、staticなクラス、分割代入などを利用して、投入するパラメータが増えても、その引数の数を変えずに、
新しいパラメータを増やすインターフェースを作成するべきです。たとえば、jsの例でいうと
function simpleArgumentsExample(setting) {
let {a,b,c} = setting;
}
という実装にしておくと、settingの項目が増えたとしても、分割代入するパラメータを増やすことで、引数の数自体を、増やさずに済みます。
また、構造体などの場合は、厳密に引数で渡されてくるパラメータが決まっているため、コンパイラなどがエラーを出力してくれるなどの恩恵を受けることができるため、型定義ができるプログラミング言語では、型を作成するのが良いでしょう。
struct A {
public SampleStruct(int a, int b) { this.a = a; this. = b; }
public int a {get;set;}
public int b {get;set;}
}
void simeArgumentsExample (A setting) {
Console.WriteLine(setting.c); //errorになるが、accessor cを追加すれば、settingの項目を増やすことができる。
}
引き数修正後は必ずモックによるテストを入れましょう。引き数に入力されたデータはMockで取得し、検証が可能です。
引き数の数やデータの構造が変わったら必ずテストで縛る実装を行いましょう。
名前と処理が一致しないコード
僕が経験した開発現場では
getUsersData
という様な名前のメソッドが有りました。
普通の発想であれば、このメソッドがUsersテーブルのデータが引っ張ってユーザーのデータが配列で返ってくるのではと思いきや、なんとUsersを呼び出すためのwhere句が書いた文字列がreturnされてきます。恐るべきアブノーマルメソッドです。また、このメソッドの恐ろしいところはなんと更にクエリ文字列を引っ張ってくるだけではなく、外部に露出している全くスコープに関係ないpublic変数を書き換えており、このメソッドを経由しないとフラグが変更されず、事故が起こる事が想定される設計になっていました。さらに、こんなメソッドがありました。
function getTargetUserIds (conditions) {
// 謎のクエリが走るメソッド
// ...
return []; // 空配列が返ってくる
}
ユーザーIDが手に入ると思いきや、空の配列が返って来ます。怖いですね。この後、なぜか普通にこの配列に対してユーザーIDをjoinする実装があり、何か経緯があってこうなっていたのだと思いますが、経緯があったとしても断罪すべき実装でしょう。
対処法
忘れさられたメソッドの項目でも説明しますが、この項目に従って、基本的に最初に消せるものは全て消してしまいます。上記のような状態になるともう何も信じられません。中身を読むまで名前と処理内容が正しいかどうか検証不可能なので、1つずつ中身を読むしかなく、一番しんどいパターンです。ただし、この方法も効率化することが出来ます。デバッグツールなどで使用されている変数一覧を取得していきます。そこからpublicに漏れ出ている可能性があるものをリスティングし、それを検索にかけることで、意外と早く片をつけられたりします。ネストされた関数を呼び出している場合は、その関数の中も同じように調べていきます。publicに漏れ出ている可能性のある変数が特定できれば、それを本当にそこで変更する必要があるのか検証し、できるだけ副作用を消していきます。さらに、名前を適切で把握しやすい名前に書き換え、副作用の無くなったメソッドはテストを書きやすいため、テストを書いていきます。さらにそのメソッドの入力値・出力値が解れば、いま書かれているメソッドの実装が実はもっと少なくなることに気づき、リファクタリングが容易になります。
めちゃくちゃ地道ですが面倒臭がらないこととやることを決めておけば意外とサクサク片付きます。
忘れ去られたメソッド
どこからも使われておらず定義だけが残されているメソッドです。
対処法
これも厄介なパターンです。privateやprotected、sealed、internalなど、スコープが限られている場合は探索範囲が少なくて済みますが、publicメソッドは、検索掛けるしかありません。IDEなどで使用範囲の表示が出来る場合は、それに従うと楽に特定できますが、フレームワークで実装されているエンドポイントなどと自動で紐付いていると、結構苦労します。RailsやLaravelなどのエンドポイントを一覧できるようなシェルがあれば、多少楽ができます。
この場合全ての関数定義を一つずつ検索にかけ使われているかどうかを調べるしかありません。原則は大きなスコープから(つまりエンドポイントや、エントリポイントから)メソッドを一つずつ検証します。なぜならば、そこから消せるメソッドを見つけると芋づる式にメソッドを消せることがあるからです。更に、この方法はエンドポイントやエントリポイントを閉じることで芋づる式にメソッドを消せることも示唆しており、忘れ去りたいメソッドにも応用が効きます。この場合、stackTraceで呼び出されたメソッドを検出したあと、それを検索にかけ、そのエンドポイントでしか使われていないメソッドを一掃していきます。エンドポイントを閉じたり置き換える方法は、もう直したくないと判断したコードを一気に捨て去るのに有用です。
非常にコストが掛かるため、このコストが払えないという判断になると、マイクロサービスに置き換えるとかの選択肢の方が現実的になっています。
ただし、モデルが100〜200で一つ平均30個程度の関数量であれば、マンパワーでなんとか出来ない作業ではないです。やりたいかどうかはまた別のお話。
if文だらけの巨大メソッド
プログラミング覚えたてみたいな人がよくやるパターンです。僕のとある後輩もフィルター方式でやると8パターンで済む分岐を64パターン書いてたりして、たいへんだったねと労ったことも有りました。そのパターンのなかにコピペコードが大量にあったのもいい思い出です。よくコードを見ると、ある重複処理のためにその重複処理の前後で条件分岐が書かれている事がよくあります。さらに、その条件分岐は単独だったり複数条件だったり散らばってたりするわけですが、実は同じ条件を多用してるところが多かったりします。
対処法
条件分岐の条件を眺め、そもそもその条件分岐で行う処理が複雑すぎるのであれば、そもそも条件分岐である必要があるのかどうかを考えます。処理が短い場合は、重複条件を一箇所にまとめ、最低限の処理で済むように変更しますが、大抵把握が困難になっている場合は必要以上に複雑に条件を分岐し過ぎているので、条件分岐になっている処理を全て、別々の関数に切り出します。以下のような感じです。
function spaghetti(a,flag) {
var result = [];
if(a=='hoge' && flag == true) {
someHogeTrueAction() //hoge固有の処理
}
else if(a=='fuga') {
someFugaAction1() //fuga固有の処理
}
var resultHoge = someActionA() // 分岐に関わらない何らかの共通処理
if(a=='hoge') {
someHogeAction() //hoge固有の処理
}
var resultFuga = someActionB(resultHoge) // 分岐に関わらない何らかの共通処理
if(a=='fuga'){
someFugaAction2() //fuga固有の処理
}
if(flag == true) return resultHoge
return resultFuga;
}
上の条件はもはやresulHogeとresultFugaもどんな処理が返ってくるのか全く想像がつきません。こんな風になってしまったら、もはや条件分岐をせずに別メソッドに切り分けた方がましです。以下のようにそれぞれの処理の条件分岐をaの値だけに限定し、共通処理を重複させてでも個別のメソッドに切り出してみましょう。
function switchFunction(a,flag) {
if(a == 'hoge') {
return prettyFunctionHoge(a,flag)
} else if (a == 'fuga') {
return prettyFunctionHoge(a,flag)
}
}
function prettyFunctionHoge(a,flag) {
var result = [];
// NOTE: ここではsomeHogeTrueActionとsomeActionAの順序関係があると仮定
if(flag == true) someHogeTrueAction()
var resultHoge = someActionA()
if(flag == true) return resultHoge
return someActionB(resultHoge)
}
function prettyFunctionFuga(a,flag) {
var result = [];
someFugaAction1()
var resultHoge = someActionA()
if(flag == true) return resultHoge
return someActionB(resultHoge)
}
以上のように、だいぶ下のコードの方が分岐で行われる処理が見えやすくなったと思いませんか?一般的に無理に共通化して難読化して言った処理は無理に共通化せず、重複処理があっても処理を分離した方がお互いの実行が排他的になるため、修正が行いやすく、可読性が上がります。
また、無理にelse ifや排他的条件を使わず、条件マッチの段階でreturnさせるというGaurd節を用いる事で更に見やすくする事ができます
function switchFunction(a,flag) {
if (a == 'hoge') return prettyFunctionHoge(a,flag)
if (a == 'fuga') return prettyFunctionHoge(a,flag)
return defaultValue
}
// 略
モデルがモデルを呼び、気づけば、モデルの横断を繰り返していた
これもよくあるパターンです。
対処法
忘れ去られたメソッドパターンと、名前と処理が一致しないコードパターンの手法を両方用います。一般的にでかいスコープからメソッドを検証していき内部で呼ばれるメソッドの副作用をなくしたあと、呼ばれているモデルに存在する必要がないメソッドであれば、適切なモデルに再配置したりメソッドの置き換えを行います。
役割が重複したオブジェクトや処理が別々に使われており、どちらのオブジェクトの参照関係のあるのかがわからなくなった
よくある上に非常に厄介な問題です。
対処法
これも基本的に忘れ去られたメソッドパターンと同様一つずつ解決していくしかないです。循環参照や重複参照になっていた場合、様々な問題になりますが、基本的には、モジュールのインポートをしたライブラリでそのコードを使う必要があるのかどうかを調べます。今まで同様大きなスコープから調べ、参照関係を剥がしていきます。重複したものはどちらかの機能に統合して要らない方を削除します。これも地道なので、こうなってるコードを直すのは断念するほうが早いケースもあります。また、自分で定義したモジュールであれば、それぞれのスコープの規模に応じて、呼び出し場所を一箇所のクラスやエントリポイント、エンドポイント毎に集約し、その実行開始から終了までに掛かるすべての処理を一度集約し、あらゆるファイルから定義されている処理を統合していき、分割して元に戻していくという処理をしていくと地道ですがゴールが見えます。集約するまえに、判断が付いたものは新しく切り出し場所に移動していくことでこの作業を行っている人の依存関係の判別前と判別後の判断が付きます。これも基本的にはマンパワーとコストと相談になります。これも一回コード毎エントリポイントを取り替える方が速かったりします。
コピペ
プログラム書いてるとほぼ高確率で自分でやらかしたり、出くわすと思うので説明は言わずもがなかなと。
対処法
コピペされてるものがほぼ全部いっしょであれば、処理を適切にモジュール化します。微妙に差分がある場合は、できるだけ共通化をして関数化して無理なら断念し、別メソッドに個別のリファクタリングを施すとうまくいきます。量が多くなりがちなので、つらいですが、確実に直せるゴールが見えやすいタイプなので、意外とマシな部類です。
もうどうにも手が負えないコード
これはあらゆるアンチパターンが複雑に絡み合い、もう誰もが把握する状態になってしまった、もしくは管理者がいなくなり、サーバのアクセス権限などを失い、コードもデプロイ先にあるものが動き続けているだけの状態などです。そんな事あるのかという感じもしますが、最終手段として、どんな場合でも通用する方法が全リファクタリングを諦め部分的にあがくという手段です。多少のデグレは覚悟の上であれば、WEBサイトさえ生きていれば、なんとかなるかもしれません。
WEBページにはアクセスできる・Relational化は最後に行う
ブラウザコンソールで、htmlファイル、js、cssなどを記録し、formデータの送信先のendpointをNoSQLに全て記録し、保存するようにします。あとは、CRUDのエンドポイントを作成し、NoSQLと接続します。
htmlは必要があれば、htmlを参考にしながらフレームワークなどのテンプレートを参考にしつつnginxなどのWEBサーバで接続先を古いアプリケーションに設定しつつ、CRUDのエンドポイントが完成したものからnginxで接続します。
フロントはそれぞれUIが動作した時の動作をフルスクラッチで実装します。NoSQLからRDBに移行したい場合は、DBのカラムが数十・数百に及んでいるパターンと同じストラテジーをとります。もはやフルスクラッチですが、使いまわせるものがあるだけましなレベルですね。
マイクロサービス化を検討する
kubernetesはいいものです。だって、接続権限さえあれば、環境設定全部わかってしまうのですもの。リファクタリングの原則で開発者の共通認識が揃う事が大事だと書きましたが、前任者や後継者との共通認識をツールによって揃える事ができるという例です。リファクタリングで消耗するのが嫌だったら、簡単に捨てる準備ができてるものでちょっとずつ置き換えていったらどうかという方針になります。昔はこのような豪快な手段が撮りづらかったのですが、現在は情報もたくさんあり、現実的な解にもなり得ます。
補足
今回の記事で紹介する方法はほとんどオリジナルで考えたものですが、どこかの書籍や情報源で得た知識を元に書いているため、完全なオリジナルではないかもしれません。
- Micro Service Architecture by Sam Newman
- Domain Driven Development by Eric Evans
- Refactoring by Martin Fowler
- Code Complete by Steve McConnell
やその他様々なフレームワークの設計思想から学んだ知識などに寄ってるかもしれません。もしどこかで読んだことある方法だなと思ったら是非教えてください。
最後に、リファクタリングは最初から正しくコードを書ければ、そんなに頻度が多くならないはずなので、最初からきれいなコードを書けることに勝るものはく、リファクタリングに多くの時間を費やすほど損します。DDDのように、一度正しく作ってしまえば、コンテキストの範囲で責務の明瞭な分割によって責務によって同等オブジェクトの置換をするのが容易であり、ロバストである設計を最初から目指すのが良いと思います。
ただ、不幸にもこの世の中には事業の成長と技術者のQOLを阻害してくる恐ろしい怪物コードが存在してしまうため、運悪くそのようなプロジェクトにジョインしてしまったとしても、プロジェクトのコア機能を変えずに内部構造変革できる力は実はどこのサービスでも大きな価値をもたらす力だったりします。
理想の形に戻す方法を考え追求することは、ある意味プログラムの仕様を幅広く知り、問題解決にたどり着く力を養う題材としてとても良いと思っています。モンスターコードを見てしまった時には、やばい所に来てしまったと思う前に、このモンスター倒しがいあるぞ!と思えるといいんじゃないかなと思いました。
でも、最近はkubernetesを覚えて、別に倒さずに共存する方が楽で良いやん、って思う世の中にもなってきていますので、もし、モンスターにかまってる時間が無駄だと思う人はマイクロサービスの手法を覚えるといいと思います。ケースにもよりけりですが、最近はそっちの方がリファクタリング頑張るより早いと思っています。
何はともあれ、皆様のリファクタリングが成功することを祈っております!