web-dev-qa-db-ja.com

Cスタイルと混在する大規模なコードベースのレガシーC ++プロジェクトのリファクタリング

背景:

現在、C++プロジェクトに取り組んでいます。ビジネスで使用するため、このプロジェクトはオープンソースではありません。ただし、これはかなり大きなコードベース(約10k行のコード)と、いくつかの外部リンクライブラリおよびバイナリを含むプロジェクトです。問題は、コードベースの80%近くがC++スタイルではなくCスタイルで記述されていることですが、メインインターフェイスはC++ 11標準で記述されており、C++ 11でコンパイルする必要があります。 Cスタイルコードの最も不便な部分の1つは、たくさんのgotoステートメントが使用されていることです。gotoを使用する目的は、コードがスレッドセーフであることを確認し、コードをクリーンアップすることです。ゴミをすぐに。 (彼らは働いた)私が最初にC++を学んだとき、私はC++ 11から始めました、そしてコードがスレッドセーフであり、使用されているリソースが時間内にクリーンアップされることを保証するためにRAIIはハッキースタイルを置き換える現代的な方法だと思います。

パズル:

プロジェクトに一連の新しい機能を追加するよう割り当てられています。私の変更のいくつかは、これらのCスタイルのコードベースで行う必要があります。そして、それらの1つを変更した場合、それらすべてをC++ 1xスタイルでリファクタリングしないのはなぜですか?競合には限られた時間しか与えられません。80%でさえ、すべてをリファクタリングすることはできません。これらのCスタイルコードベースもCスタイルベースのライブラリを使用しています。コードをCスタイルからC++スタイルに変更したかどうかはわかりません。これらの外部ライブラリは以前と同じように機能しますか?これらの非推奨のCライブラリを置き換える同等のC++ライブラリはありますか?だから私のパズルは:限られた時間を与えられて、大規模なコードベースのレガシーC++プロジェクトをCスタイルからC++スタイルに効果的にリファクタリングする方法は?トレードオフとは何ですか?トレードオフは検討に値しますか?

2
Lingbo Tang

次のようなgoto cleanupスタイルを使用したCコード:

int function(int argument)
{
  int result;
  opaque_handle handle = NULL;
  char* text = NULL;
  size_t size;

  handle = extlib_create();
  if (!handle) {
    result = extlib_failed;
    goto cleanup;
  }
  size = extlib_size(handle, argument);
  text = malloc(size);
  if (!text) {
    result = oom;
    goto cleanup;
  }
  extlib_read(handle, text, size, argument);
  // etc.

  result = success;
  goto cleanup;

cleanup:
  if (handle) extlib_destroy(handle);
  if (text) free(text);
  return result;
}

各リソースをRAIIラッパーでラップし、おそらくいくつかの例外ラッパーを使用することで、常にC++にリファクタリングできます。

class extlib_handle {
  opaque_handle handle;
public:
  explicit extlib_handle(opaque_handle handle) : handle(handle) {}
  extlib_handle(const extlib_handle&) = delete;
  extlib_handle& operator =(const extlib_handle&) = delete;
  ~extlib_handle() { extlib_destroy(handle); }
  operator opaque_handle() const { return handle; }
};

template <typename V>
bool resize_nothrow(V& v, typename V::size_type sz)
try {
  v.resize(sz);
  return true;
} catch (std::bad_alloc&) {
  return false;
}

int function(int argument)
{
  extlib_handle handle(extlib_create());
  if (!handle) return extlib_failed;

  size_t size = extlib_size(handle, argument);
  std::vector<char> text;
  if (!resize_nothrow(text, size)) return oom;

  extlib_read(handle, text.data(), size, argument);
  // etc.

  return success;
}

このリファクタリングは、関数の外部のコードにはまったく影響せず、再利用可能なRAIIラッパーのコレクションをゆっくりと構築できます。

この手法を使用して、実際に触れるCコードの部分だけをリファクタリングし、他の部分はリファクタリングできません。他の人に触れると不必要なリスクが発生します(間違いを犯してコードにバグが追加され、それがなければ手つかずになる可能性があります-とにかく新しいバグが存在する可能性があるため、とにかく触らなければならないコードでは、このリスクは軽減されます。テスターからの注目が高まり、新しいコードの記述でエラーが発生しにくくなります)。

いつものように、できればテストを書いてください。しかし、サードパーティの依存関係を置き換えることができないため、コードはテストに適していません。 thatのリファクタリングは時間のかかる作業です。

6
Sebastian Redl

コードをCスタイルからC++スタイルに変更したかどうかわかりません。これらの外部ライブラリは以前と同じように機能しますか?これらの非推奨のCライブラリを置き換える同等のC++ライブラリはありますか?

