Injectables vs. Newables
First published at Tuesday 24 October 2017
Warning: This blog post is more then 7 years old – read and use with care.
Injectables vs. Newables
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:
Newables must not depend on injectables and
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:
<?php
class Warehouse
{
private $productRepository;
public function __construct(ProductRepository $productRepository)
{
$this->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:
<?php
class Warehouse
{
private $productRepository;
private $pickedItems = [];
// ...
public function pickItem($id, $count)
{
$productDetails = $this->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:
<?php
class Warehouse
{
private $productRepository;
public function pickItem($id, $count)
{
$productDetails = $this->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
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:
Newables must not depend on injectables and
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
andRequestStack
in Symfony. Do yourself a big favor and use these injectables only when really necessary!
Subscribe to updates
There are multiple ways to stay updated with new posts on my blog: