Patterns In Code: Streamlining Your Software Development with Effective Design Strategies

Patterns In Code: Streamlining Your Software Development with Effective Design Strategies

As a software developer, you know that building a successful application involves more than just writing code. It requires careful planning and organization to ensure that your application is efficient, scalable, and maintainable. One key-way to achieve these goals is by implementing effective design patterns in your code.

This article explain some commonly used software design patterns,their use cases and benefits. It focuses on the following key questions:

-What Is Software Design Pattern In A Nutshell?

-Why Do You Need Design Pattern?

-What Are The Classification Of Design Pattern?

-What Are The Limitations Of Design Pattern?

Code examples are implemented using the Typescript programming language. So why wait? Lets dive into it.

What Is Software Design Pattern In a Nutshell?

Design patterns are reusable solutions to common problems in software design. They provide a way to design flexible and maintainable software systems by promoting code reuse and ensuring that systems are designed in a flexible and scalable way.

Why Do You Need Design Pattern?

Software design patterns are important because they provide a common language for developers to communicate about how to solve problems in software design. They are a set of best practices that have been proven to solve common design problems in the most effective and reusable way.

Design patterns also make it easier to maintain and update software because they provide a consistent way to structure the code. This makes it easier for new developers to understand the codebase and for existing developers to make changes to the code without introducing new bugs or issues.

In addition, design patterns promote reusability of code. If a design pattern has been tested and proven to work in one project, it can likely be reused in a similar project, saving time and effort.

Overall, using software design patterns can lead to more robust, maintainable, and scalable software systems.

What Are The Classification Of Design Pattern?

There are several categories of design patterns, including creational, structural, and behavioral.

1-Creational Design Pattern

are design patterns that deal with object creation mechanisms, trying to create objects in a manner suitable to the situation. The basic form of object creation could result in design problems or in added complexity to the design. Creational design patterns solve this problem by somehow controlling this object creation.

Here are some example of the creational pattern:

i-Singleton design pattern

is a software design pattern that ensures that a class has only one instance, while providing a global access point to this instance. The singleton pattern is useful when exactly one object is needed to coordinate actions across the system.

</> Code Example:

 class Database {
  private static instance: Database;

  private constructor() {}

  static getInstance(): Database {
    if (!Database.instance) {
      Database.instance = new  Database();
    }
    return Database.instance;
  }

  query(query: string): void {
    console.log(`Executing query: ${query}`);
  }
}

const db1 = Database.getInstance();
db1.query("SELECT * FROM users");

const db2 = Database.getInstance();
db2.query("SELECT * FROM posts");

console.log(db1 === db2); // true

Some benefits of using the singleton design pattern are:

-It controls object creation by allowing the creation of only one instance of a class.

-It provides a global access point to this instance, making it easy to access the instance from anywhere in the code.

-It can be used to store shared data and share resources among multiple classes.

Applicability

-When you need to ensure that only one instance of a class is created, such as a configuration class or a logger class.

-When you need to access the same instance of a class from multiple places in your code.

-When you want to ensure that a class is thread-safe, as the singleton pattern ensures that only one instance of the class is created, even in a multithreaded environment

ii-Factory design pattern

Is a creational design pattern that defines an interface for creating an object, but lets subclasses decide which class to instantiate. It allows a class to defer instantiation to subclasses. In other-words; It defines a method for creating an object instead of direct constructor call.

</> Code Example:


interface Shape {
  draw(): void;
}

class Rectangle implements Shape {
  draw() {
    console.log("Drawing a rectangle");
  }
}

class Circle implements Shape {
  draw() {
    console.log("Drawing a circle");
  }
}

class ShapeFactory {
  static createShape(type: "rectangle" | "circle"): Shape {
    switch (type) {
      case "rectangle":
        return new Rectangle();
      case "circle":
        return new Circle();
      default:
         throw new Error("Invalid shape type");
    }
  }
}

const rectangle = ShapeFactory.createShape("rectangle");
rectangle.draw();

const circle = ShapeFactory.createShape("circle");
circle.draw();

Applicability

One common use case for the factory pattern is when you have a super class with multiple subclasses and you want to be able to create objects of the correct subclass type based on the data that you have.

