https://doeken.org/assets/img/decorator-vs-proxy-pattern.jpg
There are two patterns in PHP that are very similar; The Decorator Pattern and The Proxy Pattern. Because they are so
similar, you can quickly mistake one for the other. Does that matter? Maybe not, but I think it’s good to know the
differences when communicating about them.
Similarities between Decorators and Proxies
Both the Decorator Pattern and the Proxy Pattern revolve around the idea of wrapping an instance of an existing
interface (let’s call that the inner instance) with a class that implements that same interface and delegates their
function calls to the same functions on their inner instance.
These patterns are very useful for adding or changing functionality of an instance without breaking encapsulation. It
can also change or extend functionalities of final
functions and classes. And because they usually serve one purpose
they can easily be tested.
Example
interface SubscriberInterface {
public function subscribe(string $email): void;
}
class SubscriberDecorator implements SubscriberInterface {
private SubscriberInterface $inner_subscriber;
public function subscribe(string $email): void {
$this->inner_subscriber->subscribe($email);
}
}
In this example you can see that our SubscriberDecorator
implements the SubscriberInterface
and it also requires
some instance of the SubscriberInterface
. After that it delegates the subcribe()
function to the same function on
that instance.
Differences between Decorators and Proxies
When it comes to naming a class a Decorator or a Proxy you have to look at its intent. What is the class actually doing
with the instance it is wrapping?
Required vs. Optional dependency
You might have noticed I didn’t include a __construct()
method in the previous example. This was intentional, because
this is where the first difference can be apparent.
A Decorator requires an instance of the interface it is wrapping, while a Proxy does not require such an
instance. A Proxy can receive an instance, but is also allowed to create this instance itself. So you can create
a new
Proxy on its own, while a Decorator needs another instance as dependency.
// Decorator
public function __construct(public SubscriberInterface $inner_subscriber){}
// Proxy
public function __construct(?SubscriberInterface $inner_subscriber = null){
$this->inner_subscriber = $inner_subscriber ?? new InnerSubscriber();
}
Additive vs. Restrictive
Decorators are additive; meaning they only add new functionality by wrapping the function call and returning the
original value. It can however do anything before or after that call. You can for example log every value when a
function is called or dispatch an event. Just make sure to return the original value.
Proxies are restrictive; meaning they can change the behavior of a function or even restrict calling a specific function
by throwing an exception.
Tip: Both Decorators and Proxies are allowed to add any extra functions or parameters that are not on the interface. It can therefore be wise to implement some magic
__isset()
,__get()
and__call()
methods on the Decorator or Proxy to pass these calls along to their inner instance as well. This way you can still call those methods and parameters even if you add multiple decorators on top.
public function __call($name, $arguments)
{
return $this->inner_subscriber->{$name}(...$arguments);
}
public function __get($name)
{
return $this->inner_subscriber->{$name};
}
public function __isset($name)
{
return isset($this->inner_subscriber->{$name});
}
General purpose vs. Specific purpose
Decorators serve a general purpose. It will add some functionality regardless of the instance it is wrapping. This means
that multiple decorators should be able to be applied on top of one another in any random order and still produce the
same result and added functionality.
Proxies serve a more specific purpose. It will mostly be used to change or append functionality to a specific instance
of the interface. Proxies also aren’t commonly stacked on top of one another as a single proxy is usually enough.
Tips for Decorators and Proxies
Here are a few tips you might consider when working with Decorators and Proxies.
Make a base abstraction
If you create multiple Decorators or Proxies of the same interface it can be beneficial to create an abstract class
of
the interface or a trait
that satisfies the interface, where every function is already deferred to the function on
the inner instance. If you are a package creator, you might even consider providing this implementation inside the
package. This way a Decorator or Proxy can extend
or use
this implementation and only (re)declare the functions
it needs.
interface SubscriberInterface
{
public function subscribe(string $email): bool;
public function unsubscribe(string $email): bool;
}
trait SubscriberTrait { ... }
{
private SubscriberInterface $inner_subscriber;
public function subscribe(string $email): bool
{
return $this->inner_subscriber->subscribe($email);
}
public function unsubscribe(string $email): bool
{
return $this->inner_subscriber->unsubscribe($email);
}
public function __call($name, $arguments)
{
return $this->inner_subscriber->{$name}(...$arguments);
}
public function __get($name)
{
return $this->inner_subscriber->{$name};
}
public function __isset($name)
{
return isset($this->inner_subscriber->{$name});
}
}
// You can now extend this class, or implement the interface and trait.
abstract class SubscriberDecorator implements SubscriberInterface
{
use SubscriberTrait;
public function __construct(SubscriberInterface $inner_subscriber)
{
$this->inner_subscriber = $inner_subscriber;
}
}
Single responsibility Decorators
It might be tempting to add multiple features onto a Decorator, but the beauty of them is that they can be added or
removed without changing the underlying code. So try to make tiny Decorators that focus on one thing and apply these on
top of each other. Again, this simpleness makes them easier to test as well.
Examples
You can find a couple of nice examples of Decorators and Proxies in Symfony.
Their developer toolbar shows a lot of information regarding events and cache, for example. They log this information by
decorating the current EventDispatcher with
a TraceableEventDispatcher and
the current cache adapter with
a TraceableAdapter
within the dev
environment.
An example of a Proxy can be found in
the DeflateMarshaller of
the symfony/cache
package. This Marshaller is restrictive due to its dependency on gzinflate()
& gzdeflate()
and
it’s changes to the output of the inner instance.
Thanks for reading
I hope you enjoyed reading this article! If so, please leave a 👍 reaction or a 💬 comment and consider subscribing to
my newsletter! I write posts on PHP almost every week. You can also follow me
on twitter for more content and the occasional tip.
Laravel News Links