The source code for this blog is available on GitHub.
Note
Top

Design Patterns in JavaScript

Cover Image for Design Patterns in JavaScript
Chen Han
Chen Han

Builder Pattern

When I first see the pattern from Yup or Zod, it amazed me. How can I write the code pattern like yup.string().required("First Name is mandatory"). It's incredibly readable and easy to maintain

The code is an example of a common design pattern in software development called the Builder Pattern. It makes the whole paragraph more readable with this pattern

The code pattern seen in libraries like Yup or Zod is impressive, such as yup.string().required("First Name is mandatory"). It is highly readable and easy to maintain, making it a preferred approach in software development.

The code pattern is an example of the Builder Pattern, a widely used design pattern that separates the construction of an object from its representation. By using this pattern, validation schemas can be created step-by-step with ease, with each method call returning a new builder object. The resulting code syntax is fluid, intuitive, and easy to read, as seen in the code pattern provided for validating a "First Name" input field.

Builder Pattern with class syntax

When first thinking of how to implement design pattern, i'm always sterotypical that only class pattern works. The following is a builder from product in my project

There are too many things pertaining to product, and that's the reason why I make it with builder pattern. I hope it can initialize with either the data structure you provide or fetch it from server with async function fromServer Also we can transform the product to order items structure, omnisend products structure, or any third party's data structure. I also want to implement recommended product for individualswith randomPickProducts

When considering how to implement a design pattern in my project, I used to think that only class patterns would work. I recently implemented a Builder pattern for creating product objects with class syntax.

This code is a builder pattern for handling products. It allows for initializing the products either with data provided or by fetching them from the server using the async function fromServer. Additionally, it provides methods to transform the products to order items structure, omnisend products structure, or any third-party data structure. The randomlyPickProducts method enables generating recommended products for individuals. This design pattern facilitates code organization and maintainability by separating the product-related logic into a dedicated class.

export class ProductsBuilder {
  private _products: Product.GetProductResponseWithCategories[];
  private _limit: number;

  constructor({ limit, products }: { 
    limit?: string | undefined,
    products: Product.GetProductResponseWithCategories[]
  }) {
    this._products = isEmpty(products) ? [] : products;
    this._limit = limit ? parseInt(limit, 10) : undefined || 20;
  }
  async fromServer() {
    const serverProducts = await getServerProducts(this._limit);
    this._products =
      serverProducts.products as Product.GetProductResponseWithCategories[];

    return this;
  }

  formatProducts() {
    this._products = this._products.map((product) => ({
      ...product,
      url: "/store" + product.custom_url.url,
      image: product.primary_image.url_standard,
    }));

    return this;
  }

  filterProducts(filterIds: number[]) {
    this._products = this._products.filter((p) => !filterIds.includes(p.id));

    return this;
  }

  randomlyPickProducts({ pick }: { pick: string | undefined }) {
    this._products = pick
      ? getMultipleRandom(this._products, +pick)
      : this._products;

    return this;
  }

  get products() {
    return this._products;
  }
}

Let's break it down:

  • export class ProductsBuilder: This declares a class called ProductsBuilder that will serve as the builder for creating product objects.
  • private _products: Product.GetProductResponseWithCategories[]; and private _limit: number;: These are private properties of the ProductsBuilder class. _products is an array of product objects, and _limit is a number representing the maximum number of products to retrieve.
  • constructor({ limit, products }: { limit?: string | undefined, products: Product.GetProductResponseWithCategories[] }): This is the constructor method for the class. It takes an object as its argument that contains the optional limit parameter and the required products parameter. The limit parameter is an optional string that is parsed into a number, and the products parameter is an array of objects that contain the product details.
  • async fromServer(): This is a method of the ProductsBuilder class that retrieves products from a server using an asynchronous getServerProducts function. It sets _products to the products returned by the server and returns the ProductsBuilder object.
  • formatProducts(): This is a method of the ProductsBuilder class that formats the product objects by adding a url and image property to each object. The url property is set to a string containing the product's custom URL, and the image property is set to the product's primary image URL. It returns the ProductsBuilder object.
  • filterProducts(filterIds: number[]): This is a method of the ProductsBuilder class that filters out products with IDs included in an array of filterIds. It returns the ProductsBuilder object.
  • randomlyPickProducts({ pick }: { pick: string | undefined }): This is a method of the ProductsBuilder class that randomly selects a specified number of products from the _products array. It takes an object with an optional pick property that can be a string or undefined, and sets _products to the randomly selected products if pick is truthy. Otherwise, it leaves _products unchanged. It returns the ProductsBuilder object.
  • get products(): This is a getter method of the ProductsBuilder class that returns the _products array.

And then we can use it like this:

const builder = new ProductsBuilder({ limit: "30" });
const products = await builder.fromServer().formatProducts().filterProducts([1, 2, 3]).products;

The ProductsBuilder class can be used to build a product list in a step-by-step process. For example:

const productsBuilder = new ProductsBuilder({ products });

