Skip to main content

Solid Principles in Angular

Published 6 min readMarkdown
Solid Principles in Angular

What is SOLID Principles in Angular?

SOLID principles are design principles used in object-oriented programming to create maintainable and scalable software. In Angular, these principles can guide the structure and design of services, components, and overall architecture. Here's a brief overview of the SOLID principles applied to Angular.

S-O-L-I-D stands for

S - Single Responsibility Principle (SRP): A class or component, service should have only one responsibility or reason to change. In Angular, this means each service or component should focus on one specific task. For example, a service should handle API calls, not UI logic.

O - Open/Closed Principle (OCP): Classes should be open for extension but closed for modification. In Angular, you can follow this by writing reusable and extendable components and services. For example, instead of changing existing logic, you can extend it by using inheritance or dependency injection.

L - Liskov Substitution Principle (LSP): Subtypes must be substitutable for their base types. In Angular, if a service or component extends another, it should be able to replace the parent without altering the application's functionality.

I - Interface Segregation Principle (ISP): Clients should not be forced to depend on methods they do not use. In Angular, instead of having large services with many responsibilities, you should create smaller, focused services that only contain relevant methods, making them easier to use and test.

D - Dependency Inversion Principle (DIP): High-level modules should not depend on low-level modules; both should depend on abstractions. In Angular, this is achieved through dependency injection, where services and components depend on abstractions (interfaces or tokens) rather than concrete implementations. This allows easier swapping of implementations.

1. Single Responsibility Principle (SRP)

Application in Angular:

In Angular, each service or component should focus on a single responsibility. For example, a service that handles user authentication should only deal with authentication-related tasks, while a component responsible for rendering the UI should not contain business logic.

Example:

Suppose we have a component that manages both the user login process and renders user details. This violates SRP because it mixes business logic (authentication) with UI logic (rendering user details).

Without SRP:

export class UserComponent {
  user: any;

  login(username: string, password: string): void {
    // Logic to authenticate user
    // Violates SRP: This component is handling both UI and business logic
    this.user = { username: username, isAuthenticated: true };
  }

  displayUser(): void {
    // Display user details in the UI
  }
}

With SRP:

// AuthService is responsible for authentication only
@Injectable({
  providedIn: 'root'
})
export class AuthService {
  login(username: string, password: string): any {
    // Handle authentication logic
    return { username: username, isAuthenticated: true };
  }
}

// UserComponent is responsible for rendering the user interface
export class UserComponent {
  user: any;

  constructor(private authService: AuthService) {}

  login(username: string, password: string): void {
    this.user = this.authService.login(username, password);
  }

  displayUser(): void {
    // Handle user UI display
  }
}

Real-world Use Case:

Think of a restaurant. The chef is responsible for cooking food, and the waiter is responsible for serving the food. The chef doesn’t serve, and the waiter doesn’t cook. In Angular, services are like chefs (they handle logic), and components are like waiters (they manage the UI).

2. Open/Closed Principle (OCP)

Application in Angular:

Angular promotes modularity and allows extending components or services without modifying existing ones. For instance, you can extend a service by creating a new class that inherits from it and overrides some behavior.

Example:

Suppose we have a PaymentService that handles different types of payments. Instead of modifying the original class every time a new payment method is added, we can create new classes that extend it.

Without OCP:

export class PaymentService {
  processPayment(type: string): void {
    if (type === 'CreditCard') {
      // Handle credit card payment
    } else if (type === 'Paypal') {
      // Handle PayPal payment
    } else {
      throw new Error('Unknown payment method');
    }
  }
}

With OCP:

export interface PaymentMethod {
  processPayment(): void;
}

// CreditCardPayment class
export class CreditCardPayment implements PaymentMethod {
  processPayment(): void {
    // Handle credit card payment
  }
}

// PayPalPayment class
export class PayPalPayment implements PaymentMethod {
  processPayment(): void {
    // Handle PayPal payment
  }
}

// PaymentService class, open for extension, closed for modification
export class PaymentService {
  processPayment(paymentMethod: PaymentMethod): void {
    paymentMethod.processPayment();
  }
}

Real-world Use Case:

In a bank, different departments (savings, loans, credit cards) deal with various transactions. If the bank introduces a new type of transaction, it doesn’t alter the core structure of these departments but adds a new department to handle it. Similarly, Angular services should allow new functionality (new departments) without changing existing services (departments).

3. Liskov Substitution Principle (LSP)

Application in Angular:

This principle ensures that if you have a service or class that is a subclass of another, it should be able to be used interchangeably without causing issues. This is essential when extending services or components in Angular.

Example:

If we have a BaseComponent and a DerivedComponent, the derived component should behave in a way that the base component can be replaced by it without breaking functionality.

Violating LSP:

export class BaseComponent {
  render(): void {
    console.log('Rendering BaseComponent');
  }
}

export class DerivedComponent extends BaseComponent {
  render(): void {
    throw new Error('Rendering not supported');
  }
}

Complying with LSP:

export class BaseComponent {
  render(): void {
    console.log('Rendering BaseComponent');
  }
}

export class DerivedComponent extends BaseComponent {
  render(): void {
    console.log('Rendering DerivedComponent');
  }
}

Real-world Use Case:

Imagine a family with a parent and a child. If the parent assigns a task to the child (like cleaning a room), the child should be able to perform the task without issues. In Angular, subclasses should be able to perform tasks defined by their parent classes.

4. Interface Segregation Principle (ISP)

Application in Angular:

In Angular, this principle is applied by creating smaller, focused services or interfaces instead of monolithic ones. Each service or interface should have a specific purpose, making the application easier to maintain and extend.

Example:

Suppose we have a UserService that handles user data but also includes methods for updating user settings, which not all components need.

Without ISP:

export interface UserService {
  getUserDetails(): any;
  updateUserSettings(settings: any): void;
}

With ISP:

export interface UserDetailsService {
  getUserDetails(): any;
}

export interface UserSettingsService {
  updateUserSettings(settings: any): void;
}

Real-world Use Case:

Imagine different types of workers: a chef who only cooks and a waiter who only serves. You wouldn’t ask the waiter to cook. Similarly, in Angular, you separate services/interfaces so components don’t depend on methods they don’t need.

5. Dependency Inversion Principle (DIP)

Application in Angular:

Angular’s dependency injection system makes it easier to follow this principle by allowing services to depend on abstractions (interfaces) instead of concrete implementations. This makes your code more flexible and testable.

Example:

Suppose a Component directly depends on a DatabaseService. This tightly couples the component to the database, making it harder to change the database implementation later.

Without DIP:

export class DatabaseService {
  getData(): any {
    // Logic to get data from a specific database
  }
}

export class DataComponent {
  constructor(private dbService: DatabaseService) {}

  loadData(): void {
    this.dbService.getData();
  }
}

With DIP:

export interface DataSource {
  getData(): any;
}

export class DatabaseService implements DataSource {
  getData(): any {
    // Logic to get data from a specific database
  }
}

export class DataComponent {
  constructor(private dataSource: DataSource) {}

  loadData(): void {
    this.dataSource.getData();
  }
}

Real-world Use Case:

In a company, a manager (high-level) doesn’t care about the specifics of how employees (low-level) work, but rather that tasks get done. The manager depends on roles (abstractions) rather than specific employees. Similarly, Angular components should depend on abstractions (interfaces) rather than concrete implementations.

No comments yet. Be the first to comment.