コードの再利用-実際に起こること

「コードの再利用」について話すことは、プログラマーの間で非常に人気があります-そして、ほとんど彼らはそれについて前向きに話します。 私たちが設計したデザインは「ユニバーサル」で「他のプロジェクトでの使用に適している」と言いたいです。 なぜこれが良いことだと考えられるのかは簡単に理解できます。誰もが、既存の経験を活用して、前のプロジェクトの2倍の速度で次のプロジェクトを実装したいと考えています。



しかし、実際にはこれに関しては、ほとんどの場合、何かがおかしくなります。 このテーマに関する非常に賢いアイデアが1つあります。「コードを少なくとも3つの異なる場所で適用できるようになるまで、コードを再利用可能にしないでください。」 このアドバイスは非常に良いと思います。特定のケースで「今ここ」で問題を解決できるような再利用可能なコードを書くことに夢中にならない(または役立つ)多くの状況を見ました。



これは、再利用は常に歓迎すべき高貴な目標であるという理論の欠陥を示しています。



なぜ再利用しませんか?



再利用可能なコードの作成を主張するのは簡単です。コードを一度作成してデバッグし、いくつかの場所でそれを利用すると、製品/製品のビジネス価値がすぐに向上します。



はい、いいえ。 早すぎるコードの一般化は非常に現実的な問題です(早すぎる最適化と同様)。 ほとんどの場合、実際のタスクで同じコード(または非常に類似したバリエーション)を数回書くまで、人々はコードの一部を再利用する本当の可能性を見ることはできません。 一方、空想上のプログラマーは、空中にそのような抽象的な城を建て、最初の仕事を解決せず、再利用はできません。



「パターン」と呼ばれる今流行の文化現象をどうして思い出せないのでしょうか。 パターンはもともと純粋に説明的なものでした。 あちこちのプログラマは、いくつかの一般的なアイデア、アプローチを見つけ、名前を付けました。 そのような名前のエンティティをかなりの量蓄積すると、人々は突然カートを馬の前に置きました。 プログラミングではパターンが必須になり、それぞれが明確に定義されたルールに従って厳密に実装されなければならないことがわかりました。 タイプXのシステムを構築していて、Yパターンを使用するこのようなシステムがすでに市場に3つある場合は、実装に1つあるはずです。



バランスが必要です。 明らかに、完全なコピーアンドペーストという考えは悪質です。 しかし、潜在的な再利用を考えてすべてのコードを書くべきだという考えは、同様に悪質です。



別の興味深い要素があります。 ほとんどの場合、ソフトウェアを開発するときには、すでに記述したコードを再利用せずに(再利用可能として記述されていても)、新しいものを記述します。 これは論理的ではなく、これがなぜ起こるのかを理解することは非常に重要であり、同じものを新しい言語やパラダイムで何度も書き換えるのをやめることができます。



コードを再利用しないのはなぜですか?



人生の実際の例です。 ゲームのビデオエンジンでコールバック処理システムを設計したい。 しかし、以前の同様のプロジェクトに取り組む過程で、私の会社の他の開発者によって設計されたいくつかの同様のシステムをすでに持っています。 それらのほとんどは同じ原則に基づいて構築されています。「イベントのソース」があり、イベントにサブスクライブするメカニズムがあります。イベントが発生した場合、各署名者を引き出してイベントを通知する必要があります。 シンプル。



これは、Guild Wars 2のソースコードに、この単純なアーキテクチャアイデアの6つの異なる実装に関するゲームが含まれているだけです。 一部はクライアントにあり、他はサーバーにあり、他はクライアントとサーバー間でメッセージを使用していましたが、一般的にはすべて同じことを行い、実装は基本的に同じでした。



これは、リファクタリング、コードの統合、重複コンポーネントの削減が良いアイデアのように思える場合の典型的な例です。 それはただのGuild Wars 2は、数百万行のコードの巨大な巨人であり、その中の基本的なメカニズムの1つを採用してリメイクするものにはなりたくありません。



