Properly designing packages (i.e., reducing the coupling between packages and increasing cohesion within packages) promotes the design and development of resilient, robust, and maintainable Java applications. Proper design offers valuable guidance when implementing common architectural patterns. I will use two such patterns, layering and partitioning, to illustrate the importance of the relationships between the packages that compose your applications.
Learn more about Java packages from these Builder articles:
- "Properly package your Java classes"
- "Java package functionality"
- "Abstract packages ensure flexibility"
- "Build a relationship with your packages"
Layering is a common architectural pattern employed to help break up complicated software. Each layer is responsible for exposing a principal service, and layers are typically divided based on their primary functional responsibilities. Packages are an excellent way to represent the logical layers composing an application. Higher-level layers (or a package representing a layer) sit atop lower-level layers, using their services. Lower-level layers, however, know nothing of the higher-level layers that use them. Furthermore, each layer usually hides its lower layers from the layers above, though the transparency of individual layers must be a conscious decision. A common layering scheme is presented in Table A.
Defining meaningful application layers is important because well-defined layers promote maintainability and reusability, which is central to many of the heuristics I’ve discussed previously. In fact, you’ll find that the heuristics presented are central in creating a robust layered application.
When layering an application, consciously defining the coupling between layers is paramount. Subsequently, designing package relationships is vital. Additionally, because higher-level layers should use the services provided by lower-level layers, but not vice versa, package relationships should be unidirectional.
A purpose for layering our application is to increase reusability. It’s likely, and desirable, that lower-level layers will exhibit higher degrees of reuse than higher-level layers. You should consider making a package’s published interface well known in situations where reusing a layer is frequent. Creating a well-defined interface makes it much easier for other developers to understand and use a service.
However, layering must be done judiciously. Defining more application layers results in each layer needing to present more fine-grained services. This can be a challenging task when determining a layer’s responsibilities. You must give it careful consideration and be conscientious in designing layers at a single level of granularity to help ensure reusability and maintainability.
Partitioning an application focuses on separating logic not central to the business from the logic that is central to the business. Similar to layering, partitions are typically represented logically using packages. Two predominant partitions are illustrated in Table B.
When identifying meaningful partitions, it’s imperative that you emphasize the purpose of individual packages. Again, the package heuristics play an important role in helping create meaningful partitions. Each partition must represent a highly cohesive package. An important consideration when designing cohesive packages is to ensure that they expose services at the appropriate level of granularity. Remember, the granularity of a package, whether it exists in the vertical or horizontal partition, is vital to a client’s ability to reuse the package.
Additionally, services in the vertical partition strive to reuse services offered by services in the horizontal partition. It’s likely that horizontal services are reused extensively within an application and desirably across applications. Subsequently, it’s vital that you consider how classes will be reused together and how they will change together. If horizontal services are reused by multiple applications, considering how individual services can be deployed independently becomes increasingly important.
Patterns are rarely ever used in a vacuum. It’s likely that each of these architectural patterns will play a key role in defining most object-oriented software architectures. Instead of layering or partitioning an application, you’ll usually layer and partition an application. In doing so, you strive to layer within your partitions and partition your layers. Figure A illustrates the abstract dependency model where you have two layers divided into two partitions. The direction of the arrows illustrates the allowable dependencies.
Package heuristics offer guidance
Each pattern presents a proven architectural solution that is leveraged when developing our applications. These patterns tend to emphasize the relationships between packages composing your applications, and central to each of these patterns are many package heuristics, which offer valuable guidance when choosing to apply an architectural pattern.
While the heuristics certainly reinforce the concepts present in common architectural patterns, they aren't specific to any single pattern. Alternatively, these heuristics are omnipresent, and in the absence of a pattern, adhering to our heuristics should yield results that are as positive as though a pattern were actually used.