SOLID principles Using JavaScript
Explain solid principles using JavaScript and Typescript examples.
SOLID
SOLID stands for 5 of the following principles,
- Single Responsibility Principle
- Open-Closed Principle
- Liskov Substitution Principle
- Interface Segregation Principle
- Dependency Inversion Principle
Single Responsibility Principle
The Single Responsibility Principle implies,
- There should be one single reason to change a module
When violating the single responsibility principle,
class ConcertLineups {
constructor(maximumBandLimits) {
this.lineups = [];
}
addBandsToLineup(bandName) {
this.lineups.push(bandName);
}
displayLineups() {
console.log(this.lineups);
}
}
const concertLineups = new ConcertLineups();
concertLineups.addBandsToLineup('Warfaze');
concertLineups.addBandsToLineup('Karnival');
concertLineups.addBandsToLineup('SBC');
concertLineups.displayLineups();
Here the ConcertLineups
has two reasons to change,
- How we are adding the lineups
- How we display the lineups
Now we are just console
the lineups. Later there can be a requirement, we want to persist the log or send the log to an analytics service.
A better approach using single responsibility principles is,
class Logger {
log(message) {
console.log(message);
}
}
class ConcertLineups {
constructor(maximumBandLimits) {
this.lineups = [];
this.logger = new Logger();
}
addBandsToLineup(bandName) {
this.lineups.push(bandName);
}
displayLineups() {
this.logger.log(this.lineups);
}
}
const concertLineups = new ConcertLineups();
concertLineups.addBandsToLineup('Warfaze');
concertLineups.addBandsToLineup('Karnival');
concertLineups.addBandsToLineup('SBC');
concertLineups.displayLineups();
Here we isolate the logging in a separate module. Any time our logging mechanism is being changed, we can simply update the Logger
class.
Open Closed Principle
Open closed principle implies that,
- A module should be open for extension but closed for modification
Consider the following code, if we update another payment, we need to add another switch case. This modifies the existing class PaymentProcessor
.
class StripePayment {
constructor(paymentType) {
this.paymentType = paymentType;
}
}
class PaypalPayment {
constructor(paymentType) {
this.paymentType = paymentType;
}
}
class PaymentProcessor {
constructor(paymentAdaptar) {
this.paymentAdaptar = paymentAdaptar;
}
pay() {
switch(this.paymentAdaptar.paymentType) {
case 'Stripe':
makeStripePayment();
break;
case 'Paypal':
makePaypalPayment();
break;
}
}
}
const makeStripePayment = () => {
// make payment
}
const makePaypalPayment = () => {
// make payment
}
Instead, we can use the following way, where any time, we need a new payment adapter, we can pass it to the PaymentProcessor
, with no need to update the class.
class StripePayment {
constructor(paymentType) {
this.paymentType = paymentType;
}
makePayment () {
// pay with stripe
}
}
class PaypalPayment {
constructor(paymentType) {
this.paymentType = paymentType;
}
makePayment() {
// pay with paypal
}
}
class PaymentProcessor {
constructor(paymentAdaptar) {
this.paymentAdaptar = paymentAdaptar;
}
pay() {
this.paymentAdaptar.makePayment();
}
}
Liskov Substitution Principle
Liskov Substitution Principle implies that,
- Objects of the superclass should behave like objects of the subclass, they should be replaceable
Consider the following example, we have a superclass, Vehicle
. We created two sub classes Car
and Cycle
from the Vehicle
. The Vehicle
has a method startEngine
. But it appears the Cycle
does not have an engine.
So the object of Vehicle
and the object of Cycle
does not behave identically.
class Vehicle {
constructor(name) {
this.name = name;
}
startEngine() {
console.log(`${this.name} engine started`);
}
}
class Car extends Vehicle {
constructor(name) {
super(name);
}
}
class Cycle extends Vehicle {
constructor(name) {
super(name);
}
startEngine() {
throw new Error(`${this.name} does not have an engine.`)
}
}
const car = new Car('My Car');
car.startEngine();
const cycle = new Cycle('My Cycle');
cycle.startEngine();
To resolve this issue, we can create two more subclasses after Vehicle
, one MotorVehicle
that has an engine and another ManualVehicle
that does not have an engine.
class Vehicle {
constructor(name) {
this.name = name;
}
}
class MotorVehicle {
constructor(name) {
this.name = name;
}
startEngine() {
console.log(`${this.name} engine started`);
}
}
class ManualVehicle {
constructor(name) {
this.name = name;
}
startMoving() {
console.log(`${this.name} started moving`);
}
}
class Car extends MotorVehicle {
constructor(name) {
super(name);
}
}
class Cycle extends ManualVehicle {
constructor(name) {
super(name);
}
startEngine() {
throw new Error(`${this.name} does not have an engine.`)
}
}
const car = new Car('My Car');
car.startEngine();
const cycle = new Cycle('My Cycle');
cycle.startMoving();
Interface Segregation Principle
The interface Segregation Principle implies an interface should not have any property that is not used or required by the class.
In the following example, we have a Shape
interface that is implemented in the Square
class and Cube
class. We can calculate the volume for a cube, not for a square. So when we implement Shape
for the Square
class, it throws an error for the volume
method.
interface Shape {
area: () => void;
volume: () => void;
}
class Square implements Shape {
height: number;
width: number;
constructor(height: number) {
this.height = height;
this.width = height;
}
area () {
console.log(this.height * this.width);
}
volume() {
throw new Error('Volume can not valculated on 2d shape');
}
}
class Cube implements Shape {
height: number;
width: number;
length: number;
constructor(height: number, width: number, length: number) {
this.height = height;
this.width = height;
this.length = length;
}
area () {
console.log(this.height * this.width);
}
volume () {
console.log(this.height * this.width * this.length);
}
}
const square = new Square(5);
square.area();
square.volume();
const cube = new Cube(5, 6, 7);
cube.area();
cube.volume();
To resolve the issue, we can introduce an extended interface Shape3D
. This new interface will have the volume
method and Will use when creating the Cube
class.
interface Shape {
area: () => void;
}
interface Shape3D extends Shape {
volume: () => void;
}
class Square implements Shape {
height: number;
width: number;
constructor(height: number) {
this.height = height;
this.width = height;
}
area () {
console.log(this.height * this.width);
}
}
class Cube implements Shape3D {
height: number;
width: number;
length: number;
constructor(height: number, width: number, length: number) {
this.height = height;
this.width = height;
this.length = length;
}
area () {
console.log(this.height * this.width);
}
volume () {
console.log(this.height * this.width * this.length);
}
}
const square = new Square(5);
square.area();
const cube = new Cube(5, 6, 7);
cube.area();
cube.volume();
Dependency Inversion Principle
The dependency Inversion Principle implies,
- High-level modules should not depend on low-level modules, both should depend on abstractions
- Abstractions should not depend on details, details should depend on abstractions
In the following code, we are making CourseService
class according to the CourseController
. Here the Courservice
should not depend on CourseController
, instead, both should depend on the interface.
class CourseService {
get() {
console.log('All the courses');
}
}
class CourseController {
constructor(courseService) {
this.courseService = courseService;
}
getAllCourse() {
this.courseService.get();
}
}
const courseService = new CourseService();
const courseController = new CourseController(courseService);
courseController.getAllCourse();
To resolve this, need a service to which the CourseService
can depend on,
interface Service {
get: () => void;
}
class CourseService implements Service {
get() {
console.log('All the courses');
}
}
class CourseController {
courseService: Service;
constructor(courseService: Service) {
this.courseService = courseService;
}
getAllCourse() {
this.courseService.get();
}
}
const courseService = new CourseService();
const courseController = new CourseController(courseService);
courseController.getAllCourse();
Resources
- Clean Code: SOLID - Beau teaches JavaScript
- clean-code-javascript
- Web Dev, Simplified SOLID Design Principles
- Web Dev, Single Responsibility Principles
- Web Dev, Open Close Principles
- Web Dev, Liskov Substitution Principle
- Web Dev, Interface Segregation Principle
- Web Dev, Dependcy Inverson Principle
- Alternate of Inheritance, Liskov Substitution Principle Problem
- Educative.io