At work I’ve run into one of those situations where I have several object design paths to choose from, yet none of them is standing out as a clear winner. This post aims to set up a small version the problem along with some potential solutions, in hopes that my readers may have some advice (or other suggestions).
Problem context
Suppose you’ve got an application that communicates with some car audio system hardware. When the app was first created, there was only one type of system, and it was coded to the following interface:
interface IAudioSystem { void TurnOn(); void TurnOff(); void TuneToFmStation(decimal frequency); void TuneToAmStation(decimal frequency); void PlayCassette(Cassette tape); }
Some time has passed and technology has improved. Let’s assume that cassettes are not available in the next generation audio system, and that playing CDs is a new feature. The rub is that the application needs to support either system until all of the units with cassette players get phased out.
Solution: Expand the interface
One solution is to make a superset interface…
interface IAudioSystem { void TurnOn(); void TurnOff(); void TuneToFmStation(decimal frequency); void TuneToAmStation(decimal frequency); void PlayCassette(Cassette tape); void PlayCompactDisc(CompactDisc disc); }
But now how does RadioWithCassettePlayer respond to PlayCompactDisc(), and how does RadioWithCdPlayer respond to PlayCassette()? This is a textbook Liskov Substitution Principle violation.
You could have those not-applicable methods throw NotSupportedExceptions, but now your calling code has to be careful about dealing with exceptions that it never had to expect before.
Solution: Verify capabilities
A forum post suggested having some kind of query component to the interface — for example, CanPlayCassette() and CanPlayCompactDisc(). However, this is a violation of the Hollywood Principle. That is, you should just be able to tell the IAudioSystem instance to do something without first asking whether it will blow up in your face.
Solution: Decorator pattern
The decorator pattern seems like it would almost work. However, the examples I’ve seen only modify existing traits of the base object — not removing or adding new ones.
Solution: Generic functionality
The leading solution is something akin to having something like this:
interface IAudioSystem { void TurnOn(); void TurnOff(); void TuneToFmStation(decimal frequency); void TuneToAmStation(decimal frequency); void Play<T>(T media); }
That fixes the interface. However, what happens when you have this…
class RoadTripBuddyInChargeOfTunes { public RoadTripBuddyInChargeOfTunes(IAudioSystem audioSystem) { ... } public void PlaySomethingICanSingAlongWith() { var tape = new ClassicQueenCassette(); audioSystem.Play<Cassette>(tape); } }
…and audioSystem happens to be an instance of RadioWithCdPlayer?
Comments welcome
I’m sure this situation comes up fairly often, thus I’m not the first person to deal with it. Please leave a comment below if you’d like to chime in.
Thanks in advance!
5 Comments