For example, suppose you have a super class called "Shape" and subclasses "Square", "Rectangle", and "Circle". You might use the factory pattern to create a "Shape" object based on a string that specifies the type of shape, such as "Rectangle", "circle", or "square".

Benefits of using the factory design pattern include:

-It allows you to create objects without specifying the exact class of object that will be created. This can be useful if you don't know in advance which class of object you need, or if the class of object might change over time.

-It promotes loose coupling between the creator and the concrete classes. The creator class depends on the abstract super class, but not on the specific subclasses. This means that you can change the concrete classes independently of the creator class.

-It allows you to centralize object creation and make it more flexible. You can change the object creation process by changing the factory class, rather than by changing all of the client classes that create objects.

-It allows you to abstract the creation of objects from the client, so the client does not have to know how objects are created.

iii-Builder design pattern

is a creational design pattern that allows a program to create complex objects step by step. It separates the construction of an object from its representation, so that the same construction process can create different representations.

</> Code Example:


class Meal {
  private items: string[] = [];

  addItem(item: string) {
    this.items.push(item);
  }

  getCost(): number {
    let cost = 0;
    this.items.forEach(item => {
      cost += Menu.getItemPrice(item);
    });
    return cost;
  }

  showItems(): void {
    console.log("Items in the meal:");
    this.items.forEach(item => {
      console.log(`  - ${item}`);
    });
  }
}

class MealBuilder {
  private meal: Meal;

  constructor() {
    this.meal = new Meal();
  }

  addBurger() {
    this.meal.addItem("burger");
  }

  addDrink() {
    this.meal.addItem("drink");
  }

  addFries() {
    this.meal.addItem("fries");
  }

  build(): Meal {
    return this.meal;
  }
}

class Menu {
  static getItemPrice(item: string): number {
    switch (item) {
      case "burger":
        return 5.0;
      case "drink":
        return 2.0;
      case "fries":
        return 3.0;
      default:
        throw new Error("Invalid item");
    }
  }
}

const mealBuilder = new MealBuilder();
mealBuilder.addBurger();
mealBuilder.addDrink();
mealBuilder.addFries();
const meal = mealBuilder.build();

console.log(`Total cost: $${meal.getCost()}`);
meal.showItems();

Applicability

-Creating complex objects: The builder design pattern is often used to create complex objects that have many different parts or configurations. For example, it could be used to build a car with various options such as color, engine size, and type of wheels.

-Constructing objects in multiple steps: The builder design pattern is useful when an object requires multiple steps to construct, such as when it requires input from multiple sources or when it has optional components.

-Creating objects with a large number of optional parameters: The builder design pattern can be used to create objects with a large number of optional parameters, such as a form builder that allows a user to customize the fields and layout of a form.

Benefits of using the builder design pattern include:

-Separation of concerns: The builder design pattern allows the construction of an object to be separated from its representation. This makes it easier to change the representation without changing the construction process.

-Improved readability: The builder design pattern can make the code for creating complex objects easier to read, because it allows the construction process to be broken down into smaller, more manageable steps.

-Increased flexibility: The builder design pattern allows a program to create different representations of an object by using the same construction process. This increases the flexibility of the program, because it can create different kinds of objects without changing the underlying code.

iv-Object pool design pattern

is a software creational design pattern that allows a program to reuse objects from a pool of objects, rather than creating a new object each time one is needed.

</> Code Example:


class ObjectPool<T> {
  private objects: T[] = [];

  public borrowObject(): T {
    if (this.objects.length > 0) {
      return this.objects.pop();
    } else {
      return this.createObject();
    }
  }

  public returnObject(object: T) {
    this.objects.push(object);
  }

  protected createObject(): T {
    // This method should be implemented by a subclass
    throw new Error("Not implemented");
  }
}

class ConnectionPool extends ObjectPool<DbConnection> {
  protected createObject(): DbConnection {
    // Create and return a new DbConnection object
  }
}

const pool = new ConnectionPool();

const conn1 = pool.borrowObject();
// Use conn1
pool.returnObject(conn1);

const conn2 = pool.borrowObject();
// Use conn2
pool.returnObject(conn2);

Object pool can be beneficial in several ways:

-Improved performance: Creating a new object can be time-consuming, especially if the object requires a lot of resources to create. By reusing objects from a pool, the program can avoid this overhead and improve performance.

-Reduced resource consumption: Creating a large number of objects can consume a significant amount of resources, such as memory or database connections. By reusing objects from a pool, the program can reduce its resource consumption.

-Better resource management: An object pool can be used to manage a limited resource, such as database connections. By reusing objects from the pool, the program can ensure that the resource is used efficiently and does not become overburdened.

Applicability

-Database connection pools: A database connection pool is a common use case for the object pool pattern. The pool contains a set of reusable database connections, which can be borrowed by a program when needed and returned to the pool when they are no longer needed.

-Thread pools: A thread pool is a set of reusable threads that can be borrowed by a program and returned to the pool when they are no longer needed. This can be used to improve the performance of a program that needs to perform a large number of concurrent tasks.

-Memory pools: A memory pool is a set of reusable blocks of memory that can be borrowed and returned by a program. This can be used to improve the performance of a program that needs to allocate and deallocate large amounts of memory.

2-Structural design patterns

are design patterns that ease the design by identifying a simple way to realize relationships between entities. These patterns focus on creating relationships between objects to give you new ways to create relationships that can be more simple.

Here are some examples of structural design patterns:

2i-Facade design pattern

is a structural design pattern that provides a simplified interface to a complex system. It acts as a wrapper around the complex system and exposes a set of simpler interfaces that are easier to use.

</> Code Example:


 class SubsystemA {
  public operationA(): void {
    console.log('Subsystem A: Performing operation A');
  }
}

class SubsystemB {
  public operationB(): void {
    console.log('Subsystem B: Performing operation B');
  }
}

class Facade {
  private subsystemA: SubsystemA;
  private subsystemB: SubsystemB;

  constructor(subsystemA: SubsystemA, subsystemB: SubsystemB) {
    this.subsystemA = subsystemA;
    this.subsystemB = subsystemB;
  }

  public operation(): void {
    console.log('Facade: Performing operation');
    this.subsystemA.operationA();
    this.subsystemB.operationB();
  }
}

// The facade can be used like this:
const subsystemA = new SubsystemA();
const subsystemB = new SubsystemB();
const facade = new Facade(subsystemA, subsystemB);
facade.operation();
// Output:
// Facade: Performing operation
// Subsystem A: Performing operation A
// Subsystem B: Performing operation B

Applicability

-When you have a complex system with a large number of classes and you want to provide a simpler interface to the system.

-When you want to decouple the client from the implementation details of the system.

-When you want to provide a consistent interface to a set of interfaces in a subsystem.

-When you want to wrap a legacy system with a modern interface.

Benefits of using the facade design pattern include:

-It provides a simpler interface to a complex system. Thi s can make it easier for clients to use the system, as they don't have to worry about the complexity of the underlying implementation

-It can improve the testability of the system. By separating the complex system into a facade and a set of underlying classes, you can more easily test the individual components of the system.

2ii-Proxy design pattern

is a structural design pattern that provides an object that acts as a substitute for another object. Proxies are used to provide additional functionality or control over the original object.

</> Code Example:



 class Image {
  private _url: string;

  constructor(url: string) {
    this._url = url;
  }

  public draw(): void {
    console.log(`Drawing image from URL: ${this._url}`);
  }
}

class ImageProxy {
  private _image: Image;
  private _url: string;

  constructor(url: string) {
    this._url = url;
  }

  public draw(): void {
    if (!this._image) {
      this._image = new Im age(this._url);
    }
    this._image.draw();
  }
}

// The proxy can be used like this:
const imageProxy = new ImageProxy('http://example.com/image.jpg');
imageProxy.draw();
// Output: Drawing image from URL: http://example.com/image.jpg

Applicability

-Remote proxies: These are used to provide a local representation of a remote object, allowing a client to access the remote object as if it were local. This is often used in distributed systems to allow clients to interact with remote objects over a network.

-Virtual proxies: These are used to create a placeholder for an object that is expensive to create or that is not needed until it is actually used. This can help to improve performance by delaying the creation of the object until it is actually needed.

