DZone and Manning Publications have partnered to bring you an exclusive chapter from 'Dependency Injection' (by Dhanji R. Prasanna). This chapter covers organizing code in modules, watching out for tight coupling, designing with loose coupling, testing code in discrete units, and working with key rebinding.
Building Modular Applications
So far, we’ve looked at what it means for objects to be well behaved and classes to be well designed in the small. But in any real-world project, there’s also a big picture to be considered. Just as there are principles that help our design of objects in the micro, there are principles for good design in the macro sense. In the coming sections,
we’ll talk about these design principles and see how to relate them to dependency injection.
Chief among these is testing. DI facilitates testing and testable code, the latter being an extremely important and often-overlooked condition to successful development. Closely tied to this is the concept of coupling, where good classes are linked to the dependencies in ways that facilitate easy replacement and therefore testing—bad ones
are not. We’ll see how to avoid such cases, and finally we’ll look at the advanced case of modifying injector configuration in a running application.
First, let’s start at in the micro level and explore the role objects play as the construction units of applications.
4.1 Understanding the Role of an Object
We’re all very familiar with objects; you work with them every day and use them to model problems, effortlessly. But let’s say for a moment you were asked to define what an object is—what might you say?
You might say an object is:
- A logical grouping of data and related operations
- An instance of a class of things
- A component with specific responsibilities
An object is all these things, but it is also a building block for programs. And as such, the design of objects is paramount to the design of programs themselves. Classes that have a specific, well-defined purpose and stay within clearly defined boundaries are well behaved and reliable. Classes that grow organically, with functionality bolted on when and where required, lead to a world of hurt.
A class can itself be a member of a larger unit of collective responsibilities called a module. A module is an independent, contractually sound unit that is focused on a broad part of business responsibility. For example, a persistence module may be responsible for storing and retrieving data from a database.
A module may not necessarily be meant for business functionality alone. For example, a security module is responsible for guarding unwarranted access to parts of an application. Modules may also be focused on infrastructure or on application logic but typically not both. In other words, a module is:
- Whole —A module is a complete unit of responsibility. With respect to an application,
this means that modules can be picked up and dropped in as needed.
- Independent —Unlike an object, a module does not have dependencies on other
modules to perform its core function. Apart from some common libraries, a
module can be developed and tested independently (that is, in an isolated environment).
- Contractually sound —A module conforms to well-defined behavior and can be
relied on to behave as expected under all circumstances.
- Separate —A module is not invasive of collaborators, and thus it is a discrete unit
These qualities of a module are largely important in relation to its collaborators. Many modules interacting with one another through established, patent boundaries form a healthy application. Since modules may be contributed by several different parties (perhaps different teams, sister projects, or even external vendors), it’s crucial that they follow these principles. Swapping in replacement modules, for example, replacing persistence in a database with a module that provides persistence in a data cluster or replacing a web presentation module with a desktop GUI, ought to be possible with a minimum of fuss. So long as the overall purpose of the application is maintained, a system of well-designed modules is tolerant to change. Much as different incarnations of an object graph provide variant implementations of a service, different assemblies of modules provide different application semantics. A module is thus an independent, atomic unit of reuse.
Objects and modules that collaborate typically have strong relationships with each other. Often the design of a dependency is influenced by its collaborators. However, each object has its area of responsibility, and well-designed objects stick to their areas without intruding on their collaborators. In other words, each object has its area of concern. Good design keeps those concerns separated.
The above introductory excerpt was taken from Dependency Injection, published in August 2009. It is being reproduced here by permission from Manning Publications. Manning early access books and ebooks are sold exclusively through Manning. Visit the book's page for more information.