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 16.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 5. 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 17.1, where the class-level ('static') methods are underlined:
Compared to the enumeration app discussed in Chapter 7, 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 (BookCategoryEL
in our example) to be used as the range of the
category
attribute .
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 "Category" column to the display table of the
"Retrieve/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 the
books.js
view code file.
Adding a "Category" 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.
Disabling the "Category" selection field in the "Update
book" use case, after selecting a book to be updated, if the
selected book has a category
value (in order to
prevent any changes of the category
attribute's
value).
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 7.
Code the model class Book
in the form of a ES6
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 ES6
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 using the ES6 syntax for 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 (!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 = 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 user interfaces 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 "Category" column to the view table of the
"Retrieve/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 the
v/books.js
file:
if (book.category) { switch (book.category) { case BookCategoryEL.TEXTBOOK: row.insertCell().textContent = book.subjectArea + " textbook"; break; case BookCategoryEL.BIOGRAPHY: row.insertCell().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
displaySegmentFields
, when a corresponding book category
has been selected:
handleCategorySelectChangeEvent = function (e) { const formEl = e.currentTarget.form, // the array index of BookCategoryEL.labels categoryIndexStr = formEl.category.value; if (categoryIndexStr) { displaySegmentFields( formEl, BookCategoryEL.labels, parseInt( categoryIndexStr) + 1); } else { 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.