-Protection proxies: These are used to control access to an object, only allowing certain operations to be performed on the object depending on the identity or privileges of the client.

-Smart references: These are used to add additional behavior or functionality to an object reference, such as automatically loading an object from a database or cache when it is accessed.

Some benefits of using the proxy design pattern include:

-It allows you to create a wrapper or placeholder for another object in order to control access to that object.

-It can be used to add additional behavior or functionality to an existing object without changing the object itself.

-It can be used to reduce the number of direct connections between objects, which can make it easier to modify the implementation of an object without affecting other objects.

2iii-Adapter design pattern

is a structural design pattern that allows you to convert the interface of one class into another interface that is compatible with a specific client. This can be used to allow two incompatible classes to work together by adapting one of the classes to the interface of the other.

</> Code Example:


class Adaptee { 
public specificRequest(): string { 
return "Specific request."; 
} 
}
 interface Target {
 request(): string; 
};
class Adapter implements Target {
 private adaptee: Adaptee;
 constructor(adaptee: Adaptee) { 
this.adaptee = adaptee;
 }
 public request(): string { 
return `Adapter: ${this.adaptee.specificRequest()}`; 
}
 } 
const adaptee = new Adaptee();
 const adapter = new Adapter(adaptee); console.log(adapter.request());
 // Output: Adapter: Specific request.

Applicability

-Integrating new classes into an existing system: You can use an adapter to integrate a new class into an existing system, even if the interface of the new class is not compatible with the rest of the system.

-Supporting multiple versions of an interface: If you need to support multiple versions of an interface, you can use an adapter to convert between the different versions.

Some benefits of using the adapter design pattern include:

-It allows you to reuse existing classes that have the desired functionality, even if their interfaces are not compatible with the rest of your code.

-It can make it easier to modify or extend an existing system, since you can add adapters for new classes without having to change the existing code.

-It can help to decouple the client code from the implementation of the adapted class, making it easier to change the implementation without affecting the client.

3-Behavioral design pattern

are a category of design patterns that focus on communication between objects and the way they operate together. These patterns aim to improve the flexibility and reuse of the code by creating separation between the objects that interact with each other.

Some examples of behavioral design patterns include:

3i-Iterator design pattern

is a behavioral design pattern that allows you to access the elements of an aggregate object sequentially without exposing the underlying representation of the object. This can help to make it easier to work with different types of objects and collections, by providing a consistent way to access the elements of the object or collection.

</> Code Example:


 class Iterator {
  // The iterator maintains a current position and an array of items
  private current = 0;
  private items: any[];

  constructor(items: any[]) {
    this.items = items;
  }

  // The iterator has a `hasNext` method that returns a boolean indicating
  // whether the iterator has more items to iterate over
  public hasNext(): boolean {
    return this.current < this.items.length;
  }

  // The iterator has a `next` method that returns the next item in the iteration
  // and moves the current position forward by one
  public next(): any {
    return this.items[this.current++];
  }
}

// The iterator can be used like this:
const iterator = new Iterator([1, 2, 3, 4,  5]);
while (iterator.hasNext()) {
  console.log(iterator.next());
}
// Output: 1 2 3 4 5

Applicability

-When you want to provide a way to access the elements of an object or collection in a consistent way, regardless of the underlying representation of the object.

-When you want to abstract the implementation of an object or collection from the code that uses it, to make it easier to modify the implementation without affecting the code that uses it.

-When you want to support multiple traversals of an object or collection, such as allowing a user to iterate through the elements of a collection multiple times.

Some benefits of using the iterator design pattern include:

-It allows you to access the elements of an object or collection in a consistent way, without exposing the underlying representation of the object.

-It allows you to abstract the implementation of the object or collection from the code that uses it, which can make it easier to modify the implementation without affecting the code that uses it.

3ii-Command design pattern

is a behavioral design pattern that allows you to encapsulate a request or an operation as an object, and separate it from the object that actually performs the request. This can help to decouple the objects that make a request from the objects that actually execute the request, which can make it easier to modify the implementation of the request or the objects that execute the request without affecting each other.

</> Code Example:


