All systems change during their life cycles
Ivar Jacobson
Uncle Bob
Bertrand Meyer
Barbara Liskov
David Parnas
A module should be open for extension but closed for modification.
Change behaviour by adding code, not changing it!
public function whenSomethingHappened(StatusWasChanged $event)
{
$messageText = 'some message';
$appointment = $this->appointments->findByAggregateId($event->aggregateId());
foreach ($appointment->participants as $participant) {
$this->sendEmail($messageText, $practice, $participant);
$this->sendWebNotification('someEvent', $messageText, $participant);
}
}
public function whenSomethingHappened(StatusWasChanged $event)
{
$messageText = 'some message';
$appointment = $this->appointments->findByAggregateId($event->aggregateId());
foreach ($appointment->participants as $participant) {
$this->sendEmail($messageText, $practice, $participant);
if ($participant->phone()) {
$this->sendSms($messageText, $participant->phone());
}
$this->sendWebNotification('someEvent', $messageText, $participant);
}
}
public function whenSomethingHappened(StatusWasChanged $event)
{
$messageText = 'some message';
$appointment = $this->appointments->findByAggregateId($event->aggregateId());
foreach ($appointment->participants as $participant) {
if (!$participant->isAvailableForNotifications($practice->timezone())) {
continue;
}
if ($participant->notifications()->someEvent()->email()) {
$this->sendEmail($messageText, $practice, $participant);
}
if (
$participant->notifications()->someEvent()->phone() &&
$participant->phone()
) {
$this->sendSms($messageText, $participant->phone());
}
if ($participant->notifications()->someEvent()->browser()) {
$this->sendWebNotification('someEvent', $messageText, $participant);
}
}
}
public function whenSomethingHappened(StatusWasChanged $event)
{
$appointment = $this->appointments->findByAggregateId($event->aggregateId());
foreach ($appointment->participants as $participant) {
$this->notifier->send(SomeNotification::for($participant));
}
}
class Notifier
{
// ...
public function send(Notification $notification)
{
$this->notifications->remember(NotificationEntry::for($notification));
if ($this->shouldNotDisturbFor($notification)) {
return;
}
if ($this->isSmsNotificationsEnabled($notification)) {
// send via sms
}
if ($this->isEmailNotificationsEnabled($notification)) {
// send via email
}
if ($this->isBrowserNotificationsEnabled($notification)) {
// send via websockets
}
}
}
Each module should have one and only one reason to change
class Notifier
{
// ...
public function send(Notification $notification)
{
$this->notifications->remember(NotificationEntry::for($notification));
if ($this->shouldNotDisturbFor($notification)) {
return;
}
if ($this->isSmsNotificationsEnabled($notification)) {
$this->smsNotifier->send($notification->message(), $notification->phone());
}
if ($this->isEmailNotificationsEnabled($notification)) {
// delegate to email notifier
}
if ($this->isBrowserNotificationsEnabled($notification)) {
// delegate to browser notifier
}
}
}
Same type = Same Behaviour
Subtypes should be substitutable for their base types
interface DateTimeInterface
{
public function modify(): \DateTimeInterface;
// ...
}
class DateTime implements DateTimeInterface
{
public function modify(): \DateTimeInterface {
return $this;
}
}
class DateTimeImmutable implements DateTimeInterface
{
public function modify(): \DateTimeInterface {
return new self(...);
}
}
Many client specific interfaces are better than one general purpose interface
interface EntityManagerInterface extends ObjectManager
{
public function getCache();
public function getConnection();
public function getExpressionBuilder(); public function beginTransaction();
public function transactional($func);
public function commit();
public function rollback();
public function createQuery($dql = '');
public function createNamedQuery($name);
public function createNativeQuery($sql, ResultSetMapping $rsm);
public function createNamedNativeQuery($name);
public function createQueryBuilder();
public function getReference($entityName, $id);
public function getPartialReference($entityName, $identifier);
public function close();
public function copy($entity, $deep = false);
public function lock($entity, $lockMode, $lockVersion = null);
public function getEventManager();
public function getConfiguration();
public function isOpen();
public function getUnitOfWork();
public function getHydrator($hydrationMode);
public function newHydrator($hydrationMode);
public function getProxyFactory();
public function getFilters();
public function isFiltersStateClean();
public function hasFilters();
}
Depend upon Abstractions. Do not depend upon concretions.
interface PaymentMethod
{
/**
* Return true if implementation supports
* this payment method
*/
public function supports(string $method): bool;
/**
* Generated invoice for given payment method
*/
public function pay(Money $amount): Invoice;
}
Depend in the direction of stability.
What about all that talk about screwing up future events?
class RegisterCustomerHandler
{
public function handle(RegisterUserCommand $command)
{
$user = $this->users->registerUser($command->userCredentials);
$this->referrals->enroll($user->asId(), $command->referral);
$this->orders->createCustomer($user->asId(), $command->customerInfo);
}
}