DEV Community

Cover image for Design Patterns Every Angular Developer Should Know
bytebantz
bytebantz

Posted on • Edited on

1

Design Patterns Every Angular Developer Should Know

New developers might not consider how the code will handle future changes or how easily it can be reused, expanded, or maintained. In the worst-case scenario, developers might even need to completely rebuild the project from scratch when things become too hard to fix. To avoid these issues, design patterns were created.

Design patterns are like blueprints that make your code easier to manage and change over time.

Design patterns are tried-and-tested solutions for common design issues in software development. Instead of giving a full solution, they act as best practices for solving common software design challenges.

Design patterns in Angular help you write cleaner, more organized code that makes your app easier to build, grow, and fix over time.

Why Design Patterns are important

1. Scalability

Design Patterns help break your app into small, reusable pieces. This makes it easy to add new features later without messing up existing ones.

2. Maintainability

Following design patterns ensures your code has a clear, organized structure.

For example using patterns like the Singleton Pattern makes the app more maintainable because you only have to change one piece of code in the service, and all the components that depend on it will automatically adapt.

3. Testability

Design patterns promotes the separation of concerns which makes it easier to mock and test components

4. Code Reusability

Patterns encourage writing reusable code.

For example, patterns like Factory and Singleton help you organize similar tasks in one place, so you can easily reuse them without rewriting the same logic.

1. Singleton Pattern

The Singleton Pattern ensures that a class only has one instance and allows that instance to be shared globally.

In Angular, this is often used for services, making sure only one instance of a service is used throughout the app.

A service becomes a singleton when it's provided at the root level. To do this, you use the providedIn: 'root' setting in the service’s metadata.

Example:

import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root' // This makes the service a singleton
})
export class MyService {
  constructor() { }

  // Your service methods here
}

Enter fullscreen mode Exit fullscreen mode

2. Facade Pattern

The Facade Pattern offers a simplified interface to interact with complex subsystems.

When you don’t use the Facade Pattern, components directly communicate with multiple services to handle different operations. This increases complexity as multiple components may replicate the same logic, leading to code duplication.

With the Facade Pattern instead of components calling multiple services, they interact with the facade. This makes the components leaner and easier to maintain, while business logic remains in one centralized place (the facade)

Example

@Injectable({
  providedIn: 'root',
})
export class CartFacade {
  constructor(private cartService: CartService, private orderService: OrderService) {}

  // Add product to cart
  addToCart(product: Product) {
    this.cartService.addItem(product);
  }

  // Remove product from cart
  removeFromCart(product: Product) {
    this.cartService.removeItem(product);
  }

  // Process checkout using the current cart
  checkout() {
    const cart = this.cartService.getCart();
    this.orderService.processOrder(cart);
  }

  // Get the current items in the cart
  getCartItems() {
    return this.cartService.getCart();
  }
}

Enter fullscreen mode Exit fullscreen mode

To use the Facade Pattern in your Angular components, you simply inject the facade service into the component.

constructor(private cartFacade: CartFacade) {}

Enter fullscreen mode Exit fullscreen mode

3. Factory Pattern

The Factory Pattern is a way of creating objects without specifying the exact class of the object that will be created.

It handles the object creation process and conceals the details of how instances are instantiated.

The Factory Pattern can create objects dynamically, which is useful of you don’t know in advance the exact objects/services your application will need at runtime

Example

Let's say you want to create different types of user notifications (e.g., email, SMS). You can implement the Factory Pattern as follows:

// src/app/notification.interface.ts // src/app/notification.interface.ts
export interface Notification {
  send(message: string): void;
}

// src/app/notification.service.ts
import { Injectable } from '@angular/core';
import { Notification } from './notification.interface';

@Injectable({
  providedIn: 'root',
})
export class EmailNotification implements Notification {
  send(message: string): void {
    console.log(`Email: ${message}`);
  }
}

@Injectable({
  providedIn: 'root',
})
export class SmsNotification implements Notification {
  send(message: string): void {
    console.log(`SMS: ${message}`);
  }
}

Enter fullscreen mode Exit fullscreen mode

Now, let’s create a factory

// src/app/notification.factory.ts
import { Injectable } from '@angular/core';
import { EmailNotification } from './notification.service';
import { SmsNotification } from './notification.service';
import { Notification } from './notification.interface';

@Injectable({
  providedIn: 'root',
})
export class NotificationFactory {
  constructor(
    private emailNotification: EmailNotification,
    private smsNotification: SmsNotification
  ) {}

  createNotification(type: string): Notification {
    if (type === 'email') {
      return this.emailNotification;
    } else if (type === 'sms') {
      return this.smsNotification;
    } else {
      throw new Error('Unknown notification type');
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

Now, in your component, you can use the factory like this:

// src/app/notification/notification.component.ts
import { Component } from '@angular/core';
import { NotificationFactory } from '../notification.factory';
import { Notification } from '../notification.interface';

@Component({
  selector: 'app-notification',
  template: `
    <h1>Send Notification</h1>
    <button (click)="send('email')">Send Email</button>
    <button (click)="send('sms')">Send SMS</button>
  `,
})
export class NotificationComponent {
  constructor(private notificationFactory: NotificationFactory) {}

  send(type: string) {
    const notification: Notification = this.notificationFactory.createNotification(type);
    notification.send(`This is a ${type} notification!`);
  }
}

Enter fullscreen mode Exit fullscreen mode

4. Strategy Pattern

Strategy Pattern deals with behaviors (algorithms) and how they are used.

Strategy patterns are useful when you need to switch between different algorithms or functionalities based on the context.

Example

This service will dynamically select the payment method based on the user’s choice.

@Injectable({
  providedIn: 'root',
})
export class PaymentService {
  constructor(private paypalPayment: PayPalPayment, private creditCardPayment: CreditCardPayment) {}

  processPayment(method: 'paypal' | 'creditcard', amount: number): string {
    if (method === 'paypal') {
      return this.paypalPayment.processPayment(amount);
    } else {
      return this.creditCardPayment.processPayment(amount);
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

Conclusion

Design patterns provide crucial benefits in terms of scalability, maintainability, testability, and code reusability. In Angular, using patterns like Singleton, Facade, Factory, and Strategy will help you build applications that are easier to manage and expand. These patterns are essential for any large or growing application where clean, organized, and maintainable code is a priority.

Check out my deep dives on → Gumroad

Heroku

Amplify your impact where it matters most — building exceptional apps.

Leave the infrastructure headaches to us, while you focus on pushing boundaries, realizing your vision, and making a lasting impression on your users.

Get Started

Top comments (0)

5 Playwright CLI Flags That Will Transform Your Testing Workflow

  • 0:56 --last-failed
  • 2:34 --only-changed
  • 4:27 --repeat-each
  • 5:15 --forbid-only
  • 5:51 --ui --headed --workers 1

Learn how these powerful command-line options can save you time, strengthen your test suite, and streamline your Playwright testing experience. Click on any timestamp above to jump directly to that section in the tutorial!

👋 Kindness is contagious

Found this article valuable? Consider leaving a ❤️ or sharing your thoughts!

Got it!