Абстрактный базовый класс с интерфейсами как поведением?

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

Некоторые элементы управления могут содержать другие элементы управления, некоторые могут содержаться только (как дочерние элементы), поэтому я подумываю создать два интерфейса для этих функций:

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 {}

Первая «проблема», о которой я могу подумать, заключается в том, что интерфейсы в основном устанавливаются в камне после их публикации. Но давайте предположим, что я смогу сделать свои интерфейсы достаточно хорошими, чтобы избежать необходимости вносить в них изменения в будущем.

Другая проблема, которую я вижу в этом проекте, заключается в том, что каждый из этих классов должен будет реализовывать свои интерфейсы, и дублирование кода будет происходить быстро. Например, в метках и кнопках метод DrawText () происходит из интерфейса ITextHolder или в каждом классе, полученном из управления дочерними элементами IContainer.

Мое решение этой проблемы состоит в том, чтобы реализовать эти «дублированные» функции в выделенных адаптерах и переадресовывать вызовы к ним. Таким образом, и Label, и Button будут иметь элемент TextHolderAdapter, который будет вызываться внутри методов, унаследованных от интерфейса ITextHolder.

Я думаю, что этот дизайн должен оградить меня от необходимости иметь много общих функций в базовом классе, которые могут быстро раздуться с помощью виртуальных методов и ненужного «шумового кода». Изменения в поведении будут реализованы за счет расширения адаптеров, а не производных от Control классов.

Я думаю, что это называется «Стратегия», и, хотя на эту тему существуют миллионы вопросов и ответов, я хотел бы спросить вас о вашем мнении относительно того, что я принимаю во внимание для этого дизайна, и какие недостатки вы можете вспомнить в мой подход.

Я должен добавить, что есть почти 100% вероятность того, что будущие требования потребуют новых классов и новых функциональных возможностей.

grapkulec
источник
Почему бы не просто наследовать от System.ComponentModel.Componentили , System.Windows.Forms.Controlили любой из других существующих базовых классов? Зачем вам нужно создавать собственную иерархию управления и заново определять все эти функции с нуля?
Коди Грей
3
IChildкажется ужасным именем.
Райнос
@Cody: во-первых, я не могу использовать сборки WinForms или WPF, во-вторых, давай, это всего лишь пример проблемы дизайна, о которой я спрашиваю. Если вам легче думать о фигурах или животных. мои классы должны вести себя подобно элементам управления WinForms, но не во всех отношениях и не совсем так, как они
grapkulec
2
Нет смысла говорить, что вы не можете использовать сборки WinForms или WPF, но вы сможете использовать любую пользовательскую платформу управления, которую вы создали. Это серьезный случай переизобретения колеса, и я не представляю, для чего это возможно. Но если вам абсолютно необходимо, почему бы просто не последовать примеру элементов управления WinForms? Это делает этот вопрос в значительной степени устаревшим.
Коди Грей
1
@graphkulec, поэтому это комментарий :) Если бы у меня была полезная обратная связь, я бы ответил на ваш вопрос. Это все еще ужасное имя;)
Raynos

Ответы:

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

Это означает, что элемент управления может отображать информацию, но не может быть изменен пользователем.

umlcat
источник
хотя мне не нравятся все эти виртуальные методы в базовом классе, я дам им второй шанс в моих мыслях о дизайне. и вы напомнили мне, что мне действительно нужно свойство IsReadOnly :)
grapkulec
@grapkulec Поскольку это базовый класс, вы должны указать, что производные классы будут иметь эти свойства, но методы, управляющие поведением, могут быть изменены для каждого класса ;-)
umlcat
ммки, я понимаю вашу точку зрения и спасибо, что
нашли
1
Я принял это как ответ, потому что, как только я начал реализовывать свой «крутой» дизайн с интерфейсами, мне быстро потребовались виртуальные сеттеры, а иногда и геттеры. Это не значит, что я вообще не использую интерфейсы, я ограничил их использование местами, где они действительно необходимы.
grapkulec
3

