Design Patterns: Observer
The Observer pattern is a Behavioural design pattern that defines a one-to-many dependency between objects.
Overview
The Observer pattern is a Behavioural design pattern that defines a one-to-many dependency between objects. When the state of one object (the subject) changes, all its dependents (observers) are notified and updated automatically.
Imagine a notification system. Every time a new order is placed, you might want to:
Send an email.
Add an audit log.
Update a dashboard.
With the Observer Pattern, each action is handled by a separate “observer” class. Each observer is notified of order events without tightly coupling them to the order logic, making the codebase clean, scalable, and easy to extend.
Example
A Subject class (usually an event, i.e a user registers) is required, along with Observer classes, which can be attached to a Subject.
First, lets define some interfaces:
interface Observer
{
public function handle(): void;
}
interface Subject
{
public function attach(Observer $observer): self;
public function detach(Observer $observer): self;
public function notify(): void;
}
The Observer interface ensures all observers must implement, ensuring they have a handle
method and we can also type hint to this interface when attaching and detaching Observers to a subject.
The Subject interface enforces the requirement of attach
, detach
and notify
methods.
Next, we create a subject class, in this instance an UserRegisters
class. Instances of the Observer interface can be attached or detached. When notify
is called, all the attached observers are executed (their handle
method is called).
class UserRegisters implements Subject
{
/**
* @var array<int, Observer>
*/
public array $observers = [];
#[\Override]
public function attach(Observer $observer): self
{
$this->observers[] = $observer;
return $this;
}
#[\Override]
public function detach(Observer $observer): self
{
$this->observers = array_filter($this->observers, fn($class): bool => $class !== $observer);
return $this;
}
#[\Override]
public function notify(): void
{
foreach ($this->observers as $observer) {
$observer->handle();
}
}
}
Ok, so lets add a few Observers:
class LogActivity implements Observer
{
#[\Override]
public function handle(): void
{
// Logic to log activity
}
}
class SaveUserAccount implements Observer
{
#[\Override]
public function handle(): void
{
// Logic to save a user account
}
}
class SendWelcomeEmail implements Observer
{
#[\Override]
public function handle(): void
{
// Logic to send welcome email
}
}
But how do we implement this? Good question, here’s a few examples:
// Attach LogActivity, SendWelcomeEmail and SaveUserAccount to the UserRegisters subject.
$subject = (new UserRegisters())
->attach(new LogActivity())
->attach(new SendWelcomeEmail())
->attach(new SaveUserAccount());
// Notify the observers
$subject->notify();
// Attach ONLY SendWelcomeEmail to the UserRegisters subject.
$subject = (new UserRegisters())
->attach(new SendWelcomeEmail());
// Notify the observers
$subject->notify();
Behaviours are defined at run-time, notice how easy it is to add and remove behaviours? Here’s some advantages and disadvantages:
Advantages
Loose Coupling: Observers are decoupled from the subject, meaning the subject only needs to know the observer implements a specific interface. This promotes modular code and makes it easy to extend functionality by adding new observers without changing existing code.
Flexibility: You can easily add or remove observers at runtime without altering the subject's code, making it straightforward to introduce new functionality or notifications in response to state changes.
Automatic Updates: Observers automatically receive updates when the subject changes, ensuring consistency without manually updating each observer.
Disadvantages
Complexity: Adding multiple observers can introduce complexity, particularly if they perform asynchronous actions. The system's behaviour can become harder to predict if multiple observers are dependent on specific timings.
Memory Usage: Observers remain in memory as long as they are registered, which can cause memory leaks if not properly removed when they are no longer needed.
Debugging Challenges: Debugging and testing can become challenging, as issues in one observer may cause unexpected behaviour across the system, making it difficult to isolate problems.
Want to see the Observer Pattern in action? Check out this example: Github