Creating packages, assigning classes to packages, and creating a package hierarchy is usually not a top priority in software design. This, however, presents a missed opportunity to make your design more readable and maintainable. This article helps you take your Java class packaging skills to the next level, and not just for the holiday season.
Packages as Namespaces
The very first and basic usage for packages is to have them as namespaces for projects. Each project, module, or library lives in its own naming scheme, which makes all classes they contain uniquely named among all projects of the world, which enables a global marketplace of libraries like the Maven Central Repository.
Package names for projects usually start with the reverse domain name of the company or developer and end with the project name, like:
While this is a very important aspect of packages, it is not particularly interesting nor difficult to do, so this article is more about what happens on the inside of an application. Packages are hierarchical and do not end at the project level! The project itself is organized into sub-packages, which themselves may have sub-packages, etc. This is where it gets interesting...
Packages as Groupings
When we first begin to use packages, most of us think of them as a way to create some order among classes. If there are too many classes in the same directory, we just want to split them up somehow to be able to find things.
So our first intuition is to group classes into sub-packages according to some criteria we see fit. Let's take a look at the Package structure in Project Weld, the official implementation of the CDI specification, included in WildFly JEE Application Server:
AbstractCDI.java Container.java annotated/ bootstrap/ logging/ manager/ xml/ ...
This project has hundreds of classes, so obviously it needs to be split somehow into packages. The above excerpt shows that the developers created a mostly technical package structure. While
annotated could be business-oriented perhaps, packages
manager are clearly technical.
Let's call this packaging strategy " grouping ".
A package structure on a given level is a grouping if the root package depends on any of its sub-packages.
The dependency graph for the above classes and packages shows that the
root package does depend on some of its sub-packages:
The parallel arrows between most of these packages that point back and forth indicate circular dependencies. It means that every package is essentially dependent on every other package, which means changes will rarely be localized. This is a maintenance nightmare and should be avoided.
Circular dependencies are, however, not a direct consequence of "grouping". A "grouping" can be a quite reasonable approach, but has the following more subtle properties:
- It is mostly made for the convenience of the writer
- It misses an opportunity to visualize the design
- It misses an opportunity to communicate with the reader
Why are these points important? Let's look at how code is read ...
Getting to Know an Application
How does a reader navigate in code on the package level? Well, that depends on what the reader is after.
The reader might be a new colleague or someone yet unfamiliar with the codebase, trying to get his or her bearings, trying to understand key features and the purpose of the application. For a reader like this, it is probably a good idea to start reading at the beginning, with key abstractions that do not require any additional understanding of the code, i.e. packages that have no further dependencies.
It would be convenient if the key abstractions that do not depend on anything else are be in the top (root) package, or else it would require a dependency analyzer to find the "entry point" into the application. So if somebody asks the question: What is this application/library about? The answer should be in the root package.
This also requires the root package to not just contain data structures or Java Beans, but the actual logic, even if just in an abstract form as interfaces. If it would just contain Java Beans, for example, it would be impossible to tell what the application actually does. A consequence is that sub-packages cannot contain "additional" functionality, only more details. If they contain some functionality not seen one level up, then the reader could not build a complete picture of what the application does, so they would be forced to dive into all the sub-packages to make sure nothing is missed.
Of course, if more detail is needed, then a reader might descend into sub-packages. At every level, the situation should be the same. The current package would have some abstractions, and all sub-packages would have more details of those — and only those — functionalities.
So that leaves us with the following rules to make our packages reader-friendly:
- Packages should never depend on sub-packages
- Sub-packages should not introduce new concepts, just more details
Let's look at an example from Project Gerec:
It is a clear hierarchy in which the
root package does not depend on any of the sub-packages. This means a reader can start with the classes there without having to understand what the sub-packages are about. Some of the top level classes are:
HttpRequest.java HttpResponse.java MediaType.java Header.java ResourceReference.java ...
All of these listed classes, and most of the other ones on this level, are pure interfaces with proper business methods, so the overall logic and feature set of the library can be understood just by looking at the root package. The sub-packages do not introduce new functionality, just more details, specializations, and implementations of the root classes.
Maintaining an Application
A second common use-case for reading code is trying to find the right place to apply a change. A change is usually some business-relevant detail that has to be modified or implemented. Sometimes, the developer making the change knows exactly where to apply the change in code or is familiar enough with the application to know the possible places to start looking.
If the application or the team is bigger, however, the chance that the developer getting a task knows the exact places to search becomes less probable. In these cases, the developer has to search through the packages.
The search has to start somewhere, and it seems logical to start again at the
root package. Select the correct sub-package to investigate from there, going down the right path to the package that contains the right classes. This, however, can only work if the root package is the most abstract one and sub-packages only introduce more details. So the two rules from the previous chapter would help in this case, too!
There is a finer point here, too. We can help the reader select the correct path by giving them more clues in the package names. If the package names contain technical terms like: "entity", "service", "manager", "usecase", "database", "resource", "view", etc., we effectively assume that the reader will know our architecture. Maybe you think that is a reasonable assumption to make, and perhaps it is in most cases. It is, however, unnecessary to make this assumption.
We do know that the reader is here to make a change. This change most likely has a business context with which the reader will be somewhat familiar. So the following package names would probably help more and assume less prior knowledge: "shoppingcart", "checkout", "search", "favorites", etc.
Let's see an example from the Spring-Framework Petclinic example project. These are the first sub-packages under the root package:
model/ repository/ service/ util/ web/
Which package would you pick for the following changes:
- Change the date format for new visits
- Mark pets as deceased instead of removing them
- If more owners live in one household, list all pets for that household
Where would you look for the same changes in this hierarchy:
pet/ owner/ visit/
While this hierarchy might look unfamiliar to some and would take perhaps more effort to write, it does seem to be much easier to navigate.
Almost everyone starts by using packages as a tool to organize classes basically for themselves, applying a structure which seems logical at the time they write the code.
A packaging can be much more powerful, however. It can contain more knowledge and more help for the reader, increasing the maintainability of software. This article highlights three rules to take your packaging to this next level:
- Packages should never depend on sub-packages.
- Sub-packages should not introduce new concepts, just more details.
- Packages should reflect business-concepts, not technical ones.