web-dev-qa-db-ja.com

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

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

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

この種の連鎖リファクタリングを将来的に回避するにはどうすればよいですか?それは私の以前のクラスの症状にすぎませんか?

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

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

このリファクタリングを始める前は、インターフェースを備えたシステムがありましたが、実装ではdynamic_cast私が出荷したすべての可能な実装を通して。これは明らかに、インターフェイスから単に継承することはできないことを意味します。次に、実装アクセス権のない人がこのインターフェイスを実装することは不可能であることを意味します。だから私はこの問題を修正し、誰もがそれを実装できるようにパブリック消費用のインターフェースをオープンしたいと思い、インターフェースの実装は必要な契約全体であると明らかにしました-明らかに改善です。

私がこれまでに行ったすべての場所を見つけて射殺したとき、私は特定の問題であることが判明した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がすべての呼び出し(つまり、大まかに言えばeverything)を行うすべての場合、必要なコンテキスト(ある場合)を取得する必要がある場所を決定する必要がありました。から、およびそれらを古いfから新しいfおよび新しいgに分割する方法。

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

52
DeadMG

前回、予期しない結果を伴ってリファクタリングを開始しようとしたとき、ビルドやテストを安定化できなかった1日の後で、コードベースをあきらめて、リファクタリング前の状態に戻しました。

次に、何が問題であるかを分析し始め、小さなステップでリファクタリングを行う方法をより適切に計画しました。したがって、リファクタリングのカスケードを回避するための私のアドバイスは、次のとおりです。いつ停止するかを知っている、制御不能にならないようにしてください!

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

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

69
Doc Brown

それは私の以前のクラスの症状にすぎませんか?

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

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

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

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

53
Telastyn

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

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

17
Jules

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

希望的思考のデザイン

目標は優れていますOO新機能の設計と実装回避リファクタリングも目標です。

ゼロから始めて、デザインを作成します新機能の場合これは、希望どおりのものです。時間をかけてうまくやってください。

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

  • 新しい機能のコードを挿入/実装するために必要なシームを作成するのに十分なだけリファクタリングします。
    • リファクタリングへの抵抗は、新しい設計を推進すべきではありません。
  • APIを使用して、クライアント向けクラスを記述します。これにより、新機能と既存のcodezをお互いに無知のままに保つことができます。
    • オブジェクト、データ、結果をやり取りするために文字変換します。最小限の知識の原則はのろわれます。既存のコードがすでに行っているよりも悪いことは何もしません。

ヒューリスティックス、教訓など.

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

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

「構造」がすべてです。構造とは、単一責任の原則の実現です。機能性を高めるデザイン。コードはクラス階層全体にわたって短く、単純なままです。新しい設計の時間は、テスト、リワーク、およびレガシーコードジャングルによるハッキングの回避の間に補われます。

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

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

12
radarbob

(素晴らしい)本から Michael Feathersによるレガシーコードで効果的に作業する

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

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

9
Kenny Evitt

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

解決策は"それを行わないでください"でなければなりません。これが実際のプロジェクトで起こります。古いAPIの多くは、結果として醜いインターフェイスや放棄された(常にnull)パラメータを持っているか、DoThisThing()と同じ機能を持つDoThisThing2()という名前の関数を使用して、完全に異なるパラメータリストを提供しています。その他の一般的なトリックには、フレームワークの大きなチャンクを越えて密輸するためにグローバルまたはタグ付きポインターに情報を隠しておくことが含まれます。 (たとえば、私は、ライブラリがオーディオコーデックを呼び出す方法を変更するよりもはるかに簡単だったので、オーディオバッファーの半分に4バイトのマジック値しか含まれていないプロジェクトがあります。)

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

6
pjc50

自動テスト。 TDD熱心である必要はなく、100%カバーする必要もありませんが、自動テストを使用すると、自信を持って変更を行うことができます。さらに、非常に結合度の高いデザインがあるようです。 SOLID原則について読む必要があります。これらの原則は、ソフトウェア設計におけるこの種の問題に対処するために特別に作成されています。

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

  • レガシーコードで効果的に機能、羽
  • リファクタリング、ファウラー
  • 成長するオブジェクト指向ソフトウェア、テストによるガイド、FreemanおよびPryce
  • クリーンコード、マーティン
3
asthasr

それは私の以前のクラスの症状にすぎませんか?

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

この種の連鎖リファクタリングを将来的に回避するにはどうすればよいですか?

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

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

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

  2. その方向にあなたを連れて行く最も単純な変更を行います。

  3. コンパイラとテストスイートを使用して機能するかどうかを確認します。それがステップ7に続く場合は、ステップ4に進みます。

  4. 紙に、現在の変更を機能させるために変更する必要があることを書き留めます。現在のタスクから新しいタスクに矢印を描きます。

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

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

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

このようにして、短い間隔で有効なコードベースを作成できます。他のチームの変更をマージすることもできます。そして、あなたはあなたがまだやらなければならないことを知っていることの視覚的な表現を持っています、これはあなたが努力を続けるか、それともやめるべきかを決めるのに役立ちます。

3
Jens Schauder

...機能を追加するためにプロジェクトをリファクタリングしました。

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

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

...しかし、実際には、内部の仕組みを変更してコンテンツを追加する必要がある場合もありますが、リファクタリングではなく、変更と呼んでいます。

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

混乱するところです。インターフェースは、実装をその使用方法から分離するための境界として意図されています。インターフェイスに触れるとすぐに、いずれかの側(実装または使用)のすべても同様に変更する必要があります。これは、あなたが経験した限り遠くまで広がる可能性があります。

その場合、消費するクラスは、新しいクラスに関して現在のインターフェイスで実装できないため、新しいインターフェイスも必要です。

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


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

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

2
dagnelies

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

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

2
200_success