Solid Principles Interview Questions - You Might Get Them
In this post you will learn what are SOLID principles and how to use them in Javascript.
A lot of people for some reason think that SOLID is something super old, we don't need it nowadays and we don't need to learn what is it at all. This is entirely wrong.
First of all you can get SOLID principles questions on the interviews really often. Secondly, it's a base of good programming for any language.
Every single developer must understand these principles and remember about them every single day.
What is SOLID? These are 5 different principles which start with each letter of this word. So it is S O L I D.
Single responsibility principle
Let's start from the S. It means that our code must be build with a single responsibility principle.
If we have a module there should be only one actor which changes it. Let's look at this example.
class TodoList {
constructor() {
this.items = []
}
addItem(text) {
this.items.push(text)
}
removeItem(index) {
this.items = items.splice(index, 1)
}
toString() {
return this.items.toString()
}
}
Here we have a class TodoList
which is our module. We have some methods inside and data. This is a good code. It's a class with a single responsibility which works with the entity TodoList
.
When these class has a bad architecture?
class TodoList {
...
saveToFile(data, filename) {
fs.writeFileSync(filename, data.toString())
}
loadFromFile(filename) {
// Some implementation
}
}
Here we added 2 new methods saveToFile
and loadFromFile
. These 2 methods are completely unrelated to our TodoList
at all as they are related to work with the database.
Which actually means that we broke single responsibility principle by throwing things which are not related to our class inside.
What is a good solution here?
class TodoList {
...
}
class DatabaseManager {
saveToFile(data, filename) {
fs.writeFileSync(filename, data.toString())
}
loadFromFile(filename) {
// Some implementation
}
}
We moved these 2 methods in a separate class DatabaseManager
which works with the database. With such code we correctly implemented single responsibility principle. We are working with TodoList
entity in it's own class and we are working with the database in the DatabaseManager
class.
Open / Closed principle
The second letter is O and it is the open / closed principle
. What does it mean?
Our module must be opened to extension but closed to modification
class Coder {
constructor(fullName, language, hobby, education, workplace, position) {
this.fullName = fullName
this.language = language
this.hobby = hobby
this.education = education
this.workplace = workplace
this.position = position
}
}
class CoderFilter {
filterByName(coders, fullName) {
return coders.filter(coder => coder.fullName === fullName)
}
filterBySize(coders, language) {
return coders.filter(coder => coder.language === language)
}
filterByHobby(coders, hobby) {
return coders.filter(coder => coder.hobby === hobby)
}
}
Here we have a class Coder
with lots of properties inside our contructor. Also there is an additional class CoderFilter
and we implement different methods to filter our data by name, size or hobby.
This is actually breaking this principle. Why is that? Because every time when we want to filter a new property we must create a new method.
Which actually means that we must modify our existing code inside CoderFilter
every single time when we are making change in data.
We must modify our code inside CoderFilter
so it doesn' break this rule.
class CoderFilter {
filterByProp (array, propName, value) {
return array.filter(element => element[propName] === value)
}
}
This implementation is filter agnostic. It can work with any filter without need to make a specific implementation of the filter.
It doesn't really matter if we are filtering by name, size, hobby. This method will cover them all.
Additionally to that our filterByProp
method accepts an array and not just coders list. Because essentially we can apply this code to any array and make CoderFilter
even more generic like Filter
.
Liskov substitution principle
Our next letter is L. What does it mean? If you have a method in your base type then it must work to derived type. Let's look on the example.
class Rectangle {
constructor(width, height) {
this._width = width
this._height = height
}
get width() {
return this._width
}
get height() {
return this._height
}
set width(value) {
this._width = value
}
set height(value) {
this._height = value
}
getArea() {
return this._width * this._height
}
}
class Square extends Rectangle {
constructor(size) {
super(size, size)
}
}
Here we have a Rectangle
class which accepts width
and height
. We also have 2 getters and 2 setters.
We also created a Square
class which extends our class and provides a single argument because our width
and height
are equal.
Liskov principle means that our children classes must work exactly the same as our parent class if then are not overrided.
const square = new Square(2)
square.width = 3
console.log(square.getArea()) // 6
But here is the problem. We create a square by providing 2
inside. Then we assign width
which calls our setter. But when we call getArea
we are getting 6
and not 9
.
It happens because with our setter we overrode just width
but not height
. Which actually means that we broke our principle and implemented child not correctly.
Our implementation must update both width
and height
automatically.
class Square extends Rectangle {
constructor(size) {
super(size, size)
}
set width(value) {
this._width = this._height = value
}
set height(value) {
this._width = this._height = value
}
}
Here we overrode width
and height
setters to implement it correctly. Now we are getting 9
with our code which is correct.
Interface segregation principle
The next principle is for letter I
.
If your class implements some interface then it should not be forced to implement it if it is not needed.
Actually inside Javascript we don't have interfaces at all this is why I show you example with the Typescript.
interface VehicleInterface {
drive(): string;
fly(): string;
}
class FutureCar implements VehicleInterface {
public drive() : string {
return 'Driving Car.';
}
public fly() : string {
return 'Flying Car.';
}
}
class Car implements VehicleInterface {
public drive() : string {
return 'Driving Car.';
}
public fly() : string {
throw new Error('Not implemented method.');
}
}
class Airplane implements VehicleInterface {
public drive() : string {
throw new Error('Not implemented method.');
}
public fly() : string {
return 'Flying Airplane.';
}
}
Here we have an interface for our Vehicle
which has fly and drive. It is totally fine for our FutureCar
class but it is wrong for Airplane
and a Car
because we must implement all methods of the interface even when they are not needed.
In our case Car
doesn't need fly
method and Airplane
doesn't need drive
method.
In order to fix this code we can simply create 2 different interfaces.
interface CarInterface {
drive() : string;
}
interface AirplaneInterface {
fly() : string;
}
class FutureCar implements CarInterface, AirplaneInterface {
public drive() {
return 'Driving Car.';
}
public fly() {
return 'Flying Car.'
}
}
class Car implements CarInterface {
public drive() {
return 'Driving Car.';
}
}
class Airplane implements AirplaneInterface {
public fly() {
return 'Flying Airplane.';
}
}
Now we apply both interfaces on Future
can and only needed interface on Car
and Airplane
and our code doesn't break our interface segregation principle.
Dependency inversion principle
And the last principle that we have says that "High level modules should not be dependent on low level modules". Let's have a look on the example.
class FileSystem {
writeToFile(data) {
// Implementation
}
}
class ExternalDB {
writeToDatabase(data) {
// Implementation
}
}
class LocalPersistance {
push(data) {
// Implementation
}
}
class PersistanceManager {
saveData(db, data) {
if (db instanceof FileSystem) {
db.writeToFile(data)
}
if (db instanceof ExternalDB) {
db.writeToDatabase(data)
}
if (db instanceof LocalPersistance) {
db.push(data)
}
}
}
Here we have 3 low level classes which write data to different places and a single PersistanceManager
class which works with all them.
The main problem here is that our parent is tight to all 3 low level classes because it must know which method to call in each of them.
Much better would be to name all this method the same and remove the dependency completely.
class FileSystem {
save(data) {
// Implementation
}
}
class ExternalDB {
save(data) {
// Implementation
}
}
class LocalPersistance {
save(data) {
// Implementation
}
}
class PersistanceManager {
saveData(db, data) {
db.save(data)
}
}
As we changed names to save
we can just call save
method in our parent without knowing which instance we are working with.
Want to conquer your next JavaScript interview? Download my FREE PDF - Pass Your JS Interview with Confidence and start preparing for success today!