SOLID JavaScript: The Liskov Substitution Principle
The Single Responsibility Principle
The Open/Closed Principle
The Liskov Substitution Principle
The Interface Segregation Principle
The Dependency Inversion Principle
This is the thrid installment in the SOLID JavaScript series which explores the SOLID design principles within the context of the JavaScript language. In this installment, we’ll be covering the Liskov Substitution Principle.
The Liskov Substitution Principle
The Liskov Substitution Principle relates to the interoperability of objects within an inheritance hierarchy. The principle states:
Subtypes must be substitutable for their base types.
In object-oriented programming, inheritance provides a mechanism for sharing code within a hierarchy of related types. This is achieved through a process of encapsulating any common data and behavior within a base type, and then defining specialized types in terms of the the base type. To adhere to the Liskov Substitution Principle, a derived type should be semantically equivalent to the anticipated behavior of its base type.
To help illustrate, consider the following code:
function Vehicle(my) {
my = my || {};
my.speed = 0;
my.running = false;
this.speed = function() {
return my.speed;
};
this.start = function() {
my.running = true;
};
this.stop = function() {
my.running = false;
};
this.accelerate = function() {
my.speed++;
};
this.decelerate = function() {
my.speed--;
};
this.state = function() {
if (!my.running) {
return "parked";
}
else if (my.running && my.speed) {
return "moving";
}
else if (my.running) {
return "idle";
}
};
}
This listing shows a Vehicle constructor which provides a few basic operations for a vehicle object. Let’s say this constructor is currently being used in production by several clients. Now, consider we’re asked to add a new constructor which represents faster moving vehicles. After thinking about it for a bit, we come up with the following new constructor:
function FastVehicle(my) {
my = my || {};
var that = new Vehicle(my);
that.accelerate = function() {
my.speed += 3;
};
return that;
}
After testing out our new FastVehicle constructor in the browser’s console window, we’re satisfied that everything works as expected. Objects created with FastVehicle accelerate 3 times faster than before and all the inherited methods function as expected. Confident that everything works as expected, we decide to deploy the new version of the library. Upon using the new type, however, we’re informed that objects created with the FastVehicle constructor break existing client code. Here’s the snippet of code that reveals the problem:
var maneuver = function(vehicle) {
write(vehicle.state());
vehicle.start();
write(vehicle.state());
vehicle.accelerate();
write(vehicle.state());
write(vehicle.speed());
vehicle.decelerate();
write(vehicle.speed());
if (vehicle.state() != "idle") {
throw "The vehicle is still moving!";
}
vehicle.stop();
write(vehicle.state());
};
Upon running the code, we see that an exception is thrown: “The vehicle is still moving!”. It appears that the author of this function made the assumption that vehicles always accelerate and decelerate uniformly. Objects created from our FastVehicle aren’t completely substitutable for objects created from our base Vehicle constructor. Our FastVehicle violates the Liskov Substitution Principle!
At this point, you might be thinking: “But, the client shouldn’t have assumed all vehicles will behave that way.” Irrelevant! Violations of the Liskov Substitution Principle aren’t based upon whether we think derived objects should be able to make certain modifications in behavior, but whether such modifications can be made in light of existing expectations.
In the case of this example, resolving the incompatibility issue will require a bit of redesign on the part of the vehicle library, the consuming clients, or perhaps both.
Mitigating LSP Violations
So, how can we avoid Liskov Substitution Principle Violations? Unfortunately, this isn’t always possible. To avoid LSP violations altogether, you would be faced with the difficult task of anticipating every single way a library might ever be used. Add to this the possibility that you might not be the original author of the code you’re being asked to extend and you may not have visibility into all the ways the library is currently being used. That said, there are a few strategies for heading off violations within your own code.
Contracts
One strategy for heading off the most egregious violations of the LSP is to use contracts. Contracts manifest in two main forms: executable specifications and error checking. With executable specifications, the contract for how a particular library is intended to be used is contained in a suite of automated tests. The second approach is to include error checking directly in the code itself in the form of preconditions, postconditions and invariant checks. This technique is known as Design By Contract, a term coined by Bertrand Meyer, creator of the Eiffel programming language. The topics of automated testing and Design By Contract are beyond the scope of this series, but I’ll leave you with the following recommendations for when to use each.
- Always use Test-Driven Development to guide the design of your own code
- Optionally use Design By Contract techniques when designing reusable libraries
For code you’ll be maintaining and consuming yourself, the use of Design By Contract techniques tends to just add a lot of unnecessary noise and overhead to your code. If you’re in control of the input, a test suite serves as a better control for specifying how a library is intended to be used. If you’re authoring reusable libraries, Design By Contract techniques serve both to guard against improper usage and as a debugging tool for your users.
Avoid Inheritance
Another strategy for avoiding LSP violations is to minimize the use of inheritance where possible. In the book Design Patterns – Elements of Reusable Object-Orineted Software by. Gamma, et. al., we find the following advice:
Favor object composition over class inheritance
While some of the book’s discussion concerning the advantages of composition over inheritance is only relevant to statically typed, class-based languages (e.g. the inability to change behavior at run time), one of the issues relevant to JavaScript is that of coupling. When using inheritance, the derived types are coupled with their based types. This means that changes to the base type may inadvertently affect derived types. Additionally, composition tends to lead to smaller, more focused objects which are easier to maintain for static and dynamic languages alike.
It About Behavior, Not Inheritance
Thus far, we’ve discussed the Liskov Substitution Principle within the context of inheritance, which is perfectly appropriate given that JavaScript is an object-oriented language. However, the essence of the Liskov Substitution Principle isn’t really concerned with inheritance, but behavioral compatibility. JavaScript is a dynamic language, therefore the contract for an object’s behavior isn’t determined by the type of the object, but by the capabilities expected by the object. While the Liskov Substitution Principle was originally conceived as a guiding principle for inheritance, it is equally relevant to the design of objects adhering to implicit interfaces.
To illustrate, let’s consider an example taken from the book Agile Software Development, Principles, Patterns, & Practices by Robert C. Martin: the rectangle example.
The Rectangle Example
Consider that we have an application which uses a rectangle object defined as follows:
var rectangle = {
length: 0,
width: 0
};
Later, it’s determined that the application also needs to work with a square. Based on the knowledge that a square is a rectangle whose sides are equal in length, we decide to create a square object to use in place of rectangle. We add length and width properties to match the rectangle object’s definition, but we decide to use property getters and setters so we can keep the length and width values in sync, ensuring it adheres to the definition of a square:
var square = {};
(function() {
var length = 0, width = 0;
Object.defineProperty(square, "length", {
get: function() { return length; },
set: function(value) { length = width = value; }
});
Object.defineProperty(square, "width", {
get: function() { return width; },
set: function(value) { length = width = value; }
});
})();
Unfortunately, a problem is discovered when the application attempts to use our square in place of rectangle. It turns out that one of the methods computes the rectangle’s area like so:
var g = function(rectangle) {
rectangle.length = 3;
rectangle.width = 4;
write(rectangle.length);
write(rectangle.width);
write(rectangle.length * rectangle.width);
};
When the method is invoked with square, the product is 16 rather than the expected value of 12. Our square object violates the Liskov Substitution Principle with respect to the function g. In this case, the presence of the length and width properties was a hint that our square might not end up being 100% compatible with rectangle, but we won’t always have such obvious hints. Correcting this situation would likely require a redesign of the shape objects and the consuming application. A more flexible approach might be to define rectangles and squares in terms of polygons. Regardless, the important take away from this example is that the Liskov Substitution Principle isn’t just relevant to inheritance, but to any approach where one behavior is being substituted for another.
Next time, we’ll discuss the forth principle in the SOLID acronym: The Interface Segregation Principle.