Don't surprise the engineers
Everybody loves kinds of surprises, but there are no such surprises in applicable software engineering!
Barbara Liskov is a brilliant computer scientist. She is widely known for the substitution principle in OOP named after her - the Liskov Substitution Principle.
In Uncle Bob's interpretation, LSP sounds like this:
Functions that use the base type should be able to use subtypes of the base type without knowing about it.
In even more simple language:
To safely use inheritance, you must ensure the ability to correctly pass a child object to methods/functions expecting a parent object.
Subclasses should extend, but not change the observable behavior of the parent class.
By behavior, we mean not only method signatures but also that state changes in the subtype must be **compatible** with state changes in the parent class. This is an important point.
So, in addition to the basic formulations, today I offer you to link LSP in your mind with the following phrase:
Don't surprise other programmers!
Surprise in engineering is very, very bad
In everyday life, emotions are very important, especially pleasant surprises.
They cause a big emotional response, and the event is better imprinted in our memory than ordinary hustle.
Surprise in programming is almost always an unpleasant surprise.
Last time, we talked about OCP from the perspective of functional programming and in general. In this post, I said that I feel a strong kinship between these two principles.
Here it is - in the responsibility for our modules and interfaces.
Breaking Changes are always trouble, we must avoid them at all costs if our system/module/library has even one user!
And "user" here can be not only another programmer or end-user but also another sub-system that depends on us.
I will never get tired of repeating this because I've seen too many such damn surprises.
A man's word is his bond
Adhering to the interface contract is good. That's essentially what LSP is about.
At the same time, adhering to contracts is good not only in OOP, but also in FP, and in programming in general. You just need to understand what a "contract" is in your context.
In the world of functional programming, there's something called type classes. But it's not really about types and not about classes either.
A type class is more like an "interface" that indicates that something can perform this method on this class. And the class here is a blurry concept, and you shouldn't immediately associate it with your favorite OOP.
Here's the key difference from "regular" interfaces in OOP, which are explicitly linked to a class and state - "this class can have such methods" - A type-class is a slightly more independent entity.
Type classes are probably fully implemented only in Haskell, in the sense of the purest and most complete implementation of the concept.
Nevertheless, our beloved interpreted languages have similar tools.
For example, in Python, there's singledispatch
in functools
, which is an implementation of dynamic dispatching based on the first argument.
This is not @overload
, with singledispatch
, Python checks types at runtime and chooses the right function.
Unfortunately, singledispatch is not supported by type checkers, but there's dry-python made by Nikita Sobolev that does the same thing, only with a sleek style, and mypy understands it.
In short, a proper implementation of a type class is a set of functions that the compiler or interpreter deals with. In some sense, it's similar to pattern-matching, but at a much lower (or higher :) level.
Why do you need to know all this? I don't know. To not break your own and other users' legs?
We have tons of ways to program systems correctly. Tons of languages, tons of paradigms. All this variety of methods can be confusing, drive us into implementation details, and turn us into crazy keyboard monkeys.
It doesn't matter what language you write in, and what programming paradigms you believe in. Instead, as I mentioned earlier, I offer you to focus on clear engineering and human principles:
Don't surprise other engineers.
Looking at the interface, take full responsibility for implementing all its invariants, pre-conditions, post-conditions, and other promises.