Параллельные иерархии - частично одинаковые, частично разные

12

Есть довольно много подобных вопросов там 1 ,2 ,3 ,4 , но в этом вопросе, похоже, не совсем так, и решения не кажутся оптимальными.

Это общий вопрос ООП, предполагая, что полиморфизм, дженерики и миксины доступны. Фактический язык, который нужно использовать - это OOP Javascript (Typescript), но это та же проблема в Java или C ++.

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

3 параллельные иерархии классов, центральный столбец показывает общие части, левый столбец - иерархия холста, а правый столбец - иерархия SVG.

Это только для иллюстрации ; это не фактическая диаграмма классов. Чтобы прочитать это:

  • Все, что находится в общей иерархии (в центре), является общим для обеих иерархий Canvas (слева) и SVG (справа). Под долей я имею в виду как интерфейс, так и реализацию.
  • Что-либо только в левом или правом столбцах означает поведение (методы и члены), специфичное для этой иерархии. Например:
    • И левая, и правая иерархии используют абсолютно одинаковые механизмы проверки, показанные как один метод ( Viewee.validate()) в общей иерархии.
    • Только у иерархии холста есть метод paint(). Этот метод вызывает метод рисования для всех дочерних элементов.
    • Иерархия SVG должна переопределить addChild()метод Composite, но это не относится к иерархии холста.
  • Конструкции из двух боковых иерархий не могут быть смешаны. Фабрика гарантирует это.

Решение I - Дразнить Помимо Наследования

Наследование Фаулера, кроме Наследования, похоже, здесь не работает, потому что между двумя параллелями есть некоторое расхождение.

Решение II - Mixins

Это единственное, о чем я сейчас могу думать. Две иерархии разрабатываются отдельно, но на каждом уровне классы смешиваются в общий класс, который не является частью иерархии классов. Опуская structuralвилку, будет выглядеть так:

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

Обратите внимание, что каждый столбец будет в своем собственном пространстве имен, поэтому имена классов не будут конфликтовать.

Вопрос

Кто-нибудь может увидеть недостатки этого подхода? Кто-нибудь может придумать лучшее решение?


добавление

Вот пример кода, как это будет использоваться. Пространство имен svgможет быть заменено на canvas:

var iView        = document.getElementById( 'view' ),
    iKandinsky   = new svg.Kandinsky(),
    iEpigone     = new svg.Epigone(),
    iTonyBlair   = new svg.TonyBlair( iView, iKandinsky ),
    iLayer       = new svg.Layer(),
    iZoomer      = new svg.Zoomer(),
    iFace        = new svg.Rectangle( new Rect( 20, 20, 100, 60) ),
    iEyeL        = new svg.Rectangle( new Rect( 20, 20, 20, 20) ),
    iEyeR        = new svg.Rectangle( new Rect( 60, 20, 20, 20) );

iKandinsky.setContext( iTonyBlair.canvas.getContext( '2d' ) );
iEpigone.setContext( iTonyBlair.canvas.getContext( '2d' ) );

iFace.addChildren( iEyeL, iEyeR );
iZoomer.setZoom( new Point( 2, 2 ) );
iZoomer.addChild( iFace );
iLayer.addChild( iZoomer );
iTonyBlair.setContent( iLayer );

По сути, во время выполнения клиенты составляют иерархию экземпляров подклассов Viewee; вот так:

Изображение, показывающее иерархию объектов, таких как слой, прямоугольник, скроллер и т. Д.

Скажем, все эти зрители взяты из иерархии холста, они отображаются путем обхода иерархии, вызывающей paint()каждого зрителя. Если они из иерархии svg, пользователи знают, как добавить себя в DOM, но paint()обхода нет.

Izhaki
источник
Рекомендуемое чтение: обзор дизайна: по теме или нет?
Роберт Харви
Может быть, попробовать полнофункциональный шаблон проектирования Decorator (Erich Gamma et als, Design Patterns)?
Zon
Что за зритель? Что означает «параллель» как существительное (в отличие от прилагательного)?
Тулаинс Кордова
У вас есть множественное наследование?
Тулаинс Кордова
Содержат ли классы Canvas или SVG дополнительное состояние или данные, которые не являются общими? Как вы используете классы? Можете ли вы показать пример кода, показывающего, как можно использовать эти приветственные слова?
Эйфорическая

Ответы:

5

Второй подход лучше разделяет интерфейсы, следуя принципу разделения интерфейсов.

Однако я бы добавил интерфейс Paintable.

Я также изменил бы некоторые имена. Не нужно создавать путаницу:

// common

public interface IComposite {
    public void addChild(Composite e);
}

