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 calledProductsBuilderthat will serve as the builder for creating product objects.private _products: Product.GetProductResponseWithCategories[];andprivate _limit: number;: These are private properties of theProductsBuilderclass._productsis an array of product objects, and_limitis 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 optionallimitparameter and the requiredproductsparameter. Thelimitparameter is an optional string that is parsed into a number, and theproductsparameter is an array of objects that contain the product details.async fromServer(): This is a method of theProductsBuilderclass that retrieves products from a server using an asynchronousgetServerProductsfunction. It sets_productsto the products returned by the server and returns theProductsBuilderobject.formatProducts(): This is a method of theProductsBuilderclass that formats the product objects by adding aurlandimageproperty to each object. Theurlproperty is set to a string containing the product's custom URL, and theimageproperty is set to the product's primary image URL. It returns theProductsBuilderobject.filterProducts(filterIds: number[]): This is a method of theProductsBuilderclass that filters out products with IDs included in an array offilterIds. It returns theProductsBuilderobject.randomlyPickProducts({ pick }: { pick: string | undefined }): This is a method of theProductsBuilderclass that randomly selects a specified number of products from the_productsarray. It takes an object with an optionalpickproperty that can be a string or undefined, and sets_productsto the randomly selected products ifpickis truthy. Otherwise, it leaves_productsunchanged. It returns theProductsBuilderobject.get products(): This is a getter method of theProductsBuilderclass that returns the_productsarray.
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.
