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
}
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();
}
}
To use the Facade Pattern in your Angular components, you simply inject the facade service into the component.
constructor(private cartFacade: CartFacade) {}
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}`);
}
}
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');
}
}
}
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!`);
}
}
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);
}
}
}
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
Top comments (0)