Хороший вопрос! Первое, что я заметил, это то, что метод draw не имеет параметров, где он рисует? Я думаю, что он должен получить некоторый объект Surface или Graphics в качестве параметра (как, конечно, интерфейс).

Еще одна вещь, которую я нахожу странным, - это интерфейс IContainer. У него есть GetChildметод, который возвращает один дочерний элемент и не имеет параметров - может быть, он должен вернуть коллекцию или, если вы переместите метод Draw в интерфейс, он может реализовать этот интерфейс и иметь метод draw, который может рисовать свою внутреннюю дочернюю коллекцию, не подвергая ее ,

Может быть, для идей вы могли бы взглянуть и на WPF - это, на мой взгляд, очень хорошо продуманный фреймворк. Также вы можете взглянуть на шаблоны дизайна Composition и Decorator. Первый может быть полезен для элементов контейнера, а второй - для добавления функциональности к элементам. Я не могу сказать, насколько хорошо это будет работать на первый взгляд, но вот что я думаю:

Чтобы избежать дублирования, у вас может быть что-то вроде примитивных элементов - например, текстового элемента. Затем вы можете создавать более сложные элементы, используя эти примитивы. Например, a Borderявляется элементом, у которого есть единственный дочерний элемент. Когда вы вызываете его Drawметод, он рисует границу и фон, а затем рисует своего потомка внутри границы. А TextElementтолько рисует какой-то текст. Теперь, если вам нужна кнопка, вы можете создать ее, составив эти два примитива, то есть поместить текст в рамку. Здесь Граница - это что-то вроде декоратора.

Пост становится довольно длинным, поэтому дайте мне знать, если вы найдете эту идею интересной, и я могу привести несколько примеров или больше объяснений.

Георги Стоянов
источник
1
Отсутствие параметров не является проблемой, это всего лишь набросок реальных методов. Что касается WPF, я разработал систему верстки, основанную на их идее, поэтому я думаю, что у меня уже есть составная часть, что касается декораторов, мне придется подумать об этом. Ваше представление о примитивных элементах звучит разумно, и я обязательно попробую. Благодарность!
grapkulec
@grapkulec Добро пожаловать! Насчет параметров - да, все в порядке, я просто хотел убедиться, что правильно понимаю ваши намерения. Я рад, что вам нравится идея. Удачи!
2

Вырезав код и перейдя к сути вашего вопроса «Является ли мой дизайн абстрактным базовым классом и интерфейсами не как тип, а скорее как поведение, это хороший дизайн или нет», я бы сказал, что в этом подходе нет ничего плохого.

На самом деле я использовал такой подход (в моем механизме рендеринга), где каждый интерфейс определял поведение ожидаемой подсистемы потребления. Например, IUpdateable, ICollidable, IRenderable, ILoadable, ILoader и так далее и так далее. Для ясности я также разделил каждое из этих «поведений» на отдельные частичные классы, такие как «Entity.IUpdateable.cs», «Entity.IRenderable.cs», и попытался сохранить поля и методы как можно более независимыми.

Подход с использованием интерфейсов для определения поведенческих паттернов также согласуется со мной, потому что обобщенные параметры, ограничения и параметры варианта типа (ntra) работают очень хорошо вместе.

Одна из вещей, которые я заметил об этом методе проектирования, была: если вы когда-нибудь определяете интерфейс с помощью нескольких методов, вы, вероятно, не реализуете поведение.

Ани
источник
Вы сталкивались с какими-либо проблемами или в какой-то момент вы сожалели о таком дизайне и должны были бороться со своим собственным кодом, чтобы на самом деле добиться поставленных целей? например, внезапно оказалось, что вам нужно создать так много реализаций поведения, что это просто не имело смысла, и вы хотели, чтобы вы просто придерживались старой темы «создайте базовый класс с миллионами виртуалов и просто унаследуйте и переопределите чертовски из него»?
grapkulec