business rules to make sure there is no way to violate the business rules. write code to make sure users (and developers) can't violate these rules.
Use objects as consistency boundaries.
Users with / without email address: ProspectiveCustomer and PayingCustomer
Make the implicit explicit
Don't use getters and setters
frameworks force us to have setters and getters, there are ways around it: create layers in between:
value objects are the heart and soul of DDDD
Entity:
Value Object (eg Email, School Year (which is different to a calendar year), Distances, Measurements, Datetime, Money, Currency, DateRange Object)
Encapsulate state and behaviour with Value Objects
$order = new Order;
$order->setCustomer($customer);
$order->setProducts($products);
$order->setStatus(Order::UNPAID);
// ...
$order->setPaidAmount(500);
$order->setPaidCurrency('EUR');
$order->setStatus(Order::PAID);
we need to get closer to the domain language. Make it readable to the domain expert.
1) Create a PaymentStatus Value Object
$order->setStatus(new PaymentStatus(PaymentStatus::UNPAID));
$order->setStatus(new PaymentStatus(PaymentStatus::PAID));
2) Create Money Value Object
$order->setPaidMonetary(new Money(500, new Currency('EUR')));
3) an order is always for a customer and a bunch of products. So instead of doing a set on the outside assume it's unpaid and create in a constructor
$order = new Order($customer, $products);
// set PaymentStatus in Order::__construct()
4) Pay
$order->pay(new Money(500, new Currency('EUR')));
Encapsulate Operations when you need to change items in an entity
5) Make Pay better - to get closer to the language.
This is as close as you can get to expressing in the domain language
$customer->payFor($order, new Money(500, new Currency('EUR')));
If you couple everything to everything you are going to get into trouble
$customer->isPremium() // boolean flag
Business rule: You have to order 3 times to be a premium customer
1) Specification Pattern
interface CustomerSpecification {
public function isSatisfiedBy(Customer $customer);
}
Very simple interface. Takes customer as an argument and returns true or false:
So for premium Customer:
class CustomerIsPremium implements CustomerSpecification
{
private $orderRepository;
public function __construct(OrderRepository $orderRepository) {
...
}
public function isSatisfiedBy(Customer $customer) {
$count = $his->orderRepository->countFor($customer);
return $count >= 3;
}
}
$customerIsPremium = new CustomerIsPremium($orderRepository);
if ($customerIsPremium->isSatisfiedBy($customer)) {
// send special offer
}
Maybe you would Inject the Order Repository with Dependency Injection
Because this business rule is in one place it is easy to change it (in one place).
And it is easily testable:
$customerIsPremiume = new CustomerIsPremium;
$aCustomerWith2Orders = ...
$aCustomerWith3Orders = ...
assertFalse($customerIsPremium->isSatisfiedBy($aCustomerWith2Orders));
assertTrue($customerIsPremium->isSatisfiedBy($aCustomerWith3Orders));
Domain Expert says 'we have different premium rules for different types of customer'
DRY is not about code duplication it is about knowledge - a single source of truth! We had a single source of truth about what made a customer premium, we can still do that if we have different rules:
Turn the CustomerIsPremium class into an interface, the implementations can be totally different:
interface CustomerIsPremium
extends CustomerSpecification
final class CustomerWith3OrdersIsPremium
implments CustomerIsPremium
final class CustomerWith500EuroTotalIsPremium
implments CustomerIsPremium
final class CustomerWhoBoughtLuxuryProductsIsPremium
implments CustomerIsPremium
...
Usage:
final class SpecialOfferSender
{
private $customerIsPremium;
public function __construct(
CustomerIsPremium $customerIsPremium) {...}
public function sendOffersTo(Customer $customer)
{
if ($this->customerIsPremium->isSatisfiedBy($customer)) {
// send offers
}
}
}
Dependency Injection in Symfony:
depending on the customer/tenant string we create a differnet object but give it the same key. Each one needs a customer.is.premium object:
amazon customers:
ebay customers:
others
Use specifications to encapsulate rules about object selection
Better approach is 'collection orientated repository':
interface CustomerRepository
{
public function add(Customer $customer);
public function remove(Customer $customer);
/** @return Customer */
public function gind(CustomerId $customerId);
/** @return Customer[] */
public function findAll();
/** @return Customer[] */
public function findRegisterdIn(Year $year);
}
These are methods that work just as well in in memory collections - the interface would look exactly the same. You would never have methods like persist or save in here. This interface might save objects to the database in the background. But imprtantly you can swap/replace/implement this inteface with an in-memory collection (great for unit tests), a database, or a Doctrine repository or MongoDB repository but the client code still thinks it's a customer repository with collection style methods.
Use repositories to create the illusion of an in-memory collection (even though in background it is persisting to a database).
So to implement Get a list of premium Customers - We already have the specificaiton so we just add a new method to our CustomerRepository, add a findSatisfying method...:
interface CustomerRepository
{
/** @return Customer[] */
public function findSatisfying(CustomerSpecification $customerSpecification);
// generalised
$objects = $repository->findSatisfying($specification);
}
Now we implemet this by looping over the collection and filter:
class DbCustomerRepostory implements CustomerRepository
{
/** @return Customer[] */
public function findSatisfying(CustomerSpecification $customerSpecification) {
return array_filter(
$this->findAll(),
function (Customer $customer) use ($specification) {
return $specification->isSatisfiedBy($customer);
}
);
}
}
foreach loop instead of array_filterWe have now enforced our business rule using this specification. But this won't be very efficient if you have a lot of customers! We need a proper database query.
It could be quite simple SELECT count(*) from order....
final class CustomerWith3OrdersIsPremium implments CustomerSpecification {
public function asSql() {
return '<SELECT></SELECT>LCT * from Customer...';
}
// class DbCustomerRepostory
public function findSatisfying($specificaition) {
return $this->db->query($specification-asSql());
}
}
}
So we have replaced array_filter method with asSql() or dQL for Doctrine etc
This is called
Double Dispatchto preserve encapsulation.
On the outside we are telling the repository to give us all the customers satisfying this specification, on the inside the repository says - ok Specification give me your representation in sql and i will us that to query the database.
If you want to get rid of the query and code version of the same business rule there are ways round it but they are not very interesting or useful...
We still have our in-code nieve array filter as well as our SQL query so it is still easy to test:
$expectedCustomers = array_filter(
$repository->findAll(),
//filter...
);
$actualCustomers = $repository->findSatisfying($specification);
assertThat($expectedCustomers, equalsTo($actualCustomers));
Test by comparing different representations of the same business rule
There are always round these rules it (using Reflection or some other black magic! etc) but should be easier to spot!