Abstract Modules

by kirk knoernschild

Statement

Depend upon the abstract elements of a module.

Description

Modules  heavily depended upon have many incoming dependencies. In other words, you may have many modules that all depend on a single modules. On one hand, this is a good thing because you’ve managed to maximize reuse. But reuse has it’s challenges. If the modules you’re reusing heavily requires a change, the ramifications of that change can ripple throughout all dependent modules. Changing a heavily reused module can be quite a maintenance headache.

There are two types of changes a modules can undergo. If you change a method signature of a public method on a public class, you can introduce compile errors into other areas of the application. While this might seem the most severe, it’s the easiest to correct. The negative ramifications of this type of change are fairly obvious, and if you do change a method signature, it’s important that you search for all referencing classes and make the correction. As a general rule, instead of changing the method signature, I’ll typically deprecate the old version and create the new version of the method with the updated parameters. This allows other developers to incrementally make changes.

You might feel safer if you only have to change a method implementation, or possibly a private method. But changing an implementation can have a farther reaching impact than you might expect. While you’ll certainly isolate all compile errors to the module, a much more stealth problem arises. Something must have driven the functional change being made. In the case of changing a module that is heavily depended upon, at least one of the dependent modules is the driver for that change. But you cannot only consider that single context when testing the change, but must also consider all other dependent modules. No compiler will tell you if you’ve introduced a change that won’t work with other modules not considered by the decision that drove the change. It’s much easier to make a change if you know that the effect of change is contained to just a single module.

Some modules within an application are very widely used. A module that can be used by any other module is a global package. An example of a global module is one containing utility classes that provide helpful methods to manipulate strings and format dates. You have to be very cautious when changing global modules. A small problem can cause widespread failure.

When you’re designing the dependencies between modules, depending upon only the abstract elements increases flexibility. It gives you the ability to more easily extend and maintain areas of your application by defining new modules with classes that implement or extend the abstraction. Most important, clients of the modules now have the freedom to receive different implementations because they are no longer coupled to an implementation, but instead are coupled to an abstraction (ie. an abstract class or interface).

In Figure 1, you can see the client1 package is dependent on the service package. Examining the contents of client1, you’ll notice that the dependency of the Client class is on the Service interface. This is supported by the associated code. Therefore, the client1 package is dependent upon only the abstract elements in service. This relationship is enforced, and cannot be violated, because the ServiceImpl class is given only package scope.

AbstractModule

Figure 1: Depending on the Abstract Elements of a Module

The stability [MARTIN] of a package is a metric used to help determine the likelihood that a package will experience change. In Chapter 14, I examine this stability metric. Stable packages resist change, and are desirably the most heavily depended upon packages in the system. In Figure 7.4.1, you see the service package used by three other packages. The service package should be as stable, or resistant to change, as possible.

Implementation Variations

Depending on only the abstract elements of a package carries a price. Creating the implementing class can no longer be done by using the new keyword to create an instance of the implementing class. Instead, you have to consider some of the following options:

  • Object Factory – To avoid dependencies on the concrete elements of a package, an object factory can be used to create the appropriate concrete instances. This offers a few advantages. First, the factory is the only area of the application referencing the concrete class. Adding new concrete classes that extend the abstraction is much easier. Second, if there are rules associated with creating the instances, these rules are well encapsulated within the factory. If the rules change, you’ll have a single maintenance point.
  • Dynamic Creation – In some situations, you’ll find that using the Class class is more appropriate than an object factory. I’ve found this approach to work best in a couple of cases. In a web application, if I need to create certain classes at server startup, I’ll use the Class class and specify the concrete class to instantiate in a startup properties file. This allows me to create new classes that can be plugged into my application when the server starts up by simply defining the new class and then specifying it’s fully qualified name in the appropriate properties file. The second scenario is when I’m using an Abstract Factory [GOF]. Specifying how the appropriate concrete factories get created is also useful to specify in a properties file. In most other cases, I’ll use the object factory approach described above.

I don’t think it’s imperative that all dependencies on a package be abstract all the time. There are situations where referencing the concrete class directly is appropriate, and it eases the burden of designing a creation mechanism. However, you can certainly depend on some abstractions in a package, while also referencing concrete classes in that same package. It’s no coincidence that the most stable packages are packages that also have the majority of incoming dependencies on abstract classes or interfaces. This leads to two interesting observations.

Abstract classes or interfaces should absorb most incoming dependencies, whereas outgoing dependencies should originate from concrete classes. While simpler, you’ll want to avoid a heavily dependent package with a lot of concrete classes  because a single change can have a significant ripple effect across all dependent packages. You’ll also want to avoid packages with a lot of abstract packages that have no incoming dependencies. I’ll leave it as an exercise to the reader to determine why such a package is utterly worthless. In Chapter 12, I’ll explore some metrics that can help evaluate the robustness of your package structure.

The example in Heuristic 7.4 clearly illustrated the value of a dependency on only the abstract elements of a package. I’m not going to rehash that material here. You might, however, be interested in referring to Heuristic 11.2, which is closely related to Heuristic 7.5, but offers an additional perspective.

Consequences

If you have to change a stable module, you’ll experience a ripple effect throughout much of the application. Unless it’s a very simple change, the ripple effect is almost certain to cause a problem somewhere. Of course, you’ll probably have a gut feeling telling you what to expect of your change, and you should listen. The more you have to change stable packages, the less maintainable you’ll find the application.

Sample Code

LoanSampleAbstractModules

Figure 1: Abstract Module

Wrapping Up

If Heuristic 7.2 is arguably the most important, Heuristic 7.5 arguably yields the most flexibility. While the resulting design is a bit more complex, you’ll find you can achieve some pretty amazing results when applying patterns in conjunction with Heuristic 7.5. When layering an application, your resulting package structure should be uni-directional. But in some cases, you might have a very strong desire to violate your layered relationships because of the perceived need to reference a method on a class in an upper level layer. Heuristics 11.2 and 11.3 offer interesting perspectives, and some interesting techniques that will allow you  to make explicit calls to methods on classes in upper layers without jeopardizing the hard work you’ve put into designing your package relationships.

The focus on package relationships helped me establish some clear and concise goals. Because the underlying datasource might change, I wanted to avoid coupling to a relational database, and this meant avoiding any dependency on java.sql. The desire to remain independent of java.sql create the need for a few additional classes to ensure I realized my goals. While not the only contributor, package relationships certainly played a large role in driving my class design.

As you’ve seen with most of the package heuristics, layering is a central theme. But a number of other design patterns have also emerged. In our example, the SelectDAO is a Strategy [GOF],  the FileDataCache is a Facade[GOF], and the BaseDAO serves as a factory for it’s implementers.

There are also a few other situations in the examples that  could use some attention. Passing the String and SortedMap as parameters to the retrieve method on the SelectDAO lacks  flexibility. While outside the scope of this discussion, I discuss some options in Heuristic 11.1.