Utility Domain Objects
Encapsule the domain logic in one place easily in JavaScript
TL;DR. Utility Domain Objects are objects packed with functions responsible for materializing all the domain logic related to an entity.
💭 Context
In Object-Oriented Programming, domain functions are those materialized within the class itself: getters, setters, and all methods used to query and modify the domain.
In functional programming, the challenge is where to locate these methods if there are no classes to house them.
One solution is to have one or more files with these loose utility functions, but the problem is that we don’t have an easy way to concentrate all these functions and easily know what methods are available for each entity.
🚀 Solution: UDOs
Utility Domain Objects (or UDOs) are objects that contain all the domain methods of an entity or an aggregate root. These are located in the same file where the type is defined, making them easy to access and extend functionality.
The object being exported, in the latest versions of TypeScript, can have the same name as the type. TypeScript can detect whether you’re using the type
or the const
.
We can leverage this object to add the constructor or creator, which becomes the only responsible entity for knowing how to create new objects of this type. In this constructor, we can add domain validations to prevent objects with nonsensical property values from being created.
Finally, you can encapsulate access to certain properties of other entities to comply with the Law of Demeter.
For example, an entity Product
with all its domain logic encapsulated in its UDO:
export type Product {
id: string
name: string
price: number,
images: Image[],
}
export const Product = {
create: (id: string, name: string, price: string, images?: Image[]) => {
if (price < 0) throw new Error("Price can't be less than 0")
return { id, name, price, images: images ?? [] };
},
increasePriceByPercentage: (product: Product, percentage: number) => ({
...product,
price: product.price + product.price * percentage,
}),
setName: (product: Product, name: string) => ({
...product,
name,
}),
getMainImageUrl : (product: Product) => {
if (!product.images[0]) throw new Error("No images");
return Images.getUrl(product.images[0]);
}
}