Open In App

Design Patterns in Angular

Last Updated : 05 Jan, 2024
Improve
Improve
Like Article
Like
Save
Share
Report

Design patterns are communicating objects and classes that are customized to solve a general design problem in a particular context. Design patterns are general, reusable solutions to common problems that arise during the design and development of software. They represent best practices for solving certain types of problems and provide a way for developers to communicate about effective design solutions.

What are common Angular Design Patterns?

Types-of-Angular-Design-Patterns

Common Creational Design Patterns in Angular

1. Singleton pattern in Angular

Singleton pattern is a design pattern which restricts a class to instantiate its multiple objects. It is nothing but a way of defining a class. Class is defined in such a way that only one instance of the class is created in the complete execution of a program or project. It is used where only a single instance of a class is required to control the action throughout the execution. A singleton class shouldn’t have multiple instances in any case and at any cost.

Example:

Singleton classes are used for logging, driver objects, caching and thread pool, database connections.

Below is the implementation of Singleton Pattern:

Javascript




class SingletonService {
  private static instance: SingletonService;
 
  private constructor() {
    // Private constructor to prevent instantiation.
  }
 
  public static getInstance(): SingletonService {
    if (!SingletonService.instance) {
      SingletonService.instance = new SingletonService();
    }
    return SingletonService.instance;
  }
}
 
const singletonInstance = SingletonService.getInstance();


Why Singleton Pattern is useful in Angular?

Using Singleton Pattern in Angular ensures a single instance of a service throughout the application, optimizing resource usage and providing a centralised location for managing global state and shared functionality.

2. Factory method pattern in Angular

The Factory Method pattern is used to create objects without specifying the exact class of object that will be created. This pattern is useful when you need to decouple the creation of an object from its implementation. The idea is to create a factory class with a single responsibility to create objects, hiding the details of class modules from the user.

A factory pattern is one of the core design principles to create an object, allowing clients to create objects of a library in a way such that it doesn’t have a tight coupling with the class hierarchy of the library.

What is meant when we talk about libraries and clients? 

  • A library is something that is provided by some third party that exposes some public APIs and clients make calls to those public APIs to complete their tasks.
  • A very simple example can be different kinds of Views provided by Android OS. 

Example:

A logging system with flexibility to use different output methods. Create two loggers, one for the console (ConsoleLogger) and another for file output (FileLogger), both implementing a common interface (Logger). Implement a factory method (createLogger) that dynamically generates the desired logger based on specified types (‘console’ or ‘file’), allowing the client code to easily switch between logging methods.

Below is the implementation of Factory Pattern:

Javascript




interface Logger {
  log(message: string): void;
}
 
class ConsoleLogger implements Logger {
  log(message: string): void {
    console.log(message);
  }
}
 
class FileLogger implements Logger {
  log(message: string): void {
    // Log to a file.
  }
}
 
// Factory method
function createLogger(type: 'console' | 'file'): Logger {
  if (type === 'console') {
    return new ConsoleLogger();
  } else if (type === 'file') {
    return new FileLogger();
  }
}
 
const consoleLogger = createLogger('console');
consoleLogger.log('Logging to console');


Why Factory Pattern is useful in Angular?

Using Factory Pattern in Angular enables dynamic creation of instances based on conditions or configurations, promoting flexibility, and allowing for the creation of related objects without specifying their exact class.

3. Prototype pattern in Angular

Prototype allows us to hide the complexity of making new instances from the client. The concept is to copy an existing object rather than creating a new instance from scratch, something that may include costly operations.

  • The existing object acts as a prototype and contains the state of the object.
  • The newly copied object may change same properties only if required.
  • This approach saves costly resources and time, especially when object creation is a heavy process.

Example:

Imagine you have a complex object with a configuration that is time-consuming to set up. Instead of creating a new instance and configuring it from scratch each time, you can use a prototype to clone an existing instance, preserving its state.

Below is the implementation of Prototype Pattern:

Javascript




interface Prototype {
  clone(): Prototype;
}
 
class ConcretePrototype implements Prototype {
  private property: string;
 
  constructor(property: string) {
    this.property = property;
  }
 
  clone(): Prototype {
    return new ConcretePrototype(this.property);
  }
}
 
const originalObject = new ConcretePrototype('Original');
const clonedObject = originalObject.clone();


Why Prototype Pattern is useful in Angular?

Using Prototype pattern in Angular facilitates the creation of new objects by copying an existing object, reducing the need for complex instantiation logic and promoting efficient object creation, especially in scenarios where object initialization is resource-intensive.

Common Structural Design Patterns in Angular

1. Adapter Pattern in Angular

The adapter pattern convert the interface of a class into another interface clients expect. Adapter lets classes work together that couldn’t otherwise because of incompatible interfaces. To use an adapter:

  • The client makes a request to the adapter by calling a method on it using the target interface.
  • The adapter translates that request on the adaptee using the adaptee interface.
  • Client receive the results of the call and is unaware of adapter’s presence.

Example:

Integrate a new logging system (NewLogger) with an existing class (OldLogger) that has a different interface. An adapter (Adapter) that allows the old logging functionality to be used seamlessly through the new logging interface, ensuring compatibility and facilitating the transition to an updated logging system.

Below is the implementation of Adapter Pattern in Angular:

Javascript




interface NewLogger {
  logMessage(message: string): void;
}
 
class OldLogger {
  log(msg: string): void {
    console.log(msg);
  }
}
 
class Adapter implements NewLogger {
  private oldLogger: OldLogger;
 
  constructor(oldLogger: OldLogger) {
    this.oldLogger = oldLogger;
  }
 
  logMessage(message: string): void {
    this.oldLogger.log(message);
  }
}
 
