CS343 Blog Post for Week of October 30, 2023

This week, I wanted to continue writing about the SOLID software design principles. The next principle I have to cover is the Liskov substitution principle. This principle expands upon the Open/Closed principle I wrote about the previous week, providing a guideline on how a superclass and its child classes should behave.

This principle was first defined by Barbara Liskov in 1987, and later adopted by Robert C. Martin. From a paper that Barbara Liskov cowrote with Jeanette Wing, they defined the Liskov substitution principle as a mathematical property: “Let Φ(x) be a property provable about objects x of type T. Then Φ(y) should be true for objects y of type S where S is a subtype of T.”

Translated into plain English, this principle states that instances of a superclass in a program should be able to be replaced by one of its subclasses without breaking the program. Overridden methods from a subclass must accept the same input as the superclass’s original method, and the return value of a subclass’s method must be able to be used the same way as the value returned by a superclass’s original method.

Creating code that abides by this principle isn’t simple, as the compiler can only verify that the structure of your code is correct, not that any specific behavior of your code always executes when desired. Creating robust test classes and cases is the best way to check if your code is following the Liskov substitution principle, checking portions of your program using all subclasses of a particular component to ensure there are no resulting errors or loss in performance.

The author of this article series has included another code example representing a coffee machine to illustrate this design principle, as they have done in their previous articles. They present the problem of creating different subclasses of coffee machines from a generic parent class, with two classes with an “addCoffee()” method that accepts two different types of objects, CoffeeBean and GroundCoffee. The author suggests two solutions, either creating a common Coffee class that can be utilized by both the BasicCoffeeMachine class and the PremiumCoffeeMachine class, or create a common implementation of a “brewCoffee()” method that also appears in both CoffeeMachine classes. The first solution would violate the Liskov substitution principle, because each CoffeeMachine class would need to validate that they are receiving the correct input type for the addCoffee() method, as the BasicCoffeeMachine class can only use the GroundCoffee type, and not the CoffeeBean type. The author’s preferred solution is to remove the addCoffee() method from both CoffeeMachine classes and implement a common brewCoffee() method in the CoffeeMachine interface implemented by BasicCoffeeMachine and PremiumCoffeeMachine. Since all CoffeeMachines are expected to brew filtered coffee grounds, this brewCoffee() method should have that responsibility.

I want to further understand this principle because in a past project, I made a simple video game that used lots of objects of different subtypes that inherited from a single GameObject parent class. If I want to return to that project, or begin a similar one in the future, I want to make sure that I am not losing any functionality as I design and implement subclasses of a broader parent class.


Posted

in

by

Comments

Leave a comment

Design a site like this with WordPress.com
Get started