カスケードリファクタリングを回避するにはどうすればよいですか?


52

プロジェクトがあります。このプロジェクトでは、機能を追加するためにリファクタリングしたいと考え、機能を追加するためにプロジェクトをリファクタリングしました。

問題は、作業を終えたときに、それに対応するためにインターフェイスを少し変更する必要があることが判明したことです。だから私は変更を加えました。そして、新しいクラスの観点から、現在のインターフェイスでは消費クラスを実装できないため、新しいインターフェイスも必要です。今では3か月後に、無数の事実上無関係な問題を修正する必要があり、1年前にロードマップされた問題、または問題がコンパイルされる前に修正できないと単純にリストされている問題の解決を検討しています再び。

この種のカスケードリファクタリングを今後回避するにはどうすればよいですか?これは、以前のクラスが互いに緊密に依存しすぎているという単なる症状ですか?

簡単な編集:この場合、リファクタリング機能でした。これは、リファクタリングにより特定のコードの拡張性が向上し、一部の結合が減少したためです。これは、外部の開発者がより多くのことができることを意味し、それが私が提供したかった機能でした。したがって、元のリファクタリング自体は機能的な変更ではないはずです。

5日前に約束したより大きな編集:

このリファクタリングを開始する前に、インターフェイスのあるシステムがありましたが、実装では、dynamic_cast出荷したすべての可能な実装を単純に確認しました。これは明らかに、インターフェイスから継承することはできなかったことを意味し、第2に、このインターフェイスを実装するための実装アクセス権を持たない人は不可能であることを意味しました。だから私はこの問題を修正し、誰でもそれを実装できるようにインターフェースを公開し、誰でもそれを実装できるようにし、インターフェースの実装が必要な契約全体であると判断しました。

私がこれをしたすべての場所を見つけて火で殺そうとしていたとき、特定の問題であることが判明した場所を1つ見つけました。それは、さまざまな派生クラスのすべての実装の詳細と、すでに実装されているが他のどこかより優れた複製機能に依存していました。代わりにパブリックインターフェイスの観点から実装し、その機能の既存の実装を再利用することもできます。正しく機能するには特定のコンテキストが必要であることを発見しました。おおまかに言って、呼び出し元の以前の実装はちょっと似ていました

for(auto&& a : as) {
     f(a);
}

ただし、このコンテキストを取得するには、次のようなものに変更する必要がありました

std::vector<Context> contexts;
for(auto&& a : as)
    contexts.push_back(g(a));
do_thing_now_we_have_contexts();
for(auto&& con : contexts)
    f(con);

これは、以前はの一部であったすべての操作についてf、一部はgコンテキストなしで動作する新しい関数の一部にする必要があり、一部は現在の遅延の一部で行う必要があることを意味しますf。しかし、すべてのメソッドf呼び出しがこのコンテキストを必要とするわけでも、必要とするわけでもありません。一部のメソッドは、別個の手段で取得する別個のコンテキストを必要とします。そのため、最終的にf呼び出しを行うすべて(大まかに言えば、ほぼすべて)について、必要なコンテキスト(ある場合)、取得元、および古いものからf新しいものへの分割方法を決定する必要がfありましたg

そして、それが私が今いるところに行き着いた方法です。とにかく他の理由でこのリファクタリングが必要だったからです。


67
「プロジェクトをリファクタリングして機能を追加した」と言うとき、正確にはどういう意味ですか?リファクタリングは、定義によりプログラムの動作を変更しないため、このステートメントは混乱します。
ジュール

5
@Jules:厳密に言えば、この機能は他の開発者が特定のタイプの拡張機能を追加できるようにすることでした。そのため、この機能はリファクタリングであり、クラス構造をよりオープンにしました。
DeadMG

5
これは、リファクタリングに関するすべての本や記事で説明されていると思いますか?ソース管理が助けになります。ステップAを実行するには、まずステップBを実行し、次にAを廃棄してからBを実行する必要があります。
ルワン

