SOLID Principles - DeanHristov/ts-design-patterns-cheat-sheet GitHub Wiki
The idea behind the SOLID is to make software designs more understandable, flexible, and maintainable - So simple, right?
The point of this principle is to reduce complexity of our class/function by making it responsible only for one thing!
Wrong implementation
class DB {
doQuery<T>(url: string): Promise<T> {
// TODO Do some code
this.doLogs();
}
// This class shouldn't be responsible to log anything!
doLogs(): void {
//TODO Write who made the query
//TODO What is retrieved from the query
....
}
}
Correct implementation
class Logger {
private static instance: Logger;
private constructor() {}
static getInstance() {
if (!Logger.instance) Logger.instance = new Logger();
return Logger.instance;
}
doLogs(): void {
//TODO Write who made the query
//TODO What is retrieved from the query
}
}
class DB {
private readonly logger: Logger;
constructor() {
this.logger = Logger.getInstance();
}
doQuery<T>(url: string): Promise<T> {
// TODO Do some code
this.logger.doLogs();
}
}
const db = new DB();
db.doQuery<IUser>("select * from users as u where u.uid = 12345");
The main idea of this principle is to keep existing code from breaking when you implement new features. That means the class/function should be open for extension but closed for modification.
interface ICalculator {
getArea(shape: Shape): number;
}
class Shape {
readonly width: number;
readonly height: number;
readonly type: SHAPE_TYPES;
constructor(a: number, b: number, type: SHAPE_TYPES) {
this.width = a;
this.height = b;
this.type = type;
}
}
Wrong implementation
enum SHAPE_TYPES {
RECTANGLE = "RECTANGLE",
SQUARE = "SQUARE",
}
class Calculator implements ICalculator {
getArea(shape: Shape): number {
switch (shape.type) {
case SHAPE_TYPES.RECTANGLE:
return shape.width * shape.height;
break;
case SHAPE_TYPES.SQUARE:
return shape.width ** 2;
break;
}
return -1;
}
}
const calculator: Calculator = new Calculator();
const rectangle: Shape = new Shape(100, 150, SHAPE_TYPES.RECTANGLE);
const square: Shape = new Shape(100, 100, SHAPE_TYPES.SQUARE);
calculator.getArea(rectangle);
calculator.getArea(square);
Correct implementation
class RectangleCalculator implements ICalculator {
getArea(shape: Shape): number {
return shape.width * shape.height;
}
}
class SquareCalculator implements ICalculator {
getArea(shape: Shape): number {
return shape.width ** 2;
}
}
const rectangleCalculator: RectangleCalculator = new RectangleCalculator();
const squareCalculator: SquareCalculator = new SquareCalculator();
const rectangle: Shape = new Shape(100, 150, SHAPE_TYPES.RECTANGLE);
const square: Shape = new Shape(100, 100, SHAPE_TYPES.SQUARE);
rectangleCalculator.getArea(rectangle);
squareCalculator.getArea(square);
This principle encourages us to use composition instead of inheritance where is possible.
interface ICalculator {
getArea(shape: Shape): number;
}
class Calculator implements ICalculator {
private shape: Shape;
constructor(shape: Shape) {
this.shape = shape;
}
getArea(): number {
switch (this.shape.type) {
case SHAPE_TYPES.RECTANGLE:
return this.shape.width * this.shape.height;
break;
case SHAPE_TYPES.SQUARE:
return this.shape.width ** 2;
break;
}
return -1;
}
}
const rectangle: Shape = new Shape(100, 150, SHAPE_TYPES.RECTANGLE);
const square: Shape = new Shape(100, 100, SHAPE_TYPES.SQUARE);
const rectangleCalculator: Calculator = new Calculator(rectangle);
const squareCalculator: Calculator = new Calculator(square);
rectangleCalculator.getArea();
squareCalculator.getArea();
Classes shouldn’t be forced to depend on methods they do not use. In other words we are using separation of the interfaces:
Wrong implementation!
interface IVehicle {
drive(): void;
fly(): void;
}
class Car implements IVehicle {
drive(): void {
// TODO Do some code
}
fly(): void {
// the car still cannot fly!
}
}
class Plane implements IVehicle {
drive(): void {
// the Plane can only fly!
}
fly(): void {
// TODO Do some code
}
}
Correct implementation!
interface ICar {
drive(): void;
}
interface IPlane {
fly(): void;
}
class Car implements ICar {
drive(): void {
// TODO Do some code
}
}
class Plane implements IPlane {
fly(): void {
// TODO Do some code
}
}
const car: Car = new Car();
const plane: Plane = new Plane();
car.drive();
plane.fly();
High-level classes/modules shouldn’t depend on low-level classes/modules. Both should depend on abstractions. Abstractions shouldn’t depend on details. Details should depend on abstractions. In other words using interfaces/abstract classes! Example:
interface ILogistic {
deliver(): void;
}
class TruckLogistic implements ILogistic {
deliver(): void {
// TODO Do some code
}
}
class ShipLogistic implements ILogistic {
deliver(): void {
// TODO Do some code
}
}
const truck: TruckLogistic = new TruckLogistic();
const ship: ShipLogistic = new ShipLogistic();
truck.deliver();
ship.deliver();