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 its user interface and (3) in the underlying database. The starting point for our case study is the design model shown in Figure 20.2 above. In the following sections, we derive a JavaScript data model and a entity table model from the design model. The entity table model is used as a design for the object-to-JSON mapping that we need for storing the objects of our app in Local Storage.

3.1. Make the JavaScript data model

We design the model classes of our example app with the help of a JavaScript data model that we derive from the design model by essentially leaving the generalization arrows as they are and just adding checks and setters to each class, as described in Part 2 of this tutorial. 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 segmentation of Employee for simplifying the data model by eliminating the Manager subclass. This leads to the model shown in Figure 20.6 below. Notice that we have also made two technical design decisions:

  1. We have declared the segmentation of Person into Employee and Author to be complete, that is, any person is an employee or an author (or both).

  2. We have turned Person into an abstract class (indicated by its name written in italics in the class rectangle), which means that it cannot have direct instances, but only indirect ones via its subclasses Employee and Author, implying that we do not need to maintain its extension (in a map like Person.instances), as we do for all other non-abstract classes. This technical design decision is compatible with the fact that any Person is an Employee or an Author (or both), and consequently there is no need for any object to instantiate Person directly.

Figure 20.6.  The JavaScript data model of the Person class hierarchy


3.2. Make the entity table model

Since we use Local Storage as the persistent storage technology for our example app, we have to deal with simple key-value storage. For each model class with a singular name Entity, we use its pluralized name entities as a key such that its associated value is the entity table serialization of the main memory object collection Entity.instances.

We design a set of suitable JSON tables and the structure of their records in the form of a entity table model that we derive from the design model by following certain rules. We basically have two choices how to organize our JSON data store and how to derive a corresponding entity table model: either according to the Single Table Inheritance approach, where a segmentation or an entire class hierarchy is represented with a single table, or according to the Joined Tables Inheritance approach, where we have a separate table for each model class of the class hierarchy. Both approaches, which are discussed in Section 6.2, can be combined for the same design model.

In our example it seems natural to apply the Single Table Inheritance approach to the incomplete segmentation of Employee with just one segment subclass Manager, while we apply the Joined Tables Inheritance approach to the complete segmentation of Person into Employee and Author. This results in the model shown in Figure 20.7 below.

Figure 20.7.  The entity table model of the Person class hierarchy


Notice that we have replaced the {id} property modifier for designating the standard ID attribute(s) with {pkey} for indicating that the attributes concerned act as primary keys in combination with foreign keys expressed by the dashed dependency arrows stereotyped with «fkey» and annotated with the foreign key attribute (here: personId) at their source end. An example of an admissible population for this table model is the following:

Person Employee Author
{personId: 1001, name:"Gerd Wagner"} {personId: 1001, empNo: 21035} {personId: 1001, biography:"Born in ..."}
{personId: 1002, name:"Tom Boss"} {personId: 1002, empNo:23107, category: EmployeeTypeEL.MANAGER, department:"Faculty1"}
{personId: 1077, name:"Immanuel Kant"} {personId: 1077, biography:"Kant was ..."}

Notice the mismatch between the JavaScript data model shown in Figure Figure 20.5 above, which is the basis for the model classes Person, Employee and Author as well as for the main memory database consisting of Employee.instances and Author.instances on one hand side, and the entity table model, shown in Figure 20.7 above on the other hand side. While we do not have any Person records in the main memory database, we do have them in the persistent datastore based on the entity table model. This mismatch results from the complete structure of JavaScript subclass instances, which include all property slots, as opposed to the fragmented structure of database tables based on the Joined Tables Inheritance approach.

3.3. New issues

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

  1. Defining the category relationships between Employee and Person, as well as between Author and Person, using the JavaScript code pattern for constructor-based inheritance discussed in Section 1.

  2. When loading the instances of a category from persistent storage (as in Employee.loadAll and Author.loadAll), their slots for inherited supertype properties, except for the standard identifier attribute, have to be reconstructed from corresponding rows of the supertable (persons).

  3. When saving the instances of Employee and Author as records of the JSON tables employees and authors to persistent storage (as in pl.view.employees.manage.exit and pl.view.authors.manage.exit), we also need to save the records of the supertable persons by extracting their data from corresponding Employee or Author instances.

3.4. Encode the model classes of the JavaScript data model

The JavaScript data model shown in Figure 20.6 above can be directly encoded for getting the code of the model classes Person, Employee and Author as well as for the enumeration type EmployeeTypeEL.

3.4.1. Define the category relationships

We define the category relationships between Employee and Person, as well as between Author and Person, using the JavaScript code pattern for constructor-based inheritance discussed in Section 1. For instance, in model/Employee.js we define:

function Employee( slots) {
  // set the default values for the parameter-free default constructor
  Person.call( this);  // invoke the default constructor of the supertype
  this.empNo = 0;      // Number (PositiveInteger)
  // constructor invocation with arguments
  if (arguments.length > 0) {
    Person.call( this, slots);  // invoke the constructor of the supertype
    this.setEmpNo( slots.empNo);
    if (slots.category) this.setCategory( slots.category);  // optional
    if (slots.department) this.setDepartment( slots.department);  // optional
  }
};
Employee.prototype = Object.create( Person.prototype);  // inherit from Person
Employee.prototype.constructor = Employee;  // adjust the constructor property

3.4.2. Reconstruct inherited supertype properties when loading the instances of a category

When loading the instances of a category from persistent storage (as in Employee.loadAll and Author.loadAll), their slots for inherited supertype properties, except for the standard identifier attribute, have to be reconstructed from corresponding rows of the supertable (persons). For instance, in model/Employee.js we define:

Employee.loadAll = function () {
  var key="", keys=[], persons={}, employees={}, employeeRow={}, i=0;
  if (!localStorage["employees"]) {
    localStorage.setItem("employees", JSON.stringify({}));
  }  
  try {
    persons = JSON.parse( localStorage["persons"]);
    employees = JSON.parse( localStorage["employees"]);
  } catch (e) {
    console.log("Error when reading from Local Storage\n" + e);        
  }
  keys = Object.keys( employees);
  console.log( keys.length +" employees loaded.");
  for (i=0; i < keys.length; i++) {
    key = keys[i];
    employeeRow = employees[key];
    // complete record by adding slots ("name") from supertable
    employeeRow.name = persons[key].name;
    Employee.instances[key] = Employee.convertRow2Obj( employeeRow);
  }
};

3.4.3. Reconstruct and save the supertable Person when saving the main memory data

When saving the instances of Employee and Author as records of the JSON tables employees and authors to persistent storage (as in pl.view.employees.manage.exit and pl.view.authors.manage.exit), we also need to save the records of the supertable persons by extracting their data from corresponding Employee or Author instances. For this purpose, we define in model/Person.js:

Person.saveAll = function () {
  var key="", keys=[], persons={}, i=0, n=0;
  keys = Object.keys( Employee.instances);
  for (i=0; i < keys.length; i++) {
    key = keys[i];
    emp = Employee.instances[key];
    persons[key] = {personId: emp.personId, name:emp.name};
  }
  keys = Object.keys( Author.instances);
  for (i=0; i < keys.length; i++) {
    key = keys[i];
    if (!persons[key]) {
      author = Author.instances[key];
      persons[key] = {personId: author.personId, name: author.name};    	
    }
  }
  try {
    localStorage["persons"] = JSON.stringify( persons);
    n = Object.keys( persons).length;
    console.log( n +" persons saved.");
  } catch (e) {
    alert("Error when writing to Local Storage\n" + e);
  }
};