web-dev-qa-db-ja.com

サブクラスタイプを要求しないようにするための良い設計方法は何ですか?

あなたのプログラムがオブジェクトがどのクラスであるかを知る必要がある場合、通常は設計上の欠陥を示しているので、これを処理するための良い方法は何かを知りたいと思いました。 Circle、Polygon、Rectangleなど、サブクラスを継承したクラスShapeを実装しています。CircleがPolygonまたはRectangleと衝突するかどうかを知るためのアルゴリズムはさまざまです。次に、Shapeの2つのインスタンスがあり、一方が他方に衝突するかどうかを知りたいとします。そのメソッドでは、どのアルゴリズムを呼び出すかを知るために、どのサブクラス型が衝突するオブジェクトであるかを推測しますが、これは悪いデザインや練習?これが私が解決した方法です。

abstract class Shape {
  ShapeType getType();
  bool collide(Shape other);
}

class Circle : Shape {
  getType() { return Type.Circle; }

  bool collide(Shape other) {
    if(other.getType() == Type.Rect) {
      collideCircleRect(this, (Rect) other);     
    } else if(other.getType() == Type.Polygon) {
      collideCirclePolygon(this, (Polygon) other);
    }
  }
}

これは悪いデザインパターンですか?サブクラスのタイプを推測せずにこれを解決するにはどうすればよいですか?

11
Alejandro

多型

getType()またはそのようなものを使用している限り、ポリモーフィズムを使用していません。

あなたはあなたがどんなタイプを持っているかを知る必要があるように感じます。しかし、あなたがそれをクラスに押し下げる必要があることを知っている間にあなたがしたいどんな仕事でも。次に、いつそれを実行するかを伝えます。

手続き型コードは情報を取得してから決定を行います。オブジェクト指向のコードは、オブジェクトに物事を行うように伝えます。
—アレックシャープ

この原則は tell、do n't ask と呼ばれます。それに従うことで、タイプなどの詳細を広げないで、それらに作用するロジックを作成できます。それを行うと、クラスが裏返しになります。クラスが変更されたときに変更できるように、クラス内でその動作を維持することをお勧めします。

カプセル化

他の形は必要ないだろうと私に言うことができますが、私はあなたを信じていません。

カプセル化に従うと、新しいタイプを簡単に追加できるという優れた効果があります。これは、それらの詳細がifおよびswitchロジックに表示されるコードに広がらないためです。新しい型のコードはすべて1か所に配置する必要があります。

タイプ無知の衝突検知システム

タイプを気にすることなく、高性能であらゆる2D形状で機能する衝突検出システムを設計する方法を紹介します。

enter image description here

あなたはそれを描くことになっていたとしましょう。シンプルなようです。すべてのサークルです。衝突を理解するサークルクラスを作成するのは魅力的です。問題は、これにより、1000個の円が必要なときにバラバラになってしまう考え方が私たちに伝わってしまうことです。

サークルについて考えるべきではありません。ピクセルについて考える必要があります。

あなたがこれらの人を描くために使用するのと同じコードが、彼らが触れたとき、またはユーザーがクリックしているものを検出するために使用できるものであると私が言ったとしたら.

enter image description here

ここでは、各円を独自の色で描いています(目が黒の輪郭を見るのに十分な場合は、無視してください)。これは、この隠された画像のすべてのピクセルが、それを描画したものにマッピングされることを意味します。ハッシュマップはそれをうまく処理します。この方法で実際にポリモーフィズムを行うことができます。

この画像をユーザーに表示する必要はありません。最初のコードと同じコードで作成します。ちょうど異なる色で。

ユーザーが円をクリックすると、1つの円だけがその色であるため、どの円か正確にわかります。

別の円の上に円を描くと、上書きするすべてのピクセルをセットでダンプすることですばやく読み取ることができます。完了したら、衝突したすべての円にポイントを設定します。衝突を通知するために、それぞれを1回だけ呼び出す必要があります。

新しいタイプ:Rectangles