await productsBuilder.fromServer().formatProducts().filterProducts([1, 2]).randomlyPickProducts({ pick: "3" });

const productList = productsBuilder.products;

Builder Pattern with functional programming

How to implementing the Builder Pattern in JavaScript using functional programming:

const personBuilder = (name, age) => ({
  withName: (name) => personBuilder(name, age),
  withAge: (age) => personBuilder(name, age),
  build: () => ({ name, age })
});

const person = personBuilder()
  .withName("Alice")
  .withAge(25)
  .build();

In this example, personBuilder() is a higher-order function that returns an object with three methods: withName, withAge, and build. Each method returns a new personBuilder object, allowing for a fluent and easy-to-read syntax. When build() is called, it returns a new object with the name and age properties set to the values passed to the personBuilder() function. This approach separates the construction of the person object from its representation, making the code easier to read and maintain.

We can also transform the class pattern mentioned above to functional programming

type ProductsBuilderProps = {
  limit?: string | undefined,
  products: Product.GetProductResponseWithCategories[]
};

type ProductsBuilder = {
  fromServer: () => Promise<ProductsBuilder>,
  formatProducts: () => ProductsBuilder,
  filterProducts: (filterIds: number[]) => ProductsBuilder,
  randomlyPickProducts: ({ pick }: { pick: string | undefined }) => ProductsBuilder,
  getProducts: () => Product.GetProductResponseWithCategories[]
};

export const createProductsBuilder = ({ limit, products }: ProductsBuilderProps): ProductsBuilder => {
  let _products = isEmpty(products) ? [] : products;
  let _limit = limit ? parseInt(limit, 10) : undefined || 20;

  const fromServer = async () => {
    const serverProducts = await getServerProducts(_limit);
    _products = serverProducts.products as Product.GetProductResponseWithCategories[];
    return { ...createProductsBuilder({ limit: _limit, products: _products }) };
  };

  const formatProducts = () => {
    _products = _products.map((product) => ({
      ...product,
      url: "/store" + product.custom_url.url,
      image: product.primary_image.url_standard,
    }));

    return { ...createProductsBuilder({ limit: _limit, products: _products }) };
  };

  const filterProducts = (filterIds: number[]) => {
    _products = _products.filter((p) => !filterIds.includes(p.id));

    return { ...createProductsBuilder({ limit: _limit, products: _products }) };
  };

  const randomlyPickProducts = ({ pick }: { pick: string | undefined }) => {
    _products = pick
      ? getMultipleRandom(_products, +pick)
      : _products;

    return { ...createProductsBuilder({ limit: _limit, products: _products }) };
  };

  const getProducts = () => _products;

  return { 
    fromServer, 
    formatProducts, 
    filterProducts, 
    randomlyPickProducts, 
    getProducts 
  };
};

The createProductsBuilder function creates an object that has the same properties and methods as the ProductsBuilder class. The ProductsBuilderProps type specifies the type of the limit and products properties, and the ProductsBuilder type specifies the type of the object that the createProductsBuilder function returns.

Instead of using instance variables, the functional approach uses variables defined within the scope of the functions. These variables are passed to the functions that need them as parameters, and then the modified variables are used to create a new object that has the same properties and methods as the original object.

Note that this is just one way to convert a class to a functional approach, and there may be other ways to do it as well.

An use case of how we use createProductBuilder

const builder = createProductsBuilder({ 
  limit: '10', 
  products: [{ id: 1, name: 'Product 1', price: 10 }, { id: 2, name: 'Product 2', price: 20 }] 
});

await builder.fromServer().formatProducts().randomlyPickProducts({ pick: '5' });

const products = builder.products;
console.log(products); // Outputs 5 randomly picked products with formatted URLs and images

We create a new instance of the ProductsBuilder class using createProductsBuilder and pass it an object with a limit property of 10 and an array of products. We then chain together the fromServer, formatProducts, and randomlyPickProducts methods to fetch products from the server, format them, and randomly pick 5 of them. Finally, we get the resulting products array by calling the products getter and log it to the console.

Vocabulary

vocabulary regarding this article.

dedicated class

A dedicated class is a class that has a single responsibility or purpose, and focuses on a specific domain or functionality within the codebase. In other words, a dedicated class is a class that is designed to handle a specific task or functionality. By keeping the functionality of a class focused and separated from other unrelated functionalities, it can make the code easier to understand, modify, and maintain over time.

In the context of the previous answer, the ProductsBuilder class is a dedicated class because it focuses on handling the products and all the functionality related to them. This makes the codebase more organized and modular, which can help developers to work more effectively and avoid introducing bugs in other parts of the codebase when modifying or adding functionality.

Is "specifies" in "The ProductsBuilderProps type specifies the type of the bla bla" are same as defines

Yes, in this context, "specifies" means "defines" or "describes" the type of the limit and products properties that the ProductsBuilder class expects to receive. It's a way of defining a specific shape or structure for the objects that will be passed to the class constructor.

© 2024 WOOTHINK. All Rights Reserved.
Site MapTerms and ConditionsPrivacy PolicyCookie Policy