4. 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/factory code, (2) in its user interface, (3) in its controller code and (3) in the underlying database. The starting point for our case study is the design model shown in Figure 15.2 above. In the following sections we derive a JavaScript class model and a JSON table model. The JSON table model is used as a design for the object-to-JSON mapping that we need for storing the objects of our application in Parse cloud storage. And in deed, every object/data in Angular application is saved in the form of JSON.

4.1. Make the JavaScript class model

We design the model classes of our example app with the help of a JavaScript class model that we derive from the design model by essentially leaving the generalizatiion arrows as they are. However, in the case of our example app, it is natural to apply the Class Hierarchy Merge design pattern to the segmentation of Employee for simplifying the class model by eliminating the Manager subclass. This leads to the model shown in Figure 15.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, 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 instance to instantiate Person diretly.

Figure 15.6. The Simplified JavaScript class model of the Person class hierarchy


4.2. Make the JSON table model

Since we use Parse Cloud Storage as the persistent storage technology for our example app, which will define a specific global unique standard ID objectId for each record/object, we can also deal with simple key-value storage using our own standard ID personId in this case study.

To gain a better understanding of the record/object storage structure for subtyping, we design a set of suitable JSON tables and the structure of their records in the form of a JSON 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 JSON 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 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 15.7 below.

Figure 15.7.  The JSON table model of the Person class hierarchy


Notice that we have replaced the «stdid» stereotype 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 (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, type: "Manager", department:"Faculty 1"} {personId: 1077, biography:"Kant was ..."}
{personId: 1077, name:"Immanuel Kant"}

Notice the mismatch between the JavaScript class model shown in Figure 15.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 JSON table model, shown in Figure 15.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 JSON 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.

4.3. New issues

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

  1. Defining different factories for each class, and building 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, we also need to save the records of the supertable persons by extracting their data from corresponding Employee or Author instances.

4.4. Encode the Angular Factories

4.4.1. Define the supertype Person

For defining the category relationships between Employee and Person, as well as between Author and Person, we first define the superclass Person in js/modelsPackage/PersonsModel.js:

plModels.factory('Person', ['$http', function ( $http) {

  function Person() {
    this.personId = 0;
    this.name = "";
  }

  var urlPerson = "https://api.parse.com/1/classes/Person/";

  Person.loadAll = function () {
    return $http( {
      method: 'GET', url: urlPerson
    });
  };

  Person.get = function ( personId) {
    return $http( {
      method: 'GET', url: urlPerson,
      params:{ where: { personId: personId}}
    });
  };

  Person.add = function ( slots) {
    return $http( {
      method: 'POST', url: urlPerson,
      data: { 
        personId: slots.personId,
        name: slots.name
      }
    });
  };

  Person.update = function ( slots) {
    Person.get( slots.personId).success( function ( data) {
      return $http( {
        method: 'PUT',
        url: urlPerson + data.results[0].objectId,
        data: { name: slots.name}
      });
    });
  };

  Person.destroy = function ( slots) {
    Person.get( slots.personId).success( function ( data) {
      $http( {
        method: 'DELETE',
        url: urlPerson + data.results[0].objectId
      });
    });
  };

  return Person;
}]);

Similar like the factory of Book, in this factory we've defined some properties and CRUD functions for Person.

4.4.2. Define the subtype Employee

By defining relationships between Employee and Person, as well as between Author and Person, we will use constructor-based inheritance discussed in Section 1. So in the factory of Employee we should inject the factory of Person for subtyping and also define the CRUD functions:

plModels.factory('Employee', 
['$http', 'Person', function ( $http, Person) {

  var employeeSubtype = [
      "Manager"
    ];

  function Employee () {
    Person.call( this);
    this.empNo = 0;
    this.type = "";
    this.department = "";
  }
  Employee.prototype = Object.create( Person.prototype);
  Employee.prototype.constructor = Employee;
  console.log( "Employee.prototype instanceof Person: ",
               Employee.prototype instanceof Person);

  Employee.getAllSubtypes = function () {
    return employeeSubtype;
  };

  var urlEmployee = "https://api.parse.com/1/classes/Employee/";

  Employee.loadAll = function () {
    return $http( { method: 'GET', url: urlEmployee})
      .success( function ( data) {
        data.results.forEach( function (e) {
          Person.get( e.personId).success( function ( data) {
            e.name = data.results[0].name;
          });
        });
      });
  };

  Employee.get = function ( personId) {
    return $http( {
      method: 'GET', url: urlEmployee,
      params:{ where: { personId: personId}}
    });
  };

  Employee.add = function ( slots) {
    Person.add( slots);
    return $http( {
      method: 'POST', url: urlEmployee,
      data: {
        personId: slots.personId,
        empNo: slots.empNo,
        type: slots.type,
        department: slots.department
      }
    });
  };

  Employee.update = function ( slots) {
    Person.update( slots);
    return $http( {
      method: 'PUT', url: urlEmployee + slots.objectId,
      data: {
        empNo: slots.empNo,
        type: slots.type,
        department: slots.department
      }
    });
  };

  Employee.destroy = function ( slots) {
    return $http( {
      method: 'DELETE', url: urlEmployee + slots.objectId
    })
      .success( function () {
        Person.destroy( slots);
      });
  };

  return Employee;
}]);

The factory of Author is similar and simpler as Employee except the address of class url.

One more thing we must notice: when a personId is existing neither in its subclass employee nor in author records, we should clear this record in the class person, which means the destroy function in factory of Person should be improved:

Person.destroy = function ( slots) {
  return $http( {
    method: 'GET',
    url: "https://api.parse.com/1/classes/Author",
    params: { where: { personId: slots.personId}}
  })
    .success( function ( data) {
      if ( data.results.length === 0) { // no author record
        $http( {
          method: 'GET',
          url: "https://api.parse.com/1/classes/Employee",
          params: { where: { personId: slots.personId}}
        })
          .success( function ( data) {
            if ( data.results.length === 0) { // no employee record
              Person.get( slots.personId)
                .success( function ( data) {
                  $http( {
                    method: 'DELETE',
                    url: urlPerson + data.results[0].objectId
                  });
                });
            }
          });
      }
    });
};

4.5. Write the View and Controller Code

By implementing the views and controllers for Employee and Author, there is no special technic any more, just inject the corresponding factory for each controller, then invoke the corresponding function for each use case.