4
@DeadMG:これはもともと私の最初のコメントで引用したかった本です。「ゲーム "pick-up sticks"はMikado Methodの良いメタファーです。ほとんどのソフトウェアに埋め込まれたレガシーの問題である "技術的負債"を排除します。システム-実装しやすい一連のルールに従う。プロジェクトを崩壊させることなく、中心的な問題が明らかになるまで、絡み合った各依存関係を慎重に抽出します。
ルワン

2
5月、私たちが話しているプログラミング言語を明確にできますか?すべてのコメントを読んだ後、IDEを使用するのではなく、手でそれを行っているという結論に達しました。したがって、私はあなたにいくつかの実用的なアドバイスを与えることができるかどうか、知りたいです。
thepacker

回答:


69

前回、予期しない結果でリファクタリングを開始しようとしましたが、1日後にビルドやテストを安定させることができませんでした 。

それから、何がうまくいかなかったかを分析し始め、より小さなステップでリファクタリングを行うより良い計画を立てました。したがって、カスケードリファクタリングを回避するための私のアドバイスは、単に停止するタイミングを知って、物事があなたのコントロールを使い果たさないようにすることです!

時には、弾丸を噛んで1日分の仕事を捨てなければならない場合があります。3か月分の仕事を捨てるよりも間違いなく簡単です。あなたが失った日は完全に無駄ではありません、少なくともあなたは問題にアプローチしない方法を学びました。私の経験からすると、リファクタリングの小さなステップを作成する可能性は常にあります。

サイドノート:3か月分の作業をすべて犠牲にして、新しい(そしてうまくいけばより成功した)リファクタリング計画からやり直すかどうかを決定しなければならない状況にあるようです。それは簡単な決定ではないことを想像できますが、ビルドを安定させるためだけでなく、書き換え中に導入したと思われるすべての予期しないバグを修正するために、過去3か月間にさらに3か月必要なリスクはどれくらい高いかを自問してください?「リファクタリング」ではなく、それがあなたが本当にしたことだと思うので、「リライト」を書きました。プロジェクトがコンパイルされる最後のリビジョンに戻って、実際のリファクタリング( "rewrite"とは反対)を再度開始することにより、現在の問題をより迅速に解決できる可能性は低くありません。


53

これは、以前のクラスが互いに緊密に依存しすぎているという単なる症状ですか?

承知しました。他の無数の変更を引き起こす1つの変更は、カップリングの定義です。

カスケードリファクタリングを回避するにはどうすればよいですか?

最悪の種類のコードベースでは、1つの変更がカスケードされ続け、最終的に(ほとんど)すべてを変更します。広範なカップリングがあるリファクタリングの一部は、作業中の部分を分離することです。新しい機能がこのコードに触れている場所だけでなく、他のすべてがそのコードに触れている場所をリファクタリングする必要があります。

通常、それは、古いコードが古いコードのように見えて動作するが、新しい実装/インターフェースを使用するもので動作するように、いくつかのアダプターを作成することを意味します。結局のところ、インターフェイス/実装を変更するだけで、カップリングをそのままにしておけば何も得られません。豚の口紅です。


33
+1リファクタリングがひどく必要なほど、リファクタリングの範囲は広がります。それはまさにその性質です。
ポールドレーパー

4
ただし、本当にリファクタリングしている場合は、他のコードがすぐに変更に関与する必要はありません。(もちろん、最終的に他の部分をクリーンアップする必要があります...しかし、それはすぐに必要ではありません。)アプリの他の部分を「カスケード」する変更は、リファクタリングよりも大きいです-その時点で基本的に再設計または書き直し。
cHao

+1アダプターは、最初に変更するコードを分離する方法です。
winkbrace

17

リファクタリングが野心的すぎるように思えます。リファクタリングは小さなステップで適用する必要があり、各ステップは(たとえば)30分で完了することができます-または、最悪のシナリオでは、せいぜい1日で完了し、プロジェクトをビルド可能にし、すべてのテストに合格します。

