20.4. Design Patterns Example: DailyProjectFileMetric

Example 20.1, “DailyProjectFileMetrics example code” presents the core implementation of the DailyProjectFileMetrics class. For space and simplicity, the import statements have been removed, the code not specifically related to the design patterns has also been removed, and the JavaDoc comments simplified.

Example 20.1. DailyProjectFileMetrics example code

public class DailyProjectFileMetric extends DailyProjectData {
  
  /** Maps [FilePattern, FileType, SizeMetricName] to SizeMetricValue */
  private FileMetricFilePatternCache cache = null;
  
  /** The cached DailyProjectDetails summary. Computed the first time it's requested. */
  private List summaryString = null;
  
  /** The cached DailyProjectDetails drilldown. Computed the first time it's requested. */
  private List drilldowns = null;

  /** True means that (a) the Day is in the Project interval, and (b) there is relevent FileMetrics data. */
  // A person may have lots of FileMetrics data, but none for this project, so we can avoid
  // unneeded computation after a cache miss by remembering whether there are relevent FileMetrics.
  private boolean hasData;
  
  /** Set by the initialize() method so that it knows if it has been called. */
  private boolean isInitialized = false;

  /** Returns the pre-existing instance or creates a new one if necessary. */
  public static DailyProjectFileMetric getInstance(Project project, Day day)
      throws ProjectCacheException {
    DailyProjectDataCache cache = DailyProjectDataCache.getInstance();
    return (DailyProjectFileMetric) cache.get(project, day, DailyProjectFileMetric.class);
  }

  /**  Clients: do not call this constructor explicitly! Use getInstance() to retrieve from cache.   */
  public DailyProjectFileMetric(Project project, Day day) {
    super(project, day, "File Metrics");
  }
  
  /** Initializes the internal cache, sets hasData appropriately. */
  private void initialize() {
    // If we're already initialized, return immediately, otherwise note we're initialized.
    if (this.isInitialized) {
      return;
    }
    else {
      this.isInitialized = true;
    }
    // If we're not in the Project Interval, then set hasData to false and return. 
    if (!this.withinProjectStartEndDay()) {
      this.hasData = false;
      return;
    }
    // Otherwise process FileMetric data and build our internal cache.
    try {
      // Begin initialization by finding the 'latest' FileMetric data for this project/day.
      FileMetricRawDataMap dataMap = new FileMetricRawDataMap(this.project, this.day);
      // Now we can set the hasData boolean to reflect whether data exists. Return if false.
      if (dataMap.hasData()) {
        this.hasData = true;
      }
      else {
        this.hasData = false;
        return;
      }
      // Now we can process the found FileMetric data to initialize the FilePattern cache.
      this.cache = new FileMetricFilePatternCache(dataMap, dataMap.getDefaultPreferredTool());
    }
    catch (Exception e) {
      // We log that there was an error, set hasData to false, and swallow the error.
      logger.warning("DailyProjectFileMetric failed to initialize! \n" + this.project.getName()
          + " " + this.day + "\n" + StackTrace.toString(e));
      this.hasData = false;
    }
  }

  /** True if sensor data for this Project and Day exists. */
  public synchronized boolean hasSensorData() {
    this.initialize();
    return this.hasData;
  }
  
  /** Returns a List containing a single String[] with a summary of this data */
  public synchronized List getSummaryStrings(User user) {
    this.initialize();
    this.initializeSummaryAndDrillDowns();
    return this.summaryString;
  }
  
  /** Returns a list containing the drilldown of FileMetric data by top-level workspace. */
  public synchronized List getDrillDowns(User user) {
    this.initialize();
    this.initializeSummaryAndDrillDowns();
    return this.drilldowns;
  }
  
  /** Initializes this.summaryString and this.drillDowns if necessary.  */
  private void initializeSummaryAndDrillDowns() {
    if (this.hasData) {
      // We have data. 
      if (this.summaryString == null) {
        // We have not yet initialized the instance variables, so do it.
        // Compute the DailyProjectDetails summary string and drilldowns.
        FileMetricSummaryGrid grid = new FileMetricSummaryGrid(cache);
        // Cache the resulting strings, throw away the intermediate computations.
        this.summaryString = grid.getSummaryString();
        this.drilldowns = grid.getDrillDowns();
      }
    }
    else {
      // No data.
      if (this.summaryString == null) {
        // We haven't initialized the summary and drilldown yet, so do so now.
        this.summaryString = new ArrayList();
        String[] summaryStringArray = {this.getName(), "No data for this day"};
        this.summaryString.add(summaryStringArray);
        this.drilldowns = new ArrayList();
      }
    }
  }
}

Let's go over this code section by section in order to see how the design patterns have been implemented in this class.

The first section of the class declares a number of private instance variables. The first instance variable, "cache", holds an instance of FileMetricFilePatternCache. This is an example of the use of the "cache helper class" design pattern. Each instance of DailyProjectFileMetric holds one instanceof FileMetricFilePatternCache, which maintains pointers to the abstract file metrics data provided by this class.

The second and third instance variables, "summaryString" and "drilldowns", hold the data structures containing the summary and drilldowns required by the DailyProjectDetails command. These data structures are generated the first time they are requested through the use of the FileMetricSummaryGrid helper class.

The boolean hasData instance variable is set during initialization and indicates if raw sensor data for this Project and Day actually exists.

The boolean isInitialized instance variable indicates whether the initialize() method has been called. All public methods invoke the initialize() method, and this method checks this boolean internally to see if it should perform its initialization.

The next two methods are the DailyProjectFileMetric constructor and the static getInstance() method. These are "boilerplate" methods that should be implemented this same way in all DailyProjectData subclasses. Basically, the getInstance() method requests the appropriate instance from the cache, which will instantiate a new instance using the public constructor if necessary. The public constructor does not work other than to call the superclass constructor with appropriate arguments.

The initialize() method sets the isInitialized and hasData booleans. Note that since it is legal for a client to instantiate a DailyProjectData subclass with a Day that is not defined to be within the Project's start and end date interval, the superclass's withinProjectStartEndDay() method is used to set hasData to false if the passed Day is outside the Project interval. Otherwise, the initialize() method creates an instance of FileMetricRawDataMap which provides access to the FileMetric data associated with this Project and Day. (Since multiple tools can send FileMetric data, this class also encapsulates the rules used for selecting which tool's FileMetric data will be further processed by this DailyProjectData subclass.) The initialize() method next queries the FileMetricRawDataMap to determine if it found FileMetric data for this day, and if so, passes the FileMetricRawDataMap as data to be used to instantiate an instance of FileMetricFilePatternCache.

The next several methods, hasSensorData(), getSummaryStrings(User), and getDrillDowns(User), illustrate a selection of the public API for this class. All of these public methods are synchronized, and all of them call the initialize() method as the first statement in the method body. In addition, the getSummaryStrings() and getDrillDowns(User) methods call the private method initializeSummaryAndDrillDowns(). This method is used to compute and cache the data structures to be returned to the client caller, which for these methods is the DailyProjectDetails command.

The initializeSummaryAndDrilldowns() method illustrates the use of another helper class called FileMetricSummaryGrid. This class takes the cache instance built during initialization, and uses it to construct intermediate data structures useful for creating the summary string and drilldown lists. By binding the FileMetricSummaryGrid instance to a local variable, we can ensure that these intermediate data structures are available for garbage collection after they have finished their computation. We cache the completed summary string and drilldowns in an instance variable so that DailyProjectDetails will return quickly after the first time it is invoked.