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.
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.
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.
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
Merging the Manager subclass into its superclass
Employee, according to the Class Hierarchy Merge design pattern
described in Section 5.
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.
Replacing the singular (capitalized) class names (Person, Author and Employee) with pluralized lowercase table names (people, authors and employees).
Adding the «JS entity table» stereotype to all class
rectangles (people, authors and
employees).
Replacing the platform-independent datatype names with JS datatype names.
Dropping all generalization/inheritance arrows and adding all
attributes of supertables (personId and
name) to their subtables (authors and
employees).
In the case of using the JTI approach, we would also take the steps 1-5 above, but instead of step 6, we would
Copy the primary key column (personId) of the
root table (people) to all subtables
(authors and employees).
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).
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:
Defining the subclass relationships between
Employee and Person, as well as between
Author and Person, using the JS keyword
extends discussed in Section 1.
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.
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.
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.
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);
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.
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();
}
});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.