Rule #6 (OK, most of 7, 8 and 9 too): Keep All Entities Small.
This was brutal. At the beginning, I had two classes that were more than 100 lines long (the target: 50). Not only that, I hadn’t been using packaging properly, so I had one very large package and a few tiny ones.
I find it useful to think about this rule and the following three rules as somewhat of a unit, since the refactorings needed to implement them cut across them, and are also part of a feedback loop. Here are the rules:
- Keep All Entitites Small.
- Maximum 2 instance variables per class.
- Use first-class collections.
- Don’t use any getters/setters/properties.
Here are the refactorings:
- Remove Fields.
- Encapsulate Collections.
- Split Classes.
- Minimize Visibility.
Removing fields and making first class collections are all about encapsulation. One way to go about this is to try to make all fields private (i.e. no getters/setters), then move functionality and fields around, and relax visibility of the fields, until you have a working system again. This will automagically make separate classes. But, they might be too big.
Breaking up classes makes packages bigger. So, you need to cluster the new quiver O classes into packages. This is an optimization problem: You need to minimize visibility (of the classes themselves, as well as the methods) and also minimize the number of packages, so that only those classes which need to be in the same package with their friends, are. During this process, you might want to increase visibility and reintroduce getters/setters so that you can make your packages “work”. Then it’s back to the previous paragraph.
What I did
First, I tried to repackage.
Then, I did an exercise to minimize instance variables.
Then, I worked on refactoring the domain model so that collections had more functionality (First Class Collections), and the object model was more granular (Splitting Classes).
Then, I worked on repackaging again, and finally got rid of the getter/setters (Rule #9). This was trivial, especially since I cheated – I couldn’t get rid of the intValue() method on WrappedInteger! In fact, the method is central to the entire application! This doesn’t surprise me – it’s the type of cross-cutting getter that wrapping primitives makes necessary …
To split classes, I started by looking for “responsibility” and making each class do just one thing. For example, my Solver used a chart to memoize the sub-solutions as it recursively solved the large problem. I removed the chart and put it in its own class, then gave the class more functionality so that the solver had to have the bare minimum number of lines to do its memoization.
My end state has no getters, EXCEPT the “intValue()” on the WrappedInteger class. It does, however, have members of a package which use the package-local visibility of another class’s field to peek at it. In fact, the collections package is the largest one in my solution (!), with nine (yes, 9) classes. Not all are collections, but the domain model really revolves around collections. and so its name is a historical artifact. In fact, hell, I should just rename the package “model” or something. In this way it’d exclude the “primitives” package – but hmm, maybe I’ll make that a sub-package of domain. Hold on …. OK, done. That took all of 3 minutes.
About those Classy Containers
In the process of making first-class containers, I found it useful to use two levels of implementation, and sometimes three:
- Make a minimal collection which just uses the Java collection framework to do what it needs (e.g. is it a list, or a map? It is sorted?) This is just instantiating a generic collection and putting non-domain-specific functionality on it.
- Wrap the base collection with additional concerns. Usually, you want to do this by containing the base class rather than extending it. By doing this you hide all of the stuff you don’t really care about for your domain collection.
- Sometimes the class becomes large enough and with enough extra, domain-specific functionality, that you can extend the secondary class, and make the top-level class the only public one.
A First Class Collection example
The domain model revolving around sets of coins goes like this (TODO: Produce a class diagram):
A CoinSet has a bunch of CoinRolls. A CoinRoll is a roll of a single denomination (like that roll of quarters you have on your beside table at home), whereas a CoinSet has more than one denomination. The implementation of CoinRoll has 3 levels:
- CoinRollSet – This is just an extension of a Java collection template instantiation.
- CoinRollCollectionBase – This contains a CoinRollSet, implements Iterable
, and adds the stuff I’ll need for a proper object: toString, equals, hashCode, all of that. It’s still very much a low-level class. - CoinRollCollection – This is the only public class in this 3-level “hierarchy”, and has a couple domain-specific methods (containsDenomination and totalCoinCount). It also contains deepCopy, which while not domain-specific, is a good place for it since it’s like a constructor.
Another First Class Collection (but, not quite as classy)
In the case of the memoization chart mentioned above (called SolutionChart, strangely enough), the basic class can store a set of solutions for each Money amount being solved for. I used a HashMap as the basic storage, and contained it in a class with the functionality to create a solution from a CoinSet; and to decide whether the CoinSet was empty. Since I needed so little of the HashMap functionality, I didn’t even bother to extend it into its own class.
