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.