個々の変更を最小限に抑えると、リファクタリングがビルドを長時間中断させることは実際には不可能です。最悪の場合は、たとえば、新しいパラメーターを追加するなど、広く使用されているインターフェイスのメソッドにパラメーターを変更することです。ただし、これによる結果的な変更は機械的なものです。各実装でパラメーターを追加(および無視)し、各呼び出しでデフォルト値を追加します。たとえ何百ものリファレンスがあったとしても、そのようなリファクタリングを実行するのに1日もかかるべきではありません。


4
そのような状況がどのように発生するかわかりません。メソッドのインターフェイスを合理的にリファクタリングするには、変更前と同じコールの動作をもたらす、簡単に決定できる新しいパラメータセットを渡す必要があります。
ジュール

3
このようなリファクタリングを実行したいという状況は一度もありませんでしたが、それは私にとって非常に珍しいことだと言わざるを得ません。インターフェイスから機能を削除したと言っていますか?もしそうなら、それはどこに行きましたか?別のインターフェースに?それともどこか?
ジュール

5
それを行う方法は、削除するリファクタリングの前に、削除する機能の使用をすべて削除してからではなく、削除することです。これにより、作業中にコードを構築し続けることができます。
ジュール

11
@DeadMG:それは奇妙に聞こえます:あなたが言うように、あなたはもはや必要ではない1つの機能を削除しています。しかし、他方では、「プロジェクトは完全に機能しなくなる」と書いています。実際には、この機能は絶対に必要だと思われます。どうか明らかにしてください。
Doc Brown

26
@DeadMGでこのような場合、あなたは通常、新しいインタフェースを使用するように移行既存のコード、それが動作することを確認するためのテストを追加し、新しい機能を開発し、考えた後(今)余分古い機能を削除します。そのように、物事が壊れるポイントがあるべきではありません。
サピ

12

この種のカスケードリファクタリングを今後回避するにはどうすればよいですか?

希望的観測デザイン

目標は、新しい機能の優れたオブジェクト指向設計と実装です。リファクタリングを避けることも目標です。

ゼロから始めて、新しい機能のデザイン作成します。時間をかけてうまくやってください。

ただし、ここで重要なのは「機能を追加する」ことです。新しいものは、コードベースの現在の構造をほとんど無視する傾向があります。私たちの希望的観測デザインは独立しています。ただし、さらに2つのことが必要です。

  • 新しい機能のコードを挿入/実装するために必要な継ぎ目を作成するのに十分なだけリファクタリングします。
    • リファクタリングへの抵抗は、新しい設計を促進すべきではありません。
  • 新しい機能と既存のcodezを互いに無知のままにしておくAPIを使用して、クライアント向けクラスを作成します。
    • オブジェクト、データ、結果を前後に取得するために音訳します。最低限の知識の原則はきます。既存のコードがすでに行っていることより悪いことは何もしません。

ヒューリスティック、学んだ教訓など

リファクタリングは、既存のメソッド呼び出しにデフォルトパラメータを追加するのと同じくらい簡単です。または、静的クラスメソッドの1回の呼び出し。

既存のクラスの拡張メソッドは、新しいデザインの品質を最小限のリスクで維持するのに役立ちます。

「構造」がすべてです。構造は、単一責任原則の実現です。機能を促進する設計。コードは、クラス階層全体にわたって短くシンプルなままです。新しい設計の時間は、テスト、再作業、およびレガシーコードジャングルを介したハッキン​​グの回避中に構成されます。

希望的観測クラスは、目の前のタスクに焦点を合わせます。一般に、既存のクラスを拡張することは忘れてください。リファクターカスケードを再び誘導し、「より重い」クラスのオーバーヘッドに対処する必要があります。

この新しい機能の残りを既存のコードから削除します。ここでは、リファクタリングを回避するよりも、完全にカプセル化された新機能の機能が重要です。


9

マイケル・フェザーズによる「素晴らしいコード」のレガシーコードを効果的に使用する本から:

レガシーコードの依存関係を破るとき、しばしば美学の感覚を少し中断しなければなりません。いくつかの依存関係はきれいに壊れます。他の人は、設計の観点からは理想的ではないように見えます。それらは手術の切開ポイントのようなものです。作業後にコードに傷跡が残るかもしれませんが、その下のすべてが良くなる可能性があります。

