11000行のC ++ソースファイルをどうするか


229

プロジェクトにこの巨大な(11000行は巨大ですか?)mainmodule.cppソースファイルがあり、それに触れる必要があるたびに私はうんざりしています。

このファイルは非常に中心的でサイズが大きいため、ますます多くのコードを蓄積し続け、実際に縮小し始めるための良い方法は考えられません。

このファイルは、弊社製品のいくつかの(> 10)メンテナンスバージョンで使用され、積極的に変更されているため、リファクタリングするのは非常に困難です。私がそれを「はじめに」3つのファイルに「単純に」分割するとしたら、メンテナンスバージョンからの変更をマージすることは悪夢になります。また、このような長くて豊富な履歴を持つファイルを分割すると、SCC履歴内の古い変更の追跡とチェックが突然非常に困難になります。

このファイルには基本的に、プログラムの「メインクラス」(メインの内部作業ディスパッチおよび調整)が含まれているため、機能が追加されるたびに、このファイルに影響を与え、ファイルが大きくなるたびに影響を受けます。:-(

この状況で何をしますか?SCCワークフローを混乱させることなく、新しい機能を別のソースファイルに移動する方法に関するアイデアはありますか?

(ツールに関する注意:でC ++を使用しVisual Studioます。次のように使用AccuRevSCCますが、SCCここではタイプは特に問題ではないと思います。Araxis Merge実際のファイルの比較とマージに使用します)


15
@BoltClock:実際、Vimはそれを非常に速く開きます。
ereOn

58
69305行とカウント。私の同僚が彼のコードのほとんどをダンプするアプリケーションのファイル。これをここに投稿することに抵抗できませんでした。私にはこれを報告する人が誰もいません。
Agnel Kurian、2010

204
わかりません。「その仕事をやめる」コメントはどうして多くの賛成票を得ることができますか 一部の人々は妖精の国に住んでいるようで、すべてのプロジェクトはゼロから作成され、および/または100%アジャイル、TDDを使用しています...
Stefan

39
@Stefan:同様のコードベースに直面したとき、私はまさにそれを行いました。私は、10年前のコードベースで、95%の時間を費やしてクラッドを回避したり、5%を実際にコードを書いたりすることに夢中になりました。システムのいくつかの側面をテストすることは実際には不可能でした(そしてユニットテストを意味するのではなく、実際にコード実行して機能するかどうかを確認することを意味します)。6か月の試用期間はありませんでした。敗北する戦いと待ちきれなかったコードを書くことにうんざりしました。
バイナリ心配性

50
ファイルを分割する履歴追跡の側面について:バージョン管理システムのcopyコマンドを使用して、ファイル全体を何度でもコピーし、不要な各コピーからすべてのコードを削除します。そのファイルで。これにより、分割されたファイルのそれぞれが履歴を分割までたどることができるので、全体の履歴が保持されます(ファイルのほとんどのコンテンツの巨大な削除のように見えます)。
rmeador

回答:


86
  1. ファイル内で、比較的安定しており(高速で変化せず、ブランチ間でほとんど変化しない)、独立したユニットとして立つことができるコードを見つけます。これを独自のファイルに移動し、さらに言えば、すべてのブランチの独自のクラスに移動します。安定しているため、あるブランチから別のブランチへの変更をマージするときに、元のファイルとは異なるファイルに適用する必要がある(多くの)「厄介な」マージは発生しません。繰り返す。

  2. ファイル内で、基本的に少数のブランチにのみ適用され、単独で使用できるコードを見つけます。ブランチの数が少ないため、速く変化するかどうかは関係ありません。これを独自のクラスとファイルに移動します。繰り返す。

したがって、どこでも同じコード、および特定のブランチに固有のコードを削除しました。

これは、不適切に管理されたコードの核を残します-それはどこでも必要ですが、すべてのブランチで異なります(および/またはいくつかのブランチが他のブランチの背後で実行されるように絶えず変化します)、それでもあなたは単一のファイルですブランチ間をマージしようとして失敗しました。あんな事はしないで。おそらく各ブランチで名前を変更することにより、ファイルを永続的にブランチします。それはもはや「メイン」ではなく、「設定Xのメイン」です。わかりました。マージすると、同じ変更を複数のブランチに適用できなくなりますが、これはいずれにせよ、マージがうまく機能しないコードのコアです。とにかく手動でマージを管理して競合に対処する必要がある場合は、各ブランチに個別にマージを手動で適用しても無駄はありません。

たとえば、gitのマージ機能は、使用しているマージツールよりもおそらく優れているため、SCCの種類は重要ではないと言うのは間違っていると思います。したがって、核となる問題である「マージは困難」は、SCCごとに異なるタイミングで発生します。ただし、SCCを変更できる可能性は低いため、問題はおそらく無関係です。


マージに関して:私はGITとSVNを見てきたし、Perforceも見てきたし、どこでも見たことは何もないため、AccuRev + Araxisに勝るものはない。:-)(GITはこれを行うことができます[ stackoverflow.com/questions/1728922/… ]とAccuRevはできません-これがマージまたは履歴分析の一部であるかどうかは誰でも自分で決める必要があります。)
Martin Ba

