Qafoo GmbH - passion for software quality
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
:Author: Tobias Schlitt
:Date: Fri, 29 Sep 2017 08:40:59 +0200
:Revision: 4
:Copyright: All rights reserved
========================
Injectables vs. Newables
========================
:Abstract:
Many projects I join - even those that claim to already do *dependency
injection* - suffer from issues that result from mixing *injectable* and
*newable* classes. Keeping these two appart seems to be challenging for
many developers so that I try to give them a handy guide with Do's and
Dont's in this blog post.
:Description:
Many projects I join - even those that claim to already do *dependency
injection* - suffer from issues that result from mixing *injectable* and
*newable* classes. Keeping these two appart seems to be challenging for
many developers so that I try to give them a handy guide with Do's and
Dont's in this blog post.
:Keywords:
object oriented design, transaction script
Many projects I join - even those that claim to already do *dependency
injection* - suffer from issues that result from mixing *injectable* and
*newable* classes. Keeping these two appart seems to be challenging for many
developers so that I try to give them a handy guide with Do's and Dont's in
this blog post.
Lets clarify the difference first: Injectables perform actual work, talk to
external systems or to other injectables. Newables hold state (data) and
potentially perform work on this data (exclusively). As examples for these you
can peak at Domain Driven Design (DDD): Services are injectable - entities and
value objects are newable.
Two fundamental constraints apply to the concepts of newables and injectables:
a) Newables **must not** depend on injectables and
b) Injectables **must not** hold newables as their state (object attributes).
Our experience shows: Violating one of these eventually leads to hardly
debuggable side-effects and technical debt. Sticking to these is a first big
step into better maintainable software. I'll showcase this on basis of examples
violating these rules.
--------------------
Injectable Violation
--------------------
Given this simple service as an example::
productRepository = $productRepository;
}
public function pickItem($id, $count)
{
// ...
}
}
The class has only 1 dependency and 1 method: it works against the
``ProductRepository`` and ``pickItem`` allows to pick a number of items by ID
from the warehouse and to keep track of picked items. Various implementations
are possible for this functionality. One I saw is the following::
productRepository->find($id);
$this->pickedItems[] = new Pick($productDetails, $count);
}
public function getPicks()
{
return $this->pickedItems;
}
}
This implementation problematic, because the class does two entirely different
things at the same time: a) it fetches product details from somewhere (possibly
a database) and b) it keeps track of the items and counts picked from the
warehouse.
Now, what happens if two or more pick tours need to be created subsequently? Of
course, the ``$pickedItems`` collection needs to be reset for each of them.
Maybe these pick tours are even planned at entirely different places of the
application. Now these code parts would influence each other if they receive
the same ``Warehouse`` instance through dependency injection. Or even worse,
some code-parts rely on receiving the very same instance of the warehouse to
interchange a pick tour and due to some change in application configuration
this changes. Such effects are generally called **side effects** and such can
lead to extremely hard to find bugs.
So, what is the way to prevent such issues? We need to separate state artifacts
from the working code artifact. In this specific case an example solution could
be::
productRepository->find($id);
return new Pick($productDetails, $count);
}
}
and in the place using this ``Warehouse`` service::
// ...
$picks[] = $warehouse->pickItem(23, 5);
or, if you want to wrap the collected picks into an object that maintains the
collection::
$pickCollection = new PickCollection();
// ...
$pickCollection->add($warehouse->pickItem(23, 5));
Now we have two code entities that a each perform a dedicated part of the work:
a) the ``Warehouse`` is responsible for interacting with the external service
and creates the picks and b) the ``PickCollection`` keeps track of the current
state of picks.
We can also now safely pass around the ``Warehouse`` service without fear of
unwanted side-effects. On the other hand we can also pass around
a ``PickCollection`` to make it be used by various code pieces and with that
indicate explicitely that state is meant to be changed.
-----------------
Newable Violation
-----------------
We already saw the extraction of a newable in the previous example: the
``PickCollection``. In contrast to an ``injectable`` - which is injected from
the outside everywhere it is needed - a ``newable`` can be created anywhere in
the code to wrap data and represent a logical entity. For example::
class ProductDetails
{
private $title;
private $availableInCountries = [];
// ...
public function getTitle()
{
return $this->title;
}
public function isAvailableIn($countryCode)
{
return in_array($countryCode, $this->availableInCountries);
}
}
This one holds state about a product title and in which countries the product
should be available. In addition to that it wraps some simple logic. Now, what
if we introduce some ActiveRecord functionality to make storing of product
details easier? This could look like::
class ProductDetails
{
// ...
public function __construct(DatabaseConnection $connection /* ... */)
{
$this->connection = $connection;
}
public funtcion save()
{
// ... prepare SQL ...
$statement = $this->connection->prepare($sql);
// ... bind parameters ...
$statement->execute();
}
}
Pretty convenient, now? Well, yes and no: Of course it is convenient to simply
store a product by calling ``save()`` on the object. But it also has some very
bad drawbacks: Every time you want to create a product in the code now, you
need to have a ``DatabaseConnection`` available. This actually reduces the
convenience of working with the ``ProductDetails`` class a lot. The possibly
worst place is inside of unit tests: you will be required to create a mock for
``DatabaseConnection`` everytime you want to work with ``ProductDetails``.
What went wrong? The ``DatabaseConnection`` is an injectable and the rule
is: "Newables **must not** depend on injectables". Ripping out the
code parts into their own injectable is the solution here::
class CodeDetailsRepository
{
public function __construct(DatabaseConnection $connection /* ... */)
{
$this->connection = $connection;
}
public funtcion save(ProductDetails $product)
{
// ... prepare SQL ...
$statement = $this->connection->prepare($sql);
// ... bind parameters ...
$statement->execute();
}
}
-----------
Bottom Line
-----------
.. note::
Do you want to get training to learn more about Object Oriented Design?
Qafoo can help you and unlock the potential of your development team. Book
a `workshop on Object Oriented Design`__ now.
__ /services/workshops/object_oriented_design.html
Keeping newables and injectables separate helps you to avoid a large portion of
nasty bugs and eases working with either of these object categories. Follow the
simple rules:
a) Newables **must not** depend on injectables and
b) injectables **must not** hold newables as their state (object attributes).
As usual in software engineering there are cases where you cannot entirely
avoid mixing them up. Every project has one or two of these places. Try to keep
them as small as possible and to draw a solid border between these rare places
and your remaining code to keep it as clean as possible [1]_.
.. [1] For example, think about the concept of ``TokenStorage`` and
``RequestStack`` in Symfony. Do yourself a big favor and use these
injectables only when really necessary!
..
Local Variables:
mode: rst
fill-column: 79
End:
vim: et syn=rst tw=79
Trackbacks
==========
Comments
========
- ApeHanger at Tue, 24 Oct 2017 13:14:52 +0200
Nice article.
There is a typo, do s/funtcion/function/
regards ApeHanger
- Daniel at Tue, 24 Oct 2017 21:56:10 +0200
Don't you think it would have been enough to link to
http://misko.hevery.com/2008/09/30/to-new-or-not-to-new/?
- http://star-writers.com/ at Fri, 09 Mar 2018 11:16:10 +0100
Every single article is filled up with something extremely original and
unique! Thanks for doing your painstaking job!