Essential top 5 design patterns that will make your code suck less

Essential top 5 design patterns that will make your code suck less

Here's a quick summary and 5 common design patterns in software development that will make your code suck less and reduce the number of pesky bugs.

  1. Singleton: Ensures that a class has only one instance and provides a global point of access to it.
  2. Factory: Creates objects without specifying the exact class to create.
  3. Builder: Separates the construction of a complex object from its representation, allowing for different representations.
  4. Prototype: Creates objects by cloning an existing object rather than creating new objects from scratch.
  5. Adapter: Allows incompatible classes to work together by converting the interface of one class into another.

These design patterns can help developers to create more modular, reusable, and maintainable code. For the actual explanation with pros, cons, and samples, continue reading below.

1. Singleton

A Singleton is a design pattern that ensures that a class has only one instance, and provides a global point of access to it. The idea is to create a class with a method that creates a new instance of the class if one doesn't already exist, and returns the existing instance if one does. This allows for only a single instance of the class to exist within the application, which can be useful for managing resources or for providing a global point of access to an object.

Pros of singleton pattern:

  • Ensures that only one instance of a class is created, which can be useful for managing resources or providing a global point of access to an object.
  • Can improve performance by avoiding the overhead of creating multiple instances of a class.

Cons of singleton pattern:

  • Can introduce unnecessary complexity and make the code more difficult to understand and maintain.
  • Can make it more difficult to test the code, since the Singleton can be difficult to mock or stub.

When to use singleton pattern:

  • When you need to ensure that only one instance of a class is created.
  • When you want to provide a global point of access to an object.
  • When you want to improve performance by avoiding the overhead of creating multiple instances of a class.

Here is an example of a Singleton in JavaScript:

class Singleton {
  // The static instance variable holds the single instance of the class
  static instance = null;

  // The constructor is private to prevent direct construction of the object
  private constructor() {}

  // The getInstance method is used to retrieve the single instance of the class
  static getInstance() {
    if (Singleton.instance == null) {
      Singleton.instance = new Singleton();
    }
    return Singleton.instance;
  }
}

To use the Singleton, you would call the getInstance method, which will either create a new instance of the class or return the existing instance:

const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();

console.log(instance1 === instance2); // Output: true

In this example, instance1 and instance2 will be the same object, because the getInstance method always returns the same instance of the Singleton class.

2. Factory

The Factory design pattern is a creational pattern that provides a way to create objects without specifying the exact class to create. The Factory pattern defines an interface for creating objects, but lets subclasses decide which class to instantiate. This allows for a level of abstraction between the creation of objects and the implementation of those objects.

Pros of factory pattern:

  • Allows for the creation of objects without specifying the exact class to create.
  • Provides a level of abstraction between the creation of objects and the implementation of those objects.
  • Makes it easier to add new types of objects without modifying existing code.

Cons of factory pattern:

  • Can add unnecessary complexity to the code, making it more difficult to understand and maintain.
  • Can make it more difficult to see the relationship between the objects being created and the factory itself.

When to use factory pattern:

  • When you want to create objects without specifying the exact class to create.
  • When you want to provide a level of abstraction between the creation of objects and the implementation of those objects.
  • When you want to make it easier to add new types of objects without modifying existing code.

Here is an example of a Factory in JavaScript:

class Factory {
  // The create method takes a type and any necessary arguments, and returns a new object
  create(type, ...args) {
    // Check if the type is valid and throw an error if it is not
    if (typeof this[type] !== 'function') {
      throw new Error(`${type} is not a valid type`);
    }
    // Use the 'new' keyword and the spread operator to create and return a new object
    return new this[type](...args);
  }
}

To use the Factory, you would need to extend the Factory class and add methods for creating each type of object that you want to support:

class ShapeFactory extends Factory {
  // Method for creating a Circle object
  Circle(radius) {
    return new Circle(radius);
  }

  // Method for creating a Rectangle object
  Rectangle(width, height) {
    return new Rectangle(width, height);
  }
}

Once you have defined the ShapeFactory class, you can use the create method to create new objects:

const factory = new ShapeFactory();

const circle = factory.create('Circle', 10);
const rectangle = factory.create('Rectangle', 20, 30);

In this example, circle will be a Circle object with a radius of 10, and rectangle will be a Rectangle object with a width of 20 and a height of 30. The create method is able to create these objects without specifying the exact class to create, which allows for a level of abstraction between the creation of the objects and the implementation of those objects.

3. Builder

he Builder design pattern is a creational pattern that separates the construction of a complex object from its representation, allowing for different representations. The Builder pattern defines an interface for creating the parts of an object, and a separate concrete builder that implements the interface and constructs the object. This allows the same construction process to create different representations of the object.

Pros of builder pattern:

  • Separates the construction of a complex object from its representation, allowing for different representations.
  • Allows for the construction process to be shared across different builders.
  • Makes it easier to add new types of builders without modifying existing code.

Cons of builder pattern:

  • Can add unnecessary complexity to the code, making it more difficult to understand and maintain.
  • Can make it more difficult to see the relationship between the builder and the object being constructed.

When to use a builder pattern:

  • When you want to separate the construction of a complex object from its representation, allowing for different representations.
  • When you want to allow the same construction process to create different representations of the object.
  • When you want to make it easier to add new types of builders without modifying existing code.

