Understanding SOLID design principles with easy coding examples

Aneeqa Khan - Jul 15 - - Dev Community

This article provides a clear and concise overview of the SOLID design principles, accompanied by straightforward code examples to help you grasp each concept with ease.

SOLID is a set of five design principles intended to make software designs more understandable, flexible, and maintainable.

Table of Contents


The principles are particularly useful in object-oriented design and are commonly applied in front-end and back-end development. Here’s a brief overview of each SOLID principle with a code example in TypeScript:

S — Single Responsibility Principle (SRP)

A class should have one and only one reason to change, meaning it should have only one job or responsibility.

This principle encourages a focused approach, ensuring that changes or updates to one aspect of your UI won’t inadvertently affect unrelated parts.

// UserProfile.tsx
import React from 'react';

interface UserProfileProps {
  username: string;
  email: string;
}

const UserProfile: React.FC<UserProfileProps> = ({ username, email }) => {
  return (
    <div>
      <h2>{username}</h2>
      <p>{email}</p>
    </div>
  );
};

export default UserProfile;
Enter fullscreen mode Exit fullscreen mode

Here, UserProfile is responsible only for displaying user information.


O — Open-Closed Principle (OCP)

Software entities should be open for extension but closed for modification.

This approach ensures that the core components remain stable and unchanged, reducing the risk of unintended side effects when adding new functionalities.

// Alert.tsx
import React from 'react';

interface AlertProps {
  message: string;
}

const Alert: React.FC<AlertProps> = ({ message }) => {
  return <div className="alert">{message}</div>;
};

export default Alert;

// SuccessAlert.tsx
import React from 'react';
import Alert from './Alert';

const SuccessAlert: React.FC<{ message: string }> = ({ message }) => {
  return <Alert message={`Success: ${message}`} />;
};

export default SuccessAlert;
Enter fullscreen mode Exit fullscreen mode

Alert can be extended by SuccessAlert without modifying the original Alert component.


L — Liskov Substitution Principle (LSP)

Objects of a superclass should be replaceable with objects of its subclasses without affecting the correctness of the program.

In simpler terms, if you have a base component or module, any derived components should be usable in place of the base component without causing unexpected issues.

// BaseButton.tsx
import React from 'react';

interface BaseButtonProps {
  onClick: () => void;
  label: string;
}

const BaseButton: React.FC<BaseButtonProps> = ({ onClick, label }) => {
  return <button onClick={onClick}>{label}</button>;
};

export default BaseButton;

// IconButton.tsx
import React from 'react';
import BaseButton from './BaseButton';

interface IconButtonProps extends BaseButtonProps {
  icon: string;
}

const IconButton: React.FC<IconButtonProps> = ({ onClick, label, icon }) => {
  return (
    <BaseButton onClick={onClick} label={<span><i className={icon}></i> {label}</span>} />
  );
};

export default IconButton;
Enter fullscreen mode Exit fullscreen mode

IconButton can be used anywhere BaseButton without affecting the correctness of the application.


I — Interface Segregation Principle (ISP)

No client should be forced to depend on methods it does not use. This means creating specific interfaces for specific needs.

In other words, rather than creating a single large interface, break it down into smaller, focused interfaces that are tailored to individual components.

// interfaces.ts
export interface Flyable {
  fly(): void;
}

export interface Swimmable {
  swim(): void;
}

// Bird.ts
import { Flyable } from './interfaces';

class Bird implements Flyable {
  fly() {
    console.log('Bird is flying');
  }
}

// Fish.ts
import { Swimmable } from './interfaces';

class Fish implements Swimmable {
  swim() {
    console.log('Fish is swimming');
  }
}
Enter fullscreen mode Exit fullscreen mode

Separate interfaces Flyable and Swimmable are created to ensure that classes only implement what they need.


D — Dependency Inversion Principle (DIP)

High-level modules should not depend on low-level modules but on abstractions. Both should depend on abstractions.

In simpler terms, instead of components directly depending on each other, they rely on interfaces or abstract classes, making the code more adaptable to changes.

// Logger.ts
export interface Logger {
  log(message: string): void;
}

export class ConsoleLogger implements Logger {
  log(message: string) {
    console.log(message);
  }
}

// UserService.ts
import { Logger } from './Logger';

class UserService {
  constructor(private logger: Logger) {}

  createUser(username: string) {
    this.logger.log(`User created: ${username}`);
  }
}

// App.ts
import { UserService } from './UserService';
import { ConsoleLogger } from './Logger';

const logger = new ConsoleLogger();
const userService = new UserService(logger);

userService.createUser('JohnDoe');
Enter fullscreen mode Exit fullscreen mode

Here, UserService depends on the Logger abstraction, making it flexible to change the logging mechanism without altering the UserService.


These SOLID principles help in creating software that is easy to maintain, extend, and refactor, which is essential for developing robust front-end and back-end applications.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .