Activity 25: SOLID PRINCIPLES in Angular

Funda, Mark Harvey D. BSIT 4TH Year

Introduction of SOLID Principles

The SOLID principles were first introduced by the famous Computer Scientist Robert J. Martin (a.k.a Uncle Bob) in his paper in 2000. But the SOLID acronym was introduced later by Michael Feathers.

Uncle Bob is also the author of bestselling books Clean Code and Clean Architecture, and is one of the participants of the "Agile Alliance".

Therefore, it is not a surprise that all these concepts of clean coding, object-oriented architecture, and design patterns are somehow connected and complementary to each other.

Let's look at each principle one by one. Following the SOLID acronym, they are:

Single Responsibility Principle

  • Definition: A class/module should have only one reason to change, i.e., it should have one responsibility or task.

  • Application in Angular:
    Each component, service, or directive should focus on a single task. Separation of concerns ensures maintainability.

Example Code:

// Violating SRP
@Component({
  selector: 'app-user-profile',
  template: '<div>{{ user.name }}</div>',
})
export class UserProfileComponent {
  user = { name: 'John' };

  fetchData() {
    // API call logic
  }

  logActivity() {
    // Logging user activity logic
  }
}

// Correcting SRP
@Component({
  selector: 'app-user-profile',
  template: '<div>{{ user.name }}</div>',
})
export class UserProfileComponent {
  @Input() user: { name: string };
}

// Service for API calls
@Injectable({
  providedIn: 'root',
})
export class UserService {
  fetchData() {
    // API call logic
  }
}

// Service for logging
@Injectable({
  providedIn: 'root',
})
export class LoggerService {
  logActivity(activity: string) {
    console.log(activity);
  }
}

Real-world Use Case:

  • A UserService handles API calls, and LoggerService logs events. This separation makes testing easier and reduces coupling.

Open/Closed Principle (OCP)

  • Definition: Classes/modules should be open for extension but closed for modification.

  • Application in Angular:
    Leverage inheritance or dependency injection to extend functionality without modifying the original code.

Example Code:

typescriptCopy code// Base Service
@Injectable({
  providedIn: 'root',
})
export class NotificationService {
  notify(message: string) {
    console.log('Notification:', message);
  }
}

// Extended Service
@Injectable({
  providedIn: 'root',
})
export class EmailNotificationService extends NotificationService {
  notify(message: string) {
    console.log('Email Notification:', message);
  }
}

Real-world Use Case:

  • A NotificationService is extended to support email notifications without altering its base implementation.

Liskov Substitution Principle (LSP)

  • Definition: Subtypes must be substitutable for their base types without altering the correctness of the program.

  • Application in Angular:
    Use interfaces or abstract classes to ensure child components or services behave consistently.

Example Code:

typescriptCopy code// Base Interface
export interface Notification {
  notify(message: string): void;
}

// Implementations
export class SMSNotification implements Notification {
  notify(message: string) {
    console.log('SMS:', message);
  }
}

export class PushNotification implements Notification {
  notify(message: string) {
    console.log('Push:', message);
  }
}

// Usage
function sendNotification(notification: Notification, message: string) {
  notification.notify(message);
}

sendNotification(new SMSNotification(), 'Hello!');
sendNotification(new PushNotification(), 'Welcome!');

Real-world Use Case:

  • SMS and push notifications can be swapped in the sendNotification function without breaking the application.

Interface Segregation Principle (ISP)

  • Definition: A class/module should not be forced to implement interfaces it does not use.

  • Application in Angular:
    Create small, focused interfaces that are specific to a task.

Example Code:

typescriptCopy code// Violating ISP
export interface MultiFunctionDevice {
  print(): void;
  scan(): void;
  fax(): void;
}

// Correcting ISP
export interface Printer {
  print(): void;
}

export interface Scanner {
  scan(): void;
}

export interface FaxMachine {
  fax(): void;
}

export class AllInOnePrinter implements Printer, Scanner, FaxMachine {
  print() {
    console.log('Printing...');
  }

  scan() {
    console.log('Scanning...');
  }

  fax() {
    console.log('Faxing...');
  }
}

Real-world Use Case:

  • Devices with specific functionality like printers or scanners implement only the necessary interfaces.

Dependency Inversion Principle (DIP)

  • Definition: High-level modules should not depend on low-level modules but on abstractions.

  • Application in Angular:
    Use dependency injection to decouple modules.

Example Code:

typescriptCopy code// Abstract class for storage
export abstract class StorageService {
  abstract saveData(data: string): void;
}

// LocalStorage implementation
@Injectable({
  providedIn: 'root',
})
export class LocalStorageService extends StorageService {
  saveData(data: string) {
    localStorage.setItem('data', data);
  }
}

// Component depending on abstraction
@Component({
  selector: 'app-storage',
  template: `<button (click)="save()">Save</button>`,
})
export class StorageComponent {
  constructor(private storageService: StorageService) {}

  save() {
    this.storageService.saveData('Angular SOLID Principles');
  }
}

Real-world Use Case:

  • A StorageService abstraction allows switching from LocalStorageService to another storage mechanism without changing the component code.

Links:

https://angular.love/angular-and-solid-principles

https://www.c-sharpcorner.com/article/applying-solid-principles-in-angular-development/

https://newcubator.com/blog/web/solid-principles-in-angular

https://blog.nashtechglobal.com/applying-solid-principles-in-angular/

https://www.freecodecamp.org/news/solid-principles-explained-in-plain-english/