Liskov Substitution Principle (LSP)
I was recently asked to name some of “Uncle Bob’s” SOLID OOP design principles. One of my answers was Liskov Substitution Principle. I was told it is the least given answer, I wonder why?
Liskov Substitution Principle (wikipedia)
“objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.”
As I dive deeper into TypeScript I am looking for ways to keep my code SOLID. This is where the question that prompted this post surfaced. How do I maintain LSP in TypeScript?
LSP and TypeScript
Then it hit me. The most important part of LSP (to me) is maintaining intent or semantics. It doesn’t matter if we are talking about strong typing or replacing ducks with swans, how we subtype isn’t as important as the behavior of the subtype with respect to the parent. Does the subtype pass our expectation q?
The principle asks us to reason about what makes the program correct. If you can replace a duck with a swan and the program holds its intent, LSP is not violated. Yet, if you can replace a Mallard duck with a Pekin duck in a method that only expects Mallards and maintains the proper behavior only with Mallards, when you use a Pekin duck in the method you have broken LSP. Pekin violates LSP for the expectation we have for Mallard even if Pekin is a structurally correct subtype of Mallard, using Pekin changes the behavior of the program. Now if we have a Gadwal duck that is also a structurally correct subtype of Mallard and using it I observed the same behavior as using a Mallard, LSP isn’t violated and I am safe to use a Gadwal duck in place of a Mallard.
I believe LSP has merit in TypeScript and any programming languages in general. I believe that violation of LSP goes beyond throwing a “not implemented exception” or hiding members of the parent class. For example, if you have an interface that defines a method that should only return even numbers, then you have an implementation of the interface method that allows returning of odd numbers, LSP is violated. Pay attention to subtype behavior with respect to expectations of behavior.
Respecting the LSP in a program helps increase flexibility, reduces coupling and makes reuse a good thing, but it also helps reduce bugs and increase the value of a program when you pay attention to behavioral intent.
What do you think, should we care about LSP in TypeScript?
For me, one of the biggest reasons for breaking a method up into distinct tasks or concerns is rooted in the expressiveness of exception messages. Below is an exception message on an actual production website. I am not going to show the code, but I had to answer the question, “Where did the exception below get called in the Page_Load method?” This particular method is a monster that is over 300 lines of code with multiple points where multiple reference objects could be the reason for the null. If the method was broken down into distinct concerns, I would have a fighting chance of finding the source of the error in less than an hour. Hell, with distinct methods I can probably find the null as soon as I expand the method.
Target: Void Page_Load(System.Object, System.EventArgs) Type: System.NullReferenceException Exception Message: Object reference not set to an instance of an object. Exception StackTrace: at Page_Load(Object sender, EventArgs e) at System.Web.Util.CalliHelper.EventArgFunctionCaller(IntPtr fp, Object o, Object t, EventArgs e) at System.Web.Util.CalliEventHandlerDelegateProxy.Callback(Object sender, EventArgs e) at System.Web.UI.Control.OnLoad(EventArgs e) at System.Web.UI.Control.LoadRecursive() at System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)
Do your code a favor, do the people that will be maintaining your code a favor, break up your god classes and methods into bite sized chunks of related functionality.