十分に公正です-おそらくあなたはすでに利用可能な最高のツールを持っています。ブランチXのファイルAで発生した変更をブランチYのファイルBにマージするGitの機能は、ブランチファイルを簡単に分割できるはずですが、使用するシステムには、おそらくあなたの好みの利点があります。とにかく、私はあなたがgitに切り替えることを提案しているのではなく、SCCがここで違いをもたらすと言っているだけですが、それでもこれは割引できると同意します:-)
Steve Jessop

129

将来30000のLOCファイルを取得するときほど、マージはそれほど大きな悪夢にはなりません。そう:

  1. そのファイルへのコードの追加を停止します。
  2. それを分割します。

リファクタリングプロセス中にコーディングを停止できない場合、少なくとも大きなコードを追加せずに、この大きなファイルしばらくそのままにしておくことができます。このファイルには「メインクラス」が1つ含まれているため、そこから継承し、継承したクラスを維持できます( es)いくつかの新しい小さくてうまく設計されたファイルのオーバーロードされた関数。


@Martin:幸い、ここにファイルを貼り付けていないので、その構造についてはわかりません。しかし、一般的な考え方は、それを論理的な部分に分割することです。このような論理部分には、「メインクラス」の関数のグループを含めることも、複数の補助クラスに分割することもできます。
キリルV.リヤドビンスキー2010

3
10のメンテナンスバージョンと多くのアクティブな開発者がいるため、ファイルが十分に長い時間フリーズする可能性はほとんどありません。
Kobi

9
@Martin、あなたはトリックを実行するいくつかのGOFパターン、mainmodule.cppの関数をマップする単一のFacadeを持っている、あるいは(以下でお勧めします)、それぞれが関数にマップするCommandクラスのスイートを作成します/ mainmodule.appの機能。(私は私の答えでこれを拡大しました。)
ocodo

2
はい、完全に同意します。ある時点で、コードの追加を停止する必要があります。最終的には、コードが30k、40k、50kになる予定です。kaboomメインモジュールでセグメンテーション違反が発生しました。:-)
Chris

67

ここでコードの臭いがたくさん発生しているように思えます。まず、メインクラスは開閉の原則に違反しているようです。それはあまりにも多くの責任を処理しているようにも聞こえます。このため、コードは必要以上に脆弱であると想定します。

リファクタリング後のトレーサビリティに関する懸念は理解できますが、このクラスの維持と拡張はかなり難しく、変更を加えると副作用が発生する可能性があると思います。これらのコストは、クラスをリファクタリングするコストよりも多いと思います。

いずれにしても、コードのにおいは時間とともに悪化するだけなので、少なくともある時点で、これらのコストはリファクタリングのコストを上回ります。あなたの説明から、私はあなたが転換点を過ぎていると仮定します。

これのリファクタリングは小さなステップで行われるべきです。可能であれば、何かをリファクタリングする前に、自動テストを追加して現在の動作を確認します。次に、分離された機能の小さな領域を選び、それらをタイプとして抽出して、責任を委任します。

いずれにせよ、それは大きなプロジェクトのように聞こえるので、頑張ってください:)


18
臭いがします:Blobアンチパターンがダハウスにあるように臭いがします... en.wikipedia.org/wiki/God_object。彼のお気に入りの食事はスパゲッティコードです:en.wikipedia.org/wiki/Spaghetti_code :-)
jdehaan

@jdehaan:私はそれについて外交的であるように努めていました:)
ブライアン・ラスムッセン

+1私からも、テストなしで記述した複雑なコードでさえ、それをカバーするためにあえて触れることはありません。
ダニートーマス

49

このような問題に対して私が想像した唯一の解決策は次のとおりです。説明した方法による実際の利益は、進化の進歩性です。ここには革命はありません。

元のメインクラスの上に新しいcppクラスを挿入します。今のところ、基本的にはすべての呼び出しを現在のメインクラスにリダイレクトしますが、この新しいクラスのAPIをできるだけ明確かつ簡潔にすることを目指しています。

これが完了すると、新しいクラスに新しい機能を追加できるようになります。

既存の機能については、十分に安定するようになったら、新しいクラスに徐々に移動する必要があります。このコードの部分に対するSCCヘルプは失われますが、それについてできることはあまりありません。適切なタイミングを選択してください。

私はこれが完璧ではないことを知っていますが、それが役に立てば幸いです、そしてプロセスはあなたのニーズに適合されなければなりません!

追加情報

Gitは、1つのファイルから別のファイルにコードの断片を追跡できるSCCであることに注意してください。私はそれについて良いことを聞いたので、あなたが徐々にあなたの仕事を動かしている間、それは助けになるでしょう。