const oldLogger = new OldLogger();
const loggerAdapter = new Adapter(oldLogger);
loggerAdapter.logMessage('Logging with adapter');


Why Adapter Pattern is useful in Angular?

Using Adapter pattern in Angular allows incompatible interfaces to work together, acting as a bridge between different components. In Angular, decorators often serve as adapters, modifying or extending the behavior of classes without changing their core implementation.

2. Decorator pattern in Angular

A structural design sample known as the Decorator Pattern allows behavior to be introduced to a character object in a statically or dynamically way without influencing the behavior of other objects in the same class.

  • The decorator pattern attaches additional responsibilities to an object dynamically.
  • Decorators provide a flexible alternative to subclassing for extending functionality.

Example:

Let us assume a flexible and extensible system for calculating the cost of customized coffee orders. Define a base coffee interface (Coffee) with a cost method. Create concrete coffee classes (SimpleCoffee) and decorators (MilkDecorator, SugarDecorator) that enhance the cost based on added ingredients. Demonstrate the decorator pattern by composing various decorator instances, allowing clients to dynamically customize and calculate the cost of their desired coffee combinations.

Below is the implementation of Decorator Pattern:

Javascript




interface Coffee {
  cost(): number;
}
 
class SimpleCoffee implements Coffee {
  cost(): number {
    return 5;
  }
}
 
class MilkDecorator implements Coffee {
  private coffee: Coffee;
 
  constructor(coffee: Coffee) {
    this.coffee = coffee;
  }
 
  cost(): number {
    return this.coffee.cost() + 2;
  }
}
 
class SugarDecorator implements Coffee {
  private coffee: Coffee;
 
  constructor(coffee: Coffee) {
    this.coffee = coffee;
  }
 
  cost(): number {
    return this.coffee.cost() + 1;
  }
}
 
const myCoffee = new SugarDecorator(new MilkDecorator(new SimpleCoffee()));
console.log(myCoffee.cost()); // Output: 8


Why Decorator Pattern is useful in Angular?

Using Decorator Pattern in Angular enhances classes dynamically by attaching new behaviors or responsibilities, providing a flexible and reusable way to extend functionality. In Angular, decorators are commonly used to add metadata or modify the behavior of components, services, and directives.

Common Behavioral Design Patterns in Angular

1. Iterator pattern in Angular

Accessing the components of a combination item in a sequential manner without disclosing its underlying representation is made possible by the Iterator pattern. This can be useful when using Angular to iterate across collections.

Example:

An iterator mechanism to traverse the elements sequentially. Define a generic interface (Iterator<T>) with methods for retrieving the next element and checking if there are more elements. Create a concrete iterator class (ConcreteIterator<T>) that operates on a collection of elements, enabling clients to iterate over the collection in a systematic way.

Below is the implementation of Iterator Pattern:

Javascript




interface Iterator<T> {
  next(): T;
  hasNext(): boolean;
}
 
class ConcreteIterator<T> implements Iterator<T> {
  private collection: T[];
  private index: number = 0;
 
  constructor(collection: T[]) {
    this.collection = collection;
  }
 
  next(): T {
    return this.collection[this.index++];
  }
 
  hasNext(): boolean {
    return this.index < this.collection.length;
  }
}
 
const iterableCollection = [1, 2, 3, 4, 5];
const iterator = new ConcreteIterator(iterableCollection);
 
while (iterator.hasNext()) {
  console.log(iterator.next());
}


Why Iterator Pattern is useful in Angular?

Using Iterator Pattern in Angular simplifies the traversal of collections or lists, providing a standardized way to access elements sequentially.

2. State Pattern in Angular

A state design pattern is used when an Object changes its behavior based on its internal state. If we have to change the behavior of an object based on its state, we can have a state variable in the Object and use the if-else condition block to perform different actions based on the state. The state pattern is used to provide a systematic and lose-coupled way to achieve this through Context and State implementations.

Example:

A state machine system where an object (Context) can change its behavior as it transitions through different states. Define a common interface (State) for various states, each implementing a handleRequest method. Implement concrete state classes (ConcreteStateA, ConcreteStateB) representing distinct behaviors. The Context class maintains the current state and delegates requests to the current state. Display initializing the context with one state, transitioning to another state, and observing the change in behavior upon requests.

Below is the implementation of State Pattern:

Javascript




interface State {
  handleRequest(): void;
}
 
class ConcreteStateA implements State {
  handleRequest(): void {
    console.log('Handling request in State A');
  }
}
 
class ConcreteStateB implements State {
  handleRequest(): void {
    console.log('Handling request in State B');
  }
}
 
class Context {
  private state: State;
 
  constructor(state: State) {
    this.state = state;
  }
 
  setState(state: State): void {
    this.state = state;
  }
 
  request(): void {
    this.state.handleRequest();
  }
}
 
const context = new Context(new ConcreteStateA());
context.request(); // Output: Handling request in State A
context.setState(new ConcreteStateB());
context.request(); // Output: Handling request in State B


Why State Pattern is useful in Angular?

Using State Pattern in Angular allows an object to alter its behavior when its internal state changes, promoting a clean separation of concerns. In Angular, services or RxJS observables are used to manage state, making it easier to handle different application states and behaviors.

Conclusion

  • As a software developer, it’s essential to know different design styles.
  • In Angular optimization, using these principles can make your code more organized, scalable, and easy to maintain.
  • Design patterns provide smart solutions to common web development issues, whether you’re dealing with a single feature or adding more functions dynamically.
  • Remember to use these patterns to make your programs strong and adaptable as you work with Angular.


Like Article
Suggest improvement
Share your thoughts in the comments

Similar Reads