comments 5

Advice Needed about LSP Violation Avoidance

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

  1. This is an interesting problem. I feel like there is a way to answer the problem with a single interface, but the solution doesn’t come to me readily. On the other hand, perhaps I am misunderstanding the issue presented by the problem you have at work; however, I do have an approach that I think would work for the example listed above.

    I think the audio system would be better suited as a facade pattern. It would provide an interface that would allow the consumer the ability to turn the system on or off, play the radio, play the media device, and perform any other high level functionality that an audio system might be able to perform in the future. Each of these features would implement it own interface that would handle the operation of the subsystem.

    Using the media player as an example, as you have above, the media player would provide an interface to the audio system that would allow the consumer to play media, regardless of the media (cassette, cd, mp3, etc.). The concrete media object would then handle the needed steps to ensure it received the right input (cassette, cd, mp3, etc.) while the audio system made sure that the rendered audio made it to the right destination (speakers, headphones, etc.).

    I have seen other novel approaches to this problem in similar areas (e.g. currency) and if I am able to find them I will also forward them to you.

    • Now that I think about it, perhaps the issue is that there’s a SRP/ISP violation as well. That is, there are really three things this audio system can do: (1) be turned on/off, (2) play the radio, (3) play some other media.

      In that case, would IAudioSystem (1) implement those three interfaces (is-a relationship), or (2) have instances of each of those interfaces itself (has-a relationship)?

      This is where I can’t seem to find the middle ground of having things too separated and possibly getting into Law of Demeter violations. For example, “audioSystem.MediaPlayer.Play(myTape)” seems clunky. I’d like to be able to say “audioSystem.Play(mytape)”. I guess there are drawbacks for any design of a complex system.

      Thanks for replying!

      • I think the reason this is hard to think about is that the media player is dependent on a specific input.

        To help me think about that I thought about what would happen if someone asked me to play a cassette and I had no knowledge of the audio system. In this case I would be acting as the interface for the concrete AudioSystem facade.

        In my thought experiment I see the concrete audio system as having instances of the sub-system, but abstracting the way the client interacts with those sub systems by providing simplified controls to those sub-systems. For example, if the client turns the volume up, the idea of increasing the voltage supplied to an amplifier and several electromagnets housed in a speak is simplified to the turning of a nob or a push of a button. In this case the client wouldn’t be interacting with the sub-system directly, the facade would act on the clients behalf.

        Going back to playing the cassette; I am asked, as the interface of the facade, to play the cassette. If the media player sub-system I know about supports the cassette, the music is played. If the media player sub-system that I know about doesn’t support a cassette, the media player throws a not supported exception which the facade would handle and then pass some useful information back to the client.

        In the real world this would be similar. My friend asks me to play a cassette; I go to the audio system, which I have no prior knowledge of, and search for a cassette player; If I find a cassette player, I play the cassette, otherwise I return to my friend and say, “Sorry, my audio system doesn’t play cassettes. It can play mp3s though …”

        I still think there is a way to represent this system with is-a relationships, but modeling the audio system as a facade would not be the way to do that. I feel like having the AudioSystem implement each sub-system would couple the sub system implementations to closely to the behaviors that the AudioSystem provides.

        This could just be the difference of thinking about the audio system as an audio receiver, that has components connected to it that can be easily interchanged, and an integrated stereo system, like a boom box, which have the components hardwired together and are not easily interchangeable. There are advantages to both.

  2. I see this going in two possible ways with statically typed object oriented code…

    There’s the idea of Role Based Interfaces as defined by Martin Fowler. You’d have a base IAudioSystem and then interfaces like ICassettePlayer, ICDPlayer and so on. You could then have interfaces that implement one or many of those. IAudioSystemWithCassette : IAudioSystem, ICassettePlayer etc.

    This approach makes illegal states unrepresentable because passing a CD to an IAudioSystemWithCassette wouldn’t compile. You would potentially have many composite interfaces with this solution though which may not work with the rest of your system.

    – OR –

    If dealing with different audio players interchangeably is preferred then something like your generic option may be the better way to go (even if it is a leaky abstraction). Essentially you end up with a Servcie Locator that has various strategies to handle the incoming request. This immediately means you have to either throw an exception or (gasp) return null if no strategies in your list can handle the given media.

    This reminds me of the Simple Made Easy talk by Rich Hickey http://www.infoq.com/presentations/Simple-Made-Easy. At first glance option one seems more complicated because of the compound interfaces, but in reality option 2 will force the caller to consider the fact that what they’ve asked the class to do may fail (null, exception, or otherwise). Worse yet, the caller may not handle that behavior which could lead to other compound bugs.

    • I think I like the first option, where the shared functionality is at the top layer (IAudioSystem), and then you extend via more specific interfaces. My personal opinion is to also aim toward the Principle of Least Surprise — don’t let the compiler help you do something unless you are deliberately turning the safety off.

      Of course my actual scenario for my job-related software is more complex than this audio system example, but I think I can move some of the “what kind of audio system is available” decision making to some bootstrapping code, thus making it more seamless to the developer. The parts of the app that need to behave differently (based on if there’s a CD or cassette player) can simply ask “if (_audioPlayer is ICassettePlayer)” because they already know something cassette-specific at that point anyway.

      Your first option also lets me reap the benefits of the Interface Segregation Principle as well.

      Thanks for your comments!

Leave a Reply

Your email address will not be published. Required fields are marked *