web-dev-qa-db-ja.com

Cのような手続き型言語でOpen Closed原則に準拠する方法

ロバートマーティンの1996年の独創的な記事 "The Open-Closed Principle" 彼はCで原則に従わない例を示しています(DrawAllShapes()メソッドは変更のために閉じられていません):

enum ShapeType {circle, square};
struct Shape
{
 ShapeType itsType;
};
struct Circle
{
 ShapeType itsType;
 double itsRadius;
 Point itsCenter;
};
struct Square
{
 ShapeType itsType;
 double itsSide;
 Point itsTopLeft;
};
//
// These functions are implemented elsewhere
//
void DrawSquare(struct Square*)
void DrawCircle(struct Circle*);
typedef struct Shape *ShapePointer;
void DrawAllShapes(ShapePointer list[], int n)
{
 int i;
 for (i=0; i<n; i++)
 {
   struct Shape* s = list[i];
   switch (s->itsType)
   {
     case square:
       DrawSquare((struct Square*)s);
       break;
     case circle:
       DrawCircle((struct Circle*)s);
       break;
    }
   }
}

次に、OOP継承とポリモーフィズムを使用した上記の動作の実装C++の実装を提示します。これは、開閉原理(OCP)に準拠しています。

私の質問は、Cの手続き型機能のみを使用して、上記のCコードをどのようにリファクタリングしてOCPに準拠するかです。より一般的には、手続き型言語のコードはOCPに厳密に従うことができますか?

4
Ken

オープン/クローズの原則が何を必要とするかを考えます:入力が何であるかを本当に知らないような方法でメソッドを書く必要があります正確にしかし、何かを行う必要があります入力が何であるかに依存します

Cf.簡単な実際の例:お腹が空いている(重要な抽象化:何を食べるかを直接気にしない)ため、食事を提供する場所に行きます。ウェイターが現れ、あなたが欲しいものを尋ねます。次の2つの代替アルゴリズムダイアログを検討してください。

  • ここは、ハンバーガーレストランですか?
  • 番号。
  • ここは、シーフードレストランですか?
  • 番号。
  • ここは、ピザレストランですか?
  • 番号。
  • ここはパン屋ですか?
  • 番号
  • ここはステーキハウスですか?
  • はい。
  • すごい!プライムリブステーキが欲しいのですが、ミディアムレアでお願いします!

以上:

  • 何か食べ物を持ってきてくれませんか。
  • 承知しました!

最初のロジックに従うことの問題は、...[すべてのタイプ]を要求するのを忘れた場合、まったく食べなくなる可能性があります。したがって、それが典型的なダイアログである場合、新しい中華レストランが表示され、この可能性を含めるためにアルゴリズムダイアログを「拡張」するまでは、そこで食べることはありません。

