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 our case study is the design model shown in Figure 12.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 6) 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 13.2 below. Notice that a Person may be an Employee or an Author or both.

Figure 13.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 and as a key such that its associated string value is obtained by serializing the object collection Entity.instances with the help of the JSON.stringify method.

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 7.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 following example.

Figure 13.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 13.4 below, which has been derived from the design model shown in Figure Figure 12.8 by

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

  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 13.4. A TCI model of the Person roles class hierarchy

In the case of using the JTI approach, in addition to the steps 1-5 above, 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 13.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 pl.v.employees.manage.exit and pl.v.authors.manage.exit), 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 13.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

We define the subtype relationships between Employee and Person, as well as between Author and Person, with extends. For instance, in m/Employee.js we define:

EmployeeCategoryEL =  new Enumeration(["Manager"]);

class Employee extends Person {
  constructor ({personId, name, empNo, category, department}) {
    super({personId, name});
    this.empNo = empNo;
    if (category) this.category = category;
    if (department) this.department = department;

3.4.2. Loading the instances of the root class Person

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={}, employees={}, authors={};
  if (!localStorage["authors"]) localStorage["authors"] = "{}";
  if (!localStorage["employees"]) localStorage["employees"] = "{}";
  if (!localStorage["people"]) localStorage["people"] = "{}";
  try {
    people = JSON.parse( localStorage["people"]);
    employees = JSON.parse( localStorage["employees"]);
    authors = JSON.parse( localStorage["authors"]);
  } catch (e) {
    console.log("Error when reading from Local Storage\n" + e);
  for (let key of Object.keys( authors)) {
    try {  // convert record to (typed) object
      Author.instances[key] = new Author( authors[key]);
      // create superclass extension
      Person.instances[key] = Author.instances[key];
    } catch (e) {
      console.log(`${e.constructor.name} while deserializing` +
          `author ${key}: ${e.message}`);

Each record of the authors table is retrieved and converted to an Author object, a reference to which is copied to Person.instances. Also the records of the employees table are processed in this way, while the records of the people table are simply retrieved and converted to Person objects:

Person.retrieveAll = function () {
  for (let key of Object.keys( employees)) {
  for (let 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} while deserializing` +
          `author ${key}: ${e.message}`);

3.4.3. Saving the supertable when saving a subtable

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 exited (e.g., by closing its browser tab), When saving the instances of Employee and Author (as records of the JS entity tables employees and authors) to persistent storage in pl.v.employees.manage.exit and pl.v.authors.manage.exit, we also save the direct instances of Person (as records of the people table). This is necessary because changes to Employee or Author instances may imply changes of Person.instances.

For instance, for Employee data management, we define in v/employees.js:

pl.v.employees.manage = {
  exit: function () {