Design Patterns – Removing Object Behavior or Properties in Class Hierarchies

design-patternsliskov-substitution

A well-known shortcoming of traditional class hierarchies is that they are bad when it comes to modeling the real world. As an example, trying to represent animals' species with classes. There are actually several problems when doing that, but one that I never saw a solution to is when a sub-class "loses" a behavior or property that was defined in a super-class, like a penguin not being able to fly (there are probably better examples, but that's the first one that comes to my mind).

On the one hand, you don't want to define, for every property and behavior, some flag that specifies if it is at all present, and check it every time before accessing that behavior or property. You would just like to say that birds can fly, simply and clearly, in the Bird class. But then it would be nice if one could define "exceptions" afterward, without having to use some horrible hacks everywhere. This often happens when a system has been productive for a while. You suddenly find an "exception" that doesn't fit in the original design at all, and you don't want to change a large portion of your code to accommodate it.

So, are there some language or design patterns that can cleanly handle this problem, without requiring major changes to the "super-class", and all the code that uses it? Even if a solution only handles a specific case, several solutions might together form a complete strategy.

After thinking more, I realize I forgot about the Liskov Substitution Principle. That is why you can't do it. Assuming you define "traits/interfaces" for all major "feature groups", you can freely implement traits in different branches of the hierarchy, like the Flying trait could be implemented by Birds, and some special kind of squirrels and fish.

So my question could amount to "How could I un-implement a trait?" If your super-class is a Java Serializable, you have to be one too, even if there is no way for you to serialize your state, for example if you contained a "Socket".

One way to do it is to always define all your traits in pairs from the start: Flying and NotFlying (which would throw UnsupportedOperationException, if not checked against). The Not-trait would not define any new interface, and could be simply checked for. Sounds like a "cheap" solution, in particular if used from the start.

Best Answer

As others have mentioned you would have to go against LSP.

However, it can be argued that a subclass is merely an arbitary extension of a super class. It's a new object in it's own right and the only relation to the super class is that it's used a foundation.

This can make logical sense, rather then saying Penguin is a Bird. Your saying Penguin inherits some subset of behaviour from Bird.

Generally dynamic languages allow you to express this easily, an example using JavaScript follows below:

var Penguin = Object.create(Bird);
Penguin.fly = undefined;
Penguin.swim = function () { ... };

In this particular case, Penguin is actively shadowing the Bird.fly method it inherits by writing a fly property with value undefined to the object.

Now you may say that Penguin cannot be treated as a normal Bird anymore. But as mentioned, in the real world it simply cannot. Because we are modelling Bird as being a flying entity.

The alternative is to not make the broad assumption that Bird's can fly. It would be sensible to have a Bird abstraction which allows all birds to inherit from it, without failure. This means only making assumptions that all subclasses can hold.

Generally the idea of Mixin's apply nicely here. Have a very thin base class, and mix all other behaviour into it.

Example:

// for some value of Object.make
var Penguin = Object.make(
  /* base class: */ Bird,
  /* mixins: */ Swimmer, ...
);
var Hawk = Object.make(
  /* base class: */ Bird,
  /* mixins: */ Flyer, Carnivore, ...
);

If your curious, I have an implementation of Object.make

Addition:

So my question could amount to "How could I un-implement a trait?" If your super-class is a Java Serializable, you have to be one too, even if there is no way for you to serialize your state, for example if you contained a "Socket".

You don't "un-implement" a trait. You simply fix your inheritance hierachy. Either you can fulfill your super classes contract or you shouldn't be pretending your are of that type.

This is where object composition shines.

As an aside, Serializable does not mean everything should be serialized, it only means "state you care about" should be serialized.

You should not be using a "NotX" trait. That's just horrendous code bloat. If a function expects a flying object, it should crash and burn when you give it a mammoth.

Related Topic