public interface IViewee extends IComposite{
    public void validate();
    public List<IBound> getAbsoluteBouns();
}

public interface IVisual {
    public List<IBound> getBounds();
}

public interface IRec {
}

public interface IPaintable {
    public void paint();
}

// canvas

public interface ICanvasViewee extends IViewee, IPaintable {
}

public interface ICanvasVisual extends IViewee, IVisual {
}

public interface ICanvasRect extends ICanvasVisual, IRec {
}


// SVG

public interface ISVGViewee extends IViewee {
    public void element();
}

public interface ISVGVisual extends IVisual, ISVGViewee {
}

public interface ISVGRect extends ISVGVisual, IRect {
}
Тулаинс Кордова
источник
Я думаю, что интерфейсы могут помочь в этом случае. Я хотел бы знать причины, по которым ваш ответ был отклонен.
umlcat
не downvoter, но интерфейсы ИМХО экспоненциальных не хороший шаблон
dagnelies
@arnaud что вы подразумеваете под "экспоненциальными интерфейсами"?
Тулаинс Кордова
@ user61852 ... ну, скажем так, много интерфейсов. «экспоненциальный» был на самом деле неправильный термин, это больше похоже на «мультипликативный». В том смысле, что если бы у вас было больше «граней» (составных, визуальных, рисуемых ...) и больше «элементов» (canvas, svg ...), у вас получилось бы много интерфейсов.
dagnelies
@arnaud У вас есть точка зрения, но, по крайней мере, есть гибкая конструкция заранее, и кошмар наследования OP будет решен, когда вы не будете вынуждены расширяться. Вы расширяете некоторый класс, если хотите, а не по искусственной иерархии.
Тулаинс Кордова
3

Это общий вопрос ООП, предполагая, что полиморфизм, дженерики и миксины доступны. Фактический язык, который нужно использовать - это OOP Javascript (Typescript), но это та же проблема в Java или C ++.

На самом деле это совсем не так. Typescript имеет существенное преимущество перед Java, а именно структурную типизацию. Вы можете сделать что-то похожее в C ++ с шаблонами, напечатанными по типу утки, но это требует гораздо больших усилий.

По сути, определяйте свои классы, но не беспокойтесь о расширении или определении каких-либо интерфейсов. Затем просто определите необходимый интерфейс и примите его в качестве параметра. Затем объекты могут соответствовать этому интерфейсу - им не нужно знать, чтобы расширить его заранее. Каждая функция может объявить точно и только те биты, о которых они насмехаются, и компилятор даст вам пропуск, если последний тип встретит его, даже если классы на самом деле не расширяют эти интерфейсы явно.

Это освобождает вас от необходимости фактически определять иерархию интерфейса и определять, какие классы должны расширять какие интерфейсы.

Просто определите каждый класс и забудьте об интерфейсах - структурная типизация позаботится об этом.

Например:

class SVGViewee {
    validate() { /* stuff */ }
    addChild(svg: SVG) { /* stuff */ }
}
class CanvasViewee {
    validate() { /* stuff */ }
    paint() { /* stuff */ }
}
interface SVG {
    addChild: { (svg: SVG): void };
}
f(viewee: { validate: { (): boolean }; }) {
    viewee.validate();
}
g(svg: SVG) {
    svg.addChild(svg);
}
h(canvas: { paint: { (): void }; }) {
    canvas.paint();
}
f(SVGViewee());
f(CanvasViewee());
g(SVGViewee());
h(CanvasViewee());

Это абсолютно законный Typescript. Обратите внимание, что функции-потребители не знают и не дают ни малейшего представления о базовых классах или интерфейсах, используемых в определении классов.

Неважно, связаны ли классы по наследству. Не имеет значения, расширили ли они ваш интерфейс. Просто определите интерфейс как параметр, и все готово - все классы, которые ему соответствуют, принимаются.

DeadMG
источник
Это звучит многообещающе, но я не совсем понимаю предложение (извините, возможно, слишком много предвзятости ООП). Возможно, вы можете поделиться примером кода? Например, как svg.Viewee, так и canvas.Viewee нужен validate()метод (реализация которого идентична для обоих); тогда только svg.Viewee необходимо переопределить addChild () , тогда как только canvas. Viewee нуждается в paint () (который вызывает paint () для всех дочерних элементов - которые являются членами базового класса Composite ). Так что я не могу визуализировать это с помощью структурной типизации.
Изаки
Вы рассматриваете целый ряд вещей, которые просто не имеют значения в этом случае.
DeadMG
Так что я, вероятно, не получил ответа вообще. Было бы неплохо, если вы уточните.
Изаки
Я сделал правку. Суть в том, что базовые классы абсолютно не имеют значения, и никто не заботится о них. Они только детали реализации.
DeadMG
1
ХОРОШО. Это начинает иметь смысл. А) Я знаю, что такое утка. Б) Я не уверен, почему интерфейсы так важны в этом ответе - разбивка класса для общего поведения, вы можете спокойно пока игнорировать интерфейсы. C) Скажите, что в вашем примере у вас есть SVGViewee.addChild(), но CanvasVieweeтакже нужна точно такая же функция. Так что, кажется, имеет смысл, что оба присущи из Composite?
Изаки
3