これはすべて円で行われましたが、私はあなたに質問します:長方形で何か違うのでしょうか?

サークルの知識は検出システムに漏れていません。半径、円周、中心点は関係ありません。ピクセルと色を気にします。

個々の形状に押し込む必要があるこの衝突システムの唯一の部分は、ユニークな色です。それ以外は、形は自分の形を描くことを考えることができます。それはとにかく彼らが得意なことです。

ここで、衝突ロジックを作成するとき、どのサブタイプを持っているかは気にしません。あなたはそれを衝突するように言い、それはそれが描くふりをしている形の下に何が見つかったかを教えてくれます。タイプを知る必要はありません。つまり、他のクラスのコードを更新しなくても、必要なだけサブタイプを追加できます。

実装の選択肢

本当に、それはユニークな色である必要はありません。それは実際のオブジェクト参照であり、間接参照のレベルを保存できます。しかし、この回答に引き込まれたとき、それらはニースとは見えません。

これは実装例の1つにすぎません。確かに他にもあります。これが示すことは、これらの形状サブタイプを単一の責任に固執させるほど、システム全体がうまく機能するということです。より高速でメモリをあまり必要としないソリューションが存在する可能性がありますが、サブタイプに関する知識を広めることを余儀なくされた場合、パフォーマンスが向上してもそれらを使用するのは嫌です。明らかに必要な場合以外は使用しません。

ダブルディスパッチ

