3. Case Study 2: Implementing a Class Hierarchy

Whenever a class hierarchy is more complex, we cannot simply eliminate it, but have to implement it (1) in the app's model code, (2) in the underlying database and (3) in its user interface.

The starting point for case study 2 is the design model shown in Figure 16.8 above. In the following sections, we derive a JS class model and a JS entity table model from the design model. The entity table model is used as a design for the object-to-storage mapping that we need for storing the objects of our app with the browsers' Local Storage technology.

3.1. Make a JS class model

We design the model classes of our example app with the help of a JS class model that we derive from the design model by essentially leaving the generalization arrows as they are and just adding get/set methods and static check functions to each class. However, in the case of our example app, it is natural to apply the Class Hierarchy Merge design pattern (discussed in Section 5) to the single-subclass-segmentation of Employee for simplifying the class model by eliminating the Manager subclass. This leads to the model shown in Figure 17.2 below. Notice that a Person may be an Employee or an Author or both.

Figure 17.2. The JS class model of the Person roles class hierarchy


3.2. Make a JS entity table model

Since we use the browsers' Local Storage as the persistent storage technology for our example app, we have to deal with simple key-value storage. For each design model class with a singular (capitalized) name Entity, we use its pluralized lowercase name entities as the corresponding table name.

We design a set of suitable JS entity tables in the form of a JS entity table model that we derive from the information design model. We have to make certain choices how to organize our data store and how to derive a corresponding entity table model.

The first choice to make concerns using either the Single Table Inheritance (STI), the Table per Class Inheritance (TCI) or the Joined Tables Inheritance (JTI) approach, which are introduced in Section 6.4. In the STI approach, a segmentation (or an entire class hierarchy) is represented with a single table, containing columns for all attributes of all classes involved, as shown in the example model of Figure 17.3.

Figure 17.3. An STI model of the Person roles class hierarchy


Since the given segmentation is non-disjoint, a multi-valued enumeration attribute categories is used for representing the information to which subclasses an instance belongs.

Using the STI approach is feasible for the given example, since the role hierarchy does not have many levels and the segment subclasses do not add many attributes. But, in a more realistic example, we would have a lot more attributes in the segment subclasses of the given role hierarchy. The STI approach is not really an option for representing a multi-level role hierarchy. However, we may choose it for representing the single-segment class hierarchy Manager-is-subclass-of-Employee.

For simplicity, and because the browsers' Local Storage does not support foreign keys as required by JTI, we choose the TCI approach, where we obtain a separate table for each class of the Person segmentation, but without foreign keys. Our choices result in the model shown in Figure 17.4 below, which has been derived from the design model shown in Figure Figure 16.8 by

  1. Merging the Manager subclass into its superclass Employee, according to the Class Hierarchy Merge design pattern described in Section 5.

  2. Replacing the standard ID property modifier {id} of the personId attribute of Person, Author and Employee with {pkey} for indicating that the attribute is a primary key.

  3. Replacing the singular (capitalized) class names (Person, Author and Employee) with pluralized lowercase table names (people, authors and employees).

  4. Adding the «JS entity table» stereotype to all class rectangles (people, authors and employees).

  5. Replacing the platform-independent datatype names with JS datatype names.

  6. Dropping all generalization/inheritance arrows and adding all attributes of supertables (personId and name) to their subtables (authors and employees).

Figure 17.4. A TCI model of the Person roles class hierarchy


In the case of using the JTI approach, we would also take the steps 1-5 above, but instead of step 6, we would

  1. Copy the primary key column (personId) of the root table (people) to all subtables (authors and employees).

  2. Replace the generalization arrows with «fkey»-stereotyped dependency arrows (representing foreign key dependencies) that are annotated at their source end with the name of the subtable's primary key (here: personId).

3.3. New issues

Compared to the model of our first case study, shown in Figure 17.1 above, we have to deal with a number of new issues in the model code:

  1. Defining the subclass relationships between Employee and Person, as well as between Author and Person, using the JS keyword extends discussed in Section 1.

  2. When loading the instances of the root class (Person.instances) from persistent storage (in Person.retrieveAll), we load (1) the records of the table representing the root class (people) for creating its direct instances and (2) also the records of all other tables representing its subclasses (authors and employees) for creating their direct instances, while also adding their object references to the root class population (to Person.instances). In this way, the root class population does not only contain direct instances, but all instances.

  3. When saving the instances of Employee and Author as records of the JS entity tables employees and authors to persistent storage in Employee.saveAll and Author.saveAll (invoked in employees.js and authors.js), we also save the direct instances of Person as records of the people table.