さて、既存のコードをやり直さないでください。 彼は、結局のところ、そのように動作します。 しかし、将来について考えてみましょう。 人々はゲームのプレイを止めることはありません。つまり、プログラマーはゲームの制作をやめることはありません。 つまり、エンジンがあり、これらのエンジンにはコールバックの標準化された優れたライブラリが必要であり、誰もが一目で気に入ってくれるでしょう。 書きましょうか? さあ!



他の人が使えるようにオープンソースコードを書きたいです。 一方で、ギルドウォーズ2のようなモンスターが必要なものすべてを見つけるには十分に強力である必要がありますが、一方で、1つのゲーム(またはプラットフォーム)に純粋に固有のものを含めるべきではありません。また、再利用可能なコードを書きたいです。



しかし実際には、そのような外部の(オープンではあるが)ライブラリを使用しない理由はたくさんあります。 第一に、このようなライブラリには、必要な機能がまったくないため、追加する必要があります。 第二に、このライブラリの依存関係は大きな障壁になります。



依存関係のいくつかは単純で明白です。 FooクラスはBarクラスを継承します。つまり、それらは依存関係にあります-これは理解できることです。 しかし、もっと興味深い形式の依存関係があります。 コールバックのライブラリを作成して公開するとします。 その中のどこかに、ライブラリには、サブスクライバに関する情報を保存するコンテナが必要になります。 さて、イベントの通知が必要な人。 どのように考えても、考えがどうであれ、コンテナが必要です。 コンテナをどのように実装しますか? まあ、私たちは石器時代ではありません。 とにかく、この記事はコードの再利用についてです。 (ゲーム開発の世界以外で)明白な答えは、C ++標準ライブラリからコンテナを取得することです。 std :: vector またはstd :: mapまたはその両方にすることができます。



ゲームでは、何らかの理由で、標準ライブラリの使用がしばしば禁止されています。 ここでは理由を説明しません。どこかで読んでください。 プロジェクトで使用されるライブラリを選択できない場合があることを事実として受け入れてください。



そのため、いくつかのオプションが残っています。 標準C ++ライブラリに応じてライブラリを実装できます。これにより、潜在的なユーザーの半分はすぐに使用できなくなります。 プラットフォームで利用できないものをすべて取り除くために、彼らは私のライブラリのコードを書き直さなければなりません。 潜在的に書き換えられるコードの量は、ライブラリ内のコードのある種の「再利用」について話すのが面倒になるほどで​​す。



2番目のオプションは、ライブラリ内に自分でコンテナを実装することです。 実際、リンクリストやベクターのような単純なコンテナは、それほど難しくありません。 しかし、これは、コードを再利用するという観点からすると、オプションはさらに悪くなります-そのようなコンテナは標準ライブラリにあり、おそらく私たちのライブラリを使用したいユーザーのライブラリにあります。 そして、ここでコンテナタイプの別のセットを追加しています! それはどのようなコードの再利用ですか?それどころか、屋根の上に追加のエンティティを作成しました。



契約プログラミング



契約プログラミングの考え方はまったく新しいものではありませんが、実際にはあまり使用されません。 それでは、上記のコンテナーの形式で単純な依存関係から始めましょう。



class ThingWhatDoesCoolStuff { std::vector<int> Stuff; };
      
      





このコードにより、ThingWhatDoesCoolStuffクラスがstd :: vectorに依存するようになります。これは、標準ライブラリのstd :: vectorを使用できないユーザーにとっては便利ではありません。 コードを少しわかりやすくしましょう:



 template <typename ContainerType> class ThingWhatDoesCoolStuff { ContainerType Stuff; }; //     : ThingWhatDoesCoolStuff<std::vector<int>> Thing;
      
      





クライアントはかなり長くて奇妙な型名を書く必要がありましたが(もちろん、typedefまたはusingを使用して視覚的に単純化することができます)。



さらに、コードでコンテナを使用し始めるとすぐにすべてが壊れます:



 template <typename ContainerType> class ThingWhatDoesCoolStuff { public: void AddStuff (int stuff) { Stuff.push_back(stuff); } private: ContainerType Stuff; };
      
      