Краткая информация

Решение 3: Шаблон проектирования программного обеспечения "Параллельная классовая иерархия" - ваш друг.

Длинный расширенный ответ

Ваш дизайн НАЧИНАЕТСЯ ПРАВО. Он может быть оптимизирован, некоторые классы или члены могут быть удалены, но идея «параллельной иерархии», которую вы применяете для решения проблемы, ПРАВИЛЬНА.

Имейте дело с той же концепцией несколько раз, обычно в иерархиях управления.

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

(1) Вы когда-нибудь разделяли один класс на единую иерархию классов?

(2) Вы когда-нибудь разделяли один класс на несколько классов без иерархии?

Если вы применили эти предыдущие решения отдельно, то это способ решить некоторые проблемы.

Но что, если мы объединим эти два решения одновременно?

Объедините их, и вы получите этот «шаблон дизайна».

Реализация

Теперь давайте применим шаблон проектирования программного обеспечения «Параллельная классовая иерархия» в вашем случае.

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

Вы хотите избежать дублирования кода или членов («непротиворечивость»), но вы не можете объединить эти классы непосредственно в один из-за различий между ними.

Итак, ваши иерархии очень похожи на этот рисунок, но, тем не менее, их более одного:

................................................
...............+----------------+...............
...............|     Common::   |...............
...............|    Composite   |...............
...............+----------------+...............
...............|      ...       |...............
...............+-------+--------+...............
.......................|........................
.......................^........................
....................../.\.......................
.....................+-+-+......................
.......................|........................
...............+-------+--------+...............
...............|     Common::   |...............
...............|     Viewee     |...............
...............+----------------+...............
...............|      ...       |...............
...............+-------+--------+...............
.......................|........................
.......................^........................
....................../.\.......................
.....................+-+-+......................
.......................|........................
..........+------------+------------+...........
..........|.........................|...........
..+-------+--------+........+-------+--------+..
..|     Common::   |........|     Common::   |..
..|     Visual     |........|   Structural   |..
..+----------------+........+----------------+..
..|      ...       |........|      ...       |..
..+----------------+........+----------------+..
................................................

Figure 1

В этом, еще не сертифицированном, шаблон проектирования, несколько одинаковых иерархий, объединены в одну иерархию, и каждый общий или общий класс расширяется подклассами.

Обратите внимание, что это решение является сложным, поскольку вы уже имеете дело с несколькими иерархиями, поэтому это сложный сценарий.

1 Корневой класс

В каждой иерархии есть общий «корневой» класс.

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

Некоторые из этих участников могут быть объединены, некоторые из этих участников не могут быть объединены.

Итак, что может сделать разработчик, так это создать базовый корневой класс и создать подкласс для эквивалентного случая для каждой иерархии.

На рисунке 2 вы видите диаграмму только для этого класса, в которой каждый класс хранит свое пространство имен.

Члены, опущены, к настоящему времени.

................................................
...............+-------+--------+...............
...............|     Common::   |...............
...............|    Composite   |...............
...............+----------------+...............
...............|      ...       |...............
...............+-------+--------+...............
.......................|........................
.......................^........................
....................../.\.......................
.....................+-+-+......................
.......................|........................
..........+------------+------------+...........
..........|.........................|...........
..+-------+--------+........+-------+--------+..
..|     Canvas::   |........|      SVG::     |..
..|    Composite   |........|    Composite   |..
..+----------------+........+----------------+..
..|      ...       |........|      ...       |..
..+----------------+........+----------------+..
................................................

Figure 2

Как вы можете заметить, каждый «составной» класс больше не находится в отдельной иерархии, а объединен в одну общую или общую иерархию.

Затем давайте добавим элементы, которые одинаковы, можно переместить в суперкласс, а элементы, которые отличаются, в каждый базовый класс.

И, как вы уже знаете, «виртуальные» или «перегруженные» методы определены в базовом классе, но заменены в подклассах. Как на рисунке 3.

