This article (by Richard Martin?) looks like a good introduction to Dependency Inversion, which appears to be mostly an attempt to reduce downward dependencies (the so-called Traditional Design Approach).
As far as I can tell though, it does not really invert dependencies; you don’t end up with your low level I/O library depending on your business logic. Rather it decouples components, making both ‘high’ and ‘low’ level libraries depend on some abstract/interface library, or a runtime configuration that defines the assembly.
So, Dependency Inversion advocates creating an interface and implementation for all public classes. But it seems to me this is blindly following the ‘decoupling’ principle so far that it stops helping in practice and starts to hurt.
Where do package interfaces go?
For a start, where does the abstracted interface go? If the interface goes in the higher level library (so that it defines what it expects from the implementation), then your low level utility depends on business logic, and this is Bad – you can’t reuse it in another part of the business. It would be foolish if your TCP/IP library had to implement an interface defined in the invoicing part of your code.
If you put the interface in the utility library, then if you replace the library with another one your interface is going to be replaced by the one in the new library – with its own package name and potential differences. You might as well just use the implementation class and not have to create a redundant interface and factory class.
So interfaces really shouldn’t go in the higher library, and they’re redundant in the utility library.
To implement proper decoupling you need adaptors or façades between each coupling (see commons-logging, Java’s XML parser, etc) which gives you more code and packages to maintain. It also obscures execution: every boundary between libraries become a barrier, as it’s not clear at the point of use which library is being called upon.
Yet the desire to decouple is supposed to reflect the desire to make maintenance easier.
But coupling is… good…
Or rather, a library with a straightforward public API that defines its features is good. This API gives you a contract about how to use the library. This contract is enforced by the compiler, and by all decent IDEs, which will no doubt offer extra coding help based on those contracts. By coding to those contracts, we get compile time checking, and any student on work experience can follow the code.
The main bonus for dependency inversions is so that you can swap out low level libraries and replace them with another. For example, you may want to replace your XML parser with a new one that is much faster. But few libraries can genuinely be blindly replaced in practice as they have different features – in fact it’s likely you’re swapping libraries in order to use those new features.
If you’re using the libraries through a lowest-common-denominator interface then you’ll have to work around it using ‘generic’ methods, and that usually means you’re bypassing any compile-time checks. If at runtime a different library is used, there will be ‘undefined behaviour’ when these extra features are not available. For example, ‘property’ switches in the early Java XML libraries could be used to switch validation on and off, but some libraries allowed this and some did not, and some required different properties to do the same job.
In that case we end up with dependencies not only on the common abstracted interface, but also several implementation libraries, and on the mechanisms of deployment, to include extra checks that the right libraries are included.
Instead I much prefer early-defined, specific, suitable, easy, compile-time checks; they’re the easy first-stage check that my code is sound, they help my IDE help me, they help those who review my code and, most importantly, those who use it for real.
By coupling our components through extra interface/factory packages, we can assemble different applications at runtime, rather than having to compile a particular assembly first. This might be done through a configuration file (picocontainer) or ‘automagically’ by putting the right implementations on the classpath (commons-logging). This gives you tremendous flexibility at runtime, allowing you to specify, say, faster or more robust libraries as required at various customer sites.
On the other hand, the first requires learning the format for a secial configuration file, editing it, validating it and controlling it. Which is, essentially, just more programming. The second can make classpath set ups and debugging the most frustrating nightmare – and on-site too.
Case – Commons logging
“If only all library writers used commons logging”, the reasoning goes, “then it would forward log messages to the mechanism preferred by the owning application, or component container such as Tomcat”. If only All Had Obeyed. Now of course, we have some libraries writing to log4j and some to JLog and some to System.out and some to their own… and some to commons-logging. And commons-logging gets confused about which library it’s supposed to write to, because there are several loaded.
It’s a particularly easy target because there were very few widespread logging libraries by the time it was implemented, so writing adaptors between them was not hard. If you wanted to use a library that logged to log4j in Websphere (which uses JLog) you could write a log4j handler that forwarded the messages to JLog. And now that JDK 1.4 includes a logging library, you could argue that all low level libraries should use that – if, that is, you think low level libraries should be logging at all.
You can see the principle behind it though; by having a single common adaptor, we could move our libraries between applications (or our components between containers) without worrying about the logging environment. In fact though, it couldn’t solve the problem; even now some library writers use log4j over JDK 1.4 logging, despite the latter being ‘zero-effort’ to include, because log4j offers them more. Instead it made the problem worse, by adding in new runtime class loader problems as well as the config file editing required by all the logging libraries in use.
On an Extra Bonus Whinge about commons-logging; logging must be reliable and robust. Bad Things happen when starting up complex applications – especially if some of it is defined at runtime – and the log must report them if you have any hope to look even vaguely professional on site.
Randomly moving jar files from directory to directory for an unfamiliar web server between mute crashes because the logger has failed is frustrating enough, but doing so sat in a customer’s office is an experience I do not want to repeat.
Where you have a well-defined requirement for different components to be used in different runtime environments, a configurable factory makes a lot of sense. I, however, would recommend that that factory be specific to task rather than a generic one like picocontainer, because with picocontainer you lose type constraints and early failures become late failures.
Swapping between libraries that share the same API is fairly rare; people swap to use new features. Application-wide search-and-replace on library-using code is straightforward with modern IDEs. When writing a library, define your API well with as much contractual information as possible (typing, named attributes, etc), and as little exposure to the workings as possible. This reduces effort writing, reviewing, debugging and maintaining code, and in deploying applications, that overwhelm occasional library changes.
All this applies equally well, of course, to services…
[An old article from my boring old site, brought to the blogosphere for fame and fortune]