Gitは、私が正しく理解していれば、コードファイルの断片を表すblobの概念を中心に構築されています。これらの部分を別のファイルに移動すると、Gitはそれらを変更しても検出します。以下のコメントで言及されているLinus Torvaldsのビデオを除いて、私はこれについて何か明確なことを見つけることができませんでした。


GITがそれをどのように実行するか、GITを使用してそれをどのように実行するかについてのリファレンスは、大歓迎です。
Martin Ba

@Martin Gitが自動的に行います。
Matthew

4
@Martin:Gitはそれを自動的に行います-ファイルを追跡しないため、コンテンツを追跡します。実際には、gitで「1つのファイルの履歴を取得する」だけでは困難です。
アラファンギオン2010

1
@Martin youtube.com/watch?v=4XpnKHJAok8は、トーバルズがgitについて語る講演です。彼は話の後半でそれについて述べています。
Matthew

6
@Martin、この質問を見て:stackoverflow.com/questions/1728922/...
Benjol

30

孔子は言う:「穴から抜け出すための最初のステップは、穴を掘るのをやめることです。」


25

推測させてください:異なる機能セットを持つ10のクライアントと「カスタマイズ」を促進するセールスマネージャーですか?私は以前にそのような製品に取り組んだことがあります。本質的に同じ問題がありました。

巨大なファイルを持つことは問題であると認識していますが、さらに多くの問題は、「最新」に保つ必要がある10個のバージョンです。それは複数のメンテナンスです。SCCはそれを簡単にすることができますが、正しくすることはできません。

ファイルを部分に分割する前に、10個のブランチを互いに同期させて、すべてのコードを一度に表示して整形できるようにする必要があります。これは一度に1つのブランチで実行でき、同じメインコードファイルに対して両方のブランチをテストできます。カスタム動作を強制するには、#ifdefやフレンドを使用できますが、定義された定数に対して通常のif / elseを使用することをできるだけお勧めします。このようにして、コンパイラはすべてのタイプを検証し、おそらく「死んだ」オブジェクトコードをとにかく排除します。(ただし、デッドコードに関する警告をオフにすることもできます)。

すべてのブランチで暗黙的に共有されるそのファイルのバージョンが1つだけになると、従来のリファクタリング方法を開始する方が簡単になります。

#ifdefsは主に、影響を受けるコードが他のブランチごとのカスタマイズのコンテキストでのみ意味を持つセクションに適しています。これらは同じブランチマージスキームの機会でもあると主張するかもしれませんが、独り占めしないでください。一度に1つの巨大なプロジェクトをお願いします。

短期的には、ファイルが大きくなるように見えます。これはOKです。あなたがしていることは、一緒にする必要があるものを一緒にすることです。その後、バージョンに関係なく明らかに同じ領域が表示されるようになります。これらはそのままにすることも、自由にリファクタリングすることもできます。他の領域はバージョンによって明らかに異なります。この場合、いくつかのオプションがあります。1つの方法は、違いをバージョンごとの戦略オブジェクトに委任することです。もう1つは、共通の抽象クラスからクライアントバージョンを派生させることです。しかし、異なるブランチで開発の10の「ヒント」を持っている限り、これらの変換はどれも不可能です。


2
目標はソフトウェアの1つのバージョンを持つことであることに同意しますが、構成ファイル(ランタイム)を使用し、コンパイル時のカスタマイズを行わない方が良いでしょう
Pedersen

または、各顧客のビルドの「構成クラス」です。
tc。

コンパイル時または実行時の構成は機能的に無関係であると思いますが、可能性を制限したくありません。コンパイル時の構成には、クライアントが構成ファイルをハックして、展開可能な「テキストオブジェクト」コードではなくソースツリーにすべての構成を配置するため、追加機能をアクティブにできないという利点があります。逆に言えば、ランタイムの場合はAlternateHardAndSoftLayersに向かう傾向があります。
Ian

22

これで問題が解決するかどうかはわかりませんが、ファイルのコンテンツを互いに独立した(まとめた)小さいファイルに移行することをお勧めします。また、ソフトウェアの約10の異なるバージョンが浮かんでいて、すべてを台無しにせずにすべてサポートする必要があることもわかります。

まず第一に、これが簡単で、ブレーンストーミングの数分で解決する方法はありません。ファイルにリンクされている関数はすべてアプリケーションに不可欠であり、それらを切り取って他のファイルに移行するだけでは問題を解決できません。

私はあなたがこれらのオプションしか持っていないと思います:

  1. 移行しないで、既存のものを使い続けてください。おそらくあなたの仕事を辞めて、さらに優れたデザインの本格的なソフトウェアの作業を開始してください。クラッシュを1回または2回生き残るのに十分な資金がある長期プロジェクトに取り組んでいる場合、極端なプログラミングが常に最良の解決策とは限りません。

  2. ファイルが分割されたら、ファイルをどのように表示するかについてレイアウトを作成します。必要なファイルを作成し、アプリケーションに統合します。関数の名前を変更するか、関数をオーバーロードして追加のパラメーターを取得します(おそらく単純なブール値ですか?)。コードで作業する必要がある場合は、作業する必要がある関数を新しいファイルに移行し、古い関数の関数呼び出しを新しい関数にマッピングします。メインファイルはこのようにしておく必要があります。また、アウトソーシングされた時期を正確に把握している特定の機能については、メインファイルに加えられた変更を確認できます。

  3. ワークフローが過大評価されており、深刻なビジネスを行うためにはアプリケーションの一部を書き直す必要があることを同僚に説得してください。


