For a lot of real-world situations, I'm a big fan of "event driven programming", or as I think about it, "bus oriented programming". Instead of coupling objects directly, objects just publish events to a shared bus, and the events get routed to the other objects which care about them, in a declarative fashion. It's not a "write all programs" this way sort of stance, it's just there are a lot of real world cases where I've found this to be very effective.
Ricardo's co-worker seems to share my opinions, but perhaps maybe not quite my reasoning. This is the PHP code for handling checking out of their storefront:
public function dispatch()
{
try {
if (!$this->getCheckoutSession()->started) {
$this->start();
} else {
$this->populate();
}
$this->check();
} catch (EmptyBasketException $e) {
$event = $this->dispatcher->dispatch(CheckoutEmptyBasketEvent::create($this->getOrder(), $this->getCheckoutSession()));
} catch (UserHasNoAddressException $e) {
$event = $this->dispatcher->dispatch(CheckoutUserHasNoAddressEvent::create($this->getOrder(), $this->getCheckoutSession()));
} catch (NoBillingAddressException $e) {
$event = $this->dispatcher->dispatch(CheckoutBillingAddressEmptyEvent::create($this->getOrder(), $this->getCheckoutSession()));
} catch (NoDeliveryAddressException $e) {
$event = $this->dispatcher->dispatch(CheckoutDeliveryAddressEmptyEvent::create($this->getOrder(), $this->getCheckoutSession()));
} catch (NoDeliveryException $e) {
$event = $this->dispatcher->dispatch(CheckoutDeliveryMethodNotSetEvent::create($this->getOrder(), $this->getCheckoutSession()));
} catch (InvalidUserPaymentException $e) {
$event = $this->dispatcher->dispatch(CheckoutUserPaymentInvalidEvent::create($this->getOrder(), $this->getCheckoutSession()));
} catch (NoPreferredPaymentException $e) {
$event = $this->dispatcher->dispatch(CheckoutNoPreferredPaymentEvent::create($this->getOrder(), $this->getCheckoutSession()));
} catch (NoUserPaymentException $e) {
$event = $this->dispatcher->dispatch(CheckoutUserPaymentInvalidEvent::create($this->getOrder(), $this->getCheckoutSession()));
} catch (PaymentMethodNotAvailableForOrderException $e) {
$event = $this->dispatcher->dispatch(CheckoutPaymentNotAvailableEvent::create($this->getOrder(), $this->getCheckoutSession()));
} catch (ExpiredAmazonAccessTokenException $e) {
$event = $this->dispatcher->dispatch(CheckoutAmazonTokenExpiredEvent::create($this->getOrder(), $this->getCheckoutSession()));
} catch (AmazonAccessTokenNotFoundException $e) {
$event = $this->dispatcher->dispatch(CheckoutAmazonTokenNotFoundEvent::create($this->getOrder(), $this->getCheckoutSession()));
} catch (AmazonOrderReferenceNotFoundException $e) {
$event = $this->dispatcher->dispatch(CheckoutAmazonOrderReferenceIdNotFoundEvent::create($this->getOrder(), $this->getCheckoutSession()));
} catch (DeliveryAddressInvalidException $e) {
$event = $this->dispatcher->dispatch(CheckoutDeliveryAddressInvalidEvent::create($this->getOrder(), $this->getCheckoutSession()));
} catch (PaypalOrderAmountChangedException $e) {
$event = $this->dispatcher->dispatch(CheckoutPaypalOrderAmountChangedEvent::create($this->getOrder(), $this->getCheckoutSession()));
} catch (PaypalPreparedOrderIdMismatchException $e) {
$event = $this->dispatcher->dispatch(CheckoutPaypalPreparedOrderIdMismatchEvent::create($this->getOrder(), $this->getCheckoutSession()));
} catch (AddressMissingPhoneException $e) {
$event = $this->dispatcher->dispatch(CheckoutAddressMissingPhoneEvent::create($this->getOrder(), $this->getCheckoutSession()));
}
return $event ?? null;
}
Good idea: making sure you clearly handle every exception. Bad idea: duplicating every exception type as an event type and then just passing that off to the bus. It's one way to lose every advantage of structured exception handling and decouple code to the point of undebuggability.
Ricardo, however, is optimistic:
Thankfully, this part of the code is soon to be replaced by a nicer architecture.
Here's hoping it's actually soon, and not the usage of "soon" where it's a synonym for "never".