SOLID: Liskov’s Substitution Principle — With Real Life Example
In this article, I will talk about the Liskov’s Substitution Principle, which corresponds to the 3rd letter of SOLID, and then I will show a real-life example.

The Liskov Substition Principle was first proposed by Barbara Liskov in 1988. Barbara Liskov and its definition in many sources on the internet are very foreign to a software developer. In this article, I will describe it in an understandable way.
Liskov’s point is in its simplest form: In the case of Polymorphism, a class should not contain any functions or properties that it will not use. It is a very important principle that no change is made when classes deriving from the same interface are used interchangeably. Remember, when you polymorphism, your smart IDE automatically implements all the methods and throws an error that is not implemented. That’s what Liskov is against. No class should have an empty method or value. This principle is in perfect harmony with the Open-Closed Principle.
For example, let’s make a cache application. Let’s make a file or Redis cache depending on the situation. For a better understanding of the principle, let’s first start with a case against the Principle. Then let’s rectify this anomaly and make Liskov an future us happy!
Let’s have an interface like the forwarding:
export default interface ICacheService {
setCache<T>(key: string, value: T): Promise<boolean>;
getCache<T>(key: string): Promise<T>;
checkCacheExists(key: string): Promise<boolean>;
setCacheWithTimeout<T>(
key: string,
value: T,
timeout: number
): Promise<boolean>;
}
As can be seen here, we have the functions of saving to cache, fetching from cache, querying cache and saving to cache with timeout.
Hint: Here, timeout caching is a Redis specific method, and using it filestream is not possible unless you do some flips.

Here’s the smart IDE implementation I’m talking about. I’II click the implement all members
button here and we’II vibrate Liskov!
Ok, now I clicked this button and filled in the required fields. We have a FileStreamCacheService that looks like the following.
import ICacheService from "../ICacheService";
import fs from "fs";
export default class FileStreamCacheService implements ICacheService {
private cacheDir: string = "./cacheFiles/";
private cacheExtension: string = ".txt";
constructor() {}
private calculateCachePath = (key: string): string => {
return this.cacheDir + key + this.cacheExtension;
};
checkCacheExists = async (key: string): Promise<boolean> => {
return fs.existsSync(this.calculateCachePath(key));
};
getCache = async <T>(key: string): Promise<T> => {
return new Promise<T>((resolve, reject) => {
fs.readFile(this.calculateCachePath(key), (err, result) => {
if (err) return reject(err);
resolve(JSON.parse(result.toString()));
});
});
};
setCache = async <T>(key: string, value: T): Promise<boolean> => {
return new Promise<boolean>((resolve, reject) => {
fs.writeFile(
this.calculateCachePath(key),
JSON.stringify(value),
(err: any) => {
if (err) return reject(false);
resolve(true);
}
);
});
};
setCacheWithTimeout = <T>(
key: string,
value: T,
timeout: number
): Promise<boolean> => {
return Promise.resolve(false);
};
}
Here I want you to focus on the setCacheWithTimeout function. We definitely don’t use it for the FileStream service. Now let’s write our Redis service and explain why I Repeat it here.
Our Redis Service will be as follows when we fill in the relevant fields:
import ICacheService from "../ICacheService";
import redis from "redis";
export default class RedisCacheService implements ICacheService {
private redisClient: redis.RedisClientType;
constructor(redisClient: redis.RedisClientType) {
this.redisClient = redisClient;
}
checkCacheExists = async (key: string): Promise<boolean> => {
return (await this.redisClient.exists(key)) === 1;
};
getCache = async <T>(key: string): Promise<T> => {
const result = await this.redisClient.get(key);
if (!result) throw new Error(`Not found: ${key}`);
return JSON.parse(result);
};
setCache = async <T>(key: string, value: T): Promise<boolean> => {
const result = await this.redisClient.set(key, JSON.stringify(value));
return result === "OK";
};
setCacheWithTimeout = async <T>(
key: string,
value: T,
timeout: number
): Promise<boolean> => {
const result = await this.redisClient.setEx(
key,
timeout,
JSON.stringify(value)
);
return result === "OK";
};
}
As you can see, we have used the setCacheWithTimeout function here. So why do we still have a problem?
For example, let’s first create a class called Product and create the Toyota Supra product. Let’s add this to the Redis cache so that it disappears after 3 seconds. And after 3 seconds, let’s check if it’s still there.
import redis, { createClient } from "redis";
import ICacheService from "./cache/ICacheService";
import RedisCacheService from "./cache/redis/RedisCacheService";
const createRedis = async (): Promise<redis.RedisClientType> => {
const client: redis.RedisClientType = createClient();
await client.connect();
return client;
};
class Product {
name: string;
id: number;
price: number;
constructor(name: string, id: number, price: number) {
this.name = name;
this.id = id;
this.price = price;
}
}
(async () => {
const redisClient = await createRedis();
const cacheService: ICacheService = new RedisCacheService(redisClient);
const supra = new Product("Toyota Supra", 22, 10000);
const setExpireResult = await cacheService.setCacheWithTimeout<Product>(
"product_22",
supra,
3
);
console.log("is it set Supra ? ", setExpireResult);
const supraIsThereOne = await cacheService.checkCacheExists("product_22");
console.log("is there Supra ? ", supraIsThereOne);
setTimeout(async () => {
const supraIsThereTwo = await cacheService.checkCacheExists("product_22");
console.log("is there still Supra ? ", supraIsThereTwo);
}, 3100);
})();

As you can see, Toyota was found at first but deleted after 3 seconds.
Well, we want to use FileStream service instead of Redis. They’re both of type ICacheService anyway. There should be no problem, if we followed Liskov.
So, the only place we need to change is where we define the cacheService constant. there we will call new FileStreamService()
instead of new RedisCacheService(redisClient)
, let’s see if we get the same result?

As you can see, the operation failed and our cache service is no longer running. Because the FileStream service does not actually implement the setCacheWithTimeout
function and cannot run it. Instead we should update our interface as follows.
export default interface ICacheService {
setCache<T>(key: string, value: T): Promise<boolean>;
getCache<T>(key: string): Promise<T>;
checkCacheExists(key: string): Promise<boolean>;
}
export interface ITimeoutCacheService extends ICacheService {
setCacheWithTimeout<T>(
key: string,
value: T,
timeout: number
): Promise<boolean>;
}
We have to create a new interface and this interface should take care of timeout operations. While the Redis service implements this new service; FileStream service should not implement this service beacuse it does not support timeout.
That’s it! Liskov is proud of us.
In this article, we saw the 3rd letter of SOLID and perhaps the most important principle of SOLID. Feel free to report any deficiencies you see in the article or code!
Good luck.