19

正確にこの問題は、「レガシーコードを効果的に使用する」(http://www.amazon.com/Working-Effectively-Legacy-Michael-Feathers/dp/0131177052)の章の1つで処理されます


informit.com/store/product.aspx?isbn=0131177052を使用すると、この本の目次(および2つのサンプルの章)を表示できます。第20章はどのくらいの期間ですか?(それがどれほど便利であるかを感じるためだけに。)
Martin Ba

17
第20章は10,000行の長さですが、著者はそれを消化可能な塊に分割する方法を考えています... 8)
トニーデルロイ

1
約23ページですが、画像は14枚です。私はあなたがそれを手に入れるべきだと思います、あなたは私見を何をすべきかを決定しようとすることではるかに自信を感じるでしょう。
Emile Vrijdags

問題に関する優れた本ですが、それが行う推奨事項(およびこのスレッドの他の推奨事項)はすべて共通の要件を共有しています。すべてのブランチに対してこのファイルをリファクタリングする場合、これを行う唯一の方法は、すべてのブランチのファイルを作成し、最初の構造変更を行います。それを回避する方法はありません。この本では、重複するメソッドを作成して呼び出しを委任することにより、自動リファクタリングのサポートなしでサブクラスを安全に抽出するための反復的なアプローチの概要を説明していますが、ファイルを変更できない場合、これはすべて役に立ちません。
Dan Bryant

2
@Martin、本は優れていますが、テスト、リファクタリング、テストサイクルに大きく依存しています。これは、現在のところからかなり難しいかもしれません。私も同じような状況にあり、この本は私が見つけた中で最も役に立つ本でした。それはあなたが抱えている醜い問題に対する良い提案を持っています。しかし、ある種のテストハーネスを画像に含めることができない場合、世界中のすべてのリファクタリングの提案は役に立ちません。

14

mainmodule.cppのAPIポイントにマップする一連のコマンドクラスを作成することをお勧めします。

それらが配置されたら、コマンドクラスを介してこれらのAPIポイントにアクセスするために既存のコードベースをリファクタリングする必要があります。それが完了すると、各コマンドの実装を新しいクラス構造に自由にリファクタリングできます。

もちろん、11 KLOCの単一のクラスでは、その中のコードはおそらく高度に結合され、もろくなりますが、個々のコマンドクラスを作成すると、他のプロキシ/ファサード戦略よりもはるかに役立ちます。

このタスクはうらやましいものではありませんが、時間が経つにつれ、この問題に取り組まない限り悪化するだけです。

更新

コマンドパターンは、ファサードよりも望ましいと思います。

(比較的)モノリシックなファサードよりも多くの異なるコマンドクラスを維持/整理することが望ましいです。単一のファサードを11 KLOCファイルにマッピングするには、おそらくそれ自体をいくつかの異なるグループに分割する必要があります。

なぜこれらのファサードグループを理解しようとするのですか?コマンドパターンを使用すると、これらの小規模なクラスを有機的にグループ化して整理できるため、柔軟性が大幅に向上します。

もちろん、どちらのオプションも、単一の11 KLOCや成長するファイルよりも優れています。


+1私が提案したソリューションの代替案、同じアイデア:大きな問題を小さな問題に分離するためにAPIを変更します。
ブノワ・

13

重要なアドバイス:リファクタリングとバグ修正を混在させないでください。必要なのは、ソースコードが異なることを除いて、以前のバージョンと同じプログラムのバージョンです。

