Packages

Packages

The idea of a package can be applied to any model element, not just classes. Without some heuristics to group classes together, the grouping becomes arbitrary. The one I have found most useful and the one stressed most in the UML is the dependency.

I use the term package diagram for a diagram that shows packages of classes and the dependencies among them. Strictly speaking, a package diagram is just a class diagram that shows only packages and dependencies. I use these diagrams quite a lot, so I name them package diagrams, but you should remember that that is my term, not an official UML diagram name.

A dependency exists between two elements if changes to the definition of one element may cause changes to the other. With classes, dependencies exist for various reasons: One class sends a message to another; one class has another as part of its data; one class mentions another as a parameter to an operation. If a class changes its interface, any message it sends may no longer be valid.

Ideally, only changes to a class's interface should affect any other class. The art of large-scale design involves minimizing dependencies; that way, the effects of change are reduced, and the system requires less effort to change.

The UML has many varieties of dependency, each with particular semantics and stereotype. I find it easier to begin with the unstereotyped dependency and use the more particular dependencies only if I need to.

In Figure 7-1, we have domain classes that model the business, grouped into two packages: Orders and Customers. Both packages are part of an overall domain package. The Order Capture application has dependencies with both domain packages. The Order Capture UI has dependencies with the Order Capture application and the AWT (a Java GUI toolkit).

Figure 7-1. Package Diagram
graphics/07fig01.gif

A dependency between two packages exists if any dependency exists between any two classes in the packages. For example, if any class in the Mailing List package is dependent on any class in the Customers package, a dependency exists between their corresponding packages.

There is an obvious similarity between these dependencies and compilation dependencies. In fact, there is a vital difference: With packages, the dependencies are not transitive.

An example of a transitive relationship is one in which Jim has a larger beard than Grady, and Grady has a larger beard than Ivar, so we can deduce that Jim has a larger beard than Ivar.

To see why this is important for dependencies, look at Figure 7-1 again. If a class in the Orders package changes, this does not indicate that the Order Capture UI package needs to be changed. It merely indicates that the Order Capture application package needs to be looked at to see if it changes. Only if the Order Capture application package's interface is altered does the Order Capture UI package need to change. In this case, the Order Capture application is shielding the Order Capture UI from changes to orders.

This behavior is the classic purpose of a layered architecture. Indeed, these are similar to the semantics of the Java "imports" behavior but not that of the C/C++ "includes" behavior. The C/C++ includes is transitive, which means that the Order Capture UI would be dependent on the Orders package. A transitive dependency makes it difficult to limit the scope of changes with compilation. (Although most dependencies are not transitive, you can create a particular stereotyped dependency that is.)

Classes within packages can be public, private, or protected. So, the Orders package is dependent on the public methods of the public classes in the Customers package. If you alter a private method, or a public method on a private class, in the Customers package, none of the classes in the Orders package needs to change.

A useful technique here is to reduce the interface of the package by exporting only a small subset of the operations associated with the package's public classes. You can do this by giving all classes private visibility, so that they can be seen only by other classes in the same package, and by adding extra public classes for the public behavior. These extra classes, called Facades (Gamma, Helm, Johnson, and Vlissides 1995), then delegate public operations to their shyer companions in the package.

Packages do not offer answers about how to reduce dependencies in your system, but they do help you to see what the dependencies areand you can work to reduce dependencies only when you can see them. Package diagrams are a key tool for me in maintaining control over a system's overall structure.

Figure 7-2 is a more complex package diagram that contains additional constructs.

Figure 7-2. Advanced Package Diagram
graphics/07fig02.gif

First, we see that I have added a Domain package that contains the orders and customers packages. This is useful because it means that I can draw dependencies to and from the overall package, instead of many separate dependencies.

When you show a package's contents, you put the name of the package in the "tab" and the contents inside the main box. These contents can be a list of classes, as in the Common package; another package diagram, as in Domain; or a class diagram (not shown, but the idea should be obvious by now).

Most of the time, I find it sufficient to list the key classes, but sometimes a further diagram is useful. In this case, I've shown that whereas the Order Capture application has a dependency to the entire Domain package, the Mailing List application is dependent only on the Customers package.

What does it mean to draw a dependency to a package that contains subpackages? In general, it means that the dependency summarizes a lower-level dependency. That is, there is some dependency to some element within the higher-level package, but you need a more detailed diagram to see the details. Again, the precise rules vary with the particular variety of dependency.

Figure 7-2 shows the Common package marked as global. This means that all packages in the system have a dependency to Common. Obviously, you should use this construct sparingly, but common classes, such as Money, are used everywhere.

You can use generalization with packages. This means that the specific package must conform to the interface of the general package. This is comparable to the specification perspective of subtyping within class diagrams (see Chapter 4). Therefore, in accordance with Figure 7-2, the Database Broker can use either the Oracle Interface or the Sybase Interface. When generalization is used like this, the general package may be marked as {abstract} to show that it merely defines an interface that is implemented by a more specific package.

Generalization implies a dependency from the subtype to the supertype. (You don't need to show the extra dependency; the generalization itself is enough.) Putting abstract classes in a supertype package is a good way of breaking cycles in the dependency structure.

In this situation, the database interface packages are responsible for loading and saving the domain objects in a database. They therefore need to know about the domain objects. However, the domain objects need to trigger the loading and saving.

The generalization allows us to put the necessary triggering interfacevarious load and save operationsinto the database interface package. These operations are then implemented by classes within the subtype packages. There is then no dependency between the database interface package and the Oracle interface package, even though at run time, it will be the subtype package that gets called by the domain. But the domain thinks it is dealing only with the (simpler) database interface package. Polymorphism is just as useful for packages as it is with classes.

As a rule of thumb, it is a good idea to remove cycles in the dependency structure. I'm not convinced that you should remove all cycles, but you should certainly minimize them. If you do have them, try to contain them within a larger containing package. In practice, I have found cases in which I have not been able to avoid cycles between domain packages, but I do try to eliminate them from the interactions between the domain and external interfaces. Package generalization is a key element in doing this.

In an existing system, dependencies can be inferred by looking at the classes. This is a very useful task for a tool to perform. I find this handy if I am trying to improve the structure of an existing system. A useful early step is to divide the classes into packages and to analyze the dependences among the packages. Then I refactor to reduce the dependencies.