Design Patterns in JavaScript
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 calledProductsBuilder
that will serve as the builder for creating product objects.private _products: Product.GetProductResponseWithCategories[];
andprivate _limit: number;
: These are private properties of theProductsBuilder
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 optionallimit
parameter and the requiredproducts
parameter. Thelimit
parameter is an optional string that is parsed into a number, and theproducts
parameter is an array of objects that contain the product details.async fromServer()
: This is a method of theProductsBuilder
class that retrieves products from a server using an asynchronousgetServerProducts
function. It sets_products
to the products returned by the server and returns theProductsBuilder
object.formatProducts()
: This is a method of theProductsBuilder
class that formats the product objects by adding aurl
andimage
property to each object. Theurl
property is set to a string containing the product's custom URL, and theimage
property is set to the product's primary image URL. It returns theProductsBuilder
object.filterProducts(filterIds: number[])
: This is a method of theProductsBuilder
class that filters out products with IDs included in an array offilterIds
. It returns theProductsBuilder
object.randomlyPickProducts({ pick }: { pick: string | undefined })
: This is a method of theProductsBuilder
class that randomly selects a specified number of products from the_products
array. It takes an object with an optionalpick
property that can be a string or undefined, and sets_products
to the randomly selected products ifpick
is truthy. Otherwise, it leaves_products
unchanged. It returns theProductsBuilder
object.get products()
: This is a getter method of theProductsBuilder
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.