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.
- Singleton: Ensures that a class has only one instance and provides a global point of access to it.
- Factory: Creates objects without specifying the exact class to create.
- Builder: Separates the construction of a complex object from its representation, allowing for different representations.
- Prototype: Creates objects by cloning an existing object rather than creating new objects from scratch.
- 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.