Simple class hierarchies can be eliminated by applying the Class Hierarchy Merge design pattern. The starting point for our case study is the simple class hierarchy shown in the information design model of Figure 12.1 above, representing a disjoint (but incomplete) rigid segmentation of Book
into TextBook
and Biography
. This model is first simplified by applying the Class Hierarchy Merge design pattern, resulting in the following model:
We can now derive a JS class model from this design model.
We make the JS class model in 3 steps:
Replace the platform-independent datatypes (used as the ranges of attributes and parameters) with JS datatypes. This includes the case of enumeration-valued attributes, such as category
, which are turned into number
-valued attributes restricted to the enumeration integers of the underlying enumeration type.
Decorate all properties with a «get/set» stereotype for indicating that they have implicit getters and setters.
Add property check functions, as described in Chapter 8 of Volume 1 . The checkCategory
function, as well as the checks of the segment properties need special consideration according to their implied semantics. In particular, a segment property's check function must ensure that the property can only be assigned if the category
attribute has a value representing the corresponding segment. We explain this implied validation semantics in more detail below when we discuss how the JS class model is coded.
This leads to the JS class model shown in Figure 13.1, where the class-level ('static') methods are underlined:
Compared to the enumeration app discussed in Chapter 11 of Volume 1, we have to deal with a number of new issues:
In the model code we have to take care of
Adding the constraint violation class FrozenValueConstraintViolation to errorTypes.js
.
Coding the enumeration type to be used as the range of the category
attribute (BookCategoryEL
in our example).
Coding the checkCategory
function for the category
attribute. In our example this attribute is optional, due to the fact that the Book
segmentation is incomplete. If the segmentation, to which the Class Hierarchy Merge pattern is applied, is complete, then the category
attribute is mandatory.
Coding the check functions for all segment properties such that they take the category as a second parameter for being able to test if the segment property concerned applies to a given instance.
Refining the serialization function toString()
by adding a category
case distinction (switch
) statement for serializing only the segment properties that apply to the given category.
Implementing the Frozen Value Constraint for the category
attribute in Book.update
by updating the category
of a book only if it has not yet been defined. This means it cannot be updated anymore as soon as it has been defined.
In the UI code we have to take care of
Adding a "Special type" (or "Category") column to the display table of the "List all books" use case in books.html
. A book without a special category will have an empty table cell, while for all other books their category will be shown in this cell, along with other segment-specific attribute values. This requires a corresponding switch statement in pl.v.books.retrieveAndListAll.setupUserInterface
in the books.js
view code file.
Adding a "Special type" choice widget (typically, a selection list), and corresponding form fields for all segment properties, in the forms of the "Create book" and "Update book" use cases in books.html
. Segment property form fields are only displayed when a corresponding book category has been selected. Such an approach of rendering specific form fields only on certain conditions is sometimes called dynamic forms.
The JS class model can be directly coded for getting the code of the model classes of our JS front-end app.
Code the enumeration type BookCategoryEL
to be used as the range of the category
attribute with the help of the meta-class Enumeration
, as explained in Chapter 11 of Volume 1 .
Code the model class Book
in the form of a JS class
definition with get
and set
methods as well as static check functions.
These steps are discussed in more detail in the following sections.
The enumeration type BookCategoryEL
is coded with the help of our library meta-class Enumeration
at the beginning of the Book.js
model class file in the following way:
BookCategoryEL = new Enumeration([ "Textbook", "Biography"]);
We code the model class Book
in the form of an ES2015 class definition where the category
attribute as well as the segment attributes subjectArea
and about
are optional, with getters, setters and static check functions for all properties:
class Book { constructor ({isbn, title, year, category, subjectArea, about}) { this.isbn = isbn; this.title = title; this.year = year; // optional properties if (category) this.category = category; if (subjectArea) this.subjectArea = subjectArea; if (about) this.about = about; } get isbn() {...} static checkIsbn( isbn) {...} static checkIsbnAsId( isbn) {...} set isbn( isbn) {...} get title() {...} static checkTitle( t) {...} set title( t) {...} get year() {...} static checkYear( y) {...} set year( y) {...} get category() {...} static checkCategory( c) {...} set category( c) {...} get subjectArea() {...} static checkSubjectArea( sA, cat) {...} set subjectArea( s) {...} get about() {...} static checkAbout( a, cat) {...} set about( a) {...} }
Notice that the constructor function is defined with a single record parameter making use of the ES2015 feature of function parameter destructuring.
We code the checkCategory
and setCategory
methods for the category
attribute in the following way:
static checkCategory( c) {
if (c === undefined || c === "") {
return new NoConstraintViolation(); // category is optional
} else if (!util.isIntegerOrIntegerString(c) || parseInt(c) < 1 ||
parseInt(c) > BookCategoryEL.MAX) {
return new RangeConstraintViolation(
"Invalid value for category: "+ c);
} else {
return new NoConstraintViolation();
}
};
set category( c) {
var validationResult = null;
if (this.category) { // already set/assigned
validationResult = new FrozenValueConstraintViolation(
"The category cannot be changed!");
} else {
validationResult = Book.checkCategory( c);
}
if (validationResult instanceof NoConstraintViolation) {
this._category = parseInt( c);
} else {
throw validationResult;
}
}
While the getters for segment properties (in this example: subjectArea
and about
) follow the standard pattern, their checks and setters have to make sure that the property applies to the category of the instance being checked. This is achieved by checking a combination of a property value and a category, as in the following example:
static checkSubjectArea( sA, c) {
if (c === BookCategoryEL.TEXTBOOK && !sA) {
return new MandatoryValueConstraintViolation(
"A subject area must be provided for a textbook!");
} else if (c !== BookCategoryEL.TEXTBOOK && sA) {
return new ConstraintViolation("A subject area must not " +
"be provided if the book is not a textbook!");
} else if (sA && (typeof(sA) !== "string" || sA.trim() === "")) {
return new RangeConstraintViolation(
"The subject area must be a non-empty string!");
} else {
return new NoConstraintViolation();
}
}
In the serialization function toString
, we serialize the category attribute and the segment properties in a switch
statement:
toString() {
var bookStr = "Book{ ISBN:"+ this.isbn +", title:"+
this.title +", year:"+ this.year;
switch (this.category) {
case BookCategoryEL.TEXTBOOK:
bookStr += ", textbook subject area:"+ this.subjectArea;
break;
case BookCategoryEL.BIOGRAPHY:
bookStr += ", biography about: "+ this.about;
break;
}
return bookStr + "}";
};
In the update method of a model class, we only set a property if it is to be updated, that is, if there is a corresponding argument slot with a value that is different from the old property value. In the special case of a category
attribute with a Frozen Value Constraint, we need to make sure that it can only be updated, along with an accompanying set of segment properties, if it has not yet been assigned. Thus, in the Book.update
method, we perform the special test if book.category === undefined
for handling the special case of an initial assignment, while we handle updates of the segment properties subjectArea
and about
in a more standard way:
Book.update = function ({isbn, title, year, category, subjectArea, about}) { const book = Book.instances[isbn], objectBeforeUpdate = util.cloneObject( book); var noConstraintViolated=true, updatedProperties=[]; try { ... if (category && book.category !== category) { book.category = category; updatedProperties.push("category"); } else if (category === "" && "category" in book) { throw FrozenValueConstraintViolation( "The book category cannot be unset!"); } if (subjectArea && book.subjectArea !== subjectArea) { book.subjectArea = subjectArea; updatedProperties.push("subjectArea"); } if (about && book.about !== about) { book.about = about; updatedProperties.push("about"); } } catch (e) { ... } ... };
The app's user interface (UI) consists of a start page that allows navigating to data management pages (in our example, to books.html
). Such a data management page contains 5 sections: manage books, list and retrieve all books, create book, update book and delete book, such that only one of them is displayed at any time (by setting the CSS property display:none
for all others).
We have to take care of handling the category
attribute and the segment properties subjectArea
and about
both in the "Retrieve and list all books" use case as well as in the "Create book" and "Update book" use cases by
Adding a segment information column (with heading "Category") to the display table of the "Retrieve and list all books" use case in books.html
.
Adding a "Category" selection field, and input fields for all segment properties, in the forms of the "Create book" and "Update book" use cases in books.html
. The form fields for segment properties are only displayed, when a corresponding book category has been selected.
We add a "Special type" column to the display table of the "List all books" use case in books.html
:
<table id="books">
<thead><tr><th>ISBN</th><th>Title</th><th>Year</th><th>Category</th></tr></thead>
<tbody></tbody>
</table>
A book without a special category will have an empty table cell in this column, while for all other books their category will be shown in this column, along with other category-specific information. This requires a corresponding switch statement in pl.v.books.retrieveAndListAll.setupUserInterface
in the view/books.js
file:
if (book.category) { switch (book.category) { case BookCategoryEL.TEXTBOOK: row.insertCell(-1).textContent = book.subjectArea + " textbook"; break; case BookCategoryEL.BIOGRAPHY: row.insertCell(-1).textContent = "Biography about "+ book.about; break; } }
In both use cases, we need to allow selecting a special category of book ('textbook' or 'biography') with the help of a selection field, as shown in the following HTML fragment:
<div class="field"> <label>Category: <select name="category"></select></label> </div> <div class="field Textbook"><!-- conditional field --> <label>Subject area: <input type="text" name="subjectArea" /></label> </div> <div class="field Biography"><!-- conditional field --> <label>About: <input type="text" name="about" /></label> </div>
Notice that we have added "Textbook" and "Biography" as additional values of the class
attribute of the segment field container elements. This supports the rendering and un-rendering of "Textbook" and "Biography" form fields, depending on the value of the category
attribute.
In the handleCategorySelectChangeEvent
handler, segment property form fields are only displayed, with pl.v.app.displaySegmentFields
, when a corresponding book category has been selected:
pl.v.books.handleCategorySelectChangeEvent = function (e) { var formEl = e.currentTarget.form, categoryIndexStr = formEl.category.value; if (categoryIndexStr) { pl.v.app.displaySegmentFields( formEl, BookCategoryEL.labels, parseInt( categoryIndexStr) + 1); } else { pl.v.app.undisplayAllSegmentFields( formEl, BookCategoryEL.labels); } };
Recall that the category selection list contains a no-selection option "---" with the empty string as its return value, and a list of options formed by the enumeration labels of BookCategoryEL.labels such that their value is the corresponding array index (starting with 0) as a string. Consequently, the variable categoryIndexStr
has either the value "" (empty string) or one of "0", "1", "2", etc.