Manage Relationships

by kirk knoernschild

Statement

Design module relationships.

Description

A relationship between two modules exists when a class within one module imports at least a single class within another module. In other words:

If changing the contents of a module, M2, may impact the contents of another module, M1, we can say that M1 has a Physical Dependency on M2. [JOUP02]

In Figure 1, you can see a component diagram and accompanying code that illustrates two different types of module dependencies.

DirectAndIndirect

Figure 1: Direct and Indirect Dependencies

Dependencies can manifest themselves in different ways. The most straightforward is a direct dependency, where a client module depends directly on a service module, as show at left in Figure 1. A direct dependency, the kind shown in Figure 7.1, is where a module uses the services offered by another. Direct dependencies require you to include the dependent module in both the build classpath and the runtime classpath.  Neglecting to include the dependent module in the build classpath will result in compile errors, and neglecting to deploy the dependent module may result in run-time errors if a class in the dependent module is referenced at run-time by the module using it. If it’s never referenced at run-time, the classloader doesn’t try loading the class, and no problems arise. However, if at run-time the class is referenced, and the module isn’t found on the classpath, an exception arises. It’s a rather dangerous practice to ignore run-time dependencies that are required build time dependencies, since changing the behavior of the using component will still result in a valid compile, but could result in a run-time ClassNotFoundException.

Indirect, or transitive, dependencies involve at least three module, were a service module is also a client module dependent on some other module.  The module diagram shown at the right side of Figure 1 illustrates an indirect dependency, where the service module is a client module of the subsystem module and a service module to the client module. Here, the client module has an indirect relationship to the subsystem module, and if something changes in the subsystem module, it may ripple through the other modules.

If you’re performing a LevelizedBuild, indirect dependencies need not be included in the build-time classpath. The same rules for run-time inclusion hold for indirect dependencies as for direct dependencies. To resolve the potential problems cited above with build-time versus run-time contention, I’d suggest creating a Test Module that is executed  as part of the build. This allows you to verify the functionality of the module being compiled, and you can easily experiment with the build-time classpath to determine if a module absolutely must be deployed. But remember, it is dangerous to deploy only dependent modules used at a given point in time, since changing the behavior of the client component can unknowingly alter what’s required at run-time.

Implementation Variations

When designing module relationships, there are some important implementation details to consider. For example, a service module must be included in the build classpath of the client module. Failing to do so will result in a compile error. If the client module references a class in the service module at runtime, then the service module must also be included in the runtime classpath, as well. Failing to do so will result in a ClassNotFoundException.

Modules with excessive incoming and outgoing dependencies are the most difficult to manage because they are widely reused but also difficult to change and test. Because of this, it’s ideal if modules are either heavily depended upon or heavily depend upon another module. Unfortunately, this isn’t always possible. In these cases, other modularity patterns can help ease the tension when modules have a lot of incoming and outgoing dependencies.

Understanding the relationships between modules makes it easier to isolate the impact of change to a specific set of modules, which is not something easily done at the class level. We saw an example of this in Section  5.5.1. In the example above, changes to the client module clearly indicate that the impact of change is isolated only to the client module. Likewise, changes to the service module indicate the impact of change could spread to other classes within the service module as well as classes within the client module. Without designing and understanding module relationships, it’s difficult to understand the impact of change.

Java does not enforce that a package and corresponding classes be included in only a single phsycal entity. In fact, there are some situations where this is obviously violated. For instance, the javax.servlet.http.HttpServletRequest class can be found in both the j2ee.jar and servlet-api.jar components. While it’s feasible to place the same class in multiple .jar files, you have to exercise caution when doing so for two reasons. First, if you are compiling with one .jar file and deploying with another, you run the risk of inconsistent versions. Your application may compile with the correct version, but experience run-time problems if the deployed .jar file contains classes from a different version. Second, only one version of a class can be deployed. If you deploy multiple versions of a class, you’ll risk a ClassCastException at run-time if the ClassLoader attempts to load both classes. This can be an especially tricky problem to debug. So while it’s possible to bundle a class in multiple .jar files, it is not recommended.

