web-dev-qa-db-ja.com

動作としてインターフェースを持つ抽象基本クラス?

C#プロジェクトのクラス階層を設計する必要があります。基本的に、クラスの機能はWinFormsクラスに似ているので、WinFormsツールキットを例に取ってみましょう。 (ただし、WinFormsやWPFは使用できません。)

すべてのクラスが提供する必要のあるいくつかのコアプロパティと機能があります。寸法、位置、色、可視性(true/false)、Drawメソッドなど.

設計のアドバイスが必要です。実際には型ではなく、動作のような抽象基本クラスとインターフェイスを備えた設計を使用しました。これは良いデザインですか?そうでなければ、何がより良いデザインになるでしょう。

コードは次のようになります。

abstract class Control
{
    public int Width { get; set; }

    public int Height { get; set; }

    public int BackColor { get; set; }

    public int X { get; set; }

    public int Y { get; set; }

    public int BorderWidth { get; set; }

    public int BorderColor { get; set; }

    public bool Visible { get; set; }

    public Rectangle ClipRectange { get; protected set; }

    abstract public void Draw();
}

一部のコントロールには他のコントロールを含めることができ、一部は(子として)のみ含めることができるため、これらの機能のために2つのインターフェイスを作成することを考えています。

interface IChild
{
    IContainer Parent { get; set; }
}

internal interface IContainer
{
    void AddChild<T>(T child) where T : IChild;
    void RemoveChild<T>(T child) where T : IChild;
    IChild GetChild(int index);
}

WinFormsコントロールはテキストを表示するため、これもインターフェイスに入ります。

interface ITextHolder
{
    string Text { get; set; }
    int TextPositionX { get; set; }
    int TextPositionY { get; set; }
    int TextWidth { get; }
    int TextHeight { get; }

    void DrawText();
}

一部のコントロールは、親コントロール内にドッキングできます。

enum Docking
{ 
    None, Left, Right, Top, Bottom, Fill
}

interface IDockable
{
    Docking Dock { get; set; }
}

...そして具体的なクラスをいくつか作成しましょう:

class Panel : Control, IDockable, IContainer, IChild {}
class Label : Control, IDockable, IChild, ITextHolder {}
class Button : Control, IChild, ITextHolder, IDockable {}
class Window : Control, IContainer, IDockable {}

私がここで考えることができる最初の「問題」は、インターフェースが公開されると、基本的には固定されたものになるということです。しかし、将来的にインターフェースを変更する必要がないように、インターフェースを十分に改善できると想定しましょう。

この設計で私が目にする別の問題は、これらのクラスのすべてがそのインターフェースを実装する必要があり、コードの複製がすぐに発生することです。たとえば、LabelおよびButtonでは、DrawText()メソッドはITextHolderインターフェイスから派生するか、または子のIContainer管理から派生するすべてのクラスで派生します。

この問題に対する私の解決策は、この「重複した」機能を専用アダプターに実装し、呼び出しをそれらに転送することです。したがって、LabelとButtonの両方に、ITextHolderインターフェイスから継承されたメソッド内で呼び出されるTextHolderAdapterメンバーがあります。

この設計は、仮想メソッドと不要な「ノイズコード」ですぐに肥大化する可能性がある基本クラスの多くの一般的な機能を必要としないように私を保護すると思います。動作の変更は、Controlから派生したクラスではなく、アダプタを拡張することによって行われます。

これは「ストラテジー」パターンと呼ばれていると思います。このトピックについては何百万もの質問と回答がありますが、このデザインで私が考慮していること、および考えられる欠陥について、ご意見をお聞かせください。私のアプローチ。

さらに、将来の要件で新しいクラスと新しい機能が必要になる可能性がほぼ100%あることも付け加えておきます。

14
grapkulec

[1] virtual "getter"& "setters"をプロパティに追加します。この機能が必要なため、別のコントロールライブラリをハックする必要がありました。

abstract class Control
{
    // internal fields for properties
    protected int _Width;
    protected int _Height;
    protected int _BackColor;
    protected int _X;
    protected int _Y;
    protected bool Visible;

    protected int BorderWidth;
    protected int BorderColor;


    // getters & setters virtual !!!

    public virtual int getWidth(...) { ... }
    public virtual void setWidth(...) { ... }

    public virtual int getHeight(...) { ... }
    public virtual void setHeight(...) { ... }

    public virtual int getBackColor(...) { ... }
    public virtual void setBackColor(...) { ... }

    public virtual int getX(...) { ... }
    public virtual void setX(...) { ... }

    public virtual int getY(...) { ... }
    public virtual void setY(...) { ... }

    public virtual int getBorderWidth(...) { ... }
    public virtual void setBorderWidth(...) { ... }

    public virtual int getBorderColor(...) { ... }
    public virtual void setBorderColor(...) { ... }