interface Command { 
execute(): void;
 undo(): void;
 }
 class ConcreteCommand implements Command { 
private receiver: Receiver; 
private state: string; 
constructor(receiver: Receiver, state: string) {
 this.receiver = receiver; 
this.state = state;
 } 
public execute(): void { 
this.receiver.action(this. state);
 }
public undo(): void {
 this.receiver.action(this.state); 
} 
} 
class Receiver { 
public action(state: string): void { 
console.log(`Receiver: Action performed with state "${state}"`);
 } 
} 
class Invoker {
 private command: Command;
 public setCommand(command: Command): void {
 this.command = command;
 } 
public executeCommand(): void { 
this.command.execute();
 }
 public undoCommand(): void { 
this.command.undo(); 
} 
}
 const receiver = new Receiver(); 
const command = new ConcreteCommand(receiver, "ON"); const invoker = new Invoker(); invoker.setCommand(command); invoker.executeCommand();
 // Output: Receiver: Action performed with state "ON" invoker.undoCommand(); 
// Output: Receiver: Action performed with  state "ON"

Applicability

-When you want to parametrize objects with operations, such as in a graphical user interface where objects in a menu or a toolbar need to execute a specific operation when clicked.

-When you want to implement reversible operations, such as undo/redo functionality.

-When you want to support deferred execution of operations, such as the ability to queue requests and execute them at a later time.

Some benefits of using the command design pattern include:

-It allows you to decouple the objects that make a request from the objects that execute the request, which can make it easier to modify the implementation of either without affecting the other.

-It allows you to queue requests, track the history of requests, or support undo/redo functionality.

3iii-Strategy design pattern

is a behavioral design pattern that allows you to define a family of algorithms, encapsulate each one as an object, and make them interchangeable. This can help to make your code more flexible and reusable by allowing you to easily switch between different algorithms at runtime.

</> Code Example:


interface Strategy { 
execute(): void;
 } 
class ConcreteStrategyA implements Strategy { 
public execute(): void{ 
console.log("Executing strategy A"); 
} 
} 
class ConcreteStrategyB implements Strategy {
 public execute(): void{
 console.log("Executing strategy B");
 } 
} 
class Context { 
private strategy: Strategy; constructor(strategy: Strategy){ 
this.strategy = strategy; 
} 
public setStrategy(strategy: Strategy):void { 
this.strategy = strategy; 
} 
public executeStrategy(): void { 
this.strategy.execute(); 
} 
} 
const context = new Context(new ConcreteStrategyA());
 context.executeStrategy();
 // Output: Executing strategy A context.setStrategy(new ConcreteStrategyB()); context.executeStrategy(); 
// Output: Executing strategy B

Applicability

-When you have a class that needs to perform one of several related operations, and you want to be able to easily switch between these operations at runtime.

-When you want to isolate the implementation of an algorithm from the rest of your code, to make it easier to modify the algorithm or add new algorithms in the future.

Some benefits of using the strategy design pattern include:

-It allows you to encapsulate different algorithms as objects and make them interchangeable, which can make your code more flexible and reusable.

-It allows you to easily switch between different algorithms at runtime, which can make it easier to modify the behavior of your code without making changes to the underlying implementation.

Observer design pattern

is a behavioral pattern that allows an object (the subject) to be observed by one or more other objects (the observers). The subject maintains a list of its observers and automatically notifies them of any changes to its state.

</> Code Example:

interface Subject { 
subscribe(observer: Observer): void; unsubscribe(observer: Observer): void; 
notify(): void; 
} 

interface Observer { 
update(subject: Subject): void; 
} 

class ConcreteSubject implements Subject {
 private observers: Observer[] = []; subscribe(observer: Observer): void {
 this.observers.push(observer); 
} 
unsubscribe(observer: Observer): void { 
const index = this.observers.indexOf(observer); this.observers.splice(index, 1);
 }
 notify(): void { 
this.observers.forEach(observer => observer.update(this)); 
}
 }

class ConcreteObserver implements Observer { 
constructor(private subject: Subject) { 
this.subject.subscribe(this); 
}
update(subject: Subject): void {
 // Handle  update from subject 
} 
}

Applicability

