Introduction to the Law of Demeter
Software programming is a balanced mix of art (sometimes a euphemism for improvisation) and a bunch of well-proven heuristics used to tackle certain problems and solve them in a decent fashion. Few will disagree that the artistic side is by far the hardest one to polish and distill over time. On the other hand, taming the forces behind the heuristics is fundamental for being able to developing software that rests on the foundation of good design.
With so many heuristics stating how and why software systems should cling to a specific approach, it’s pretty disappointing not seeing a broader implementation of them in the world of PHP. For example, the Law of Demeter is probably one of the most underrated in the language’s realm.
Effectively, the law’s “talk to your closest friends” mantra still seems to be in a pretty immature state in PHP, something that contributes to rot in the overall quality of several object-oriented code bases. Some popular frameworks are actively pushing it forward, trying to be more committed to the law’s commandments. Throwing blame around for infringing the Law of Demeter is pointless, as the best way to mitigate such breakages is to simply be pragmatic and understand what’s actually under the law’s hood hence consciously applying it when writing object-oriented code.
In an attempt to join the just cause and dig a little bit deeper into the law from a practical point of view, in the next few lines I’ll be demonstrating through some hands-on examples why something so simple as adhering to the law’s principles can be a real boost when designing loosely-coupled software modules.
Knowing Too Much Isn’t a Good Thing
Often referred to as the Principle of Least Knowledge, the rules promoted by the Law of Demeter are easy to digest. Simply put, and assuming that you have a beautifully-crafted class which implements a given method, the method in question should be constrained to call other methods that belong to the following objects:
- An instance of the method’s originating class.
- Objects that are arguments of the target method.
- Objects that are created by the target method.
- Objects that are dependencies of the method’s originating class.
- Global objects (ouch!) that can be accessed by the originating class within the target method.
Although the list is a world away from being formal (for one that’s a little more formal, check out Wikipedia), the points are pretty easy to understand.
In traditional design, the fact that an object knows way too much about another (and this implicitly includes knowing how to access a third one) is considered wrong because there are situations where the object has to unnecessarily traverse from top to bottom a clumsy mediator to find the actual dependencies it needs to work as expected. This is, for obvious reason, a serious design flaw. The caller has a pretty extensive and detailed knowledge about the mediator’s internal structure, even if this one is accessed through a few getters.
Moreover, using an intermediary object to get to the one required by the caller makes a statement on its own. After all, why use such a tangled path to acquire a dependency or invoke one of its methods if the same result can be achieved by injecting the dependency directly? The process doesn’t make any sense at all.
Let’s say we need to build up a file storage module which uses internally a polymorphic encoder to pull in and save data to a given target file. If we were intentionally sloppy and hooked up the module to an injectable service locator, its implementation would look like this:
<?php
namespace LibraryFile;
use LibraryDependencyInjectionServiceLocatorInterface;
class FileStorage
{
const DEFAULT_STORAGE_FILE = "data.dat";
private $locator;
private $file;
public function __construct(ServiceLocatorInterface $locator, $file = self::DEFAULT_STORAGE_FILE) {
$this->locator = $locator;
$this->setFile($file);
}
public function setFile($file) {
if (!is_readable($file) || !is_writable($file)) {
throw new InvalidArgumentException(
"The target file is invalid.");
}
$this->file = $file;
return $this;
}
public function write($data) {
try {
return file_put_contents($this->file,
$this->locator->get("encoder")->encode($data),
LOCK_EX);
}
catch (Exception $e) {
throw new $e(
"Error writing data to the target file: " .
$e->getMessage());
}
}
public function read() {
try {
return $this->locator->get("encoder")->decode(
@file_get_contents($this->file));
}
catch(Exception $e) {
throw new $e(
"Error reading data from the target file: " .
$e->getMessage());
}
}
}
Leaving out of the picture some irrelevant implementation details, the focus is on the constructor of the FileStorage
class and its write()
and read()
methods. The class injects an instance of a still undefined service locator, which is used later on for acquiring a dependency (the aforementioned encoder) in order to fetch and store data in the target file.
This is a typical infringement of the Law of Demeter considering that the class first goes through the locator and in turn reaches the encoder. The caller FileStorage
knows too much about the locator’s internals, including how to access the encoder, which definitively isn’t an ability I would sing praises about. It’s an artifact intrinsically rooted to the nature of service locators (and that’s why some see them as an anti-pattern) or any other kind of static or dynamic registries, something that I pointed out before.
To have a more general view of the issue, let’s check the locator’s implementation:
<?php
namespace LibraryDependencyInjection;
interface ServiceLocatorInterface
{
public function set($name, $service);
public function get($name);
public function exists($name);
public function remove($name);
public function clear();
}
<?php
namespace LibraryDependencyInjection;
class ServiceLocator implements ServiceLocatorInterface
{
private $services = [];
public function set($name, $service) {
if (!is_object($service)) {
throw new InvalidArgumentException(
"Only objects can register with the locator.");
}
if (!in_array($service, $this->services, true)) {
$this->services[$name] = $service;
}
return $this;
}
public function get($name) {
if (!$this->exists($name)) {
throw new InvalidArgumentException(
"The requested service is not registered.");
}
return $this->services[$name];
}
public function exists($name) {
return isset($this->services[$name]);
}
public function remove($name) {
if (!$this->exists($name)) {
throw new InvalidArgumentException(
"The requested service is not registered.");
}
unset($this->services[$name]);
return $this;
}
public function clear() {
$this->services = [];
return $this;
}
}
In this case I implemented the locator as a plain dynamic registry with no additional bells or whistles so it’s easy to follow. You can decorate it with some extra functionality if you’re in the mood.
The last thing we must do is create at least one concrete implementation of the corresponding encoder so that we can put the file storage class to work. This class should do the trick pretty nicely:
<?php
namespace LibraryEncoder;
interface EncoderInterface
{
public function encode($data);
public function decode($data);
}
<?php
namespace LibraryEncoder;
class Serializer implements EncoderInterface
{
public function encode($data) {
if (is_resource($data)) {
throw new InvalidArgumentException(
"PHP resources are not serializable.");
}
if (($data = serialize($data)) === false) {
throw new RuntimeException(
"Unable to serialize the data.");
}
return $data;
}
public function decode($data) {
if (!is_string($data)|| empty($data)) {
throw new InvalidArgumentException(
"The data to be unserialized must be a non-empty string.");
}
if (($data = @unserialize($data)) === false) {
throw new RuntimeException(
"Unable to unserialize the data.");
}
return $data;
}
}
With the encoder set, now let’s get things rolling using all the sample classes together:
<?php
use LibraryLoaderAutoloader,
LibraryEncoderSerializer,
LibraryDependencyInjectionServiceLocator,
LibraryFileFileStorage;
require_once __DIR__ . "/Library/Loader/Autoloader.php";
$autoloader = new Autoloader();
$autoloader->register();
$locator = new ServiceLocator();
$locator->set("encoder", new Serializer());
$fileStorage = new FileStorage($locator);
$fileStorage->write(["This", "is", "my", "sample", "array"]);
print_r($fileStorage->read());
The violation of the law is in this case a rather furtive issue hard to track down from the surface except for the use of the locator’s mutator which suggests that at some point the encoder will be accessed and consumed in some form by an instance of FileStorage
. Regardless, we know the infringement is just right there hidden from the outside world, a fact that not only reveals too much about the locator’s structure, but couples unnecessarily the FileStorage
class to the locator itself.
Just by sticking to the law’s rules and getting rid of the locator, we’d be removing the coupling, while at the same providing FileStorage
with the actual collaborator it needs to do its business. No more clunky, revealing mediators along the way!
Fortunately, all this babble can be easily translated into working code with just a pinch of effort. Just check the enhanced, Law of Demeter-compliant version of the FileStorage
class here:
<?php
namespace LibraryFile;
use LibraryEncoderEncoderInterface;
class FileStorage
{
const DEFAULT_STORAGE_FILE = "data.dat";
private $encoder;
private $file;
public function __construct(EncoderInterface $encoder, $file = self::DEFAULT_STORAGE_FILE) {
$this->encoder = $encoder;
$this->setFile($file);
}
public function setFile($file) {
// the sample implementation
}
public function write($data) {
try {
return file_put_contents($this->file,
$this->encoder->encode($data), LOCK_EX);
}
catch (Exception $e) {
throw new $e(
"Error writing data to the target file: " .
$e->getMessage());
}
}
public function read() {
try {
return $this->encoder->decode(
@file_get_contents($this->file));
}
catch(Exception $e) {
throw new $e(
"Error reading data from the target file: " .
$e->getMessage());
}
}
}
That was easy to refactor, indeed. Now the class directly consumes any implementers of the EncoderInterface
interface, avoiding going through the internals of an unnecessary intermediate. The example is unquestionably trivial, but it does make a valid point and demonstrates why adhering to the Law of Demeter’s commandments is one of the best things you can do to improve the design of your classes.
Still, there’s a special case of the law, covered in depth in Robert Martin’s book Clean Code: A Handbook of Agile Software Craftsmanship, that deserves a particular analysis. Just think this through for a moment: what would happen if FileStorage was defined to acquire its collaborator via a Data Transfer Object (DTO), like this?
<?php
namespace LibraryFile;
interface FileStorageDefinitionInterface
{
public function getEncoder();
public function getFile();
}
<?php
namespace LibraryFile;
use LibraryEncoderEncoderInterface;
class FileStorageDefinition implements FileStorageDefinitionInterface
{
const DEFAULT_STORAGE_FILE = "data.dat";
private $encoder;
private $file;
public function __construct(EncoderInterface $encoder, $file = self::DEFAULT_STORAGE_FILE) {
if (!is_readable($file) || !is_writable($file)) {
throw new InvalidArgumentException(
"The target file is invalid.");
}
$this->encoder = $encoder;
$this->file = $file;
}
public function getEncoder() {
return $this->encoder;
}
public function getFile() {
return $this->file;
}
}
<?php
namespace LibraryFile;
class FileStorage
{
private $storageDefinition;
public function __construct(FileStorageDefinitionInterface $storageDefinition) {
$this->storageDefinition = $storageDefinition;
}
public function write($data) {
try {
return file_put_contents(
$this->storageDefinition->getFile(),
$this->storageDefinition->getEncoder()->encode($data),
LOCK_EX
);
}
catch (Exception $e) {
throw new $e(
"Error writing data to the target file: " .
$e->getMessage());
}
}
public function read() {
try {
return $this->storageDefinition->getEncoder()->decode(
@file_get_contents($this->storageDefinition->getFile())
);
}
catch(Exception $e) {
throw new $e(
"Error reading data from the target file: " .
$e->getMessage());
}
}
}
It’s definitely an interesting slant for implementing the file storage class as it now uses an injectable DTO for transferring and consuming internally the encoder. The question that begs answering is if this approach really violates the law. In a purist sense it does, as the DTO is unquestionably a mediator exposing its whole structure to the caller. However, the DTO is just a plain data structure which, unlike the earlier service locator, has no behavior at all. And precisely the purpose of data structures is… yes, to expose its data. This means that as long as the mediator doesn’t implement behavior (which is exactly the opposite to what a regular class does, as it exposes behavior while hiding its data), the Law of Demeter will remain neatly preserved.
The following snippet shows how to use the FileStorage
with the DTO in question:
<?php
$fileStorage = new FileStorage(new FileStorageDefinition(new Serializer()));
$fileStorage->write(["This", "is", "my", "sample", "array"]);
print_r($fileStorage->read());
This approach is a lot more cumbersome than just directly passing the encoder into the file storage class, but the example shows that some tricky implementations, which at first blush seem to be flagrant breakers of the law, are, in general, pretty harmless as long as they make use of data structures with no behavior attached to them.
Closing Thoughts
With a prolific variety of tangled, sometimes esoteric, heuristics making their way through OOP, it seems pointless to add just another one to the pile, which apparently doesn’t have any visible positive impact in the design of layer components. The Law of Demeter, though, is everything but a principle with little or no application in the real world.
Despite of its flourishy name, the Law of Demeter is a powerful paradigm whose primary goal is to promote the implementation of highly-decoupled application components by eliminating any unnecessary mediators. Just follow its commandments, without falling into blind dogmatism of course, and you’ll see the quality of your code improve. Guaranteed.
Image via Fotolia