................................................
.............+--------------------+.............
.............|       Common::     |.............
.............|      Composite     |.............
.............+--------------------+.............
.............| [+] void AddChild()|.............
.............+---------+----------+.............
.......................|........................
.......................^........................
....................../.\.......................
.....................+-+-+......................
.......................|........................
..........+------------+------------+...........
..........|.........................|...........
..+-------+--------+........+-------+--------+..
..|     Canvas::   |........|      SVG::     |..
..|    Composite   |........|    Composite   |..
..+----------------+........+----------------+..
..|      ...       |........|      ...       |..
..+----------------+........+----------------+..
................................................

Figure 3

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

2 Подкласса

Вернемся к первой диаграмме. Каждый «составной» класс имел подкласс «Viewee» в каждой иерархии.

Процесс повторяется для каждого класса. Обратите внимание, что на рисунке 4 класс «Common :: Viewee» происходит от «Common :: Composite», но для простоты класс «Common :: Composite» на диаграмме опущен.

................................................
.............+--------------------+.............
.............|       Common::     |.............
.............|       Viewee       |.............
.............+--------------------+.............
.............|        ...         |.............
.............+---------+----------+.............
.......................|........................
.......................^........................
....................../.\.......................
.....................+-+-+......................
.......................|........................
..........+------------+------------+...........
..........|.........................|...........
..+-------+--------+........+-------+--------+..
..|     Canvas::   |........|      SVG::     |..
..|     Viewee     |........|     Viewee     |..
..+----------------+........+----------------+..
..|      ...       |........|      ...       |..
..+----------------+........+----------------+..
................................................

Figure 4

Вы заметите, что «Canvas :: Viewee» и «SVG :: Viewee», НЕ ДОЛЖНЫ происходить из своего соответствующего «Composite», а вместо этого из общего «Common :: Viewee».

Вы можете добавить участников, сейчас.

......................................................
.........+------------------------------+.............
.........|            Common::          |.............
.........|            Viewee            |.............
.........+------------------------------+.............
.........| [+] bool Validate()          |.............
.........| [+] Rect GetAbsoluteBounds() |.............
.........+-------------+----------------+.............
.......................|..............................
.......................^..............................
....................../.\.............................
.....................+-+-+............................
.......................|..............................
..........+------------+----------------+.............
..........|.............................|.............
..+-------+---------+........+----------+----------+..
..|      Canvas::   |........|         SVG::       |..
..|      Viewee     |........|        Viewee       |..
..+-----------------+........+---------------------+..
..|                 |........| [+] Viewee Element  |..
..+-----------------+........+---------------------+..
..| [+] void Paint()|........| [+] void addChild() |..
..+-----------------+........+---------------------+..
......................................................

Figure 5

3 Повторите процесс

Процесс будет продолжен, для каждого класса «Canvas :: Visual» не будет происходить из «Canvas :: Viewee», а Buit из «Commons :: Visual», «Canvas :: Structural» не будет происходить из «Canvas :: Viewee» ", Buit от" Commons :: Structural "и так далее.

4 Трехмерная иерархическая диаграмма

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

Ваши исходные независимые иерархии классов, где что-то похожее на это (рисунок 6):

.................................................
..+-----------------+.......+-----------------+..
..|      Common::   |.......|       SVG::     |..
..|     Composite   |.......|     Composite   |..
..+-----------------+.......+-----------------+..
..|       ...       |.......|       ...       |..
..+--------+--------+.......+--------+--------+..
...........|.........................|...........
...........^.........................^...........
........../.\......................./.\..........
.........+-+-+.....................+-+-+.........
...........|.........................|...........
..+--------+--------+.......+--------+--------+..
..|      Common::   |.......|       SVG::     |..
..|      Viewee     |.......|      Viewee     |..
..+-----------------+.......+-----------------+..
..|       ...       |.......|       ...       |..
..+--------+--------+.......+--------+--------+..
...........|.........................|...........
...........^.........................^...........
........../.\......................./.\..........
.........+-+-+.....................+-+-+.........
...........|.........................|...........
..+--------+--------+.......+--------+--------+..
..|      Common::   |.......|       SVG::     |..
..|      Visual     |.......|      Visual     |..
..+-----------------+.......+-----------------+..
..|       ...       |.......|       ...       |..
..+--------+--------+.......+--------+--------+..
...........|.........................|...........
...........^.........................^...........
........../.\......................./.\..........
.........+-+-+.....................+-+-+.........
...........|.........................|...........
..+--------+--------+.......+--------+--------+..
..|      Common::   |.......|       SVG::     |..
..|       Rect      |.......|       Rect      |..
..+-----------------+.......+-----------------+..
..|       ...       |.......|       ...       |..
..+-----------------+.......+-----------------+..
.................................................

