接口作为行为的抽象基类?


14

我需要为我的C#项目设计一个类层次结构。基本上,类的功能类似于WinForms类,因此让我们以WinForms工具包为例。(但是,我不能使用WinForms或WPF。)

每个类都需要提供一些核心属性和功能。尺寸,位置,颜色,可见性(对/错),绘制方法等。

我需要设计建议我使用了一个带有抽象基类和接口的设计,这些基类和接口不是真正的类型,而更像是行为。这是一个好的设计吗?如果没有,那将是一个更好的设计。

代码如下:

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();
}

有些控件可以包含其他控件,有些只能包含(作为子控件),因此我正在考虑为这些功能创建两个接口:

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都将具有TextHolderAdapter成员,该成员将在从ITextHolder接口继承的方法内部被调用。

我认为这种设计应该使我避免在基类中必须具有许多通用功能,而这些功能会很快因虚方法和不必要的“噪声代码”而变得肿。行为的更改将通过扩展适配器而不是Control派生的类来完成。

我认为这被称为“策略”模式,尽管有关该主题有数百万个问题和答案,但我想请您提出意见,以了解我对此设计的考虑以及在设计中您会想到哪些缺陷。我的方法。

我还要补充一点,未来的需求几乎有100%的机会会要求新的类和新的功能。


为什么不直接继承System.ComponentModel.Component或继承System.Windows.Forms.Control任何其他现有的基类呢?为什么需要创建自己的控件层次结构并从头开始再次定义所有这些功能?
科迪·格雷

3
IChild看起来像一个可怕的名字。
雷诺斯2011年

@Cody:首先,我不能使用WinForms或WPF程序集,其次-来吧,这只是我所询问的设计问题的一个示例。如果您认为形状或动物更容易。我的类需要表现得与WinForms控件类似,但并非在所有方面都完全一样,也不完全像它们一样
grapkulec 2011年

2
不能使用WinForms或WPF程序集并没有多大意义,但是您可以使用要创建的任何自定义控件框架。这是重新发明轮子的一个严重案例,我无法想象有什么可能的目的。但是,如果您绝对必须这样做,为什么不仅仅遵循WinForms控件的示例呢?这使这个问题几乎过时了。
科迪·格雷

1
@graphkulec这就是为什么发表评论的原因:)如果我有实际有用的反馈可以回答您的问题。它仍然是一个可怕的名字;)
Raynos 2011年

Answers:


5

[1] 在您的属性中添加虚拟的 “ getters”和“ 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

这意味着控件可以显示信息,但不能由用户更改。


尽管我不喜欢基类中的所有这些虚拟方法,但我会在考虑设计时给它们第二次机会。并且您提醒我,我确实需要IsReadOnly属性:)
grapkulec 2011年

@grapkulec因为它是基类,所以必须指定派生类将具有这些属性,但是,控制行为的方法可以根据每个类的目的而改变;-)
umlcat 2011年

mmkey,我明白你的意思,谢谢你花时间回答
grapkulec 2011年

1
我接受这个答案是因为一旦开始使用接口实现我的“酷”设计,我很快就会发现自己需要虚拟设置器,有时也需要使用getter。这并不意味着我完全不使用接口,我只是将它们的使用限制在真正需要它们的地方。
grapkulec 2011年

3

好问题!我注意到的第一件事是draw方法没有任何参数,它在哪里绘制?我认为它应该接收一些Surface或Graphics对象作为参数(当然是接口)。

我发现奇怪的另一件事是IContainer接口。它有GetChild一个返回单个子对象且没有参数的方法-也许它应该返回一个集合,或者如果您将Draw方法移到一个接口,则它可以实现此接口,并具有一个可以绘制其内部子对象而不暴露它的draw方法。 。

也许您也可以考虑WPF的想法-我认为这是一个设计良好的框架。您也可以查看“合成”和“装饰器”设计模式。第一个可用于容器元素,第二个可用于向元素添加功能。我无法说一看效果如何,但是我的想法是:

为了避免重复,您可以使用诸如原始元素之类的东西-如text元素。然后,您可以使用这些原语构建更复杂的元素。例如,a Border是具有单个子元素的元素。调用其Draw方法时,它将绘制边框和背景,然后在边框内绘制其子级。A TextElement仅绘制一些文本。现在,如果您想要一个按钮,则可以通过组合这两个基元来构建一个按钮,即将文本放入边框内。在这里,边界就像装饰器一样。

这篇文章的篇幅越来越长,所以如果您觉得这个想法有趣,请告诉我,我可以举一些例子或更多的解释。


1
缺少参数不是问题,毕竟这只是真实方法的草图。关于WPF,我已经根据他们的想法设计了布局系统,所以我认为我已经准备好了组成部分,对于装饰工,我必须考虑一下。您对原始元素的想法听起来很合理,我绝对可以尝试一下。谢谢!
grapkulec

@grapkulec不客气!关于参数-是的,没关系,我只是想确保我正确理解了您的意图。我很高兴您喜欢这个主意。祝好运!

2

删除代码,然后转到问题的核心:“我的抽象基类和接口设计不是类型,而是行为是好的设计。”,我想说这种方法没有错。

实际上,我已经使用了这种方法(在我的渲染引擎中),其中每个接口都定义了消耗子系统所期望的行为。例如,IUpdateable,ICollidable,IRenderable,ILoadable,ILoader等。为了清楚起见,我还将这些“行为”中的每一个都划分为单独的部分类,例如“ Entity.IUpdateable.cs”,“ Entity.IRenderable.cs”,并尝试保持字段和方法尽可能独立。

使用接口定义行为模式的方法也与我一致,因为泛型,约束和协变量类型参数可以很好地协同工作。

我观察到的关于这种设计方法的一件事是:如果您发现自己定义了不只几种方法的接口,那么您可能就没有实现行为。


您是否遇到任何问题,或者在任何时候后悔了这种设计,并且不得不与自己的代码抗争才能真正完成预期的工作?例如突然发现您需要创建如此多的行为实现,以至于它没有任何意义,并且希望您只是坚持旧的主题“用数百万个虚函数创建基类,并继承并覆盖其中”。
grapkulec
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.