Specificaties combineren
08 november 2023
3 minutes
Een van die mooie software ontwerp patronen die we dagelijks gebruiken is het specificatiepatroon. Het kan gebruikt worden om specifieke bedrijfsregels af te dwingen. In een eenvoudige vorm kan het patroon er als volgt uitzien:
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);
}
}
Zoals u kunt zien, controleert de regel of een bepaald product voldoet aan bepaalde zakelijke criteria om te kunnen worden verkocht. Een extra methode is toegevoegd om deze regel te bewaken: als niet aan de regel wordt voldaan, wordt een uitzondering gegooid.
Nu denk je misschien: je valideert hier alleen maar enkele eigenschappen - waarom zou je die in een extra klasse stoppen? De titel verraadt misschien al het antwoord op deze vraag: combineerbaarheid.
Het grote idee achter het specificatiepatroon is dat je booleaanse logica kunt toepassen op de regels. Stel je voor dat je vanuit een centrale voorraad producten verkoopt via meerdere winkels. Het product moet actief zijn en voorraad hebben om verkoopbaar te zijn. Het is mogelijk dat een winkel een bepaald product niet kan leveren vanwege transportproblemen. In dat geval kunnen we 2 specificatieregels combineren om na te gaan of een product bij die winkel kan worden besteld.
StoreCanDeliverProductRule (store, product) =>
ProductIsForSaleRule(product)
&& StoreCanDeliverProduct(store, product)
Ook al blijft elke regel op zichzelf vrij rechtlijnig, je kunt zeer complexe bedrijfsspecificaties bouwen door eenvoudige testbare regels te combineren.
Een bijkomend voordeel van deze aanpak ten opzichte van gewone validatie van eigenschappen, is dat je een eenvoudigere manier hebt om regels binnen de applicatie te veranderen. Als elke beslissing wordt genomen door bedrijfsregels, zal het wijzigen van een regel tot gevolg hebben dat de applicatie onmiddellijk deze nieuwe regels kent. U hoeft niet overal in uw codebase te zoeken naar if statements die van toepassing zijn op die specifieke regels die u probeert te valideren.
Zelfs als een regel slechts uit 1 eigenschap validatie bestaat, is het volkomen logisch om het in een regel op te nemen. Stel je voor dat je winkel een "actieve" eigenschap heeft. Het lijkt misschien een absolute overkill om een regel speciaal hiervoor te maken:
final class StoreIsActiveRule
{
public function check(Store $store): bool
{
return $shop->isActive();
}
}
Die regel kan in de toekomst echter veranderen: Een winkel kan specifieke openingstijden hebben en op het internet zijn aangesloten om bestellingen op te nemen. In dat geval zou de specificatie kunnen evolueren tot iets dergelijks:
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()
);
}
}
Misschien is het zelfs zinvoller om te componeren:
StoreIsActiveRule(store, moment) =>
StoreActivityProbe(store)
&& StoreIsOpenedAt(store, moment)
Stop met het opnieuw uitvinden van voorwaarden en begin vandaag met het samenstellen van regels. Je zult ons later dankbaar zijn! 🤸