1つの方法は、最小の関数/パーツを独自のファイルに分割し、ヘッダーを含めてインクルードすることです(したがって、main.cppを#includesのリストに変換します。これにより、コード自体に匂いがします*違いますC ++の達人ですが、少なくともファイルに分割されています)。

次に、すべてのメンテナンスリリースを「新しい」main.cppまたは構造が何であれ、切り替えてみます。繰り返しますが、他の変更やバグ修正はありません。それらを追跡することは地獄と混乱するためです。

もう1つ:すべてを一度にリファクタリングするために1つの大きなパスを作成することを望んでいるのと同じくらい、噛むよりも多くのことをかむことがあるかもしれません。たぶん、1つまたは2つの「パーツ」を選択し、それらをすべてのリリースに組み込んでから、顧客にいくつかの価値を追加します(結局、リファクタリングは直接的な価値を追加しないため、正当化する必要があるコストです)。 1つまたは2つの部分。

明らかに、分割されたファイルを実際に使用し、main.cppに常に新しいものを追加するだけでなく、チームである程度の訓練が必要ですが、繰り返しになりますが、1つの大規模なリファクタリングを実行するのは最善の策ではない場合があります。


1
+1で因数分解し、#includeで戻します。これを10のブランチすべてで行った場合(作業は少しありますが、管理可能です)、他の問題、つまり、すべてのブランチに変更を送信するという問題がありますが、その問題は発生しません。 (必要に応じて)拡大しました。醜いですか?はい、まだありますが、問題に少しの合理性をもたらす可能性があります。本当に大きな製品のメンテナンスとサービスに数年費やしてきましたが、メンテナンスには多くの苦痛が伴うことを知っています。少なくともそれから学び、他の人への警告の物語として役立ちます。
ジェイ

10

Rofl、これは私の古い仕事を思い出させます。参加する前は、すべてが1つの巨大なファイル(C ++も含む)の中にあったようです。次に、それらを(インクルードを使用して完全にランダムなポイントで)約3つ(まだ巨大なファイル)に分割しました。このソフトウェアの品質は、ご想像のとおり、恐ろしいものでした。プロジェクトの合計は約4万LOCでした。(コメントはほとんど含まれていませんが、コードが重複しています)

結局、プロジェクトを完全に書き直しました。プロジェクトの最悪の部分を一からやり直すことから始めました。もちろん、私はこの新しい部分と残りの部分の間の可能な(小さい)インターフェースを念頭に置いていました。次に、この部分を古いプロジェクトに挿入しました。必要なインターフェイスを作成するために古いコードをリファクタリングせずに、単に置き換えただけです。それから私はそこから小さな一歩を踏み出し、古いコードを書き直しました。

これには約半年かかり、その間バグ修正以外に古いコードベースの開発はありませんでした。


編集:

サイズは約4万LOCのままでしたが、新しいアプリケーションには、8年前のソフトウェアよりも多くの機能が含まれており、初期バージョンのバグはおそらく少ないです。書き直しの理由の1つは、新しい機能が必要であり、古いコード内にそれらを導入することがほぼ不可能だったことです。

ソフトウェアは、組み込みシステム、ラベルプリンター用でした。

追加すべきもう1つの点は、理論的にはプロジェクトがC ++であったということです。しかし、それはまったくOOではなく、Cであった可能性があります。新しいバージョンはオブジェクト指向でした。


9
リファクタリングのトピックで「ゼロから」と聞くたびに、子猫を殺します。
Kugel

私は非常によく似た状況にありましたが、グリップを取得するために必要なメインプログラムループは約9000 LOCでした。そしてそれは十分に悪かった。
AndyUK

8

大丈夫ですから、ほとんどの場合、量産コードのAPIを書き換えることは、最初から悪い考えです。2つのことが起こる必要があります。

1つは、実際にチームにこのファイルの現在の製品バージョンでコードをフリーズさせることを決定させる必要があります。

2つ目は、この本番バージョンを取得し、前処理ディレクティブを使用してビルドを管理するブランチを作成し、大きなファイルを分割する必要があります。JUSTプリプロセッサディレクティブ(#ifdefs、#includes、#endifs)を使用してコンパイルを分割する方が、APIを再コーディングするよりも簡単です。SLAと継続的なサポートは間違いなく簡単です。

ここでは、クラス内の特定のサブシステムに関連する関数を単純に切り出して、mainloop_foostuff.cppというファイルに入れ、mainloop.cppの適切な場所に含めることができます。

または

より時間はかかりますが、堅牢な方法は、物事がどのように含まれるかについて二重の間接的な内部依存構造を考案することです。これにより、物事を分割しながら、相互依存関係を処理できます。このアプローチには位置コーディングが必要であるため、適切なコメントと組み合わせる必要があることに注意してください。

このアプローチには、コンパイルするバリアントに基づいて使用されるコンポーネントが含まれます。

基本的な構造は、次のようなステートメントのブロックの後に、mainclass.cppにMainClassComponents.cppという新しいファイルが含まれることです。

#if VARIANT == 1
#  define Uses_Component_1
#  define Uses_Component_2
#elif VARIANT == 2
#  define Uses_Component_1
#  define Uses_Component_3
#  define Uses_Component_6
...

#endif

#include "MainClassComponents.cpp"

MainClassComponents.cppファイルの主な構造は、次のようにサブコンポーネント内の依存関係を解決するために存在します。

#ifndef _MainClassComponents_cpp
#define _MainClassComponents_cpp

/* dependencies declarations */

#if defined(Activate_Component_1) 
#define _REQUIRES_COMPONENT_1
#define _REQUIRES_COMPONENT_3 /* you also need component 3 for component 1 */
#endif

#if defined(Activate_Component_2)
#define _REQUIRES_COMPONENT_2
#define _REQUIRES_COMPONENT_15 /* you also need component 15 for this component  */
#endif

/* later on in the header */

#ifdef _REQUIRES_COMPONENT_1
#include "component_1.cpp"
#endif

#ifdef _REQUIRES_COMPONENT_2
#include "component_2.cpp"
#endif

#ifdef _REQUIRES_COMPONENT_3
#include "component_3.cpp"
#endif


#endif /* _MainClassComponents_h  */

そして、各コンポーネントに対して、component_xx.cppファイルを作成します。

もちろん、私は数値を使用していますが、コードに基づいてより論理的なものを使用する必要があります。

プリプロセッサーを使用すると、APIの変更を心配することなく、物事を分割することができます。

生産が決まったら、実際に再設計を行うことができます。


これは、成果はあるが当初は苦痛な経験の結果のように見えます。
JBRウィルキンソン2010

実際、これはBorland C ++コンパイラでPascalスタイルの使用をエミュレートしてヘッダーファイルを管理するために使用された手法です。特に彼らが彼らのテキストベースのウィンドウシステムの最初の移植をしたとき。
Elf King

8

まあ私はあなたの痛みを理解します:)私はいくつかのそのようなプロジェクトにも参加してきましたが、それはきれいではありません。これには簡単な答えはありません。

効果的な方法の1つは、すべての関数にセーフガードを追加し始めることです。つまり、引数のチェック、メソッドの事前/事後条件を確認し、最終的にすべてのユニットテストを追加して、ソースの現在の機能をキャプチャします。これを取得したら、コードをリファクタリングする準備が整ったことになります。何かを忘れた場合は、アサートとエラーのポップアップが表示され、警告が表示されます。

時には、リファクタリングが利益よりも多くの痛みをもたらす場合もあります。次に、元のプロジェクトを疑似メンテナンス状態のままにして、最初から開始し、獣から機能を徐々に追加する方がよい場合があります。


4

ファイルサイズを減らすことではなく、クラスサイズを減らすことを考慮する必要があります。ほぼ同じですが、別の角度から問題を見るようになります(@Brian Rasmussenが示唆するように、クラスには多くの責任があるようです)。


いつものように、私は反対票について説明を求めたいと思います。
ビョルンPollex

4

あなたが持っているのは、ブロブと呼ばれる既知の設計アンチパターンの古典的な例です。ここで私がポイントする記事を読むのに少し時間をかけてください。多分あなたは何か役に立つものを見つけるかもしれません。さらに、このプロジェクトが見た目と同じくらい大きい場合は、制御できないコードに成長しないように設計を検討する必要があります。


4

これは大きな問題の答えではありませんが、問題の特定の部分に対する理論的な解決策です。

  • 大きなファイルをサブファイルに分割する場所を見つけます。それらの各ポイントにコメントを特別な形式で入力します。

  • それらのポイントでファイルをサブファイルに分割するかなり自明なスクリプトを記述します。(おそらく、特別なコメントには、スクリプトがそれを分割する方法の指示として使用できるファイル名が埋め込まれています。)分割の一部としてコメントを保持する必要があります。

  • スクリプトを実行します。元のファイルを削除します。

  • ブランチからマージする必要がある場合は、最初にピースを連結して大きなファイルを再作成し、マージを実行してから、再度分割します。

また、SCCファイルの履歴を保持したい場合は、個々のピースファイルがオリジナルのコピーであることをソース管理システムに通知するのが最善の方法です。次に、そのファイルに保持されていたセクションの履歴を保存しますが、もちろん、大きな部分が「削除された」ことも記録します。


4

あまり危険を冒さずにそれを分割する1つの方法は、すべての行の変更を歴史的に見ることです。他よりも安定している特定の機能はありますか?変更のホットスポット。

行が数年変更されていない場合は、あまり心配せずに別のファイルに移動できます。特定の行に影響を与えた最後のリビジョンで注釈が付けられたソースを見て、プルアウトできる関数があるかどうかを確認します。


他の人も同様のことを提案したと思います。これは短くて要点があり、これは元の問題の有効な出発点であると私は思います。
Martin Ba

3

うわー、いいですね。上司に、獣をリファクタリングするのに多くの時間を必要とすることを説明することは、試してみる価値があると思います。彼が同意しない場合、辞めることは選択肢です。

とにかく、私が提案するのは、基本的にすべての実装を破棄し、それを新しいモジュールに再グループ化することです。それらを「グローバルサービス」と呼びましょう。「メインモジュール」はそれらのサービスにのみ転送し、あなたが書く新しいコードは「メインモジュール」の代わりにそれらを使用します。これは妥当な時間内に実現可能であるはずです(ほとんどがコピーアンドペーストであるため)、既存のコードを壊すことはなく、一度に1つのメンテナンスバージョンで実行できます。まだ時間が残っている場合は、古い依存モジュールをすべてリファクタリングして、グローバルサービスも使用できます。


3

私の同情-私の以前の仕事で、私が扱う必要があるものより数倍大きいファイルで同様の状況に遭遇しました。解決策は:

  1. 問題のプログラムの関数を徹底的にテストするコードを記述します。あなたはまだこれを手にしていないように聞こえます...
  2. ヘルパー/ユーティリティクラスに抽象化できるコードを特定します。大きくする必要はありません。単に「メイン」クラスの一部ではありません。
  3. 2.で識別されたコードを別のクラスにリファクタリングします。
  4. テストを再実行して、何も壊れていないことを確認します。
  5. 時間があれば、2に進み、必要に応じて繰り返して、コードを管理しやすくします。

ステップ3で作成したクラスは、新しくクリアされた関数に適したより多くのコードを吸収するために反復が増える可能性があります。

追加することもできます:

0:Michael Feathersの本を買うレガシーコードの使用に関するを

残念ながら、この種の作業はあまりにも一般的ですが、私の経験では、機能しているものの、機能しているコードを段階的に変更できるようにすることには大きな価値があります。


2

アプリケーション全体をより賢明な方法で書き直す方法を検討してください。おそらく、その小さなセクションをプロトタイプとして書き直して、アイデアが実現可能かどうかを確認してください。

実行可能なソリューションを特定したら、それに応じてアプリケーションをリファクタリングします。

より合理的なアーキテクチャを作成するすべての試みが失敗した場合、少なくとも、ソリューションの可能性はプログラムの機能を再定義することであることがわかります。


+1-誰かが自分のダミーを唾を吐く可能性がありますが、自分の時間にそれを書き直してください。
Jon Black

2

私の0.05ユーロセント:

混乱全体を再設計し、技術要件とビジネス要件を考慮してサブシステムに分割します(=コードベースがそれぞれ異なる可能性のある多くの並列保守トラック、高度な変更可能性などが明らかに必要です)。

サブシステムに分割するときは、最も変化した場所を分析し、変化しない部分からそれらを分離します。これにより、問題箇所が表示されます。最も変更の多い部分を独自のモジュール(dllなど)に分離し、モジュールAPIをそのまま維持できるようにし、BCを常に壊す必要がないようにします。この方法では、コアを変更せずに、必要に応じて、異なるメンテナンスブランチに異なるバージョンのモジュールをデプロイできます。

再設計は別のプロジェクトにする必要がある可能性が高く、移動するターゲットに対してそれを実行しようとしても機能しません。

ソースコードの履歴については、私の意見:新しいコードでは忘れてください。ただし、必要に応じて確認できるように、履歴をどこかに保存してください。最初はそれほど必要ないでしょう。

ほとんどの場合、このプロジェクトには経営陣の賛同が必要です。開発時間を短縮し、バグを減らし、メンテナンスを容易にし、全体的な混乱を減らすことで、おそらく議論することができます。「重要なソフトウェア資産の将来性とメンテナンスの実行可能性をプロアクティブに実現する」という流れに沿ったもの:)