Managing component relationships is a critical issue when developing large enterprise applications. There are two types of component relationships that need to be managed. Incoming dependencies are present when a component has other components dependent on it. A component with a large number of incoming dependencies should be a StableComponent. The best way to ensure a component’s stability is by making it an AbstractComponent. Outgoing dependencies are present when a component is dependent on classes in one or more other components. Each time you define a new outgoing dependency on another physical entity, you are expanding the knowledge of a component. While you may be increasing the richness of what the component has to offer, you may also be limiting it’s reusability across a wider array of contexts. To minimize a components outgoing dependencies, you should consider creating AbstractComponents. AbstractComponents are especially necessary if a component has a large number of incoming dependencies.

Components with a lot of incoming dependencies are obviously reused more, and changing them requires a bit more ceremony. They must be tested thoroughly, and pushed out to all applications that use them. Changing components with many incoming dependencies is primarily a maintenance and deployment issue.
Components with a lot of outgoing dependencies can be changed easily, but not tested easily. It’s difficult to independently verify components with many outgoing dependencies, since they cannot be tested in isolation. Your goal should be to minimize outgoing dependencies, and where outgoing dependencies are necessary,  the outgoing dependency should be on AbstractComponents. By minmizing a component’s outgoing dependencies, you increase the testability of the component. A TestComponent can be created for each component that allows you to test a component in isolation, helping to ensure the component’s correctness.

It’s not completely terrible if you find that some components have many incoming dependencies, while other components have many outgoing dependencies. However, you should avoid components that have both many incoming and  outgoing dependencies where the outgoing dependencies are not on AbstractComponents. Of all scenarios, this one is a testing, maintenance, and deployment issue all wrapped into one. Worse yet, is when your many incoming and outgoing dependencies form AcyclicRelationships.

Consequences

There are a lot of things to consider when designing module relationships. In general, a module has incoming dependencies, outgoing dependencies, or a combination of each. Different forces affect modules depending on the types of dependencies they possess.

Modules with a lot of incoming dependencies are more difficult to change because they are being reused by more client modules. Because of this, it’s imperative that they undergo more thorough and rigid testing, and steps should be taken to minimize changes to these modules (like using AbstractModules).

Modules with a lot of outgoing dependencies are easier to change because they are reused by fewer modules. Unfortunately, these modules are more difficult to test in isolation because of their dependencies on other modules.

There are ways to alleviate each of these challenges. Designing abstract modules, ensuring module relationships are acyclic, and separating abstractions from the classes that realize them are each examples.

I’ve experienced some significant advantages in designing component relationships. They allow me to view the system from a perspective that is much different than when I browse the classes. A medium sized application might consist of thousands of classes, but may be composed of only a few dozen components. Browsing these component relationships allows me to more easily see how the system is structured. It’s an especially helpful way for me to see how changes might impact other areas of the system. Knowing the component dependencies enables me to identify those classes that may be affected by changing a class in a dependent component.

Defining component relationships also serves as a system of checks and balances against a system’s logical structure. When layering a system, it’s important that each layer has a clearly defined set of responsibilities. Layering components guarantees consistent responsibilities within each layer. Only importing those classes from dependent components ensures I’m well aware of the ramification of change. These relationships enforce architecture, and any new relationship created between two previously independent components should be given serious consideration.

You should exercise caution any time you import a new class where a component relationship doesn’t already exist between the components because the coupling of the components increases. Instead of willy nilly importing whatever is needed, you’re forced to make an important design decision. Do you really want to create the new dependency, or is the decoupling of components important enough to warrant additional design flexibility? This additional flexibility can often be achieved by using interfaces to enable classes to communicate with other classes in non-dependent components. In fact, this is one example of how patterns emerge within your system, and SeparateAbstractions in Chapter XX shows how interfaces can be used to avoid component coupling.

But component relationships offer more value than simply dictating class relationships. When deploying applications, a component is the minimal unit that is deployed. Attempting to deploy one component without deploying all dependent components results in run-time issues. Of course, it’s not necessary that all dependent components be deployed in the same file. Separating components into various files gives you great flexibility in how the application is deployed and how the componets are reused. Should you decide to separate dependent components across files, it’s imperative that all dependent files be deployed together, knowing you’ve given yourself greater flexibility in how the functionality contained within each component can be reused.

