As developers, we know that a flexible architecture ensures consistency throughout the system and helps accommodate growth and change. In fashioning these flexible structures, we typically devote the majority of our effort to class relationships, leaving little time for defining the system’s package structure. Yet careful consideration of package structure is important for ensuring the flexibility we design into systems.
In this article, we’ll examine the importance of planning package relationships in your Java applications. In a series of upcoming articles, I’ll examine some useful heuristics that offer guidance when designing package relationships. In addition, I’ll explore how these heuristics serve as the foundation of many architectural patterns you’ve grown accustomed to using.
Learn more about Java
Read the book Java Design: Objects, UML, and Process for more information on Java design.
When designing a system’s package structure, little time is actually spent designing the relationships between packages. Instead, packages are typically viewed as class containers. The reality of architecture, however, is that package relationships serve as the foundation of many architectural patterns. Simply understanding the facts behind an architectural pattern doesn’t bode well in customizing those patterns to fit an application context. Instead, by understanding some fundamental concepts, we’re able to express our own variations of many powerful architectural patterns.
Designing package relationships
Relationships between packages are easily depicted using standard Unified Modeling Language (UML) package diagrams. A relationship between two packages is called a package dependency. In Figure A, we see a diagram depicting two packages, client and service. Each of these packages contains a single class. Within package client, we see a class named Client, and within package service, we see a class named Service. The dotted line connecting the client and service packages implies that at least one class inside the client package has a structural relationship to at least one class inside the service package. This dotted line, known as a dependency, is a standard UML modeling element. This dotted line is directional, indicating that no class inside the service package has a structural relationship to a class inside the client package. The class relationships in Figure A reinforce the package relationship. In addition, Figure A illustrates the permissible Java code that could be written given the package relationships.
|Package dependency diagram|
In examining the relationship between the two classes, it should be evident that any change to the Service class can affect the Client class, due to the relationship between these two classes. The inverse, however, doesn't hold true. Changes to Client will not affect Service. The dependency relationship between packages is consistent with the associative relationship between classes. Because package client has a dependency on package service, it is apparent that any change to the contents of the service package can affect the contents of the client package. We can now state the following:
If changing the contents of a package, P2, affects the contents of another package, P1, we can say that P1 has a Package Dependency on P2.
The value of these package relationships when designing an application is twofold. First, we have many fewer packages in our application than we do classes. As such, it’s much more likely that developers will quickly gain insight into the architectural mechanisms employed. Examining the package relationships reveals the implications of change. In the example in Figure A, it should be evident that changes to the contents of the client package won't affect any other package because no other package is dependent on the client package. This knowledge lets us focus on the more detailed class relationships immediately, and quickly determine the effect of change on class relationships.
Package relationships also serve as a different representation of the system. Designing both package and class relationships allows us to create separate yet complimentary views of our system. Whereas the class relationships give a fine-grained view, package relationships offer a coarser view of the system. Because each of these views represents the same system, each must be consistent with the other. Any inconsistencies found between the class and package views must be corrected, either by introducing a new relationship between the packages, removing the incorrect relationship among the classes, or reallocating classes to different packages.
Package relationships should be unidirectional
System designers should strive to achieve the least degree of coupling possible. Packages with bidirectional relationships increase the coupling between those packages, constraining the system’s architectural integrity. Bidirectional relationships appear in either direct or indirect form. Direct form implies that two packages each have a dependency on the other. This is illustrated on the left side of the diagram in Figure B.
Direct bidirectional relationships are easy to identify and usually easy to rectify. Moving the common components of one of the packages to a newly defined package will create a new dependency on this newly defined package. This is illustrated on the left side in Figure C.
|Direct bidirectional relationship|
Indirect bidirectional relationships can be a bit more difficult to identify. These relationships often surface because developers aren’t aware of the allowable package relationships. Indirect bidirectional relationships involve at least three packages and occur when a cycle is found in the system’s overall package dependency structure. The easiest way to identify bidirectional package relationships is to create a package diagram representing the relationships between all packages in the system. Choose a single package as a starting point and follow the dependency relationships between each of the remaining packages. If the trace through the package relationships brings us back to the package where we started, an indirect bidirectional relationship has been identified. Resolving indirect bidirectional relationships can be a bit more difficult, yet it’s still imperative. The classes dictating the bidirectional relationship should be moved to a package that will eliminate the bidirectional relationship.
Let’s summarize the key considerations for planning package relationships in your next project.
- Package Coupling—Unidirectional relationships between packages emphasize lower coupling. While some coupling must exist, those packages most loosely coupled are more easily maintained.
- Reuse Impact—Packages with bidirectional dependencies limit reusability. Those packages exhibiting lower degrees of coupling on other packages promote reuse.
- Layering—Defining unidirectional dependencies is consistent with how we would layer a system. Typically, upper-level layers are dependent on lower-level layers. A package should reside in, not span across, a layer. As such, packages in lower-level layers should be less dependent on other packages, increasing the reusability of those packages.
Put forethought into package relationships
Package relationships are often an afterthought in system design. Not only must the system exhibit a high degree of class resiliency, but it must also exhibit a high degree of package resiliency. Careful consideration of the package relationships allows the system to grow as future changes warrant. New developers are able to see high-level views of the system and understand the architecture more easily. One of the most important considerations when designing package relationships is ensuring that packages have unidirectional relationships, making the system more flexible by reducing coupling. Lower coupling encourages reusability and promotes maintainability, the cornerstones of robust object oriented designs.