13

There are quite a few similar questions out there 1, 2, 3, 4, but non seems exactly the case in this question, nor do the solutions seem optimal.

This is a general OOP question, assuming polymorphism, generics, and mixins are available. The actual language to be used is OOP Javascript (Typescript), but it's the same problem in Java or C++.

I have parallel class hierarchies, that sometimes share the same behaviour (interface and implementation), but sometimes each has its own 'protected' behaviour. Illustrated like so:

3 parallel class hierarchies, the centre column shows the common parts, the left column is the canvas hierarchy and the right column shows the SVG hierarchy

This is for illustration purposes only; it isn't the actual class diagram. To read it:

  • Anything in the common hierarchy (centre) is shared between both the Canvas (left) and SVG (right) hierarchies. By share I mean both interface and implementation.
  • Anything only on the left or right columns means a behaviour (methods and members) specific to that hierarchy. For example:
    • Both the left and right hierarchies use exactly the same validation mechanisms, shown as a single method (Viewee.validate()) on the common hierarchy.
    • Only the canvas hierarchy has a method paint(). This method calls the paint method on all children.
    • The SVG hierarchy needs to override the addChild() method of Composite, but such is not the case with the canvas hierarchy.
  • The constructs from the two side hierarchies cannot be mixed. A factory ensures that.

Solution I - Tease Apart Inheritance

Fowler's Tease Apart Inheritance doesn't seem to do the job here, because there is some discrepancy between the two parallels.

Solution II - Mixins

This is the only one I can currently think of. The two hierarchies are develop separately, but at each level the classes mixin the common class, which are not part of a class hierarchy. Omitting the structural fork, will look like this:

The three columns again, the left and right columns are parallel hierarchies, where each class also inherent from a common class. The common classes are not part of an hierarchy

Note that each column will be in its own namespace, so class names won't conflict.

The question

Can anyone see faults with this approach? Can anyone think of a better solution?


Addendum

Here is some sample code how this is to be used. The namespace svg may be replaced with 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 );

Essentially, in run-time clients compose instances hierarchy of Viewee subclasses; like so:

An image showing a hierarchy of objects like layer, rect, scroller, etc.

Say all these viewees are from the canvas hierarchy, they are rendered by traversing the hierarchy can calling paint() on each viewee. If they are from the svg hierarchy, viewees know how to add themselves to the DOM, but there isn't paint() traversal.

9
  • Recommended reading: Design Review: on topic or not? Commented Jan 18, 2016 at 17:21
  • Maybe try full featured Decorator design pattern (Erich Gamma et als, Design Patterns)?
    – Zon
    Commented Jan 20, 2016 at 18:12
  • 1
    What's a viewee? What does "parallel" mean as a noun (as opposed to an adjective)? Commented Jan 20, 2016 at 18:58
  • Do you have multiple inheritance? Commented Jan 20, 2016 at 19:17
  • Do Canvas or SVG classes contain additional state or data that is not in Common? How do you use the classes? Can you show some example code showing how could these hiearchies be used?
    – Euphoric
    Commented Jan 20, 2016 at 19:57

4 Answers 4

5
+100

The second approach segregates interfaces better, following the interface segregation principle.

However, I would add a Paintable interface.

I would also change some names. No need to create confusion:

// 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 {
}
6
  • I think Interfaces may help for this case. I would like to know the reasons why your answer was downvoted.
    – umlcat
    Commented Jan 21, 2016 at 19:14
  • not the downvoter, but IMHO exponential interfaces are not a good pattern
    – dagnelies
    Commented Jan 21, 2016 at 22:40
  • @arnaud what do you mean by "exponential interfaces"? Commented Jan 22, 2016 at 0:03
  • @user61852 ...well, let's just say it's a lot of interfaces. "exponential" was actually a wrong term, it's more like "multiplicative". In the sense that if you had more "facets" (composite, visual, paintable...) and more "elements" (canvas, svg...), you would end up with a lot of interfaces.
    – dagnelies
    Commented Jan 22, 2016 at 9:11
  • @arnaud You have a point but at least there's a flexible design beforehand and OP's inheritance nightmare would be solved when you don't feel forced to extend. You extend some class if you want and not forced by a contrived hierarchy. Commented Jan 22, 2016 at 9:35
3

This is a general OOP question, assuming polymorphism, generics, and mixins are available. The actual language to be used is OOP Javascript (Typescript), but it's the same problem in Java or C++.

That's actually not true at all. Typescript has a substantial advantage over Java- namely, structural typing. You can do something similar in C++ with duck typed templates, but it's a lot more effort.

