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


14

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%あることを付け加えます。


なぜ、System.ComponentModel.ComponentまたはSystem.Windows.Forms.Control他の既存の基本クラスから単純に継承しないのですか?独自のコントロール階層を作成し、これらすべての機能をゼロから再度定義する必要があるのはなぜですか?
コーディグレー

3
IChild恐ろしい名前のようです。
レイノス

@Cody:最初に、WinFormsまたはWPFアセンブリを使用できません。2番目に-これは、私が尋ねている設計上の問題の一例です。形や動物を考えるのが楽なら。私のクラスは、あらゆる方法ではなく、正確に彼らのようなWinFormsのコントロールに似ていない行動する必要がある
grapkulec

2
WinFormsまたはWPFアセンブリを使用できないと言うのはあまり意味がありませんが、作成するカスタムコントロールフレームワークはすべて使用できます。これは、車輪を再発明するという深刻なケースであり、どのような目的が考えられるか想像できません。しかし、どうしても必要な場合は、WinFormsコントロールのに従ってください。そのため、この質問はほとんど廃止されました。
コーディグレー

1
@graphkulecだからコメントです:)実際に役立つフィードバックがあれば、あなたの質問に答えていただろう。まだ恐ろしい名前です;)
レイノス

回答:


5

[1] プロパティに仮想の「ゲッター」と「セッター」を追加します。この機能が必要なため、別のコントロールライブラリをハッキングする必要がありました。

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

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


基本クラスのこれらすべての仮想メソッドが好きではありませんが、デザインについての私の考えに2度目のチャンスを与えます。そして、あなたは私が本当にIsReadOnlyプロパティを必要とすることを私に思い出させました:)
grapkulec

@grapkulecそれは基本クラスであるため、派生クラスにこれらのプロパティを持たせることを指定する必要がありますが、動作を制御するメソッドはクラスの目的ごとに変更できるようになります;
umlcat

mmkey、私はあなたのポイントを見て、答えてくれてありがとう
-grapkulec

1
これを答えとして受け入れました。インターフェイスを使用して「クール」なデザインを実装し始めると、すぐに仮想セッターと場合によってはゲッターも必要になったからです。それは、インターフェイスをまったく使用しないという意味ではありません。インターフェイスの使用を本当に必要な場所に限定しただけです。
grapkulec

3

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

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

アイデアについては、WPFもご覧ください-私の意見では、非常によく設計されたフレームワークです。また、コンポジションおよびデコレータのデザインパターンを確認することもできます。1つ目はコンテナ要素に役立ち、2つ目は要素に機能を追加するのに役立ちます。私はそれが最初の一見でどれほどうまくいくかを言うことはできませんが、ここに私が思うことです:

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

投稿はかなり長くなっていますので、そのアイデアがおもしろいと思ったら教えてください。いくつかの例やより多くの説明があります。


1
paramsの欠落は問題ではなく、結局は実際のメソッドのスケッチにすぎません。WPFについては、彼らのアイデアに基づいてレイアウトシステムを設計したので、デコレータについては考えなければならないので、すでに構成部分が整っていると思います。原始的な要素のあなたのアイデアは合理的に聞こえます、私は間違いなくそれを試してみます。ありがとう!
grapkulec

@grapkulecどういたしまして!パラメータについて-はい、大丈夫です。あなたの意図を正確に理解したかっただけです。あなたがそのアイデアを気に入ってくれてうれしいです。幸運を!

2

コードを切り取り、あなたの質問の核心に行く「私のデザインは抽象基底クラスとインターフェースであり、型としてではなく、振る舞いとしてのデザインが良いかどうか」。そのアプローチには何の問題もないと思います。

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

インターフェースを使用して動作パターンを定義するアプローチも、ジェネリック、制約、co(ntra)variant型パラメーターが非常にうまく機能するため、私も同意します。

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


問題に遭遇したか、そのような設計を後悔していて、意図したことを実際に達成するために独自のコードと戦わなければならなかったのですか?たとえば、突然、意味のない多くのビヘイビアー実装を作成する必要があり、古いテーマ「100万個の仮想マシンで基本クラスを作成し、それから継承してオーバーライドする」だけにしたいのです。
grapkulec
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.