The SOLID principles are a set of design principles in OOP that help create robust, maintainable, and scalable code. Implementing them in Typescript is straightforward because of its support for classes, interfaces, and strong typing.
_In this post, I provide a brief overview of SOLID to help you understand it more easily, and I will dive into each part as soon as I have time _ =))).
Here's a detailed explanation of the SOLID principles with Typescript examples:
S - Single Responsibility Principle (SRP)
A class should have only one reason to change
This means every class should only do one thing. If it need to do multiple things, it should delegate some responsibilities to other class.
For Example:
❌Bad
class UserService {
createUser(user: string){
console.log(`User ${user} created`);
} // UserService
sendWelcomeEmail(user: string){
console.log(`Welcome email sent to ${user}`)
} //EmailService
}
✅Good
class UserService{
createUser(user: string): string
{
console.log(`User ${user} created`);
return user;
}
}
class EmailService{
sendMail(user: string){
console.log(`Send mail to ${user}`);
}
}
So what should I do apply SRP in my code correctly and efficially?
=> Group responsibilities based on the business logic.
- SendMail involves email and communication -> belongs in EmailService.
- CreateUser involves user creation (or CRUD user) -> belongs in UserServices.
O - Open/Closed Principle (OCP)
Software entities should open for extension, but closed for modification.
This mean you be able to add new functionality without changing existing code.
❌Bad
class DiscountService{
calculateDiscount(typeOfDiscount:string, amount: number) : number {
if (typeOfDiscount == 'standard') {
return amount * 0.1;
}
else if (typeOfDiscount == 'premium') {
return amount * 0.5;
}
return 0;
}
}
let standard = new DiscountService();
standard.calculateDiscount('standard', 10);
Why is it bad?
If you receive a request to add DiamondDiscount with a 100% discount and additional parameters like isMemberShip
, you would need to modify the calculateDiscount()
method like:
class DiscountService{
calculateDiscount(typeOfDiscount:string, amount: number, isMemberShip: boolean) : number {
if (typeOfDiscount == 'standard') {
return amount * 0.1;
}
else if (typeOfDiscount == 'premium') {
return amount * 0.5;
}
else if (typeOfDiscount == 'diamond') {
if (isMemberShip){
return amount;
}
}
return 0;
}
}
let standard = new DiscountService();
standard.calculateDiscount('standard', 10); // => You must change this one if update calculateDiscount
This could potentially affect other methods that are already using calculateDiscount()
(you would need to search for any code that uses calculateDiscount
and update it to include the isMemberShip
.
✅Good
interface Discount {
calculateDiscount(amount: number) : number;
}
class StandardDiscount implements Discount {
calculateDiscount(amount: number): number {
return amount * 0.1;
}
}
class DiamondDiscount implements Discount {
private isMemberShip: boolean = false;
constructor(isMemberShip: boolean) {
}
calculateDiscount(amount: number): number {
if (this.isMemberShip){
return amount;
}
return amount * 0.5;
}
}
class DisCountService {
constructor(private readonly discountStrategy: Discount) {
}
calculateDiscount(amount: number) {
return this.discountStrategy.calculateDiscount(amount);
}
}
const discount = new DisCountService( new StandardDiscount()); //Do not affect this code
All the problems are fixed with OCP.
L - Liskov Substitution Principle (LSP)
Derived classes must be subtitutable for their base classes.
This means you should be able to replace a parent class with a subclass withou breaking functionality
❌Bad
class Rectangle {
constructor(protected width: number, protected height: number) {}
setWidth(width: number) {
this.width = width;
}
setHeight(height: number) {
this.height = height;
}
getArea(): number {
return this.width * this.height;
}
}
class Square extends Rectangle {
setWidth(width: number) {
this.width = width;
this.height = width; // A square's height is always equal to its width
}
setHeight(height: number) {
this.height = height;
this.width = height; // A square's width is always equal to its height
}
}
// Usage
const shape: Rectangle = new Square(5, 5);
shape.setWidth(10);
console.log(shape.getArea()); // Expected: 50, Actual: 100
why is it bad?
-
Square
violates the bahavior expected ofRetangle
, Changing the width or height of aSquare
changes both dimensions, which is now how aRectangle
works. - Substituting a
Square
for aRectangle
lead to incorrect behavior:
const shape: Rectangle = new Square(5, 5); //Substituting a `Square` for a `Rectangle` lead to incorrect behavior
shape.setWidth(10);
✅Good:
abstract class Shape {
abstract getArea(): number;
}
class Rectangle extends Shape {
constructor(protected width: number, protected height: number) {
super();
}
getArea(): number {
return this.width * this.height;
}
}
class Square extends Shape {
constructor(protected side: number) {
super();
}
getArea(): number {
return this.side * this.side;
}
}
// Usage
const shape: Shape = new Rectangle(10, 5);
console.log(shape.getArea());
Why is it better?
-
Shape
defines a commongetArea()
method, and the bothRectangle
andSquare
provide their specific implementations. - The
Square
class no longer inherits fromRectangle
. This ensures thatSquare
does not inheritsetWidth()
orsetHeight()
methods that don’t make sense for a square.
So what should I do apply LSP in my code correctly and efficially?
=> Avoid forcing inheritance when the subclass doesn’t fully align with the parent class's behavior.
[...Continue with next lession :)]
Top comments (0)