Published on Feb 12, 2012 by Pim Elshoff
Programming #Principles #Craftsmanship #SOLID
Almost all code we write builds on code others wrote. Whether you use some micro or macro framework, company code base or open source library, code is seldomly written in a vacuum. This can lead to several problems, for which several solutions have been devised. How can we structure our software to prevent dependency problems? This article describes common pains and symptoms of bad dependency management and common techniques for dealing with project-wide dependencies.
A software dependency means that a particular piece of code uses another piece of code, function, class, package, program, system, webservice, ... you name them. For the purpose of this article I am talking about classes using other classes. To manage your softwares dependencies then becomes designing your software architecture such that the depencies are good. The following sections will talk about what's bad, with good being the absense of bad.
Let's not kid ourselves. When we're working deadlines and stuff needs to be done, dependency management is the first to go out the window. This is not a bad thing per se, as long as we keep track of the corners we're cutting and go back to fix them.
Managing dependencies can sometimes feel like we're in a circus act keeping spinning plates up. When we're on one side saving the first plate, some plate at the other side decides to tumble and fall. New insights reveal past mistakes, design choices reveal 'design cavities' and if we are not constantly evaluating our design it can be very hard to keep it all together.
There is no way in hell that we are coming back from one project-wide $connection with mysql_query() and a few years of coding. We would have to manually check every single database interaction.
That's what... Analogous to the previous point, the longer we wait with evaluating our architecture the more difficult it gets. We humans start to feel anxious when we approach pain points; we (unconsciously?) know poking there will hurt and so we avoid it, at the cost of finding and fixing the problems. Bad dependency management is the tooth ache to our dentist. And nobody likes dentists, even though their work is invaluable.
Code with many or difficult dependencies can be very concrete and specific and can be harder to reuse as a result.
Code with many or poorly defined depencies can be hell to test. It's tedious, costly or downright impossible - either way it's easier to just not test it.
Touch it and break it. Touch it and break something else. Touch it and cause a waterfall of cascading changes. No way to be sure.
Moving from mysql_query to PDO... Ew. I've done it once and it took me a week on a 25kloc code base. Never again. And don't forget that your code can be locked in to your code. Being your own vendor doesn't make it less of a lock-in.
Dependencies on private code can add a lot of cost to selling (sub-)packages of software or publishing some of your work for open source. If a package is dependent on private code a possibly major overhaul is required before it can be published.
Metrics are out of scope for this article, but I want to mention it anyway. There are several tools available that can measure and quantify dependencies. I named some in my article on Jenkins. The standard template by Sebastian Bergmann comes with a pyramid of statistics and a matrix of package dependencies that are generated by PHP_Depend. We can use these statistics and generated schemas to evaluate whether or not we're on the right track. Though you probably know whether you are on te right track by the amount of work required to make your software fully tested (or the amount of anxiety you experience when thinking about it).
You can read more on the various kinds of metrics in the php depend documentation.
Instances of classes that depend on instances of other classes need to access those somehow. This is easy when a class requires another class from the same package, but tends to be increasingly more difficult the further removed the dependencies are. Several techniques for managing such dependencies exist.
Here is an incomplete list:
Dependencies are pulled from globally available variables or static factory methods.
Example:
global $db;
$db = ...
...
// in some other scope, for example inside a method
global $db;
Dependencies are provided via constructor or setter method.
Example:
$db = ...
$a = new A($db);
...
// inside A
$b = new B($this->db);
...
// inside B
$c->doSomething($this->db);
// and so on
Dependencies are instantiated and registered with a singleton or static class. Scopes requiring a dependency ask the service locator for it. Also known as registry.
Example: Zend_Registry
Zend_Registry::set('db', $myPdoConnection);
...
// in some other scope, for example inside a method
Zend_Registry::get('db');
Dependency construction information is registered with a container that is passed along distribution channels and used there in factories to construct specific objects. DIC builds on DI.
Example: a la Symfony DI
$container = new Container();
$container->loadDependencyInformationFromYaml(); // yaml, xml, php... you name it
// inside some class that extends the ContainerAware class, for example a controller
$a = new A($container->get('db'));
// the first time 'db' is get, a db object is constructed. recurring calls give back the same object
// Example yaml content
db:
class: FullyQualifiedClassDb
arguments: [%username%, %password%, %database%]
No clue what SOLID is? Read my article
The principle at work here is the Inversion of Dependency principle (the I in SOLID), or ask for what you require, but do not fetch it yourself. We can use this principle to validate the beforementioned methods for managing dependencies. If we look at the various techniques we can distinguish two forms of acquiring dependencies inside a certain scope: Injecting from outside (DI, DIC) and pulling from inside (GS, SL).
Injecting dependencies requires us to think about where we connect the dots between our classes and packages. If we don't properly design the system we end up passing around a lot of objects, or filling a pretty large DI container. I think this is a good thing; our annoyance with dependency juggling acts as a red flag indicating where our design is lacking. DI and DIC adhere nicely to the IoD principle and so they are good solutions to the problem of accessing dependencies from within an enclosed scope.
Pulling our dependencies inside from outside of scope is a pragmatic, sensible, easy and wrong technique. Global scope and service locator fail to meet the IoD principle and should not be used. These techniques are implicit, which makes them difficult to read and complex to understand. They also do not warn us when dependencies go awry. Instead they act as morphine to our tooth aches, preventing us from feeling the pain and finally deciding to pay the dentist a visit.
Personally, I think DI was invented to solve the problems of GS and SL was invented to save bad architectures that couldn't fit into DI (they sprouted roughly in the order GS, DI, SL, DIC). DIC is a move away from SL, a move some of the big PHP frameworks are making, because of the lack of interoperability that SL causes (and component based architecture is hawt right now!).
All techniques, tricks and gimmicks aside, managing dependencies is more about actual design than abstract design patterns. You yourself are responsible for keeping the number of dependencies as low as possible and designing your system such that access to dependencies is provided in a sane, understandable manner.
There is no design pattern that can save you from bad architecture. If it's bad you need to rethink, redesign and refactor.
No trackbacks yet
Patrick v Bergen said:
Thanks for writing this passionate plea for dependency injection. It is also a great summary of alternatives and names the problems that need to be faced. Great job! 20:10, 12 February 2012Maxime said:
Very insightful. Thanks for sharing 14:14, 15 February 2012Java said:
Also see Dependency Injection 09:22, 18 February 2012Nike Shoes said:
Pulling our dependencies inside from outside of scope is a pragmatic, sensible, easy and wrong technique. Global scope and service locator fail to meet the IoD principle and should not be used. These techniques are implicit, which makes them difficult to read and complex to understand. They also do not warn us when dependencies go awry. Instead they act as morphine to our tooth aches, preventing us from feeling the pain and finally deciding to pay the dentist a visit.14:48, 20 February 2012