Skip to content

design patterns

3 posts with the tag “design patterns”

Constructing complex objects with Builder Pattern

When an object has many properties, passing a long list of parameters to a constructor can become messy and hard to read. This is often known as the “telescoping constructor” anti-pattern.

The Builder Pattern is a creational design pattern that solves this by letting you construct complex objects step by step, improving readability and maintainability.

Instead of initializing directly with a massive constructor:

// Hard to remember what each parameter means!
const user = new User("John", "Doe", 30, "john@example.com", "123 Main St", true);

Using the Builder Pattern:

class User {
constructor(
public firstName: string,
public lastName: string,
public age?: number,
public email?: string,
public address?: string,
public isActive?: boolean
) {}
}
class UserBuilder {
private user: Partial<User> = {};
// Required fields can be set in the Builder's constructor
constructor(firstName: string, lastName: string) {
this.user.firstName = firstName;
this.user.lastName = lastName;
}
setAge(age: number): this {
this.user.age = age;
return this; // Return 'this' to allow method chaining
}
setEmail(email: string): this {
this.user.email = email;
return this;
}
setAddress(address: string): this {
this.user.address = address;
return this;
}
setIsActive(isActive: boolean): this {
this.user.isActive = isActive;
return this;
}
build(): User {
// Construct the final object
return new User(
this.user.firstName!,
this.user.lastName!,
this.user.age,
this.user.email,
this.user.address,
this.user.isActive
);
}
}
// Construction is fluent, explicit, and easy to read
const user = new UserBuilder("John", "Doe")
.setAge(30)
.setEmail("john@example.com")
.setAddress("123 Main St")
.setIsActive(true)
.build();

This pattern makes your code more readable, flexible, and provides a clear separation between object construction and its representation.

Real-world Examples

The Builder pattern is extremely common in the TypeScript/JavaScript ecosystem, especially for constructing complex queries or configuration objects. Here are a few popular examples:

  1. Knex.js & TypeORM: SQL query builders heavily rely on this pattern to construct complex database queries step by step.

    // TypeORM example
    const user = await dataSource
    .createQueryBuilder()
    .select("user")
    .from(User, "user")
    .where("user.id = :id", { id: 1 })
    .getOne();
  2. Supertest (with Jest): Used for HTTP assertions, where you build a request step by step.

    await request(app)
    .get('/user')
    .set('Accept', 'application/json')
    .expect('Content-Type', /json/)
    .expect(200);
  3. Mongoose: Querying MongoDB uses a chained builder approach.

    User.find()
    .where('age').gt(18)
    .sort('-createdAt')
    .limit(10)
    .exec();

Lazy initialize object with Proxy Pattern

Sometimes, you want to lazy intialize an object such as websocket to make it’s only connected when needed, (virtual) proxy pattern can help.

Instead of initializing directly:

const socket = new Websocket(url);

Using the Proxy:

// The proxy controls a web socket instance
class WebSocketProxy {
constructor(options) {
this.options = options;
}
connect() {
if (this.socket) {
return this.socket;
}
this.socket = new Websocket(this.options.url);
return this.socket;
}
}
const socket = new WebSocketProxy({ url: 'wss://www.example.com/socketserver' });
socket.connect();

The differences between observer, pubsub, and mediator pattern

Understanding the basic differences between the 3 patterns.

Observer: the publisher must know and manage observers, observers will be attached to the specific publisher:

const observer = function(data) {
console.log('received event');
};
Publisher.addObserver(observer);
Publisher.notify('data');

Pubsub: the publisher can publish an event without knowing who are the listeners, publisher and listeners communicate through the event channel:

const subscriber = function(data) {
console.log('received event');
};
Pubsub.on('event', subscriber);
// Publisher publishes event through Pubsub object
Pubsub.publish('event', 'data');

Mediator: groups logic of domain that has indirect relationship between modules. We can use Pubsub (not required, can use other patterns) to manage the workflow :

const Mediator = {
init: function() {
Pubsub.on('event', doSomething.bind(this));
},
doSomething: function(data) {
// do other actions, like calling Ajax and notifying another event.
Ajax.get('path', data)
.then(function(res) {
Pubsub.publish('another event', res);
});
}
};