I’ve actually written quite a few batch applications in Java. Typically, these batch applications are not projects of their own, but part of a larger project that also has a web interface. For instance, a batch application might receive an incoming data feed, and save the data to a database. It may perform some  validation and flag some records in error. The web interface may provide users the ability to correct those errors, perform the same validation, and persist the cleansed data back to the database for downstream processing.

To avoid duplication in the batch and web application,it’s important to reuse the code that performs the validation and saves the information to the database. Because the batch application will likely run in a separate JVM, and usually outside the context of a J2EE container, the core components used by both batch and on-line must be separated into files independent of either the batch or on-line systems. Knowing the component relationships allows me to accomplish this much more easily.

Neglecting to define components dependencies has a tremendous impact on reuse. While you will reuse behavior at the class level, the inability to deploy self-contained units will often deflate your hopes of reusing your logic across applications. Planning your deployment strategy is a critical step in establishing a robust infrastructure. It’s a frustrating experience to find that what you thought was a reusable software component is actually dependent on code that you don’t want to deploy with a given application. Defining your PhysicalLayers and CohesiveComponents allows you to realize your reuse goals much more easily.

Sample Code

Tight coupling between modules is a bad idea. Fortunately, there hat allow us to invert and eliminate module relationships. These techniques are leveraged by a lot of the modularity patterns. The code for each of the samples can be found in the edcie project on my Google code repository. Each examples includes a build script and a simple test case. To execute them though, you’ll need GraphViz if you want to use JarAnalyzer. To invoke the build scripts without invoking JarAnalyzer, you can simply type:

ant compile

Keep in mind that each variation of the system has the exact same behavior!

Figure 2 illustrates the acyclic modules from Section 5.2.2. Obviously the customer.jar module depends on the billing.jar module, implying that billing.jar is required by customer.jar at both build and runtime. But what if we wanted to use the customer.jar module without the billing.jar module? With a bit of trickery, I can actually invert the relationship between the customer.jar and billing.jar modules.

NOTE: Start with this diagram in Abstract Modules to illustrate why we want to depend only upon abstractions. For instance, we add a new Bill. Just show how we can manage relationships between customer.jar and billing.jar module and how the build script can bundle these into their own modules. Then, move Inverting Relationships to Abstract Modules.

Note: Be sure to discuss tools that can be used to manage module relationships. JarAnalyzer, build script, and IDEs.

CustBillAcyclic

Figure 2: Acyclic Module Relationships

Inverting Relationships

I start by refactoring the Bill class to an interface. (refactoring the Bill class to an interface). Then, to avoid split packages (where classes in the same package are bundled into separate modules), I move the Bill class into the same package as the Customer class. (same package as the Customer class.) The new class diagram is shown in Figure 3, along with the inverted module relationships shown at right.

InvertingRelationships

Figure 3: Inverting Module Relationships

Eliminating Relationships

Inverting the relationships allows us to deploy the customer.jar module independent of the billing.jar module. Again, it’s all about need. But I’d like to explore another option based on another important need – the ability to test and deploy modules independently. Before inverting the relationships, I am able to test and deploy the billing.jar module independently. After inverting the relationships, I can test and deploy the customer.jar module independently. But what if I want to test or deploy both modules independently? To do this, I need to completely eliminate the relationship altogether.

As it turns out, because I’ve got a pretty flexible class structure after I inverted the relationships (lot’s of abstract coupling), I can do this by simply bundling the two interfaces – Bill and DiscountCalculator – into a separate module. No other coding changes required. I start by moving them to a new package, which we’ll case base. (moving them to a new package called base). Then, I modify my build script (modify my build script) to bundle these two interfaces into a separate base.jar module, and we have successfully eliminated the relatinonship between the bill.jar and cust.jar modules. This is illustrated below in Figure 4.

EliminatingRelationships

Figure 4: Eliminating Relationships Between Modules

Wrapping Up

Identifying the modules is the first step in designing modular software systems. Shortly following, however, is managing the relationships between those modules. It’s these relationships between modules that I refer to as the joints of the system (Section 5.4)  (the joints of the system), and it’s these areas of the system that demand the most nurturing to ensure a flexible system design. If care isn’t given to managing the relationships between modules, separating out the system into a set of modules isn’t going to provide significant advantages.