3.4. Code the model classes of the JS class model

The JS class model shown in Figure 17.2 above can be directly coded for getting the code of the model classes Person, Employee and Author as well as for the enumeration type EmployeeCategoryEL.

3.4.1. Defining subtype relationships

In the case of a superclass like Person, we define a class-level property subtypes for having a mechanism to loop over all subtypes of a superclass.

class Person {...}
Person.subtypes = [];

The property subtypes holds a list of all subtypes of the given class. This list is initially empty.

The subtype relationships between the classes Employee and Person, as well as between Author and Person, are defined with the help of the ES6 keywords extends and super. For instance, in m/Author.js we define:

class Author extends Person {
  constructor ({personId, name, biography}) {
    super({personId, name});  // invoke Person constructor
    // assign additional properties
    this.biography = biography;
  }
  get biography() {return this._biography;}
  set biography( b) {this._biography = b;}  /***SIMPLIFIED CODE: no validation ***/
  toString() {...}
}
// add Author to the list of Person subtypes
Person.subtypes.push( Author);

3.4.2. Loading the instances of a root class

When retrieving the instances of a class hierarchy's root class (in our example, Person) from a persistent data store organized according to the TCI approach, we have to retrieve not only its direct instances from the table representing the root class (people), but also all indirect instances from all tables representing its subclasses (employees and authors), as shown in the following code:

Person.retrieveAll = function () {
  var people = {};
  if (!localStorage["people"]) localStorage["people"] = "{}";
  try {
    people = JSON.parse( localStorage["people"]);
  } catch (e) {
    console.log("Error when reading from Local Storage\n" + e);
  }
  for (const key of Object.keys( people)) {
    try {  // convert record to (typed) object
      Person.instances[key] = new Person( people[key]);
    } catch (e) {
      console.log(`${e.constructor.name} ...`);
    }
  }
  // add all instances of all subtypes to Person.instances
  for (const Subtype of Person.subtypes) {
    Subtype.retrieveAll();
    for (const key of Object.keys( Subtype.instances)) {
      Person.instances[key] = Subtype.instances[key];
    }
  }
  console.log(`${Object.keys( Person.instances).length} records loaded`);
}

For any subtype (here, Author and Employee), each record is retrieved and a corresponding entry is created in the map Subtype.instances and copied to Person.instances.

3.4.3. Saving the subtables when saving a supertable

Since the app's data is kept in main memory as long as the app is running (which is as long as the app's webpage is kept open in the browser), the data has to be saved to persistent storage when the app is terminated (e.g., by closing its browser tab). When saving the instances of Person (as records of the people table) to persistent storage in v/people.js, we also save the direct instances of ist subtypes Employee and Author (as records of the JS entity tables employees and authors in v/employees.js and v/authors.js). This is necessary because changes to Person instances may imply changes of Employee or Author instances.

We do this in v/people.js:

// save data when leaving the page
window.addEventListener("beforeunload", function () {
  Person.saveAll();
  // save all subtypes for persisting changes of supertype attributes
  for (const Subtype of Person.subtypes) {
    Subtype.saveAll();
  }
});

3.5. Take care of subtypes in the UI

The view table created in the use case "Retrieve/list all people" is to show the roles "author" or "employee" of each person in a special column "Role(s)".

document.getElementById("RetrieveAndListAll")
    .addEventListener("click", function () {
  ...
  for (const key of Object.keys(Person.instances)) {
    const person = Person.instances[key];
    const row = tableBodyEl.insertRow();
    const roles = [];
    row.insertCell().textContent = person.personId;
    row.insertCell().textContent = person.name;
    for (const Subtype of Person.subtypes) {
      if (person.personId in Subtype.instances) roles.push( Subtype.name);
    }
    row.insertCell().textContent = roles.toString();
  }
});

Notice that since the class Employee has the subtype Manager, it would be desirable to see the role "manager" for any person being an instance of Employee with a category value of EmployeeCategoryEL.MANAGER. However, for simplicity, this is not implemented in the model app.