後で依存関係を破ったポイントの周りのコードをカバーできれば、その傷を癒すこともできます。


6

(特にコメントでの議論から)この「小さな」変更はソフトウェアの完全な書き直しと同じ量の作業であることを意味する自己課せられた規則であなた自身を囲んでいるように聞こえます。

解決策は「そうしない」でなければなりません。これが実際のプロジェクトで起こることです。多くの古いAPIには、結果として見苦しいインターフェイスまたは破棄された(常にnull)パラメーター、またはDoThisThing2()という名前の関数があり、DoThisThing()とまったく異なるパラメーターリストがあります。他の一般的なトリックには、大規模なフレームワークの塊を超えて密輸するために、グローバルまたはタグ付きポインターに情報を格納することが含まれます。(たとえば、ライブラリがオーディオコーデックを呼び出す方法を変更するよりもはるかに簡単だったため、オーディオバッファの半分に4バイトのマジック値しか含まれていないプロジェクトがあります。)

特定のコードなしで特定のアドバイスをすることは困難です。


3

自動テスト。TDDの熱狂者である必要はなく、100%のカバー率も必要ありませんが、自動化されたテストにより、自信を持って変更を加えることができます。さらに、カップリングが非常に高い設計になっているようです。ソフトウェア設計におけるこの種の問題に対処するために特別に策定されたSOLID原則についてお読みください。

これらの本もお勧めします。

  • レガシーコード、フェザーを効果的に使用する
  • リファクタリング、ファウラー
  • Tests、Freeman、およびPryceがガイドする成長するオブジェクト指向ソフトウェア
  • クリーンコード、マーティン

3
あなたの質問は、「将来、この[失敗]をどのように回避すればよいですか?」です。答えは、現在CIとテストを「持っている」場合でも、それらを正しく適用していないということです。コンパイルを「最初のユニットテスト」と見なし、壊れた場合は修正します。これは、テストが次のように通過するのを確認する必要があるためです。私はコードをさらに研究しています。
-asthasr

6
頻繁に使用されるインターフェイスをリファクタリングする場合は、シムを追加します。このシムはデフォルト設定を処理するため、レガシーコールは引き続き機能します。私はシムの背後にあるインターフェイスで作業し、それが完了したら、シムの代わりに再びインターフェイスを使用するようにクラスを変更し始めます。
-asthasr

5
ビルドが失敗してもリファクタリングを続けることは、推測航法に似てます。これは、最後の手段のナビゲーション手法です。リファクタリングでは、リファクタリングの方向がまったく間違っている可能性があり、その兆候(コンパイルが停止した瞬間、つまり対気速度インジケーターなしで飛行している瞬間)を既に見たことがありますが、先に進むことにしました。最終的に飛行機はレーダーから落ちます。幸いなことに、リファクタリングにブラックボックスや調査員は必要ありません。常に「最後の既知の良好な状態に復元する」ことができます。
rwong

4
@DeadMG:あなたはしかし、あなたの質問「で、「私の場合は、以前の呼び出しは、単に、もはや意味をなさない」を書いたマイナーのそれに対応するためのインタフェースの変更」。正直なところ、これらの2つの文のうちの1つだけが真実でありえます。そして、あなたの問題の説明から、インターフェースの変更が間違いなくマイナーなものではないことはかなり明らかなようです。変更を後方互換性のあるものにする方法について、本当に、もっと真剣に考えるべきです。私の経験では、それは常に可能ですが、最初に良い計画を立てなければなりません。
Doc Brown

3
@DeadMGその場合、あなたがしていることは合理的にリファクタリングと呼ぶことはできないと思います。その基本的なポイントは、一連の非常に単純なステップとして設計変更を適用することです。
ジュール

3

これは、以前のクラスが互いに緊密に依存しすぎているという単なる症状ですか?

おそらくはい。要件が十分に変更された場合、かなり素晴らしくきれいなコードベースで同様の効果を得ることができますが

この種のカスケードリファクタリングを今後回避するにはどうすればよいですか?

