20.3. Design Patterns for DailyProjectData subclasses

In order to implement a DailyProjectData subclass that provides reasonable performance, appropriate functionality, and good maintainability, we have found the following informal design patterns to be useful.

20.3.1. Lightweight Constructor

While the DailyProjectData cache returns a single instance of a DailyProjectData subclass, it cannot guarantee that only a single instance of a DailyProjectData subclass for a given Project and Day will be constructed. Under highly multi-threaded conditions, it is possible for several instances of a DailyProjectData subclass to be instantiated for the same Project and Day, though only one of these instances will make it into the cache and be returned to the multiple clients requesting it.

Because of this possibility, it is important that the constructor not be "heavyweight", in other words, not invoke methods to read in the raw sensor data and perform computation over it. If a constructor is heavyweight, then it is possible that this computation will be performed multiple times as the various instances are instantiated under multi-threaded conditions, only to have all but one of these instances discarded.

It can be helpful to keep in mind that while the DailyProjectDataCache is a true singleton instance, the various DailyProjectData subclasses are not. A given DailyProjectData subclass will be instantiated many times, once for each <Project>, <Day> tuple of interest. In addition, a DailyProjectData subclass for a given <Project>, <Day> tuple can be instantiated more than once! This can happen if the instance is instantiated, then new data for that Project and Day appears at the server, resulting in the cache clearing that instance.

20.3.2. The initialize() method

Since the constructor for a DailyProjectData subclass cannot be used to initialize the instance as it would under normal conditions, the appropriate alternative is to implement a private initialize() method along with a private boolean instance variable called isInitialized, which has the initial value of false. The initialize() method is then called as the first statement within each of the public methods of the DailyProjectData subclass. The initialize() method checks the isInitialized instance variable, and if it is true, returns immediately. Otherwise, it performs its initialization work, which typically involves reading in the raw sensor data and building some cached data structures. Before returning, it sets the isInitialized variable to true, preventing this initialization computation from being done again for this instance.

20.3.3. Synchronized methods

All public methods of the class except the lightweight constructor should be synchronized. The combination of a lightweight constructor, a private initialize() method, and synchronized public methods means that multiple instances of a DailyProjectData instance can be instantiated quickly with minimal overhead, but as soon as the first public method of any particular instance is invoked by a client, access to the instance will be serialized. Furthermore, this first access of a public method will initialize the instance's state by reading in the raw sensor data and building any internal caches useful to supporting further computation.

20.3.4. A "RawData" helper class

If your DailyProjectData analysis is relatively simple, then it is tempting to read in the raw sensor data directly as part of the implementation of the initialize() method. We suggest you resist this temptation, and instead implement a helper class that provides access to the raw sensor data needed by this DailyProjectData instance.

There are several justifications for this design. First, it makes it quite easy to control garbage collection of the raw sensor data, simply by binding an instance of your RawData helper to a local variable of the initialize() method.

Second, the sensor data must always be filtered in some fashion in order to isolate the raw data associated with the Project in question. This can be encapsulated naturally within this class.

Third, it simplifies the enhancement of the DailyProjectData class as its requirements evolve over time. For example, the DailyProjectFileMetrics class actually requires re-reading of the raw sensor data under certain circumstances after initialization takes place. By encapsulating access to the sensor data in a RawData helper class, it is extremely easy to re-read the raw data where necessary during processing.

Fourth, the RawData class simplifies debugging by providing a defined point where sensor data access occurs.

20.3.5. A "Cache" helper class

The purpose of the DailyProjectData subclass is to provide an abstraction of the raw sensor data. For example, instead of requiring clients to process individual FileMetrics sensor data entries, the clients can instead request the aggregate value of the totalLines for a top-level module. Most DailyProjectData subclasses will therefore need to maintain some sort of internal "cache" data structure which provides access to the required abstraction values, potentially computing them on demand.

It is tempting to implement this cache as an instance variable bound to some sort of Collection object. Once again, we recommend you resist this temptation, and instead implement a helper class that takes an instance of your RawData helper class as an argument to its constructor and returns an instance that mediates access to the cached abstract values. The resulting design is easier to understand, extend, and debug.

20.3.6. A "SummaryGrid" helper class

The final recommended helper class is one that focuses on the production of the data structures required by the DailyProjectDetails command and that are accessed through the required getSummaryStrings(User) and getDrillDowns(User) methods. The SummaryGrid helper class constructor accepts the Cache helper class as an argument and uses the cache data to construct the two list-of-lists required to be returned by the method. An easy optimization is to cache these two return values in private instance variables of the DailyProjectData subclass. This allows the SummaryGrid helper class to be bound to a local variable, and thus release any intermediate data structures it may have created for garbage collection when it goes out of scope.

To illustrate how these design patterns work together, the next section presents the DailyProjectFileMetric code.