When is a sub-class appropriate?
My colleague Anatoly and I regularly need to create dialogs which pop-up in front of our application. Anatoly has created a class which allows the dialog to be configured by a packet of data which contains header, any text or images, button labels, footers, and information about how the buttons are processed, like this:
public class Dialog extends Sprite
{
public function Dialog(data:DialogData)
{
... composes the dialog according to the data that is passed into it ...
}
}
Anatoly – Dialog Composition
Let’s say that Anatoly wants to create a dialog in a particular view class (please ignore any other architectural considerations – I say “view class” broadly, hoping not to rile anyone who might shout “mediator”, which is beside the point in this case):
public class SomeClassWhereDialogsAreShown
{
...
public function onActionWhichTriggersExampleDialog():void
{
// this is a simplified structure to avoid complicating the point
var header:String = "This is Header";
var body:String = "This is body";
var buttons:Array = ["OK","Cancel"];
var data:DialogData = new DialogData(header, body, buttons);
var dialog:Dialog = new Dialog(data);
showDialog(dialog);
}
// this is one of many dialogs defined here...
public function onActionWhichTriggersADifferentDialog():void { ... }
public function onActionWhichTriggersAThirdDialog():void { ... }
public function onActionWhichTriggersAFourth():void { ... }
public function onActionWhichTriggersAFifthDialog():void { ... }
}
It is important to note that I have massively over-simplified the DialogData. It could be dozens of lines long, contain nested arguments, and so on. This point becomes part of the discussion later on.
Me – Dialog Inheritance
In contrast, I want to create a dialog sub-class and package away the dialog data:
public class ExampleDialog extends Dialog
{
public function ExampleDialog()
{
// this is a simplified structure to avoid complicating the point
var header:String = "This is Header";
var body:String = "This is body";
var buttons:Array = ["OK","Cancel"];
var data:DialogData = new DialogData(header, body, buttons);
super(data);
}
}
public class SomeClassWhereDialogsAreShown
{
public function onActionWhichTriggersExampleDialog():void
{
showDialog(new ExampleDialog());
}
}
This approach allows me to encapsulate the ExampleDialog very neatly, though it requires more lines of code than the composition approach. ActionScript is verbose enough without actively seeking to add more lines of code.
Single Responsibility Principle
If the dialog needs to be changed, the inheritance approach ensures that the only place that code editing need take place is in the code which relates to the particular dialog. This conforms to the Single Responsibility Principle, based on the principle of cohesion: the degree to which lines of code form a single unit of functionality is the extent to which they should be in their own class.
I would argue that creating a particular dialog is a functional unit, especially in user-interface code. Anatoly makes the point that all our dialogs are functionally equivalent (more of which below). I suppose he thinks of functionality in terms of the developer, and I the interface.
OOP Design principles are not inviolate, but they point us towards ways of writing code which lead to more robust code. This principle points towards the dialog configuration data not being defined in the SomeClassWhereDialogsAreShown class. (which is different from saying it supports the inheritance approach!)
Liskov Substitution Princple
Creating inheritance chains can be problematic. It might be considered reasonable to create a Square class as a subclass of the Rectangle, like this:
public class Rectangle
{
...
public function set width(value:Number):void
{
_width = value;
}
public function set height(value:Number):void
{
_height = value;
}
public function get area():Number
{
return _width * _height;
}
}
public class Square extends Rectangle
{
override public function set width(value:Number):void
{
_width = _height = value;
}
override public function set height(value:Number):void
{
_height = _width = value;
}
}
However, this can cause problems. Imagine, during your code that you utilise Rectangle like this:
public class SomeOtherClass
{
public function doubleRectangleArea(rectangle:Rectangle):void
{
rectangle.width *= 2;
}
}
The doubleRectangleArea method presupposes that the Rectangle‘s width and height properties are independent. Polymorphism allows a Square to be passed into the doubleRectangleArea method, but as configured this would quadruple the square’s area. The functionality of the doubleRectangleArea method is broken because the Square-Rectangle relationship breaks Liskov Substitution Principle (LSP).
This principle serves as a general warning against inappropriate inheritance. Anatoly invoked it in our discussion as a reason to be cautious about inheritance, but I remain unconvinced – surely not all inheritance is bad, and I am pretty sure that as things stand, no particular thing about our dialogs violate LSP?
Function and Configuration
Let’s suppose you want to model two cars: A Honda Jazz and a Lotus Exige. The following class structure may be appropriate:
public class Car { ... }
public class HondaJazz extends Car { ... }
public class LotusExige extends Car { ... }
If, on the other hand, you have two colours of Honda Jazz, the following structure makes little sense:
public class HondaJazz extends Car { ... }
public class BlueHondaJazz extends HondaJazz { ... }
public class WhiteHondaJazz extends HondaJazz { ... }
More sensible would be:
public class HondaJazz extends Car
{
public function HondaJazz(color:uint) { ... }
}
In the case of the two types of car there are clear functional differences: acceleration, top-speed, fuel efficiency, to name a few. In the case of the two jazzes, there are no functional differences, merely different configurations.
Anatoly argues that different dialogs are essentially like different configurations, rather than different functionalities. Therefore, they should all be the same class, but the Dialog should be configured through parameters in its constructor.
Discussion
So what is the right approach? The Single Responsibility Principle (SRP) definitely points away from the composition approach, if the class in which the composition is made has many other responsibilities. Anatoly has offered to meet me half-way having conceded this point, instead storing the various configurations in static methods of Dialog, or elsewhere, rather than create a subclass.
To me, this still feels wrong. The Function and Configuration argument is a good one, and points towards no sub-class being created. However, I still feel that the sheer quantity of configuration that some dialogs require start to make the act of configuration feel inappropriate.
Furthermore, if all we are really doing is is configuring the same dialog over and over, then I do not think that the Liskov Substitution Principle (LSP) is a concern. If all dialogs really are functionally equivalent, then how can they violate LSP?
Aesthetically, the inheritance approach feels right to me, but it is important to respect that Anatoly comes from a very different tradition and has been coding a lot longer than me! The discussion was an interesting (and pleasant) one. If any of you can add to it, then I would be delighted to hear your points of view.
-
http://alecmce.com Alec McEachran
-
http://alecmce.com alecmce
-
http://noiseandheat.com/ mnem
-
http://noiseandheat.com/ mnem
-
http://noiseandheat.com/ mnem
-
http://noiseandheat.com/ mnem
-
Anonymous
-
http://alecmce.com Alec McEachran
-
http://noiseandheat.com/ mnem
-
Cheers
-
http://alecmce.com Alec McEachran
-
jacksondunstan
-
http://alecmce.com alecmce
-
Cheers
-
Alec
-
http://noiseandheat.com/ mnem
-
Ewan
-
Ewan