これは私が少なくとも問題に取り組み始める方法です。


2

コメントを追加することから始めます。関数が呼び出される場所と、移動できるかどうかを参照します。これは物事を動かすことができます。あなたは本当にそれがコードベースでどれほど壊れやすいかを評価する必要があります。次に、機能の一般的なビットを一緒に移動します。一度に小さな変更。



2

私がやると便利だと思うこと(そして、あなたが直面している規模ではありませんが、今やっていることです)は、メソッドをクラスとして抽出することです(メソッドオブジェクトのリファクタリング)。異なるバージョン間で異なるメソッドは異なるクラスになり、共通のベースに注入して、必要な異なる動作を提供できます。


2

この文章があなたの投稿の中で最も興味深い部分であることがわかりました:

>このファイルは、弊社製品のいくつか(> 10)のメンテナンスバージョンで使用され、積極的に変更されているため、リファクタリングするのは非常に困難です

まず、ブランチをサポートするこれらの10以上のメンテナンスバージョンの開発には、ソース管理システムを使用することをお勧めします。

次に、10個のブランチを作成します(メンテナンスバージョンごとに1つ)。

もううんざりしている感じがします!ただし、機能が不足しているためにソースコントロールが状況に対応していないか、ソースコントロールが正しく使用されていません。

次に、作業しているブランチに進みます。製品の他の9つのブランチを混乱させないという知識の下で、必要に応じてリファクタリングしてください。