コンテナにアクセスするには、アイテムを追加するためにpush_backメソッドが必要です。 もちろん、コンテナが標準ベクトルである限り、すべて問題ありません。 そうでない場合は? ユーザーが提供するコンテナタイプがAddと呼ばれる場合 コンパイルエラーが発生します。 そして、ユーザーは自分のコンテナとの互換性のためにライブラリのコードを書き直さなければなりません(今のところ、コードを再利用します)、またはコンテナとそれを使用するコードを書き直します(誰もこれをしません)。



しかし、彼らが言うように、問題は十分なレイヤーと間接性を追加することで解決できます! やってみましょう:



 //      template <typename Policy> class ThingWhatDoesCoolStuff { private: //   ,    typedef typename Policy::template ContainerType<int> Container; //        Container Stuff; public: void AddStuff (int stuff) { using Adapter = Policy::ContainerAdapter<int>; Adapter::PushBack(&Stuff, stuff); } }; //         : struct MyPolicy { //        template <typename T> using ContainerType = std::vector<T>; template <typename T> struct ContainerAdapter { static inline void PushBack (MyPolicy::ContainerType * container, T && element) { //          container->push_back(element); } }; };
      
      





すべてがどのように機能するかを見てみましょう。 まず、ポリシーテンプレートクラスを定義します。これにより、ビジネスロジックとその依存関係(コンテナーなど)を分離できます。 再利用を主張するコードは、依存関係から明確に分離する必要があります。 上記のテンプレートを使用した方法は、唯一の実装オプションではなく、優れた方法の1つです。



上記の実装の構文は、実際には簡潔ではありません。 このコードで言いたいことは、「コンテナが必要です。コンテナAPIがあります。これを理解し、適切な実装を提供して、仕事をします。」



ここでのテンプレートは、仮想関数を呼び出すオーバーヘッドを回避するために使用されます。 理論的には、基本クラスを「コンテナ」にして、その中に仮想メソッドを定義し、なんとか、なんとか、神様、私はそのような恐ろしいオプションについて考えようとするだけですでに嫌いです。 永遠に忘れましょう。



このコードの良い点は、標準C ++ライブラリを使用するプロジェクトと使用しないプロジェクトの両方で、変更なしで使用できることです。 コールバックシステムを1回だけ公開することで、プラットフォーム、環境、その他の制限に応じてユーザーがコードを編集する必要がなくなります。



また、考えなければならない欠点もあります。私のライブラリを使用したい人は誰でも、その依存関係について考え、同じコンテナに適したアダプタを書くことを余儀なくされます。 ただし、これを行う必要があるのは1回だけです(コンテナタイプのセットから完全に異なるものにプロジェクトに参加する人はほとんどいません)。



他のエンティティ(コンテナよりも複雑なエンティティ)の場合、アダプタの作成はより困難になる可能性があります。 ただし、このようなコードは非常に慎重に、理想的には上記のように再利用する必要があります-いくつかの同様のコンポーネントを既に作成し、それらのどの部分を一般的な抽象化に区別できるか、それぞれの特定の実装のままにする必要があることを十分に理解した後にのみそれらの。



おわりに



最後に、上記の例のパフォーマンスを確認できます。 デバッグビルドではラメかもしれませんが、テンプレートの使用によるリリースビルドは、最適化された効率的なコードを受け取ります。 実行時のパフォーマンスがあれば、すべてがうまくいきます。 組み立て時間はどうですか? テンプレートを使用すると、コンパイル時間が長くなります。 しかし、この例では、テンプレートは特定のタイプで1回だけインスタンス化され、コンパイル時間をテンプレートのないバージョンと大まかに比較します。 それにもかかわらず、テンプレートの複数のアプリケーションを使用すると、コンパイル時間の壊滅的な増加という状況に陥りやすくなります。これに従う必要があります。 それでも、このアプローチは、接続された一連の抽象インターフェースを定義するよりも優れたオプションだと思います。



この分解の例について私が伝えたかったのはそれだけです。 これがお役に立てば幸いです。



また、再利用コードのコンポーネントに何かを割り当てる前に、少なくとも3つの異なる場所で使用する必要があることを忘れないでください



All Articles