SOLID Principles: Behavioral Subtyping

Also known as Liskov substitution principle, it states that a derived entity should be behavioral extension of the base entity and not just syntactical extension.

The Liskov Substitution Principle “L”

This is an often overlooked principle while defining class hierarchies. Base & derived classes should have behavioral relation between them and Inheritance should not be applied just for the sake of reusing code.

As per LSP, objects of Base class should be replaceable with objects of Derived class without any change in the behavior or correctness of the application.

Inheritance signifies an Is-A relation between the derived class and the base class but at times, it can be misleading. Couple of classic examples:

  1. Square extends Rectangle: In basic geometric terms a square is a specific form of rectangle, so it might seem right for Square class to derive from Rectangle.
    • In object oriented design terms, a Rectangle has 2 sides but a Square has only 1.
    • When Square class extends Rectangle, it gets more variables than it needs and thus would need to do some work-around to achieve true behavior of a single side square.
    • Consider the following code snippet defying Behavioral Subtyping. setHeight() of Rectangle changes the value of variable height but setHeight() of Square changes both height & width. If clients replaces a Rectangle object with a Square object, then they will observe (unexpectedly) that setHeight() changes both height & width.
public class Rectangle {

  protected int height;
  protected int width;

  public int getHeight() {
    return height;
  }

  public void setHeight(int height) {
    this.height = height;
  }

  public int getWidth() {
    return width;
  }

  public void setWidth(int width) {
    this.width = width;
  }
}

public class Square extends Rectangle {

  @Override
  public int getHeight() {
    return super.getHeight();
  }

  @Override
  public void setHeight(int height) {
    super.setHeight(height);
    super.setWidth(height);
  }

  @Override
  public int getWidth() {
    return super.getWidth();
  }

  @Override
  public void setWidth(int width) {
    super.setHeight(width);
    super.setWidth(width);
  }

}
  1. Stack extends Queue: Both of these data structures have put() and get() methods. The put() method for them is same because it adds an element at the end of the data structure. The get() method is different; it implements FIFO in case of Queue and LIFO in case of Stack.
    • Defining Stack to extend Queue does provides code re usability but breaks the Liskov substitution principle because a Queue cannot be replaced with Stack without changing the underlying behavior.
    • A better class design would be something like the following. An abstract class AbstractCollection contains the common code of put() method and both Stack and Queue extend the abstract class to add concrete definition of get() method.
Figure 1

As per Wikipedia, Liskov substitution principle says “if S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program“.

In practice, applying this principle helps in deciding between Inheritance (Is-a) and Composition (Has-a) relation between classes.

Consider the following example of Rolling File Logger.

The objective is to add log messages to a file and roll the file based on the size of file. The library needs to support another functionality of rolling the log file based on time interval.

In this first approach (Figure 2) which defies Behavioral Subtyping, these two functionalities can be achieved by TimeRollingFileLogger extending SizeRollingFileLogger and overriding the method isRolloverRequired() which determines when to roll the log file.

Figure 2

A better design is to separate the Rolling Strategy from the FileLogger like:

Figure 3

In the second approach of Figure-3, FileLogger has-a RollingStrategy and Interface RollingStrategy is implemented by three different concrete classes for three specific strategies viz None, SizeBased and TimeBased.

The second approach better adheres to the behavioral relation between FileLogger and it’s rolling strategies in term of Object Oriented Design.

Conclusion

Behavioral Subtyping OR Liskov Substitution Principle helps in defining proper Object Oriented Design relation between classes by distinguishing Composition and Inheritance.

OOP Fundamentals (2 of 3)

Object Oriented Programming Fundamentals

An important aspect of OOP is reuse of classes using composition and inheritance.

Composition is using objects of existing classes into a new class. It represents ‘has-a‘ relationship. e.g. car has-a engine.

composition
Composition (has-a)

Inheritance is creating new classes as type of existing classes. It represents ‘is-a‘ relationship. In Java code, after specifying the name of derived (new) class use the keyword extends followed by name of base (existing) class .

With inheritance derived class gets all the public & protected attributes and methods of base class. There are two more access specifier (other than private and public) –

  • Protected – Like private, protected  entity is accessible to only the Class which defines it. Unlike private, protected entity of base class is available in the derived class.
  • Package – This is the default access where entity is accessible within the package but is private out of package.
inheritance
Inheritance (is-a)

The derived class may add more attribute & methods to the base class. Internally derived class has a object of base class but externally it exposes an extended interface of base class. So,  derived class is a wrapper over base class with some more specific functionality.

Initialization

Since base class is wrapped inside derived class, it becomes responsibility of derived class to do initialization of base class. Java helps here by calling the constructor of base class from the constructor of derived class, but this automatic help is available only in case of default constructors.

When dealing with constructors having arguments, the derived-class constructor needs to explicitly invoke the base-class constructor using super keyword. For example:

Class Furniture {

  Furniture(int i) {

    print(“Creating Furniture”);

  }

}

Class Table extends Furniture {

  Table(int i) {

    super (i);

    print(“Creating Table”);

  }

}

In the above example, constructor Table(int) invokes super(int) which is effectively Furniture(int). A derived class constructor must initialize the base class before doing anything else because initialization of derived may depend on the base class.