レガシコードでの作業を停止することは別として、恐れることはできません。しかし、できることは、数日、数週間、さらには数か月間、機能するコードベースがないという影響を回避する方法を使用することです。

このメソッドは「ミカドメソッド」と呼ばれ、次のように機能します。

  1. 達成したい目標を紙に書き留めます

  2. その方向に導く最も簡単な変更を行います。

  3. コンパイラとテストスイートを使用して動作するかどうかを確認します。手順7から続行する場合は、手順4から続行します。

  4. 紙の上に、現在の変更を有効にするために変更する必要があるものに注意してください。現在のタスクから新しいものに矢印を描きます。

  5. 変更を元に戻すこれは重要なステップです。それは反直感的であり、最初は身体的に痛いですが、単純なことを試しただけなので、それほど悪くはありません。

  6. 送信エラーのない(既知の依存関係がない)タスクの1つを選択し、2に戻ります。

  7. 変更をコミットし、ペーパー上のタスクを取り消し、送信エラーのないタスク(既知の依存関係なし)を選択して、2に戻ります。

これにより、短い間隔でコードベースが機能します。チームの他の部分からの変更をマージできる場所。そして、あなたがまだしなければならないことがわかっていることを視覚的に表現しているので、エンデューバーを続行するか、それを停止するかを決定するのに役立ちます。


2

リファクタリングは構造化された規律であり、適切なコードのクリーンアップとは異なります。開始する前に単体テストを作成する必要があります。各ステップは、機能を変更しないことがわかっている特定の変換で構成する必要があります。ユニットテストは、変更のたびに合格する必要があります。

もちろん、リファクタリングプロセス中に、破損を引き起こす可能性のある適用すべき変更を自然に発見します。その場合、新しいフレームワークを使用する古いインターフェイスに互換性シムを実装するために最善を尽くしてください。理論的には、システムは以前と同じように機能し、単体テストに合格する必要があります。互換性シムを非推奨インターフェースとしてマークし、より適切なタイミングでクリーンアップできます。


2

...プロジェクトをリファクタリングして、機能を追加しました。

@Julesが言ったように、リファクタリングと機能の追加は2つの非常に異なるものです。

  • リファクタリングとは、動作を変更せずにプログラムの構造を変更することです。
  • 一方、機能を追加すると、その動作が強化されます。

...しかし、実際には、自分の作品を追加するために内部の仕組みを変更する必要がある場合がありますが、リファクタリングではなく変更と呼びます。

それに対応するために、インターフェースを少し変更する必要がありました

それは物事が乱雑になる場所です。インターフェイスは、実装を使用方法から分離するための境界としての意味があります。インターフェースに触れるとすぐに、どちらかの側(実装または使用)のすべても変更する必要があります。これは、あなたが経験した限り広がる可能性があります。

その場合、新しいクラスの観点から現在のインターフェイスで消費クラスを実装することはできないため、新しいインターフェイスも必要です。

1つのインターフェースに変更が必要であることは問題ないように聞こえます...別のインターフェースに広がることは、変更がさらに広がることを意味します。何らかの形式の入力/データがチェーンを流れる必要があるようです。そうですか?


あなたの話は非常に抽象的なので、理解するのは難しいです。例は非常に役立ちます。通常、インターフェイスは非常に安定しており、相互に独立している必要があります。これにより、インターフェイスのおかげで、残りの部分を損なうことなくシステムの一部を変更できます。

...実際、カスケードコードの変更を回避する最良の方法は、正確に優れたインターフェイスです。;)


-1

物事をそのままにしておかない限り、通常はできないと思います。ただし、あなたのような状況では、より健全な開発を継続するためにリファクタリングを行う必要がある理由をチームに通知し、知らせることをお勧めします。自分で物事を修正するだけではありません。スクラムミーティング中にそれについて話し(皆さんが持っていると仮定して)、他の開発者と体系的にアプローチします。


1
これは作られたポイントを超える大幅な提供の何にも思えるし、前9つの回答で説明していません
ブヨ

@gnat:そうではないかもしれませんが、応答を単純化しました。
タリック
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.