One common use case for the Observer pattern is in implementing a notification system. For example, a social media application might use the Observer pattern to notify a user's followers whenever the user posts a new update.

Benefits of using the Observer pattern include:

-It allows for a loose coupling between objects, as the subject does not need to know the specific details of its observers.

-It allows for a one-to-many relationship between objects, so that a change to the subject can be automatically propagated to all of its observers.

-It can improve the efficiency of an application, as the subject can notify its observers as soon as a change occurs rather than waiting for them to pull the data at a later time.

3v-Visitor pattern

is a behavioral design pattern that allows an object to perform operations on the elements of a complex object structure without changing the structure itself. This is done by encapsulating the operations in a separate visitor object and calling the visitor's methods on the elements of the object structure.

</> Code Example:


interface Element { 
accept(visitor: Visitor): void;
 } 
class ConcreteElementA implements Element { 
public accept(visitor: Visitor): void {
 visitor.visitConcreteElementA(this);
 } 
} 
class ConcreteElementB implements Element { 
public accept(visitor: Visitor): void { 
visitor.visitConcreteElementB(this);
 } 
}
 interface Visitor {
 visitConcreteElementA(element: ConcreteElementA): void; visitConcreteElementB(element: ConcreteElementB): void; 
} 
class ConcreteVisitor1 implements Visitor { 
public visitConcreteElementA(element: ConcreteElementA): void { 
console.log("Visiting ConcreteElementA with ConcreteVisitor1"); 
} 
public visitConcreteElementB(element: ConcreteElementB): void {
 console.log("Visiting ConcreteElementB with ConcreteVisitor1");
 } 
}
 class ConcreteVisitor2 implements Visitor { 
public visitConcreteElementA(element: ConcreteElementA): void { 
console.log("Visiting ConcreteElementA with ConcreteVisitor2");
 } 
public visitConcreteElementB(element: ConcreteElementB): void { 
console.log("Visiting ConcreteElementB with ConcreteVisitor2");
 }
 } 
const elementA = new ConcreteElementA();
 const elementB = new ConcreteElementB(); 
const visitor1 = new ConcreteVisitor1(); elementA.accept(visitor1); 
// Output: Visiting ConcreteElementA

Applicability

-When you have a complex object structure that you want to perform operations on, and you want to keep the operations separate from th e object structure itself.

-When you want to perform the same set of operations on multiple different object structures.

-When you want to add new operations to an object structure without modifying the structure itself.

Some Benefits of using visitor design pattern include:

-One of the main benefits of the visitor design pattern is that it allows you to add new operations to an object structure without modifying the structure itself. This can make it easier to maintain and extend your codebase, as you can add new functionality without having to make changes to the underlying object structure.

Overall, the visitor design pattern can be a useful tool for improving the flexibility and extensibility of your code, and for keeping complex object structures organized and maintainable.

What Are The Limitations Of Design Pattern?

Like any software design technique, design patterns are not a one-size-fits-all solution and can have some limitations. Here are a few potential limitations of design patterns:

-Overuse: It is possible to overuse design patterns in your code, which can make your code more complex and difficult to understand. It is important to use design patterns appropriately and only when they are actually needed.

-Inflexibility: Some design patterns can be inflexible, particularly if they rely on specific implementations or conventions. This can make it more difficult to modify or extend your code in the future.

-Performance: In some cases, using design patterns can have an impact on the performance of your code. For example, some patterns may involve additional layers of abstraction or indirection, which can affect the runtime performance of your code.

Overall, it is important to carefully consider whether and how to use design patterns in your code, taking into account the specific needs and constraints of your project. Design patterns can be a useful tool, but they should be used with care and appropriate consideration.

Conclusion

Design patterns are reusable solutions to common problems that arise during the design and implementation of software systems. They provide a way to structure the code in a way that is easy to maintain, reuse, and extend. There are different types of design patterns, including creational, structural, and behavioral patterns. Each type of design pattern addresses a different aspect of the design process and can be used to solve specific problems. By using design patterns, developers can write more flexible, maintainable, and scalable code. It is important to carefully consider the problem at hand and choose the appropriate design pattern to solve it, as using the wrong pattern can lead to more complex and difficult-to-maintain code.

I hope you find this Informative :)