Here is an example of a Builder in JavaScript:

class Builder {
  // The interface defines the methods that must be implemented by concrete builders
  buildPart1() {}
  buildPart2() {}
  buildPart3() {}
}

class ConcreteBuilder extends Builder {
  // The concrete builder implements the methods defined in the interface
  buildPart1() {
    this.product = new Product();
  }

  buildPart2() {
    this.product.addComponent('component1');
  }

  buildPart3() {
    this.product.addComponent('component2');
  }
}

class Product {
  constructor() {
    this.components = [];
  }

  addComponent(component) {
    this.components.push(component);
  }
}

To use the Builder, you would need to create a ConcreteBuilder object and use its methods to build the parts of the object:

const builder = new ConcreteBuilder();

builder.buildPart1();
builder.buildPart2();
builder.buildPart3();

const product = builder.product;

In this example, product will be a Product object with the components component1 and component2. The ConcreteBuilder class implements the methods defined in the Builder interface, allowing it to construct the object in a specific way. This allows for different representations of the Product object to be created using the same construction process.

4. Prototype

The Prototype design pattern is a creational pattern that creates objects by cloning an existing object rather than creating new objects from scratch. The Prototype pattern defines a prototype object, and creates new objects by copying the prototype object and modifying the copied object as needed. This allows for the creation of new objects without the overhead of constructing them from scratch.

Pros of prototype pattern:

  • Creates objects by cloning an existing object, avoiding the overhead of creating new objects from scratch.
  • Makes it easy to create new objects by copying existing objects and modifying them as needed.
  • Avoids the need to specify the exact type of object to create.

Cons of prototype pattern:

  • Can add unnecessary complexity to the code, making it more difficult to understand and maintain.
  • Can make it more difficult to see the relationship between the prototype and the objects being created.

When to use prototype pattern:

  • When you want to create new objects by cloning an existing object, avoiding the overhead of creating new objects from scratch.
  • When you want to make it easy to create new objects by copying existing objects and modifying them as needed.
  • When you want to avoid the need to specify the exact type of object to create.

Here is an example of a Prototype in JavaScript:

class Prototype {
  // The clone method creates a new object by copying the prototype
  clone() {
    const clone = Object.create(this);
    clone.init();
    return clone;
  }
}

class ConcretePrototype extends Prototype {
  init() {
    this.prop1 = 'value1';
    this.prop2 = 'value2';
  }
}

To use the Prototype, you would create a ConcretePrototype object and use its clone method to create new objects:

const prototype = new ConcretePrototype();

const clone1 = prototype.clone();
const clone2 = prototype.clone();

console.log(clone1.prop1); // Output: 'value1'
console.log(clone2.prop1); // Output: 'value1'

In this example, clone1 and clone2 will be new objects that are copies of the ConcretePrototype object. The clone method creates a new object by copying the prototype, and then calls the init method to initialize the new object. This allows for the creation of new objects without the overhead of constructing them from scratch.

5. Adapter

The Adapter design pattern is a structural pattern that allows incompatible classes to work together by converting the interface of one class into another. The Adapter pattern defines a class that wraps an existing class and provides a new interface that is compatible with the rest of the application. This allows the existing class to be used in the application without modifying its original interface.

Pros of adapter pattern:

  • Allows incompatible classes to work together by converting the interface of one class into another.
  • Avoids the need to modify existing classes to make them compatible with the rest of the application.
  • Makes it easier to add new classes to the application without breaking compatibility with existing code.

Cons of adapter pattern:

  • Can add unnecessary complexity to the code, making it more difficult to understand and maintain.
  • Can make it more difficult to see the relationship between the adapter and the class being adapted.

When to use the adapter pattern:

  • When you want to allow incompatible classes to work together by converting the interface of one class into another.
  • When you want to avoid modifying existing classes to make them compatible with the rest of the application.
  • When you want to make it easier to add new classes to the application without breaking compatibility with existing code.

Here is an example of an Adapter in JavaScript:

class Adapter {
  // The Adapter class wraps the existing class and provides a new interface
  constructor(adaptee) {
    this.adaptee = adaptee;
  }

  // The new interface provides a method that calls the existing method and performs any necessary conversions
  newMethod() {
    return this.adaptee.existingMethod().toLowerCase();
  }
}

class Adaptee {
  // The Adaptee class provides an existing method that needs to be adapted
  existingMethod() {
    return 'Adaptee';
  }
}

To use the Adapter, you would create an Adapter object and use its newMethod method, which will call the existingMethod method of the Adaptee class and perform any necessary conversions:

const adaptee = new Adaptee();
const adapter = new Adapter(adaptee);

console.log(adapter.newMethod()); // Output: 'adaptee'

In this example, the Adapter class wraps the Adaptee class and provides a new interface that is compatible with the rest of the application. The newMethod method calls the existingMethod method of the Adaptee class and performs any necessary conversions, allowing the Adaptee class to be used in the application without modifying its original interface.


That's basically it for now. There are a few others but perhaps that is for the next post.

💡
Want to sponsor matcha.fyi? You can have your brand featured right here. Let's connect and chat about it.