    public virtual bool getVisible(...) { ... }
    public virtual void setVisible(...) { ... }

    // properties WITH virtual getters & setters

    public int Width { get getWidth(); set setWidth(value); }

    public int Height { get getHeight(); set setHeight(value); }

    public int BackColor { get getBackColor(); set setBackColor(value); }

    public int X { get getX(); set setX(value); }

    public int Y { get getY(); set setY(value); }

    public int BorderWidth { get getBorderWidth(); set setBorderWidth(value); }

    public int BorderColor { get getBorderColor(); set setBorderColor(value); }

    public bool Visible { get getVisible(); set setVisible(value); }

    // other methods

    public Rectangle ClipRectange { get; protected set; }   
    abstract public void Draw();
} // class Control

/* concrete */ class MyControl: Control
{
    public override bool getVisible(...) { ... }
    public override void setVisible(...) { ... }
} // class MyControl: Control

この提案はより「冗長」または複雑ですが、実際には非常に役立ちます。

[2]「IsEnabled」プロパティを追加します。「IsReadOnly」と混同しないでください。

abstract class Control
{
    // internal fields for properties
    protected bool _IsEnabled;

    public virtual bool getIsEnabled(...) { ... }
    public virtual void setIsEnabled(...) { ... }

    public bool IsEnabled{ get getIsEnabled(); set setIsEnabled(value); }
} // class Control

コントロールを表示する必要があるが、情報は表示しないことを意味します。

[3]「IsReadOnly」プロパティを追加します。「IsEnabled」と混同しないでください。

abstract class Control
{
    // internal fields for properties
    protected bool _IsReadOnly;

    public virtual bool getIsReadOnly(...) { ... }
    public virtual void setIsReadOnly(...) { ... }

    public bool IsReadOnly{ get getIsReadOnly(); set setIsReadOnly(value); }
} // class Control

つまり、コントロールは情報を表示できますが、ユーザーが変更することはできません。

5
umlcat

いい質問だ!最初に気づいたのは、drawメソッドにはパラメーターがないことです。どこに描画するのですか。 (もちろんインターフェイスとして)パラメータとしてSurfaceまたはGraphicsオブジェクトを受け取る必要があると思います。

私が奇妙に思うもう1つのことは、IContainerインターフェースです。それは単一の子を返し、パラメーターを持たないGetChildメソッドを持っています-おそらくそれはコレクションを返す必要があるか、またはDrawメソッドをインターフェイスに移動した場合、このインターフェイスを実装し、そのメソッドを描画できるdrawメソッドを持つことができますそれを公開せずに内部の子供たちのコレクション。

多分あなたもWPFで見ることができるアイデアのために-それは私の意見では非常によく設計されたフレームワークです。また、コンポジションとデコレータのデザインパターンを確認することもできます。 1つ目はコンテナ要素に、2つ目は要素に機能を追加するのに役立ちます。一見したところうまくいくとは言えませんが、これが私が思うことです。

重複を避けるために、プリミティブ要素のようなもの-テキスト要素のようなものを持つことができます。次に、これらのプリミティブを使用して、より複雑な要素を構築できます。たとえば、Borderは単一の子を持つ要素です。 Drawメソッドを呼び出すと、ボーダーと背景が描画され、ボーダー内に子が描画されます。 TextElementは一部のテキストのみを描画します。ボタンが必要な場合は、これらの2つのプリミティブを作成することによって1つを構築できます。つまり、テキストをボーダー内に配置します。ここでボーダーはデコレーターのようなものです。

投稿が長くなりすぎているので、そのアイデアが興味深いと思われる場合はお知らせください。いくつかの例や詳細を説明できます。

3
Georgi Stoyanov

コードを切り取って、「抽象的な基本クラスとインターフェイスを備えたデザインはタイプではなく、ビヘイビアーが優れたデザインであるかどうか」という疑問の核心に立ち向かいます。そのアプローチには何も問題がないと思います。

実際、このようなアプローチを(レンダリングエンジンで)使用しました。各インターフェースは、消費するサブシステムが期待する動作を定義しました。たとえば、IUpdateable、ICollidable、IRenderable、ILoadable、ILoaderなどです。また、わかりやすくするために、これらの「動作」をそれぞれ「Entity.IUpdateable.cs」、「Entity.IRenderable.cs」などの個別の部分クラスに分離し、フィールドとメソッドをできるだけ独立させるようにしました。

ジェネリック、制約、およびco(ntra)variant型パラメーターが非常にうまく連携するため、動作パターンを定義するためにインターフェイスを使用するアプローチも私と同じです。

この設計方法について私が観察したことの1つは、少数のメソッドでインターフェイスを定義していることに気付いた場合、おそらく動作を実装していないことです。

2
Ani