In Angular, which is a popular JavaScript framework for building web applications, several
design patterns are commonly used to structure and organize code. These design patterns help developers create maintainable, scalable, and modular applications. Here are some of the design patterns frequently utilized in Angular:
1. Singleton Pattern: Angular services are often implemented using the Singleton pattern. A service is instantiated once and shared across multiple components, allowing them to communicate and share data.
To implement the Singleton pattern in Angular, you can follow these steps:
a. Create a service using the Angular CLI:
ng generate service MySingletonService
b. There are two ways to create a single service in angular that is by using -
-> providedIn property
-> NgModule providers arrays
c. Open the generated service file (`my-singleton-service.service.ts`) and modify it as follows:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class MySingletonService {
// Your service implementation goes here
}
d. The `providedIn: 'root'` property in the `@Injectable` decorator is key to implementing the Singleton pattern in Angular. This tells Angular to provide the service at the root level, making it accessible throughout the application.
e. You can now use the `MySingletonService` in your components by injecting it into their constructors:
import { Component } from '@angular/core';
import { MySingletonService } from './my-singleton-service.service';
@Component({
selector: 'app-my-component',
template: '...',
})
export class MyComponent {
constructor(private mySingletonService: MySingletonService) {
// Access the shared service instance here
}
}
By injecting `MySingletonService` into multiple components, you will be accessing the same instance of the service across the application, ensuring data consistency and sharing.
It's important to note that Angular itself manages the lifecycle of the singleton service. It creates and maintains a single instance of the service and shares it among components that request it.
In the case of
NgModule providers array, a singleton service is created by passing the service as a value to the providers array and if the NgModule is root app module then the service will be available throughout the application as a singleton service.
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { MySingletonService } from './my-singleton-service.service';
import { AppComponent } from './app.component';
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule],
providers: [MySingletonService], // Add the service here
bootstrap: [AppComponent]
})
export class AppModule { }
That's how you can implement the Singleton pattern in Angular using a service. This allows you to share data, maintain state, and provide centralized functionality throughout your application.
2. Dependency Injection (DI) Pattern: Angular utilizes the DI pattern to manage the dependencies between components and services. With DI, the required dependencies are provided to a component or service through constructor injection or property injection, promoting loose coupling and testability.
// Component using DI
constructor(private productService: ProductService) {
// Use the productService
}
3. Observer Pattern: Angular leverages the Observer pattern through the EventEmitter class and the RxJS library. Components can emit events using EventEmitters, and other components can subscribe to these events to react accordingly.
// Component emitting an event
@Output() productSelected = new EventEmitter();
selectProduct(product: Product) {
this.productSelected.emit(product);
}
// Component subscribing to an event
4. Strategy Pattern: The Strategy pattern enables you to dynamically select and switch between different strategies at runtime based on specific conditions or requirements. By encapsulating these behaviors in separate classes, components can switch between strategies based on specific conditions.
Here's an example of implementing the Strategy pattern in Angular:
a. Define an interface that represents the common behavior of the strategies. Let's assume we have a payment processing scenario:
// payment-strategy.interface.ts
export interface PaymentStrategy {
processPayment(amount: number): void;
}
b. Implement multiple strategies by creating separate classes that implement the `PaymentStrategy` interface. Each class will provide its own implementation of the `processPayment` method:
// credit-card-strategy.ts
export class CreditCardStrategy implements PaymentStrategy {
processPayment(amount: number): void {
console.log(`Processing credit card payment of $${amount}`);
// Perform credit card payment processing logic here
}
}
// paypal-strategy.ts
export class PaypalStrategy implements PaymentStrategy {
processPayment(amount: number): void {
console.log(`Processing PayPal payment of $${amount}`);
// Perform PayPal payment processing logic here
}
}
c. Create a context class that will use the strategies and provide a method to set the active strategy:
// payment-context.ts
import { PaymentStrategy } from './payment-strategy.interface';
export class PaymentContext {
private strategy: PaymentStrategy;
setStrategy(strategy: PaymentStrategy): void {
this.strategy = strategy;
}
processPayment(amount: number): void {
this.strategy.processPayment(amount);
}
}
d. Now, you can utilize the strategies in your Angular components or services. For example:
import { Component } from '@angular/core';
import { PaymentContext } from './payment-context';
import { CreditCardStrategy } from './credit-card-strategy';
import { PaypalStrategy } from './paypal-strategy';
@Component({
selector: 'app-payment-component',
template: '...',
})
export class PaymentComponent {
constructor(private paymentContext: PaymentContext) {}
processCreditCardPayment(amount: number): void {
this.paymentContext.setStrategy(new CreditCardStrategy());
this.paymentContext.processPayment(amount);
}
processPaypalPayment(amount: number): void {
this.paymentContext.setStrategy(new PaypalStrategy());
this.paymentContext.processPayment(amount);
}
}
e. In this example, the `PaymentComponent` uses the `PaymentContext` to switch between different payment strategies (`CreditCardStrategy` and `PaypalStrategy`) based on user actions or conditions. By setting the active strategy through `setStrategy`, you can dynamically change the behavior of the payment processing logic in `processPayment`.
This implementation allows for easy extensibility, as you can add new strategies by implementing the `PaymentStrategy` interface and use them interchangeably within the `PaymentComponent` or any other component that requires payment processing functionality.
The Strategy pattern provides flexibility and maintainability by separating the implementation of different algorithms or behaviors from the client code, allowing you to change or extend strategies without modifying existing code.
5. Decorator Pattern: Angular decorators, such as @Component and @Injectable, are based on the Decorator pattern. Decorators provide a way to enhance or modify the behavior of classes or class members without directly modifying the underlying code.
a. Create a base component that represents the core functionality:
import { Component } from '@angular/core';
@Component({
selector: 'app-base-component',
template: 'Base Component',
})
export class BaseComponent {}
b. Create a decorator component that extends the base component:
import { Component, ViewChild } from '@angular/core';
import { BaseComponent } from './base-component';
@Component({
selector: 'app-decorator',
template: `
<div>
<p>This is the decorator component</p>
<ng-content></ng-content>
</div>
`,
})
export class DecoratorComponent extends BaseComponent {}
In this example, the `DecoratorComponent` is a child component that extends the functionality of the `BaseComponent`. It wraps the `BaseComponent` within itself and adds extra content using `<ng-content>`. This allows you to inject additional behavior or template content around the base component.
c. Use the decorator component in your application:
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<app-decorator>
<app-base-component></app-base-component>
</app-decorator>
`,
})
export class AppComponent {}
In the `AppComponent` template, the `BaseComponent` is wrapped within the
`DecoratorComponent` using its selector `<app-decorator>`. You can inject other components, templates, or HTML content within the `DecoratorComponent` to extend or modify the behavior of the `BaseComponent`.
By using the
Decorator pattern in Angular, you can dynamically extend or modify the functionality of existing components by wrapping them within decorator components. This approach provides flexibility, code reusability, and maintainability, as you can reuse the base components while adding specific behavior or content as needed.
6. Facade Pattern:The Facade pattern is a structural design pattern that provides a simplified interface to a complex subsystem, making it easier to use and understand. In Angular, you can apply the Facade pattern to create a simplified API or service that encapsulates the complexity of interacting with multiple components, services, or modules.
Here's an example of implementing the Facade pattern in Angular:
a. Identify a complex subsystem or set of related components/services that you want to simplify for client usage.
b. Create a Facade service that encapsulates the interactions with the complex subsystem. The Facade service will provide a simplified interface for clients to access the subsystem's functionality.
import { Injectable } from '@angular/core';
import { ComplexServiceA } from './complex-service-a';
import { ComplexServiceB } from './complex-service-b';
@Injectable()
export class FacadeService {
constructor(private serviceA: ComplexServiceA, private serviceB: ComplexServiceB) {}
// Provide simplified methods that internally call the appropriate complex subsystem methods
performOperation(): void {
this.serviceA.complexOperationA();
this.serviceB.complexOperationB();
}
}
c. Implement the complex subsystem components/services that the Facade service interacts with. These components/services handle the actual complex logic.
@Injectable()
export class ComplexServiceA {
complexOperationA(): void {
// Complex logic of service A
console.log('Performing complex operation A');
}
}
@Injectable()
export class ComplexServiceB {
complexOperationB(): void {
// Complex logic of service B
console.log('Performing complex operation B');
}
}
d. Use the Facade service in your components to simplify the usage of the complex subsystem:
import { Component } from '@angular/core';
import { FacadeService } from './facade.service';
@Component({
selector: 'app-client-component',
template: '...',
})
export class ClientComponent {
constructor(private facadeService: FacadeService) {}
performFacadeOperation(): void {
this.facadeService.performOperation();
}
}
e. In this example, the `ClientComponent` utilizes the `FacadeService` to perform complex operations without needing to interact directly with the complex subsystem (`ComplexServiceA` and `ComplexServiceB`). The `FacadeService` encapsulates the complexity and provides a simplified interface for the client component to interact with.
By using the
Facade pattern in Angular, you can simplify the usage of complex subsystems, hide their implementation details, and provide a straightforward and easy-to-use interface for clients. This promotes code maintainability, readability, and modularity by abstracting the complexity of interacting with multiple components or services behind a single facade.
7. Composite Pattern: The Composite Design Pattern is a structual design pattern that is used to compose objects into a tree-like structure. Components can be composed of other components, forming a tree-like structure. This pattern enables the creation of reusable and hierarchical UI components.
In Angular, you can apply the Composite pattern to represent hierarchical relationships between components or services.
Here's an example of implementing the Composite pattern in Angular:
a. Create an abstract class or interface that represents the common behavior for both individual objects and groups:
// component.interface.ts
export interface ComponentInterface {
operation(): void;
}
b. Implement the abstract class or interface for individual objects:
// leaf.component.ts
import { ComponentInterface } from './component.interface';
export class LeafComponent implements ComponentInterface {
operation(): void {
console.log('Performing operation on a leaf component.');
}
}
c. Implement the abstract class or interface for the composite object, which can contain both individual objects and other composite objects:
// composite.component.ts
import { ComponentInterface } from './component.interface';
export class CompositeComponent implements ComponentInterface {
private children: Component[] = [];
add(component: ComponentInterface): void {
this.children.push(component);
}
remove(component: ComponentInterface): void {
const index = this.children.indexOf(component);
if (index > -1) {
this.children.splice(index, 1);
}
}
operation(): void {
console.log('Performing operation on the composite component.');
for (const child of this.children) {
child.operation();
}
}
}
d. Use the composite object to create a tree-like structure of components:
import { ComponentInterface } from './component.interface';
import { LeafComponent } from './leaf.component';
import { CompositeComponent } from './composite.component';
// Create leaf components
const leaf1: ComponentInterface = new LeafComponent();
const leaf2: ComponentInterface = new LeafComponent();
// Create composite component
const composite: ComponentInterface = new CompositeComponent();
composite.add(leaf1);
composite.add(leaf2);
// Create another composite component
const composite2: ComponentInterface = new CompositeComponent();
composite2.add(composite);
composite2.add(leaf1);
// Perform operation on the composite structure
composite2.operation();
e. In this example, we create a tree-like structure using the Composite pattern. The `CompositeComponent` can contain both individual `LeafComponent` objects and other `CompositeComponent` objects. Calling the `operation()` method on the top-level `CompositeComponent` will recursively invoke the operation on all its children, whether they are leaf components or other composite components.
By using the
Composite pattern in Angular, you can represent complex hierarchical relationships between components or services in a uniform manner. It allows you to treat individual objects and groups of objects in a consistent way, simplifying the code and enabling recursive operations on the composite structure.
8.Factory Pattern: The Factory pattern is a creational design pattern that provides an interface for creating objects without specifying the exact class of the object that will be created. In Angular, you can apply the Factory pattern to encapsulate object creation logic and provide a centralized place for creating instances of different classes.
Here's an example of implementing the Factory pattern in Angular:
a. Define an abstract class or interface that represents the common behavior of the objects you want to create:
// product.interface.ts
export interface Product {
operation(): void;
}
b. Implement multiple classes that conform to the `Product` interface:
// product-a.ts
export class ProductA implements Product {
operation(): void {
console.log('Product A operation.');
}
}
// product-b.ts
export class ProductB implements Product {
operation(): void {
console.log('Product B operation.');
}
}
c. Create a factory class that encapsulates the object creation logic:
// product-factory.ts
import { Product } from './product.interface';
import { ProductA } from './product-a';
import { ProductB } from './product-b';
export class ProductFactory {
createProduct(type: string): Product {
if (type === 'A') {
return new ProductA();
} else if (type === 'B') {
return new ProductB();
}
throw new Error('Invalid product type');
}
}
d. Use the factory class to create instances of the desired products:
import { Component } from '@angular/core';
import { ProductFactory } from './product-factory';
import { Product } from './product.interface';
@Component({
selector: 'app-example',
template: '...',
})
export class ExampleComponent {
constructor(private productFactory: ProductFactory) {}
createProduct(type: string): void {
const product: Product = this.productFactory.createProduct(type);
product.operation();
}
}
e. In this example, the `ExampleComponent` uses the
`ProductFactory` to create instances of different products based on the provided type. By calling the `createProduct` method with the desired type ('A' or 'B'), it receives an instance of the corresponding product class and can invoke its `operation()` method.
Using the
Factory pattern in Angular provides a centralized place for creating objects and decouples the client code from the concrete classes. It allows for flexible object creation and enables easy extensibility by adding new product classes and updating the factory logic accordingly.
These are some of the design patterns commonly used in Angular. However, it's worth noting that Angular itself follows the MVC (Model-View-Controller) architectural pattern, where components serve as the controllers, templates represent views, and services act as models.