Testing Liskov Substitution Principle
In my previous post I talked about Liskov Substitution Principle in relation to TypeScript. I thought I would continue on my thoughts on LSP by defining it in terms of testing since testing has been a large part of my world for the past two years.
Here is another definition of LSP
Let q(x) be a property provable about objects x of type T. Then q(y) should be provable for objects y of type S where S is a subtype of T.
That’s how two titans of computer science Barbara Liskov and Jannette Wing defined it in 1993. Unfortunately, when I first learned of LSP through this definition the meaning of LSP alluded me because I was light on this strange genius speak. It wasn’t until I eventually stumbled on to seeing “provable” as some observable behavior or outcome that the definition of LSP clicked for me.
If I have an interface and I create an implementation of the interface, I can prove that the implementation is correct if it does what I expect it to. That expectation is represented by q in the equation above. Then if I create another implementation of the same interface, the same expectation or q should hold true with this new type. Hey, q is a test… duh.
I have an interface that can be implemented in a type that can be used to accesses source code repositories. One property of this interface that I expect is that I can get a list of all of the tags in a repository returned in a string array.
So, I create an implementation that can connect to a Git repository and it returns a string array of tags in the repository. I hook up the implementation to a UI and my client is happy because they can see all of their tags in my awesome UI on their mobile phone.
Now, they want an implementation for their SVN repository. No problem. I do another implementation and I return a string array of tags from their SVN repository. All good, expectation met, no LSP violations. I know this not because I am a genius and can do a mathematical proof, but because I wrote a functional test to prove the behavior by asserting what I expected to see in the string array (a mathematical proof at a higher abstraction for non-geniuses). When I run the test the tags returned match my expectation. With my test passing, I follow another SOLID principle, Dependency Inversion Principle (DIP), and I easily hook this up to the UI with a loose coupling. Anyway, now my client can open the UI and see a list of tags for their Git and SVN repositories. As far as they are concerned the expectation (q) is correct. My implementations satisfy the proof and my client doesn’t call an LSP violation on me.
My client says they now want to see a list of tags in their Perforce repository and I assign this to another dev team because this is boring to me now :). The team misunderstood the spec because I didn’t adequately define what a tag is for q. So, instead of returning tags in an array of strings they return a list of labels. While it is true that every tag in Perforce is a label, every label isn’t a tag. What’s even worse is the team has passing functional test that says they satisfied q. On top of this we didn’t properly QA the implementation to determine if their tests or definition of q is correct and we delivered the change to production. The client opens the UI and expects to see a list of tags from their Perforce repository and they see all the labels instead. They immediately call the LSP cops on us. This new type implementation of the interface does not meet expectation and is a violation of LSP.
Context is Key
Yes, this is a naive example of LSP, but it is how I understand it and how I apply it. If I have expectations when using an interface, abstract type, or implementation of some supertype, then every implementation or subtype should meet the expectation and be provable by the same expectation. The proof can be expressed as a mathematical equation, unit test, UI test, or visual observation as long as the expectation is properly expressed.
The point is, in order to not violate LSP we have to first have a shared understanding of the expectations expressed in our test (q). In our example, the development team had one expectation that wasn’t shared by the client and LSP was violated. To not violate LSP we have to understand how objects are expected to work, then we can define checks and tests to validate that LSP wasn’t violated.
This goes beyond just checking sub types in traditional object oriented inheritance. Every object that we create is an abstraction of something. If we create a People object, we expect it to have certain properties and behaviors. Even if we have a People type that won’t be sub typed, it can be said that it is a sub type of an actual person. The expectations that we define for People object should hold true. We expect a real person to have a name, address, and age. We could have a paper form (a People form) that we use to capture this information and the expectations are valid for the form. One day we decide to automate the form and we create an abstract People type and when we create an object of this type we expect it to have a name address, and age. We can test this object and determine if our People object violates LSP because it is a sub type of the manual form and we use the same expectations for the form and for our new object.
Now this is a little abstract mumbo jumbo, but it is a tenant that I believe is very important in software development. Don’t violate LSP!
Liskov Substitution Principle (LSP) in TypeScript
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?