あなたはあなたのmain()関数にたくさんあることを少し心配します。

どんなプロジェクトでも、main()を使用して、シミュレーションやアプリケーションオブジェクトなどのコアオブジェクトの初期化のみを実行します。これらのクラスは、実際の作業を行う場所です。

プログラム全体でグローバルに使用するために、mainのアプリケーションログオブジェクトも初期化します。

最後に、メインでリーク検出コードをプリプロセッサブロックに追加して、DEBUGビルドでのみ有効になるようにします。これが私がmain()に追加するすべてです。Main()は短くする必要があります!

あなたはそれを言う

>ファイルには基本的に、プログラムの「メインクラス」(メインの内部作業のディスパッチと調整)が含まれています

これらの2つのタスクは、コーディネーターとワークディスパッチャーという2つの個別のオブジェクトに分割できるようです。

これらを分割すると、「SCCワークフロー」が台無しになる可能性がありますが、SCCワークフローを厳守すると、ソフトウェアのメンテナンスに問題が発生するようです。それを捨てて、今は振り返ってはいけません。なぜなら、修正するとすぐに安眠できるようになるからです。

決定を下すことができない場合は、上司と一緒に歯と爪を戦い、アプリケーションのリファクタリングを行う必要があります。答えをとらないでください!


私が理解しているように、問題はこれです:弾丸を噛んでリファクタリングすると、バージョン間でパッチを運ぶことができなくなります。SCCは完全にセットアップされている可能性があります。
peterchen