Figure 6

Обратите внимание, что некоторые классы опущены, и вся иерархия «Canvas» опущена, для упрощения.

Конечная интегрированная иерархия классов может выглядеть примерно так:

.................................................
..+-----------------+.../+..+-----------------+..
..|      Common::   +--<.+--+       SVG::     |..
..|     Composite   |...\+..|     Composite   |..
..+-----------------+.......+-----------------+..
..|       ...       |.......|       ...       |..
..+--------+--------+.......+-----------------+..
...........|.....................................
...........^.....................................
........../.\....................................
.........+-+-+...................................
...........|.....................................
..+--------+--------+.../+..+-----------------+..
..|      Common::   +--<.+--+       SVG::     |..
..|      Viewee     |...\+..|      Viewee     |..
..+-----------------+.......+-----------------+..
..|       ...       |.......|       ...       |..
..+--------+--------+.......+-----------------+..
...........|.....................................
...........^.....................................
........../.\....................................
.........+-+-+...................................
...........|.....................................
..+--------+--------+.../+..+-----------------+..
..|      Common::   +--<.+--+       SVG::     |..
..|      Visual     |...\+..|      Visual     |..
..+-----------------+.......+-----------------+..
..|       ...       |.......|       ...       |..
..+--------+--------+.......+-----------------+..
...........|.....................................
...........^.....................................
........../.\....................................
.........+-+-+...................................
...........|.....................................
..+--------+--------+.../+..+-----------------+..
..|      Common::   +--<.+--+       SVG::     |..
..|       Rect      |...\+..|       Rect      |..
..+-----------------+.......+-----------------+..
..|       ...       |.......|       ...       |..
..+-----------------+.......+-----------------+..
.................................................
Figure 7

Обратите внимание, что некоторые классы опущены, и целые классы «Canvas» опущены, для упрощения, но будут аналогичны классам «SVG».

«Общие» классы могут быть представлены как один слой трехмерной диаграммы, классы «SVG» на другом уровне и классы «Canvas» на третьем уровне.

Убедитесь, что каждый слой относится к первому, в котором каждый класс имеет родительский класс «общей» иерархии.

Реализация кода может потребовать использования либо наследования интерфейса, наследования классов, либо «mixins», в зависимости от того, что поддерживает ваш язык программирования.

Резюме

Как и в любом программном решении, не торопитесь с оптимизацией, оптимизация очень важна, но плохая оптимизация может стать большей проблемой, чем исходная.

Я не рекомендую применять ни «Решение 1», ни «Решение 2».

На «Решение 1» не распространяется, поскольку наследование требуется в каждом конкретном случае.

«Решение 2», «Mixins» могут быть применены, но, после проектирования классов и иерархий.

Mixins, являются альтернативой для наследования на основе интерфейса или множественного наследования на основе классов.

Мое предлагаемое решение 3 иногда называют шаблоном проектирования «Параллельная иерархия» или шаблоном проектирования «Двойная иерархия».

Многие разработчики / дизайнеры не согласятся с этим и считают, что этого не должно быть. Но я использовал miself и другие разработчики в качестве общего решения проблем, например, вашего вопроса.

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

umlcat
источник
Спасибо за очень тщательный ответ. Я думаю, что я все понял, поэтому позвольте мне спросить это: canvas.vieweeи всем его потомкам нужен метод, называемый paint(). Ни классам, commonни svgклассам это не нужно. Но в вашем решении, иерархия в commonне canvasили , svgкак в моем решении 2. Так как именно в paint()конце концов во всех подклассах , canvas.vieweeесли нет наследства есть?
Изаки
@Izhaki Извините, если в моем ответе есть несколько ошибок. Затем paint()должен быть перемещен или объявлен в "canvas :: viewee". Общая идея шаблона остается, но некоторые элементы могут быть перемещены или изменены.
umlcat
Хорошо, так как подклассы получают это, если ни один не происходит из canvas::viewee?
Изаки
Вы использовали инструмент для создания своего ascii art? (Я не уверен, что точки действительно помогают, для чего бы это ни стоило.)
Аарон Холл
1

В статье под названием « Шаблоны проектирования для работы с иерархиями двойного наследования в C ++» дядя Боб представляет решение под названием « Лестница в небеса» . Заявлено намерение:

Этот шаблон описывает сеть отношений наследования, которая необходима, когда данная иерархия должна быть полностью адаптирована к другому классу.

И схема при условии:

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

Хотя в решении 2 нет виртуального наследования, оно в значительной степени согласуется с шаблоном « Лестница в небеса» . Таким образом, решение 2 представляется разумным для этой проблемы.

Izhaki
источник