Basically, define your classes, but do not bother extending or defining any interfaces. Then simply define the interface you need and take it as a parameter. Then, the objects can match that interface- they don't need to know to extend it in advance. Each function can declare exactly and only the bits they give a shit about, and the compiler will give you a pass if the final type meets it, even though the classes don't actually extend those interfaces explicitly.

This frees you from the need to actually define an interface hierarchy and define which classes should extend which interfaces.

Just define each class and forget about the interfaces- structural typing will take care of it.

For example:

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

This is totally legitimate Typescript. Notice that the consuming functions don't know or give a single shit about the base classes or interfaces used in the definition of the classes.

It doesn't matter if the classes are related or not by inheritance. It doesn't matter if they extended your interface. Just define the interface as the parameter, and you're done- all classes that meet it are accepted.

8
  • It sounds promising, but I don't really understand the proposal (sorry, possibly too much OOP bias). Perhaps you can share some code example? For example, both svg.Viewee and canvas.Viewee need a validate() method (the implementation of which is identical for both); then only svg.Viewee needs to override addChild(), whilst only canvas.Viewee needs paint() (which calls paint() on all children - which are a member of the base Composite class). So I can't really visualise this with structural typing.
    – Izhaki
    Commented Jan 20, 2016 at 22:59
  • You're considering a whole bunch of things that just completely don't matter in this case.
    – DeadMG
    Commented Jan 21, 2016 at 12:56
  • So I probably didn't get the answer at all. Would be nice if you elaborate.
    – Izhaki
    Commented Jan 21, 2016 at 12:58
  • I made an edit. The bottom line is that the base classes are absolutely irrelevant and nobody cares about them. They are an implementation detail only.
    – DeadMG
    Commented Jan 21, 2016 at 13:03
  • 1
    OK. This is starting to make sense. A) I know what duck typing is. B) I'm not sure why interfaces are so central in this answer - the class breakdown is for the sake of sharing common behaviour, you can safely ignore interfaces for now. C) Say in your example you have SVGViewee.addChild(), but CanvasViewee also need exactly the same feature. So seems to make sense to me that both inherent from Composite?
    – Izhaki
    Commented Jan 21, 2016 at 15:34
3

Quick Overview

Solution 3: The "Parallel Class Hierarchy" Software Design Pattern is your friend.

Long Extended Answer

Your design STARTED RIGHT. It can be optimized, some classes or members may be removed, but, the "parallel hierarchy" idea you are applying to solve a problem IS RIGHT.

Have deal with the same concept several times, usually in control hierarchies.

After a while, I ENDED DOING THE SAME SOLUTION THAN OTHER DEVELOPERS, that is called sometimes the "Parallel Hierarchy" Design Pattern or "Dual Hierarchy" Design Pattern.

(1) Have you ever split a single class into a single hierarchy of classes ?

(2) Have you ever split a single class into several classes, without a hierarchy ?

If you have applied these previous solutions, separately, then they are a way to solve some problems.

But, what if we combine these two solutions, simultaneously ?

Combine them, and, you'll get this "Design Pattern" .

Implementation

Now, let's apply the "Parallel Class Hierarchy" Software Design Pattern, to your case.

You currently have 2 or more independent hierarchies of classes, that are very similar, have similar associations or purpouse, have similar properties or methods.

You wish to avoid having duplicated code or members ("consistency"), yet, you can't merge this classes directly into a single one, due to the differences among them.

So, your hierarchies are very similar to this figure, but, yet, there are more than one:

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

Figure 1

In this, not yet certified, Design Pattern, SEVERAL SIMILAR HIERARCHIES, ARE MERGED, INTO A SINGLE HIERARCHY, and each shared or common class is extended by subclassing.

Note, that this solution is complex, because you are already dealing with several hierarchies, therefore is a complex scenario.

1 The Root Class

In each hierarchy there is a shared "root" class.

In your case, there is an independent "Composite" class, for each hierarchy, that can have some similar properties, and some similar methods.

Some of those member can be merged, some of those members can not be merged.

So, what a developer can do, is to make a base root class, and subclass the equivalent case for each hierarchy.

In Figure 2, you can see a diagram just for this class, in which each class, keeps it namespace.

The members, are omitted, by now.

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

Figure 2

As, you may note, each "Composite" classe is no longer in a separate hierarchy, but, merged into a single shared or common hierarchy.

Then, let's add the members, those that are the same, can be move to the superclass, and those that are different, to each base class.

