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.

SOLID

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!

Did you like my post? Share it with friends!
Don't miss a thing!
Follow me on Youtube, Twitter or Instagram.
Oleksandr Kocherhin
Oleksandr Kocherhin is a full-stack developer with a passion for learning and sharing knowledge on Monsterlessons Academy and on his YouTube channel. With around 15 years of programming experience and nearly 9 years of teaching, he has a deep understanding of both disciplines. He believes in learning by doing, a philosophy that is reflected in every course he teaches. He loves exploring new web and mobile technologies, and his courses are designed to give students an edge in the fast-moving tech industry.