Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I avoid breaking Liskov Substitution Principle (LSP)?

I am in a situation very similar to what Steve McConnell's in Code Complete has mentioned . Only that my problem is based of Vehicles and Trike happens to be on that by law falls in the category of Cars . Cars had four wheels until now . Any way my domain is unnecessarily complex so it is easy to stick with cats example below.

Be suspicious of classes that override a routine and do nothing inside the derived routine This typically indicates an error in the design of the base class. For instance, suppose you have a class Cat and a routine Scratch() and suppose that you eventually find out that some cats are declawed and can't scratch. You might be tempted to create a class derived from Cat named ScratchlessCat and override the Scratch() routine to do nothing. This approach presents several problems:

It violates the abstraction (interface contract) presented in the Cat class by changing the semantics of its interface.

This approach quickly gets out of control when you extend it to other derived classes. What happens when you find a cat without a tail? Or a cat that doesn't catch mice? Or a cat that doesn't drink milk? Eventually you'll end up with derived classes like ScratchlessTaillessMicelessMilklessCat.

Over time, this approach gives rise to code that's confusing to maintain because the interfaces and behavior of the ancestor classes imply little or nothing about the behavior of their descendants.

The place to fix this problem is not in the base class, but in the original Cat class. Create a Claws class and contain that within the Cats class. The root problem was the assumption that all cats scratch, so fix that problem at the source, rather than just bandaging it at the destination.

According to the text from his great book above . Following is bad

Parent Class does not have to be abstract

public abstract class Cat {
   public void scratch() {
      System.out.println("I can scratch");
   }
}

Derived Class

public class ScratchlessCat extends Cat {
   @Override
   public void scratch() {
      // do nothing
   }
}

Now he suggests creating another class Claws, but I do not understand how can I use this class to avoid the need for ScratchlessCat#Scratch.

like image 810
Java Ka Baby Avatar asked May 28 '12 00:05

Java Ka Baby


2 Answers

That not all cats have claws and are capable of scratching is a big clue that Cat should not define a public scratch method in its API. The first step is to consider why you defined scratch in the first place. Perhaps cats scratch if they can when attacked; if not they hiss or run away.

public class Cat extends Animal {
    private Claws claws;

    public void onAttacked(Animal attacker) {
        if (claws != null) {
            claws.scratch(attacker);
        }
        else {
            // Assumes all cats can hiss.
            // This may also be untrue and you need Voicebox.
            // Rinse and repeat.
            hiss();
        }
    }
}

Now you can substitute any Cat subclass for another and it will behave correctly depending on whether or not it has claws. You could define a DefenseMechanism class to organize the various defenses such as Scratch, Hiss, Bite, etc.

like image 125
David Harkness Avatar answered Nov 07 '22 10:11

David Harkness


You would still have a scratch() method, but it will not be overridden by the derived classes:

public class Cat {
  Claw claw_;
  public Cat(Claw claw) {claw = claw_;}
  public final void scratch() {
    if (claw_ != null) {
      claw_.scratch(this);
    }
  }
}

This allows you to delegate the scratching logic to the contained Claw object, if present (and not scratch if there are no claws). Classes derived from cat have no say in the matter on how to scratch, so no need to create shadow hierarchies based on abilities.

Also, because the derived classes cannot change the method implementation, there is no problem of them breaking the intended semantics of the scratch() method in the base class's interface.

If you take this to the extremes, you might find that you have a lot of classes and not many derivations -- most logic is delegated to the composition objects, not entrusted to the derived classes.

like image 21
Attila Avatar answered Nov 07 '22 11:11

Attila