And as you already know, "virtual" or "overloaded" methods are defined in the base class, but, replaced in the subclasses. Like Figure 3.

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

Figure 3

Note that there are maybe some classes without members, and, you may be tempted to remove those classes, DONT. They are called "Hollow Classes", "Enumerative Classes", and other names.

2 The Subclasses

Let's go back to to the first diagram. Each "Composite" class, had a "Viewee" subclass, in each hierarchy.

The process is repeated for each class. Note than Figure 4, the "Common::Viewee" class descend from the "Common::Composite", but, for simplicity, the "Common::Composite" class is omitted, from the diagram.

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

Figure 4

You will note, that "Canvas::Viewee" and "SVG::Viewee", DOES NOT LONGER descend from its respective "Composite", but, from the common "Common::Viewee", instead.

You can add the members, now.

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

Figure 5

3 Repeat the Process

The process will continue, for each class, "Canvas::Visual" will not descend from "Canvas::Viewee", buit from "Commons::Visual", "Canvas::Structural" will not descend from "Canvas::Viewee", buit from "Commons::Structural", and so on.

4 The 3D Hierarchy Diagram

You will finish getting sort of a 3D diagram, with several layers, the top layer, has the "Common" hierarchy and the bottom layers, has each additional hierarchy.

Your original independent class hierarchies, where something similar to this (Figure 6) :

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

Figure 6

Note that some classes are omitted, and the whole "Canvas" hierarchy is omitted, for simplicitly.

The final integrated class hierarchy may be something similar to this:

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

Note that some classes are omitted, and the whole "Canvas" classes are omitted, for simplicitly, but, will be similar to the "SVG" classes.

The "Common" classes could be represented as a single layer of a 3D diagram, the "SVG" classes in another layer, and the "Canvas" classes, in a third layer.

Check, that each layer is related to the first one, in which, each class has a parent class of the "Common" hierarchy.

The code implementation may require to use either, interface inheritance, class inheritance, or "mixins", depending in what your Programming Language supports.

Summary

As any programming solution, do not rush into optimization, optimization is very important, yet, a bad optimization, may become a bigger problem, than the original problem.

I do not recommend apply either "Solution 1" or "Solution 2".

In "Solution 1" does not apply, because, the inheritance, is required in each case.

"Solution 2", "Mixins" may be applied, but, after designing the classes and hierarchies.

Mixins, are an alternative for interface-based inheritance, or class-based multiple inheritance.

My proposed, Solution 3, is called sometimes the "Parallel Hierarchy" Design Pattern or "Dual Hierarchy" Design Pattern.

Many developers / designers won't agree with it, and believe it shouldn't exist. But, I have used by miself, and other developers, as a common solution for problems, like the one of your question.

Another missing thing. In your previous solutions, the main issue was not wheter to use "mixins" or "interfaces", but, to refine, first, the model of your classes, and later use an existing Programming Language feature.

4
  • Thanks for the very thorough answer. I think I understood it alright, so let me ask this: canvas.viewee and all its descendants need a method called paint(). Neither the common nor the svg classes need it. But in your solution, the hierarchy is in common, not canvas or svg like in my solution 2. So how exactly paint() ends up in all subclasses of canvas.viewee if there is no inheritance there?
    – Izhaki
    Commented Jan 21, 2016 at 21:30
  • @Izhaki Sorry if there are a few bugs in my answer. Then paint() should be moved or declared in "canvas::viewee". The general pattern idea stays, but some members may be required to be moved or changed.
    – umlcat
    Commented Jan 23, 2016 at 1:00
  • OK, so how does the subclasses get it, if none derive from from canvas::viewee?
    – Izhaki
    Commented Jan 26, 2016 at 20:35
  • Did you use a tool to create your ascii art? (I'm not sure the dots actually help, for what it's worth.)
    – Aaron Hall
    Commented Jul 8, 2016 at 18:25
1

In an article titled Design Patterns for Dealing with Dual Inheritance Hierarchies in C++, Uncle Bob presents a solution called Stairway to Heaven. It's stated intent:

This pattern describes the network of inheritance relationships that is needed when a given hierarchy must be adapted, in its entirety, to another class.

And the diagram provided:

A class diagram with two parallel inheritance structures, where each class on the right also virtually inherent from its twin class on the left. The left hierarchy is also based completely on virtual inheritance

Although in solution 2 there isn't virtual inheritance, it is very much in accord with the Stairway to Heaven pattern. Thus solution 2 seems reasonable to this problem.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.