"That don't impress me much!"
Shania Twain, Come on over


"What did y'all order a dead guy for?"
Jayne Cobb, Firefly


"Get the cheese to sickbay"
B'Elanna Torres, Starship Voyager


"Excuse me, but I am in the middle of fifteen things, all of them annoying"
Susan Ivanova, Babylon 5


"There is a theory which states that if ever anyone discovers exactly what the Universe is for and why it is here, it will instantly disappear and be replaced by something even more bizarre and inexplicable. There is another theory which states that this has already happened"
Douglas Adams


"Stop the pigeon! Stop that pigeon now!"
Sheriff of Nottingham, Sherwood Forest, The Adventures of Robin Hood


place
Resolving Mutual Dependencies with Interfaces
Home > SoftwareDesign > Java
Even in modern langauges such as Java and C#, mutual compile-time dependencies between classes can cause build problems when they cross package, namespace or component boundaries. While not the only solution to this particular problem, Java-style interfaces provide a means of reversing the direction of compile-time dependencies.

Even in modern languages such as Java and C#, mutual compile-time dependencies between two classes can cause build problems when they cross package, namespace or component boundaries. For more traditional languages like C++,  such dependencies frequently cause severe problems even within a namespace or component.  While not the only solution to this particular problem, Java-style interfaces provide a means of reversing the direction of compile-time dependencies. For languages that do not have a Java-style interface construct (C++, Eiffel, etc), abstract classes substitute.

Associations

In object-oriented software, it is not unusual for designs to require objects of two classes to have a reference to each other. This is especially true of significant line of business applications. Often, these pairs of references represent two concepts in the software's problem domain that are naturally related to each other. For example, a particular person is related to an order they place on a web site. The person is the customer for order, and the order represents the purchase that the person is making.

Therefore, in a simple, order processing system for this web site, it would not be unreasonable to expect to have a Customer class and an Order class, each defining a reference to the other. Presumably, a customer could place multiple orders over time and, therefore, the Customer class requires a reference to some kind of set or list of orders. Normally, this is not a problem because modern compilers handle such mutual references easily. Even if the two classes are in different packages, namespaces or components like those shown in figures 1 and 2, the compiler can handle it as long as we compile both packages together. However, if we need to compile the packages separately we have a problem; we cannot compile either of the packages before the other. In large systems with numerous such examples, this can become a significant build problem.

package OrderManagement;
...

public class Order {
...
    private Customer customer;
...
}
package CustomerManagement;
...

public class Customer {
...
    private List<Order> orders;
...

Figure 1: Mutual references between two Java classes in different packages

The Unified Modeling Language (UML) calls these types of relationship between objects of two classes, associations. Associations are drawn as a solid line linking classes they relate. Figure 2 shows how the code in figure 1 looks in UML; the asterisk showing that multiple order objects may be related to the same customer object.

Concrete Example Of Bidirectional Association
Figure 2: Example of an association spanning two package in UML

Resolving Mutual dependencies

Various solutions exist to reduce two mutual dependencies to a single dependency. The simplest approach is to ignore the dependency in one of the directions:

Strategy#1: Ignore One Direction

When presented with a bi-directional association, often it is possible to ignore one direction because features do not exist that require navigating between objects in that direction. For example, if no feature exists that requires the Order objects to call operations or access properties of the related Customer object, we can remove the reference from the Order class to the Customer class. Problem solved! Now we can compile the OrderManagement package/namespace on its own and then compile the CustomerManagement package/namespace.

other times the application's features demand traversal of the association in both directions and this approach fails.System software, real-time applications and simple applications are often stable enough for such decisions to hold true. However, with today's rapidly changing requirements, design decisions like this can very quickly cause big problems for business applications developers.

Instead of implementing both directions of an association using references, one direction can be derived each time by searching the instances of the other class for one that points back to the object we are holding.

Strategy#2: Lookup One Direction

Add a class-level or static method to one of the classes that loops through all the instances of that class selecting the subset that have a reference back to the specified object. Instead, for example, of accessing the related customer by asking an order object for its customer, we ask the Customer class for the customer object of a specified order object. Figure 3 shows the structure for this in code. Figure 4 does the same in UML.

package OrderManagement;
...

public class Order {
...
}
package CustomerManagement;
...

public class Customer {
...
public static Customer getCustomer(Order order) {
// return the customer that has the specified
// order in their list of orders
...
}
...
    private List<Order> orders;
...

Figure 3: Resolve Bi-Directional Association via static lookup

Look up in one direction

Figure 4: UML showing look up operation for one direction of association

UML Notation:

  • In UML, a stick arrow-head symbol is added to an association to show it can be traversed in a particular direction. UML 2.x adds the additional optional symbol of a cross on the other end of the association to show that it cannot be traversed in that direction.
  • An operation is shown as being static (class-level) by underlining it, the plus symbol shows it is a public operation.
  • The new operation means the Customer class has a dependency on the Order class in addition to that of the association. The Customer class needs to know about the Order class because the Order class is a parameter to that operation. General dependencies are drawn as dashed lines with stick arrow-heads but in most cases, these are left out if one or more associations exist between the two classes.

If it can be identified, the least frequently traversed direction of the association should be implemented this way. A SQL query run directly against a relational database used to persist these objects is one typical way of locating the right customer in this approach.

Java-style interfaces provide another possibility and the same can be achieved with C++-style abstract classes. However, in my opinion, the interface concept makes the picture clearer.

Strategy#3: Introduce a Java-style Interface

A mutual dependency between two classes can be resolved by introducing an interface that is implemented by one of the classes and referred to by the other. Figure 5 shows how a bi-directional association becomes two dependencies in the same direction when an interface is introduced in this way.

Bidirectional Association


becomes


Interface

Figure 5: Introducing an Interface

Now, instead of Class A having a reference to Class B, it has a reference to the new Interface A. Class B implements that interface (denoted by a dashed line with the hollow arrowhead). The direction of the links between the two packages now both go from Package B to Package A. Package A can be compiled without any knowledge of Package B.

The interface need only declare the subset of the public operations that Class A needs to call on Class B.

General Mutual Dependencies

Associations are not the only relationships that produce mutual compile-time dependencies between package, namespace or component. As shown in strategy #2, the type of a parameter of an operation can introduce a dependency to another package, namespace or component. In addition, a method that creates an instance of a class in another package as shown in figure 6 can introduce a dependency to that other package, and so on.

Mutual dependency via instantiation

Figure 6: Dependency from instantiating objects of another class

In figure 6: Class A1 has a reference to Class B that it uses to create objects of Class A2. Class A1 keeps a collection of these created objects.

UML Notation:  the solid diamond on the association between Class A1 and Class A2 indicates that this is a composition or strict whole-part relationship. Objects of Class A2 are considered parts of the concept modeled by objects of Class A1. For example,

The same technique of introducing works in these scenarios too, and one common place where this technique is often useful is across the boundary between the problem domain (PD) and system interface (SI) logical layers.

Resolving Mutual Dependencies Between PD and SI

Both business and industrial applications often need to communicate with other systems. In these cases, it is generally good practice for objects of problem domain or business classes to delegate to objects of system interface classes the task of communicating with these systems. In other words, a PD class invokes an operation of an SI class to make use of a service provided by an external system. In turn, external systems may invoke services provided by SI classes that then query or update any relevant PD classes.

This two-way communication between our software and external systems obviously introduces mutual dependencies between PD and SI classes. Therefore, we introduce interfaces to change the direction of any compile-time dependencies from PD to SI so that all dependencies between the two layers go from SI to PD.

Figure 7 shows an example where a User PD class needs to call an external system to retrieve user permission information.

System interface example

Figure 7: System interface example


In figure 7, an ISecurity interface has been created in the PD package (namespace, component, layer, etc), and objects of the User class are given an appropriate implementation of this interface to work with. The SI package defines a class, SecurityScheme, that implements the PD ISecurity interface creating PD permission objects from security information retrieveed from an external system (not shown).

Figure 7 clearly shows the direction of the compile-time dependencies is from SI to PD. This means that should the external security system ever change, as long as the ISecurity interface can still be satisfied, only classes in the SI layer need modifying to use the new security system. In addition, with the interfaces in place, a separate team of developers can work independently on the SI classes.

Additional Notes

  1. One additional consideration in the use of this technique, is how to create the SI objects that the PD classes use. One approach is to use an additional separate layer/component that creates the SI objects and gives them to the PD objects. This layer has dependencies on both PD and SI layers but neither the PD or SI layer classes know anything about this application initialization component.  Alternatively, a factory class is added to the PD layer. PD classes ask the factory class for an instance of the SI class that implements the interface. The factory class cannot simply create an instance of the required class because that immediately introduces a dependency between PD and SI in the direction that we do not want. Therefore, the factory class retrieves the name of the implementing class from a resource, property file, lookup service or equivalent and uses reflection to locate or create the instance of the relevant SI class.

  2. As well as keeping PD classes independent of the SI classes, it is good practice to keep PD classes independent of User Interface (UI) classes. This separates presentation concerns from business logic. Some flavour of publish/subscribe, observer/observable, or event notification pattern is popular here, often as part of a Model View Controller (MVC) framework. Why do we not use typically use notification and event frameworks to separate PD from SI? The important difference is the direction of the service call and desired dependency. UI classes invoke services on PD and PD notifies back changes; the desired dependency direction is from the client to the server. In contrast, PD invokes services on SI and needs updating by SI; the desired dependency direction is from the server to the client. It is straight forward for a client to request notification from a server but not nearly so straight forward the other way around.

  3. Beware the types of parameters and returns in interface operations. If a parameter or return type of an operation in the newly introduced interface  belongs to the other package the mutual dependency is not eliminated. Parameters and return types should be primitive types, classes from the standard language libraries, classes from the same package as the interface or classes from a third package that has no dependencies on the two packages.

    When all else fails one can introduce another interface for the offending parameter or return type. Imagine a SI service that returns exchange rates, the return type of the method in the PD interface can be declared as the interface, IExchangeRate. The SI service can return anything that implements that interface; the returned object could be a standard exchange board rate, a special discounted rate, or a derived cross rate. The PD class does not care but SI may need the extra info if PD passes the object back to SI on subsequent service calls.