Resource The Liskov Substitution Principle and Why It Is Useful

Discussion in 'Spigot Plugin Development' started by Choco, Aug 1, 2018.

?

Did you learn something new?

  1. Yes

  2. No

Results are only viewable after voting.
  1. Choco

    Moderator

    The Liskov Substitution Principle

    Across the Spigot Plugin Development forum, you may have seen developers frequently referring to a principle titled "Liskov Substitution Principle" but blindly shook your head yes and clicked the agree button without actually knowing what it is. This guide aims to explain what the principle entails, why you should be abiding by it and what benefit it brings your program in terms of software design, flexibility and extensibility. Do note, however, that if you do not understand OOP design and the class hierarchy (this includes abstraction and inheritance), this guide may be rather confusing. It is recommended that you understand these concepts before continuing on with this topic.

    For those that want to read more about this by themselves, I will be leaving sources to this information in the footer of the OP. I encourage you to read them for yourself and understand the principle so you may use it to its fullest extent.


    What is the principle?

    First and foremost, I want to note that the Liskov Substitution Principle is not the only principle with regards to proper Object-Oriented Programming design. In fact, there are five:

    Single Responsibility Principle [1]
    Open / Closed Principle [2]
    Liskov Substitution Principle [3]
    Interface Segregation Principle [4]
    Dependency Inversion Principle [5]

    You may read about all of these on your own, but in this thread I want to focus on the L of SOLID code design. The principle states:

    "Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it"

    Confusing, right? Well, not really. In simpler terms, this principle is detailing that an object of type S is a subtype of type T such that S may be used in place of T without altering the underlying functionality of program P. Still confusing? In place of S, use ArrayList, and in place of T, use List. Now it looks something like this:

    "An object of type ArrayList is a subtype of type List such that ArrayList may be used in place of List without altering the underlying functionality of program P"
    This is a bit more understandable and gives you an idea of what the principle enforces: inheritance and abstraction without breaking functionality given that two types are interchangeable. Continuing on with the example of List, I should be able to use an ArrayList or a LinkedList but still maintain the exact same functionality in my program. This is what the majority of developers on SpigotMC refer to when speaking about the Liskov Substitution Principle.


    Why is this important to me?

    Now you understand that the principle ensures that a program, P, will not be altered regardless of the implementation used, let's try an example where the Liskov Substitution Principle is being broken. In this thread, I will be covering two examples. One of the more common examples that you may see is the "Square vs Rectangle" problem.

    I. Square vs. Rectangle:
    Describing the difference between a square and a rectangle to a human being is simple, right? A rectangle is a shape with 4 sides where the 2 opposing sides are of equal length. A square is a shape with 4 sides where all sides are of equal length. By this logic, a square is technically considered a rectangle. So... this is easy... let's just do this with Java. (For the sake of understanding this example, Rectangle will pose as an interface)
    Code (Java):
    public interface Rectangle {

        public void setHeight(int height);
        public int getHeight();
        public void setWidth(int width);
        public int getWidth();

    }
    And now, because squares are rectangles, our Square class should obviously implement Rectangle
    Code (Java):
    public class Square implements Rectangle {

        // All fields, etc.

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

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

        // Regular getters

    }
    Wait wait wait... hold up. Our setters in our Square object are doing something unexpected. The #setHeight() method is also setting the width, and the #setWidth() method is also setting the height. According to the definition of a Rectangle, this just doesn't add up. If I had a regular rectangle, setting its height should not also affect its width. If we were to use a Square in place of a Rectangle anywhere in our code, the functionality of the program would ultimately change. This is a blatant violation of the Liskov Substitution Principle, therefore Square must not be a Rectangle.


    II. The Penguin Problem

    I've always personally disliked the Square vs. Rectangle example as it doesn't give a good indication of why the principle is necessary. However, the example of The Penguin Problem [6] has been, at least to myself, much more helpful in understanding the principle. Let's assume we want to write a program that shows birds flying on the screen. Now, we want dozens of different birds so it just makes sense to write an interface to represent these birds abstractly because in essence, they all do the same thing, fly.
    Code (Java):
    public interface Bird {

        public void setLocation(double longitude, double latitude);

        public void setAltitude(double altitude);

        public void draw();

    }
    Quick and easy. Now we can use any type of bird that we want because they can all fly. We've got tons of different birds, so let's just use them in a method to make things easier for us.
    Code (Java):
    public void drawBird(Bird bird) {
        // Other bird things
        drawBirdInAir(bird);
    }
    Easy! Now that we have a Bird object, let's add a few more birds. What about... penguins! They're cute. Everyone loves penguins. Let's add one really quickly.

    Code (Java):
    public class Penguin implements Bird {

        public void setLocation(double longitude, double latitude) {
            this.longitude = longitude;
            this.latitude = latitude;
        }

        public void setAltitude(double altitude) {
            // Flightless bird. Ha! Loser! This method does nothing
        }

    }
    But wait... penguins can't fly... Why can we set its altitude? Whatever. Let's just make a special case for it in our #drawBird(Bird) method. It's not a huge deal.
    Code (Java):
    public void drawBird(Bird bird) {
        if (bird instanceof Penguin) {
            drawBirdOnGround(bird);
        } else {
            drawBirdInAir(bird);
        }
    }
    Done! Now this abides by the Liskov Substitution Principle... right? Well... no. Let's refer back to the principle again. "Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it". Because our method, #drawBird(), has a special case for one of Bird's derived classes, Penguin, the LSP has been broken.

    There are two ways in which this may be solved. The first of which being that we could add a new method to the Bird class, #isFlightless(), which would technically abide by the Liskov Substitution Principle but is a very quick remedy that should not be relied upon. The #setAltitude() method still does nothing for pointers of type Penguin. The proper alternative would be to not assume that all birds can fly, but rather have a subtype of Bird that includes a setAltitude() method. This way, our type Penguin can implement Bird, whereas other birds that can fly may implement our subtype containing #setAltitude().

    Why do people refer to LSP when they see my ArrayLists?

    With all the above information fresh in your mind, let's take a look at a common example thrown around the forums... programming against abstraction rather than implementation types.
    Code (Java):
    public class MyClass {

        private final ArrayList<String> myList = new ArrayList<>();
        private final HashMap<Object, Long> myMap = new HashMap<>();

        public ArrayDeque<MyClass> myMethod(HashSet<Integer> parameter) {
            // Code
        }

    }
    I see this mistake far too frequently among beginners and this is generally where the Liskov Substitution Principle comes into play on these forums. But why? Well, take a look at the principle again. The declared type must be exchangeable such that the underlying functionality does not change. Unless you explicitly require that one specific type of implementation, you should be programming against abstraction because your program does not change based on implementation used. This is what the above should look like.
    Code (Java):
    public class MyClass {

        private final List<String> myList = new ArrayList<>();
        private final Map<Object, Long> myMap = new HashMap<>();

        public Deque<MyClass> myMethod(Set<Integer> parameter) {
            // Code
        }

    }

    Conclusion

    In conclusion, I hope you learned a little bit more about the Liskov Substitution Principle, why it's important and why you should pay attention to the people telling you to abide by it. It is a useful principle that makes your code more extensible and flexible when it comes to the class hierarchy. If you have any questions or concerns, please do reply to this thread, I'll try to answer the questions to the best of my ability. I am no professional on this topic, but I felt I was knowledgeable enough to discuss it in as much detail as possible while still being easy to understand. See sources below. Thank you.

    ===============================================================================================

    Liskov Substitution Principle Examples: https://www.tomdalling.com/blog/sof...ass-design-the-liskov-substitution-principle/
    Let's Make Better Software (SOLID Design): https://www.letsmakebettersoftware.com/2017/09/solid-3-liskov-substitution-principle.html
    LSP Wikipedia: https://en.wikipedia.org/wiki/Liskov_substitution_principle
     
    • Useful x 10
    • Like x 5
    • Informative x 5
    • Winner x 2
    • Agree x 1
  2. Strahan

    Benefactor

    Bored today? :)
     
  3. Choco

    Moderator

    Pretty much, yea. Figured I’d provide some useful information :D haven’t made a forum resource in a while
     
  4. Nice writeup, thanks for the thread!

    Though, I feel like you could elaborate more on why it is actually useful. I mean like, here:
    Code (Java):
    HashMap<?, ?> map = new HashMap<>();
    Now people know that it should be declared as Map instead of HashMap, but why they would do that may still be a bit unclear, and so people may just brush it off as it "does not change the functionality". You did explain it briefly, but I'd add a concrete 'why is this useful', maybe at the last example? But I don't know really, if the thread wasn't enough, not sure if the additional explanation would be.
     
    • Like Like x 1
  5. I would go as far as perhaps a Collection here, there really aren't that many different implementations that you'll use for a List if you aren't using an index, and you can even switch to an implementation of Set if you decide you are going to use Set#contains(...) a lot
     
  6. FrostedSnowman

    Resource Staff

    tl;dr
    If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T
     
  7. Choco

    Moderator

    It was an example :) I wouldn't go as far as using Collection unless absolutely necessary because while yes, ArrayList is of type Collection, ultimately, List is the parent abstraction with different implementations. Collection can be either a List or a Set which both serve two very separate purposes. List allows duplicates whereas a Set does not. Additionally, List allows you to #get() at an index, Set does not. However, sometimes, returning a type Collection for a method may be beneficial as the underlying type of collection does not matter at all. It really does depend on what you want to expose.

    I'll write more of an explanation in a short bit here

    Right, but copy/pasting from one of the sources I linked isn't going to explain why this should hold true. This topic really isn't something you can TL;DR without leaving out a lot of useful information.
     
  8. Very useful, i voted no - only since I had seen it and read the wikipedia post before, very similar if not the same as the one you said with S & T. But if I had never read it before, this is great. In my head, i used the List & ArrayList example too.
     
    • Like Like x 1