どのライブラリを使用しているか、またはどのライブラリ内の関数を使用しているかを知らなければ、言うことは不可能です。もちろん、引き続きC++からC関数を呼び出すことができ、同じ入力を提供する場合は、以前と同じように機能するはずです。

だから私のパズルは:限られた時間を与えられて、大規模なコードベースのレガシーC++プロジェクトをCスタイルからC++スタイルに効果的にリファクタリングする方法は?トレードオフとは何ですか?トレードオフは検討に値しますか?

あなたが言ったことを考えると、最善の策は関数を追加することであり、関数を呼び出すコードまたは関数が呼び出すコードをクリーンアップできる場合は、それを実行することです。私はあなたが行っている追加に直接接続されていないものには触れません。

管理者が実行する必要があると判断するまで、すべてのリファクタリングを延期することはお勧めしません。私の経験では、たとえそれが(組織として)あなたに利益をもたらすとしても、経営者はそれを行う必要があると決して決定しません。他の作業をしながら、一度に1つずつそれを行うことができる場合、通常はそれがより良いアプローチです。 (あなたの経営陣が将来的に技術的債務を返済する意思があることを知っている場合を除きます。)

ここにはいくつかのトレードオフがあります。

  1. すべてをリファクタリングすると、今よりも時間がかかるため、現実的ではありません
  2. すでに触れようとしているものだけをリファクタリングすると、それらの処理が完了するまでに時間がかかる可能性がありますが、コードベースが最新で維持が容易になります。
  3. このプロジェクトが完了するまで待ってから、新機能の外観がまったくないリファクタリングを実行できるように管理を説得しても、(私の経験では)成功する可能性はほとんどありません。
  4. リファクタリングを行わず、新しい機能を追加するだけで、以前とまったく同じくらい多くの技術的負債が残ります。おそらく、元の技術的負債があり、新しいものとのインターフェースが増えるほどです。
2
user1118321

大学時代、私のコンピューターサイエンスの教授は、「私たちの仕事はすばやく、安く、高品質です。そのうち2つを選ぶことができます」と言っていました。

あなたの場合、時間は限られているため(作業は迅速でなければなりません)、1人の開発者しかそれに取り組んでいません(したがって、この作業に多くのお金が費やされていないため、安価です)。これは、品質が必然的に低下することを意味します。そして、品質が低下すると、それはコストが上がるときです。

私へのアドバイス:コードの機能を検証する自動テストをできるだけ多く記述してくださいbeforeリファクタリングします。次に、コードをリファクタリングした後、コードがリファクタリング前と同じように機能することを確認できます。

マネージャーの良いところは、結局のところ、彼らは本当にお金だけを気にかけているということです(それは本当に彼らの仕事です)。したがって、古いコードを維持し、古いコードの上に書かれた新しいコードのバグを修正するコストがリファクタリングのコストを上回ることを指摘することで、常にこの立場を守ることができます。

品質を低下させないでください。それ以外は交渉可能であり、妥協の余地があります。

1
Vladimir Stokic

私の質問にアドバイスを提供してくれたすべての人に感謝します。あなたの提案と私自身の評価に基づいて、私はこの事件についてこの決定をしました。

gotoの何が問題で何が問題ではないですか

いくつかの標準でC++をフォーマットするための多くの投稿を読みましたが、最も一般的なポイントの1つは、gotoの使用を避けることです。これは、問題を解決するアセンブリの方法です。複数のgotosが複数のスコープのコード内にある場合(ネストされている場合はさらに悪い)、特に大規模なコードベースプロジェクトの場合、可読性と保守性が低下したコードが残ります。ただし、制御フローを処理する柔軟性を提供したい場合は、gotoを使用できる場合があります。現代の言語では条件文を使用して分岐をより詳細にしましたが、誰かが以前にgotoを使用していた場合は、そのまま保持することもできます。実際、よく知られているいくつかのよく知られたプロジェクトでは、gotoも使用しました(例:git)。

C++でCスタイルコードを記述:

ここでも、C++は複数のパラダイムを持つ言語です。それが設計された目的の1つは、さまざまなスタイルをサポートし、互換性を持たせることです。したがって、C++プロジェクトの既存のCスタイルのコードが機能している場合は、最初のままにする必要があります。

リファクタリング前の書き込みテスト:

リファクタリングがレガシーコードベースと同じように機能しているかどうかはどのようにしてわかりますか?自動テストを記述して、リファクタリングされたコードが期待される動作を生成することを確認します。これらのテストがない場合は、何もしないでください。 「総体の総体をしましょう」。限られた時間の中で、詳細なテストを書くことができません。どうすればよいですか?マイクロテストを記述し、段階的にマイクロ変更を行います。レガシープロジェクトに追加する関数のテストのみを記述します。

1
Lingbo Tang