今までは完全に無視してきました double dispatch 。できたのでそうしました。衝突するロジックがどの2つのタイプの衝突を気にしない限り、それは必要ありません。必要ない場合は使用しないでください。あなたがそれを必要とするかもしれないと思うなら、できる限りそれを扱うのを延期してください。この態度は [〜#〜] yagni [〜#〜] と呼ばれます。

さまざまな種類の衝突が本当に必要であると判断した場合は、n個の形状サブタイプが本当にn個を必要とするかどうかを確認してください。2 衝突の種類。これまでのところ、別の形状のサブタイプを簡単に追加できるように一生懸命取り組んできました。 ダブルディスパッチ実装 を使用して、四角が存在することを円に強制的に知らせて台無しにしたくありません。

とにかく何種類の衝突がありますか?少し推測すること(危険なこと)は、弾性衝突(弾力性)、非弾性(粘着性)、エネルギッシュ(爆発)、破壊的(危険)を引き起こします。もっとあるかもしれませんが、これがnより小さい場合2 衝突を過剰に設計しないでください。

これは、私の魚雷が損傷を受け入れる何かに命中したとき、それが宇宙船に命中したことを知る必要がないことを意味します。それは、「ハハ!あなたは5ポイントのダメージを受けた」とそれを伝えさえすればよい。

ダメージを与えるものは、ダメージメッセージを受け入れるものにダメージメッセージを送ります。このようにして、新しい形状について他の形状に通知せずに新しい形状を追加できます。新しいタイプの衝突の周りに広がるだけです。

宇宙船はトープに「ハハ!あなたは100ポイントのダメージを受けた」と送り返すことができます。 「あなたは今私の船体にこだわっている」と同様。そして、torpは「まあ、私は私を忘れてしまった」と返送することができます。

どちらが何であるかを正確に知ることもありません。彼らは衝突インターフェースを介して互いに対話する方法を知っているだけです。

確かに、ダブルディスパッチを使用すると、これより親密に制御できますが、 本当にそうしたいですか

少なくとも、実際の形状の実装ではなく、形状が受け入れる衝突の種類を抽象化して二重ディスパッチを行うことを考えてください。また、衝突動作は、依存関係として注入してその依存関係に委任できるものです。

パフォーマンス

パフォーマンスは常に重要です。しかし、それが常に問題であるとは限りません。テストのパフォーマンス。憶測しないでください。パフォーマンスの名の下に他のすべてを犠牲にすることは、通常、とにかくパフォーマンスの高いコードにはつながりません。

13
candied_orange

問題の説明は Multimethods (別名マルチディスパッチ)を使用する必要があるように聞こえますが、この場合は Double dispatch です。最初の答えは、ラスターレンダリングで衝突するシェイプを一般的に処理する方法について詳しく説明しましたが、OPは「ベクター」ソリューションを必要としているか、問題全体がシェイプに関して再定式化されていると思います。これはOOP説明。

引用されているウィキペディアの記事でも同じ衝突メタファーを使用しています。引用してみましょう(Pythonには他のいくつかの言語のような組み込みのマルチメソッドはありません)。

@multimethod(Asteroid, Asteroid)
def collide(a, b):
    """Behavior when asteroid hits asteroid"""
    # ...define new behavior...
@multimethod(Asteroid, Spaceship)
def collide(a, b):
    """Behavior when asteroid hits spaceship"""
    # ...define new behavior...
# ... define other multimethod rules ...

したがって、次の質問は、プログラミング言語でマルチメソッドをサポートする方法です。

7
Roman Susi

あなたの基本的な問題は、最新のOOプログラミング言語では、関数のオーバーロードは動的バインディングでは機能しないことです(つまり、関数の引数の型はコンパイル時に決定されます)。必要なのは仮想メソッドです。 1つだけではなく2つのオブジェクトで仮想的に呼び出されます。そのようなメソッドは multi-methods と呼ばれます。ただし、Java、C++などの言語では この動作をエミュレート する方法があります。 etc. ダブルディスパッチ が非常に役立ちます。

基本的な考え方は、ポリモーフィズムを2回使用することです。 2つの形状が衝突した場合、ポリモーフィズムを介してオブジェクトの1つの正しい衝突メソッドを呼び出し、一般的な形状タイプの他のオブジェクトを渡すことができます。呼び出されたメソッドでは、thisオブジェクトが円、長方形、またはその他のものであるかどうかがわかります。次に、渡された形状オブジェクトで衝突メソッドを呼び出し、それにthisオブジェクトを渡します。次に、この2番目の呼び出しは、ポリモーフィズムによって正しいオブジェクトタイプを再び見つけます。

abstract class Shape {
  bool collide(Shape other);
  bool collide(Rect other);
  bool collide(Circle other);
}

class Circle : Shape {

  bool collide(Shape other) {
    return other.collide(this);
  }

  bool collide(Rect other) {
    // algorithm to detect collision between Circle and Rect
  }

  // ...
}

class Rect : Shape {

  bool collide(Shape other) {
    return other.collide(this);
  }

  bool collide(Circle other) {
    // algorithm to detect collision between Circle and Rect
  }

  // ...
}

ただし、この手法の大きな欠点は、階層内の各クラスがすべての兄弟について知る必要があることです。これにより、後で新しい形状が追加された場合、メンテナンスの負担が大きくなります。

5
sigy

この問題は、2つのレベルで再設計する必要があります。

まず、形状から形状間の衝突を検出するためのロジックを抽出する必要があります。これは、モデルに新しい形状を追加する必要があるたびに、 [〜#〜] ocp [〜#〜] に違反しないようにするためです。すでにCircle、Square、Rectangleが定義されていると想像してください。次に、次のようにします。

_class ShapeCollisionDetector
{
    public void DetectCollisionCircleCircle(Circle firstCircle, Circle secondCircle)
    { 
        //Code that detects collision between two circles
    }

    public void DetectCollisionCircleSquare(Circle circle, Square square)
    {
        //Code that detects collision between circle and square
    }

    public void DetectCollisionCircleRectangle(Circle circle, Rectangle rectangle)
    {
        //Code that detects collision between circle and rectangle
    }

    public void DetectCollisionSquareSquare(Square firstSquare, Square secondSquare)
    {
        //Code that detects collision between two squares
    }

    public void DetectCollisionSquareRectangle(Square square, Rectangle rectangle)
    {
        //Code that detects collision between square and rectangle
    }

    public void DetectCollisionRectangleRectangle(Rectangle firstRectangle, Rectangle secondRectangle)
    { 
        //Code that detects collision between two rectangles
    }
}
_

次に、それを呼び出す形状に応じて、適切なメソッドが呼び出されるように準備する必要があります。これは、ポリモーフィズムと Visitor Pattern を使用して行うことができます。これを実現するには、適切なオブジェクトモデルを配置する必要があります。まず、すべての形状が同じインターフェースに準拠している必要があります。

_    interface IShape
{
    void DetectCollision(IShape shape);
    void Accept (ShapeVisitor visitor);
}
_

次に、親ビジタークラスが必要です。

_    abstract class ShapeVisitor
{
    protected ShapeCollisionDetector collisionDetector = new ShapeCollisionDetector();

    abstract public void VisitCircle (Circle circle);

    abstract public void VisitSquare(Square square);

    abstract public void VisitRectangle(Rectangle rectangle);

}
_

ここでは、インターフェイスの代わりにクラスを使用しています。これは、各ビジターオブジェクトにShapeCollisionDetectorタイプの属性が必要だからです。

IShapeインターフェースのすべての実装は、適切なビジターをインスタンス化し、次のように、呼び出し側オブジェクトが対話するオブジェクトの適切なAcceptメソッドを呼び出します。

_    class Circle : IShape
{
    public void DetectCollision(IShape shape)
    {
        CircleVisitor visitor = new CircleVisitor(this);
        shape.Accept(visitor);
    }

    public void Accept(ShapeVisitor visitor)
    {
        visitor.VisitCircle(this);
    }
}

    class Rectangle : IShape
{
    public void DetectCollision(IShape shape)
    {
        RectangleVisitor visitor = new RectangleVisitor(this);
        shape.Accept(visitor);
    }

    public void Accept(ShapeVisitor visitor)
    {
        visitor.VisitRectangle(this);
    }
}
_

特定の訪問者は次のようになります。

_    class CircleVisitor : ShapeVisitor
{
    private Circle Circle { get; set; }

    public CircleVisitor(Circle circle)
    {
        this.Circle = circle;
    }

    public override void VisitCircle(Circle circle)
    {
        collisionDetector.DetectCollisionCircleCircle(Circle, circle);
    }

    public override void VisitSquare(Square square)
    {
        collisionDetector.DetectCollisionCircleSquare(Circle, square);
    }

    public override void VisitRectangle(Rectangle rectangle)
    {
        collisionDetector.DetectCollisionCircleRectangle(Circle, rectangle);
    }
}

    class RectangleVisitor : ShapeVisitor
{
    private Rectangle Rectangle { get; set; }

    public RectangleVisitor(Rectangle rectangle)
    {
        this.Rectangle = rectangle;
    }

    public override void VisitCircle(Circle circle)
    {
        collisionDetector.DetectCollisionCircleRectangle(circle, Rectangle);
    }

    public override void VisitSquare(Square square)
    {
        collisionDetector.DetectCollisionSquareRectangle(square, Rectangle);
    }

    public override void VisitRectangle(Rectangle rectangle)
    {
        collisionDetector.DetectCollisionRectangleRectangle(Rectangle, rectangle);
    }
}
_

これにより、新しい形状を追加するたびに形状クラスを変更する必要がなくなり、適切な衝突検出メソッドを呼び出すために形状のタイプを確認する必要もなくなります。

このソリューションの欠点は、新しい形状を追加する場合、ShapeVisitorクラスをその形状のメソッド(例:VisitTriangle(Triangle triangle))で拡張する必要があるため、そのメソッドをすべてに実装する必要があることです。他の訪問者。ただし、これは拡張であるため、既存のメソッドは変更されず、新しいメソッドのみが追加されるという意味で、これは [〜#〜] ocp [〜#〜] に違反せず、コードオーバーヘッドは最小限です。また、クラスShapeCollisionDetectorを使用することにより、 [〜#〜] srp [〜#〜] の違反を回避し、コードの冗長性を回避します。

5
Vladimir Stokic

多分これはこの問題に取り組む最良の方法ではありません

形状の衝突を計算する数学は、形状の組み合わせに特有です。つまり、必要なサブルーチンの数は、システムがサポートする形状の数の2乗です。形状の衝突は、実際には形状に対する操作ではなく、形状をパラメーターとして使用する操作です。

オペレーターのオーバーロード戦略

基礎となる数学の問題を単純化できない場合は、演算子のオーバーロードアプローチをお勧めします。何かのようなもの:

 public final class ShapeOp 
 {
     static { ... }

     public static boolean collision( Shape s1, Shape s2 )  { ... }
     public static boolean collision( Point p1, Point p2 ) { ... }
     public static boolean collision( Point p1, Square s1 ) { ... }
     public static boolean collision( Point p1, Circle c1 ) { ... }
     public static boolean collision( Point p1, Line l1 ) { ... }
     public static boolean collision( Square s1, Point p2 ) { ... }
     public static boolean collision( Square s1, Square s2 ) { ... }
     public static boolean collision( Square s1, Circle c1 ) { ... }
     public static boolean collision( Square s1, Line l1 ) { ... }
     (...)

静的初期化子では、リフレクションを使用してメソッドのマップを作成し、一般的なCollision(Shape s1、Shape s2)メソッドに動的ディスパッチを実装します。静的初期化子には、不足している衝突関数を検出してレポートするロジックがあり、クラスのロードを拒否することもできます。

これは、C++演算子のオーバーロードに似ています。 C++では、オーバーロードできるシンボルの固定セットがあるため、オペレーターのオーバーロードは非常に混乱します。ただし、この概念は非常に興味深いものであり、静的関数で複製できます。

このアプローチを使用する理由は、衝突がオブジェクトに対する操作ではないためです。衝突は、2つの任意のオブジェクトに関する何らかの関係を示す外部操作です。また、静的イニシャライザは、いくつかの衝突機能を見逃したかどうかをチェックできます。

可能であれば、数学の問題を単純化してください

先に述べたように、衝突関数の数は、形状タイプの数の2乗です。これは、20の形状しかないシステムでは、400のルーチンが必要であり、21の形状が441であるということです。これは簡単には拡張できません。

ただし、計算を簡略化できます。衝突関数を拡張する代わりに、すべての形状をラスタライズまたは三角形分割できます。このようにして、衝突エンジンは拡張可能である必要はありません。衝突、距離、交差、マージ、その他のいくつかの機能は普遍的です。

三角化

ほとんどの3Dパッケージとゲームがすべてを三角形分割していることに気づきましたか?これは、数学を簡略化する形式の1つです。これは2D形状にも適用されます。ポリゴンは三角形分割できます。円とスプラインはポリゴンに近似できます。

繰り返しますが、衝突機能は1つです。あなたのクラスはその後になります:

public class Shape 
{
    public Triangle[] triangulate();
}

そしてあなたの操作:

public final class ShapeOp
{
    public static boolean collision( Triangle[] shape1, Triangle[] shape2 )
}

より簡単ではありませんか?

ラスタライズ

形状をラスタライズして、単一の衝突機能を持つことができます。

ラスタライズは根本的な解決策のように思えるかもしれませんが、形状の衝突をどれだけ正確に行う必要があるかに応じて、手頃な価格で高速な場合があります。 (ゲームのように)正確である必要がない場合は、低解像度のビットマップがある可能性があります。ほとんどのアプリケーションは、数学の絶対精度を必要としません。

近似で十分な場合があります。生物学シミュレーション用のANTONスーパーコンピューターはその一例です。その数学は、計算が難しい多くの量子効果を破棄し、これまでに行われたシミュレーションは、現実の世界で行われた実験と一致しています。ゲームエンジンとレンダリングパッケージで使用されるPBRコンピューターグラフィックスモデルは、各フレームのレンダリングに必要なコンピューター能力を削減する単純化を行います。実際には物理的に正確ではありませんが、肉眼で納得できるほど十分に近いです。

2
Lucas