例の「人工性」に関係なく、開閉の原理には非常に重要な結果があることを示しています...オブジェクトの責任であるすべてのタスクを割り当てます[オブジェクトに自体。だからあなたの質問は要約すると:

メソッドを抽象化する方法

さまざまな言語には独自の技術があります。これはOOPの非常に基本的な概念であるため、OOPに合わせた言語にはこれが組み込まれています。抽象化は、抽象クラス/インターフェースなどのようなものであり、実装は詳細を「埋める」。手続き型言語では、メソッド呼び出しを「エミュレート」することになります。

結果として、[〜#〜] c [〜#〜]に関する質問への短い答えは、 Robert Harveyの提案による 、関数ポインタ、これはwhatを明らかにするがhowを明らかにしないメソッドの抽象化です。

あなたの質問に対する長い答えはおそらく、詳細に エミュレートOOP Cで) の方法に関する別の答えでカバーされています。

6
Vector Zita

Cでジョブを実行するには、おそらく関数へのポインタを含む構造体を作成する必要があります。つまり、vtableを明示的または手動で定義し、すべての形状を含むユニオンの最初のアイテムとしてそれを含めます。

void drawCircle(union Shape *);
void drawSquare(union Shape *);

struct Square { 
    void (*draw)();

    Position center;
    Size size;
};

struct Circle { 
    void (*draw)();

    Position center;
    Size size;
};

union Shape {
    Circle circle;
    Square square;
};


void drawShapes(Shape *shapes, int count) { 
    for (int i=0; i<count; i++)
        shapes[i]->draw(&shapes[i]);
}

したがって、Shapeオブジェクトの配列があります。それぞれの最初の要素は、その特定の種類の形状を描画できる関数へのポインターです。したがって、新しいタイプの形状を追加する場合は、その種類の形状を描画する関数を定義する必要があり、その形状の形状「オブジェクト」を作成するときは、そのdrawを初期化する必要がありますその形状の描画関数を指すメンバー。

この関数を呼び出すと、基本的に次の順序で処理が行われます。

void drawSquare(void *data) { 
     struct Square *real_data = (struct Square *)data;

     // draw a square with the specified location/size
}

void drawCircle(void *data) {
    struct Circle *real_data = (struct circle *)data;

    // draw a circle with the specified location/size
}

これにより、開閉の原則の中間点に入ることができます。新しい形状を追加するには、そのような形状を描画する新しい関数を追加する必要があることは明らかです。また、その形状を定義するために必要な種類のデータを保持するための構造体を追加する必要もあります。その構造体には、Shapeへのポインターを受け取る描画を行うための関数へのポインターが含まれている必要があります。そしてもちろん、その新しいタイプの形状をShape共用体に追加して、適切なデータを描画関数に渡すことができるようにする必要があります。それが気になる場合は、Shapeユニオンを完全にスキップして、各関数にvoidへのポインターを渡し、vtable型を(好きな名前で)定義して、インターフェイスの関数へのポインタのみ。これは、(少し)タイプセーフティを犠牲にして、open/closedの原則に少し厳密に準拠しています。

drawShapesと、CircleまたはSquareを描画するためのすべてのコードは閉じたままにすることができます。新しい形状を追加しても、影響はありません。もちろん、(1つのdrawだけでなく)関数へのポインタを追加しても、実際にこれが大きく変わるわけではありません。通常、機能するdrawShapesの順序で関数を記述できます。それぞれが関数へのポインターの形で同じインターフェースを定義している限り、任意の型の形状(型が一致するなど)。

反対の方向では、これにはまだ多くの手動作業があり、ミスの余地が多く残されています。ポインターの一部を初期化して、それらが間違った関数を参照するようにすることは特に簡単です。そのため、それらを使用すると、期待どおりの結果が得られません。また、作成する各Shapeオブジェクト内の関数へのポインターを手動で初期化して、正しい描画関数を指すようにする必要があります(さらに、シミュレートされた仮想関数を追加すると、問題がさらに悪化します)。

3
Jerry Coffin

オープン/クローズには、2つの側面があります。

  • 閉じる:ソースコードを変更しないでください。これはCでは一般的なビジネスです。変更してはならないライブラリがたくさんあります。不透明なポインタを使用すると、使用しているデータの構造を外部に隠すことができます。

  • Open:元のコードを変更することなく、簡単に拡張することができます。これはCで行うのがはるかに難しいことであり、そのようなことに対する言語サポートはありません。それは実現可能ですが、訓練が必要です。そして、それを可能にするプログラムとデータ構造が必要です。

Cで実現可能です-例:

Shapeの場合、通常、いくつかのメンバーをShape構造体に追加することにより、abstract形状をエミュレートします。

  • private形状データメンバーを持つ構造体を指す_void *data_メンバー。
  • polymorphic形状処理の関数ポインター。たとえば、現在スイッチパーツで使用している描画関数DrawXxxx()など。将来的には、それがどのような形であるかを考えて大きな切り替えを行うのではなく、ポインタを介して描画関数を呼び出すだけです。
  • ShapeXxxFactory()ファクトリ関数は、構造を初期化することによって新しいShapeXxxxをセットアップし、データポインターと関数ポインターが正しく初期化されるようにします。

ヘッダーを公開する必要があるもの(ファクトリ関数、デストラクタなど)に制限し、_ShapeXccc.c_コンパイルユニットにプライベートな詳細を隠すことにより、encapsulate要素を使用できます。

  • 関数ポインタを介して使用されることになっている関数は静的関数としてカプセル化でき、ShapeXxxFactory()関数にローカルでのみ表示できます。
  • dataポインタは不透明なポインタであり、データの実際の構造はローカル構造で定義されます。したがって、形状固有の関数のみがdataを実際の型にキャストできます。

別のトリックは、任意の形状のいくつかの一般的な動作を実装するいくつかの「テンプレート」関数を書くことですが、それは template method と同じように特定の部分を実行するために形状固有の関数ポインターを呼び出します。繰り返しになりますが、列挙型は必要ありません。スイッチは必要ありません。

他のアプローチ

拡張性を処理する別の方法は、プラグインアプローチです。哲学的な観点からも同様です。一部の関数を動的にロードし、適切な関数を動的にポイントします。

結論と既知の用途

したがって、オープン/クローズはCで実現可能です。唯一の問題は、原則の定義をいかに「簡単」にするかです。 Cでは、これはすべて善意と規律に依存しており、C++に比べて比較的エラーが発生しやすくなります。関数ポインターやキャストでミスを犯しやすいです。

たとえば、次のようなデザインが使用されています。

  • WinAPIで、新しいタイプの windows を作成し、そのウィンドウに送信されるイベントを処理する関数を定義する場合。
  • Windowsでは COMオブジェクト 、これはバイナリインターフェイスを公開しますが、このインターフェイスの実装は適切な言語で行うことができます Cを含む
1
Christophe