Combining specifications
08 november 2023
3 minutes
One of them fine software design patterns we use on a daily basis is the specification pattern. It can be used to validate and guard specific business rules. In a simple form, the pattern can look like this:
final class ProductIsForSaleRule
{
public function guard(
Product $product
): void {
if (!$this->check($product)) {
throw ProductException::notForSale();
}
}
public function check(
Product $product
): bool {
return $product->isActive()
&& $product->stock()->greatherThan(0);
}
}
As you can see, the rule checks if a given product satisfies some business criteria in order for it to be sellable. An additional method is added to guard this rule: If the rule is not satisfied, an exception is thrown.
Now you might think: you are just validating some properties right here - why take the extra mile to put these in an additional class? The title might have given away the answer to this question : composibility.
The big idea behind the specification pattern is that you can apply boolean logic to the rules. Imagine you are selling products from a central stock through multiple stores. The product needs to be active and have stock in order to be sellable. A store might not be able to deliver a specific product because of transport issues. In that case, we can combine 2 specification rules in order to find out of a product can be ordered from that store.
StoreCanDeliverProductRule (store, product) =>
ProductIsForSaleRule(product)
&& StoreCanDeliverProduct(store, product)
Even though every rule by itself remains quite straight forward, you can build very complex business specifications by combining simple testable rules.
An additional benifit of this approach over regular property validation, is that you have an easier way to change rules inside the application. If every decision is made by business rules, changing a rule will result in the application instantly knowing these new rules. You won't have to search all over your codebase to change if statements that apply to this specific rules you are trying to validate.
Even if a rule only consists out of 1 property validation, it makes perfect sense to put it in a rule. Imagine your store has an "active" property. It might look absolute overkill to create a rule specifically for this:
final class StoreIsActiveRule
{
public function check(Store $store): bool
{
return $shop->isActive();
}
}
However, that rule might change in the future: A store might have specific opening hours and might be connected to the internet in order to take orders. In that case, the specification might evolve into something like this:
final class StoreIsActiveRule
{
public function __construct(
private StroreActivityProbe $activityProbe,
private Clock $clock
) {
}
public function check(
Store $store
): bool {
return $this->activityProbe->check($store)
&& $store->openingsHours->isOpened(
$this->clock->now()
);
}
}
In fact, it might even make more sense to compose:
StoreIsActiveRule(store, moment) =>
StoreActivityProbe(store)
&& StoreIsOpenedAt(store, moment)
Stop reinventing conditions and start composing rules today. You'll thank us later! 🤸