Application complexity keeps increasing
Design patterns / Languages / Tools / …
Agile / XP / BDD / DDD / …
« Web applications » over « Websites »
« Business rules » over « Code »
“The central idea of Specification is to separate the statement of how to match a candidate, from the candidate object that it is matched against.”Martin Fowler — http://martinfowler.com/apsupp/spec.pdf
A specification = a business rule
Specifications are composable
class BookSupportsWebReader implements Specification
{
const FORMATS_EPUB = ['epub', 'epub 3', 'epub fixed layout'];
public function isSatisfiedBy($book)
{
return in_array($book->getFormat(), self::FORMATS_EPUB)
&& $book->getProtection() !== 'adobe drm';
}
}
« A book supports the web reader if it's an ePub not protected by Adobe DRM »
class BookSupportsWebReader implements Specification
{
const FORMATS_EPUB = ['epub', 'epub 3', 'epub fixed layout'];
public function isSatisfiedBy($book)
{
return in_array($book->getFormat(), self::FORMATS_EPUB)
&& $book->getProtection() !== 'adobe drm';
}
public function andX(Specification $spec)
{
return new AndSpecification($this, $spec);
}
public function orSatisfies(Specification $spec) { /* … */ }
public function not() { /* … */ }
}
$spec = (new BookSupportsWebReader())
->andX(new AvailableInCountry('FR'))
->andX((new PublisherBlacklisted())->not());
$isViewableOnline = $spec->isSatisfiedBy($book); // bool(true)
Book
instance.
format IN :formats_epub AND protection != "adobe drm"
« A book supports the web reader if it's an ePub not protected by Adobe DRM »
$rule = 'format IN :formats_epub AND protection != "adobe drm" AND
"fr" IN countries AND NOT(publisher.blacklisted)';
// use the textual rule
$isViewableOnline = $rulerz->satisfies($book, $rule, [
'formats_epub' => ['epub', 'epub 3', 'epub fixed layout'],
]); // bool(true)
class BookSupportsWebReader implements Specification
{
public function getRule()
{
return 'format IN :formats_epub AND protection != "adobe drm"';
}
public function getParameters()
{
return [
'formats_epub' => ['epub', 'epub 3', 'epub fixed layout'],
];
}
}
// build a specification object
$spec = (new BookSupportsWebReader())
->andX(new AvailableInCountry('FR'))
->andX((new PublisherBlacklisted())->not());
$isViewableOnline = $rulerz->satisfiesSpec($book, $spec); // bool(true)
// our app uses Doctrine to query our database
$queryBuilder = $entityManager
->createQueryBuilder()
->select('book')
->from('Entity\Book', 'book');
// textual rule
$viewableOnlineBooks = $rulerz->filter($queryBuilder, $rule, [
'formats_epub' => ['epub', 'epub 3', 'epub fixed layout'],
]);
// specification object
$viewableOnlineBooks = $rulerz->filterSpec($queryBuilder, $spec);
var_dump($viewableOnlineBooks); // array<Entity\Book>
class DoctrineBookRepository implements BookRepository
{
public function findByEan($ean) { }
public function findByTitle($title) { }
public function findPublished() { }
public function findViewableOnline() { }
public function findNotViewableOnline() { }
public function findPublishedAndViewableOnline() { }
// …
}
class DoctrineBookRepository implements BookRepository
{
public function matching(Specification $spec)
{
$qb = $this->createQueryBuilder('c');
return $this->rulerz->satisfiesSpec($qb, $spec);
}
}
Dynamic rules (e-commerce coupons) / Search forms / …
Hard: Hoa\Ruler
Implied: Hoa\Compiler