Daniel Rotter
Posted on September 1, 2020
Quite a while ago I have implemented something called RouteBuilder
(has been
renamed to ViewBuider
in the meantime) in Sulu, which - as the name suggests - makes use of the
Builder pattern in order to create different routes in a Sulu
application. In addition to the Builder pattern the
Factory pattern was also used.
For me the hardest part of learning such patterns was to get an idea when it makes sense to use them. Therefore I
decided to report about my usages of design patterns, to hopefully give other people an idea how they could be applied.
So this is the first blog post of a potential series covering the Builder and Factory pattern.
The downsides of an abstract model
In order to better understand why we have chosen to use these patterns, I will try to explain the inconviences we had
to deal with before. The use case was to create something we called Route
at the time (now it has been renamed to
View
), which is a representation of a URL of our
Single Page Application. This Route
did not only contain
the URL itself, but also a reference to a React component, which should be shown if the defined
URL was active. Since we also needed to reference routes in the system, we also added a name parameter to the route.
These were the crucial parts of a route, and always had to be given, so we decided to put them into the constructor of
the Route
class. We didn't want to implement a separate React component for every single route, therefore we made
them configurable by using options placed on the route.
(new Route('sulu_tag.datagrid', '/tags', 'sulu_admin.datagrid'))
->addOption('title', 'sulu_tag.tags')
->addOption('resourceKey', 'tags')
->addOption('adapters', ['table'])
->addOption('addRoute', 'sulu_tag.add_form.detail')
->addOption('editRoute', 'sulu_tag.edit_form.detail');
The above example shows the definition of such a Route
in PHP. sulu_tag.datagrid
is the name of the route, /tags
is the route that will be displayed in the browser's address bar and sulu_admin.datagrid
is a string referencing a
React component in the frontend. This component was implemented in a highly configurable manner, so that we could not
only use it for tags in the previous example, but also for all other kind of entities we have in our system. This is
what the resourceKey
option is describing.
I would argue that if you have a little knowledge about our domain, you would be able to guess what most of these
options are doing. However, it is still cumbersome for the developer who writes code like this:
- The names of options (
title
,resourceKey
, ...) are not obvious and your IDE cannot support you via auto completion. - The content of these options can't be validated, since the
Route
is an abstraction for many different type of routes. - The instantion of the
Route
is hardcoded and the implementation can't be replaced. That was not necessary in our case, but might be necessary in others.
We knew that these obstacles are impairing the developer, but we still liked the idea of having a single Route
class,
because this was the only case we had to handle in our frontend code. Otherwise we would have to take care of a
FormRoute
, DatagridRoute
and so on in our frontend code, which didn't seem really nice as well.
Luckily the Builder pattern enabled us to overcome these obstacle by introducing well defined interfaces to create
these routes while still maintaining a single Route
class.
Instantiate objects with the Builder pattern
The Builder pattern is one of the creational patterns, which means in an OOP context that it takes care of
instantiating objects. It does so by creating a Builder
class with an interface that allows to partly build that
object. So even if you have some object that must know many different things at its construction time, you can still
pass all of them separately, if you have a Builder
as a separation layer between getting that information and the
object instantion. Let's have a look at the implementation of such a RouteBuilder
, which applies the Builder
pattern to our Route
class mentioned above:
<?php
namespace Sulu\Bundle\AdminBundle\Admin\Routing;
class DatagridRouteBuilder
implements DatagridRouteBuilderInterface
{
private $route;
public function __construct(string $name, string $path)
{
$this->route = new Route(
$name,
$path,
'sulu_admin.datagrid'
);
}
public function setResourceKey(
string $resourceKey
): DatagridRouteBuilderInterface
{
$this->route->setOption('resourceKey', $resourceKey);
return $this;
}
public function setTitle(
string $title
): DatagridRouteBuilderInterface
{
$this->route->setOption('title', $title);
return $this;
}
// Omitted some of the methods for brevity
public function getRoute(): Route
{
if (!$this->route->getOption('resourceKey')) {
throw new \DomainException(
'A route for a datagrid view needs a '
. '"resourceKey" option. You have likely '
. 'forgotten to call the "setResourceKey" '
. 'method.'
);
}
// Omitted more checks for brevity
return clone $this->route;
}
}
So instead of instantiating the Route
as shown above with all of its drawbacks, the instantiation of the Route
can
be replaced with something like this:
(new DatagridRouteBuilder('sulu_tag.datagrid', '/tags'))
->setResourceKey('tags')
->setTitle('sulu_tag.tags')
->addDatagridAdapters(['table'])
->setAddRoute('sulu_tag.add_form.detail')
->setEditRoute('sulu_tag.edit_form.detail')
->getRoute();
So with the introducation of the DatagridRouteBuilder
we have solved some of the problems mentioned above:
- By using specific methods instead of options with a string key the IDE can support you with auto completion.
- Before returning the
Route
from thegetRoute
option its options can be validated. - By introducing a
DatagridRouteBuilderInterface
the instantiation process can also be replaced.
And in addition to the DatagridBuilder
we have e.g. also introduced the FormRouteBuilder
, which does something
very similar and gets also rid of these problems. And the best thing is that the getRoute
method of both builders
still return a Route
object, so we can still easily iterate over all available routes, and know how to handle them.
In the above example the Route
class is instantiated right away and the other Builder
methods only call the
setOption
method to assign the correct values. The nice thing is that you could even postpone the creation of the
object to the getter method of the Builder
if necessary, allowing you to define an constructor making sure that the
object is valid starting from the very beginning while making the creation of such an object comfortable for the
developer.
Another advantage is that the Builder
object can also be passed as argument to other functions taking part in the
object creation process. Let's say in your application you have some configuration you want to add to a few Datagrid
routes. In that case you could implement that part in a function:
<?php
function enhanceDatagridRoute(DatagridRouteBuilder $builder) {
$builder->addDatagridAdapters(['table']);
}
At Sulu we have used a similar approach
a few times for the QueryBuilder
of Doctrine.
<?php
function enhance(
QueryBuilder $queryBuilder,
UserInterface $user = null,
string $entityClass,
string $entityAlias
) {
$queryBuilder->leftJoin(
AccessControl::class,
'accessControl',
'WITH',
'accessControl.entityClass = :entityClass '
. 'AND accessControl.entityId = ' . $entityAlias . '.id'
);
$queryBuilder->setParameter('entityClass', $entityClass);
}
This simplified example from the Sulu codebase will make a join to our access control table, and that can be reused for
every query we make. This way of doing things is enabled by the use of Builder
pattern. Doing something similar with
plain DQL
would require being much more careful about building the query from the very beginning, which might be very cumbersome
and not a good developer experience. Therefore I almost always use a QueryBuilder
instead of DQL.
Make the builder replacable by the Factory pattern
Previously the DatagridRouteBuilderInterface
was introduced and I have listed the possibility to replace the
Builder
implementation as an advantage. But actually this is still not possible, since the only difference is that
the instantiation of a different class has been hardcoded. The DatagridRouteBuilder
needs to have a separate instance
for each object being created, because it contains state about the current building process. That means we can't easily
inject it by using e.g. Symfony's dependency injection, because a service is always considered exactly one instance.
Instead another creational pattern can be used: The Factory pattern.
A factory also encapsulates the creation of an object, although it does it in a different way than the Builder pattern.
But that shouldn't stop us from using the Builder and Factory pattern in combination. A very important difference
between these patterns is that a Factory
can be instantiated a single time, due to the fact that it returns the new
object immediately and therfore does not need to keep any state. That means that we can avoid constructing this class
in our code directly (which is kind of a bad practice in a dependency injection context anyway) and can inject the
Factory
. This means that the Factory
implementation can be replaced easily, by simply changing the argument passed
to the constructor of the class using that Factory
. The following code snippet shows how such a factory could look
like:
<?php
namespace Sulu\Bundle\AdminBundle\Admin\Routing;
class RouteBuilderFactory implements RouteBuilderFactoryInterface
{
public function createDatagridRouteBuilder(
string $name,
string $path
): DatagridRouteBuilderInterface
{
return new DatagridRouteBuilder($name, $path);
}
public function createFormRouteBuilder(
string $name,
string $path
): FormRouteBuilderInterface
{
return new FormRouteBuilder($name, $path);
}
}
By doing so the creation of the above DatagridRoute
changes:
$this->routeBuilderFactory
->createDatagridRouteBuilder('sulu_tag.datagrid', '/tags')
->setResourceKey('tags')
->setTitle('sulu_tag.tags')
->addDatagridAdapters(['table'])
->setAddRoute('sulu_tag.add_form.detail')
->setEditRoute('sulu_tag.edit_form.detail')
->getRoute();
The routeBuilderFactory
member variable can be assigned by using dependency injection, making that part of the code
replacable and testable. Mission accomplished!
Conclusion
By applying the Factory
and Builder
pattern we made our code cleaner, more testable and more self-documenting. It
also introduced a language that is known among other developers as well, since Builder
and Factory
are patterns
that are widely known. This solution also allows model your domain in a way that actually makes sense, since you can
defer the creation of an object until all information is available. So the constructor can be built in a way that
guarantees you that the object state is valid.
Usually is said that the Builder
pattern is great when you want to create an object in multiple steps. But I think it
is also great when you have a complex constructor of an object, because it fixes the issue of guessing what the 7th
appearance of true
in the calling code means. So it can also be a substition if you use a PHP version that does not
support named parameters yet.
Posted on September 1, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.