@peterchen-まさに問題。SCCはファイルレベルでマージを行います。(3者間マージ)ファイル間でコードを移動する場合は、変更されたコードブロックを1つのファイルから別のファイルに手動でいじり始める必要があります。(別のコメントで他の人が言及したGIT機能は、歴史にとってちょうど良いものであり、私が知る限りマージするためのものではありません)
Martin Ba

2

あなたがそれを説明したように、主な問題は、分割前と分割後の違い、バグ修正のマージなどです。その周りのツール。PerlやRubyなどでスクリプトをハードコーディングして、プリスプリットとポストスプリットの連結を比較することで生じるノイズのほとんどを取り除くのに、それほど長い時間はかかりません。ノイズの処理に関して最も簡単な方法を実行します。

  • 連結前/連結中に特定の行を削除します(ガードを含めるなど)。
  • 必要に応じて、diff出力から他のものを削除します

チェックインがあると必ず連結が実行され、単一ファイルバージョンと比較するための準備が整うようにすることもできます。


2
  1. このファイルとコードに再度触れないでください!
  2. トリートは行き詰まっているようなものです。そこにエンコードされた機能用のアダプターの作成を開始します。
  3. 異なるユニットで新しいコードを記述し、モンスターの機能をカプセル化するアダプターとのみ通信してください。
  4. ...上記の1つだけが不可能な場合は、ジョブを終了して新しいジョブを取得します。

2
+/- 0-真剣に、このような技術的な詳細に基づいて仕事を辞めることをお勧めする人はどこに住んでいますか?
Martin Ba

1

「ファイルには基本的にプログラムの「メインクラス」(メインの内部作業ディスパッチおよび調整)が含まれているため、機能が追加されるたびに、このファイルに影響を与え、ファイルが大きくなるたびに影響を受けます。」

その大きなSWITCH(私はあると思います)がメインメンテナンスの問題になる場合、辞書とコマンドパターンを使用するようにリファクタリングし、既存のコードからすべてのスイッチロジックをローダーに削除します。

    // declaration
    std::map<ID, ICommand*> dispatchTable;
    ...

    // populating using some loader
    dispatchTable[id] = concreteCommand;

    ...
    // using
    dispatchTable[id]->Execute();

2
いいえ、実際には大きなスイッチはありません。この文章は、この混乱を説明するために私ができる最も近いものです:)
Martin Ba

1

ファイルを分割するときにソースの履歴を追跡する最も簡単な方法は、次のようなものだと思います。

  1. SCMシステムが提供する履歴を保持するコピーコマンドを使用して、元のソースコードのコピーを作成します。おそらくこの時点で提出する必要がありますが、ビルドシステムに新しいファイルについて通知する必要はまだないので、問題ありません。
  2. これらのコピーからコードを削除します。それはあなたが保つ行の歴史を壊してはいけません。

「SCMシステムが提供する履歴保持コピーコマンドを使用する」...提供されない悪いこと
Martin Ba

残念な。それだけでも、よりモダンなものに切り替える良い理由のように思えます。:-)
クリストファー・クロイツィヒ

1

私はこの状況で私がすることは少し弾丸であり、そして:

  1. (現在の開発バージョンに基づいて)ファイルをどのように分割したかを把握する
  2. ファイルに管理ロックをかけます(金曜日の午後5時以降、誰もmainmodule.cppに触れません!!!)。
  3. 長い週末を過ごして、現在のバージョンまでの10以上の(古いものから新しいものへの)メンテナンスバージョンにその変更を適用します。
  4. ソフトウェアのサポートされているすべてのバージョンからmainmodule.cppを削除します。それは新しい時代です-mainmodule.cppはもうありません。
  5. ソフトウェアの複数のメンテナンスバージョンをサポートするべきではないことを管理に納得させてください(少なくとも大きな$$$サポート契約がなければ)。あなたの顧客のそれぞれが独自のユニークなバージョンを持っているなら... yeeeeeshhhh。10以上のフォークを維持するのではなく、コンパイラディレクティブを追加します。

ファイルへの古い変更を追跡することは、「split from mainmodule.cpp」のような最初のチェックインコメントによって簡単に解決されます。最近の何かに戻る必要がある場合、ほとんどの人は変更を覚えています。それが2年後であれば、コメントはどこを見るべきかを伝えます。もちろん、誰がコードを変更したかを調べるために2年以上前に戻ることはどのくらい価値がありますか?その理由は?

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.