2. Case Study 1: Eliminating a Class Hierarchy

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 20.1 above, representing a disjoint (but incomplete) segmentation of Book into TextBook and Biography. This model is first simplified by applying the Class Hierarchy Merge design pattern, resulting in the model shown in Figure 20.4.

Figure 20.4.  The simplified information design model obtained by applying the Class Hierarchy Merge design pattern


We can now derive a JavaScript data model from this design model.

2.1. Make the JavaScript data model

We make the JavaScript data model in 3 steps:

  1. Turn the design model's enumeration type, which contains an enumeration literal for each segment subclass, into a corresponding JavaScript map where the enumeration literals are (by convention uppercase) keys associated with an integer value that enumerates the literal. For instance, for the first enumeration literal "TextBook" we get the key-value pair TEXTBOOK=1.

  2. Turn the platform-independent datatypes (defined as the ranges of attributes) into JavaScript datatypes. This includes the case of enumeration-valued attributes, such as category, which are turned into numeric attributes restricted to the enumeration integers of the underlying enumeration type.

  3. Add property checks and setters, as described in Part 2 of this tutorial. The checkCategory and setCategory methods, as well as the checks and setters of the segment properties need special consideration according to their implied semantics. In particular, a segment property's check and setter methods 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 JavaScript data model is encoded.

This leads to the JavaScript data model shown in Figure 20.5, where the class-level ('static') methods are underlined:

Figure 20.5. The JavaScript data model


2.2. New issues

Compared to the validation app discussed in Part 2 of this tutorial, we have to deal with a number of new issues:

  1. In the model code we have to take care of

    1. Adding the constraint violation class FrozenValueConstraintViolation to errorTypes.js.

    2. Encoding the enumeration type to be used as the range of the category attribute (BookCategoryEL in our example).

    3. Encoding the checkCategory and setCategory methods for the category attribute. In our example this attribute is optional, due to the fact that the book types segmentation is incomplete. If the segmentation, to which the Class Hierarchy Merge pattern is applied, is complete, then the category attribute is mandatory.

    4. Encoding the checks and setters for all segment properties such that the check methods take the category as a second parameter for being able to test if the segment property concerned applies to a given instance.

    5. Refining the serialization method toString() by adding a category case distinction (switch) statement for serializing only the segment properties that apply to the given category.

    6. 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.

  2. In the UI code we have to take care of

    1. Adding a "Special type" 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.view.books.list.setupUserInterface in the books.js view code file.

    2. Adding a "Special type" select control, 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, and their validation event handlers set, 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".

2.3. Encode the model classes of the JavaScript data model

The JavaScript data model can be directly encoded for getting the code of the model classes of our JavaScript front-end app.

2.3.1. Summary

  1. Encode the enumeration type (to be used as the range of the category attribute) as a special JavaScript object mapping upper-case keys, representing enumeration literals, to corresponding enumeration integers.

  2. Encode the model class (obtained by applying the Class Hierarchy Merge pattern) in the form of a JavaScript constructor function with class-level check methods attached to it, and with instance-level setter methods attached to its prototype.

These steps are discussed in more detail in the following sections.

2.3.2. Encode the enumeration type BookCategoryEL

The enumeration type BookCategoryEL is encoded with the help of our special meta-class Enumeration, discussed in the tutorial on enumerations, at the beginning of the Book.js model class file in the following way:

BookCategoryEL = new Enumeration([ "Textbook", "Biography"]);

2.3.3. Encode the model class Book

We encode the model class Book in the form of a constructor function where the category attribute as well as the segment attributes subjectArea and about are optional:

function Book( slots) {
  // set the default values for the parameter-free default constructor
  this.isbn = "";         // String
  this.title = "";        // String
  this.year = 0;          // Number (PositiveInteger)
/* optional properties
  this.category            // Number {from BookCategoryEL}
  this.subjectArea        // String
  this.about              // String
*/
  if (arguments.length > 0) {
    this.setIsbn( slots.isbn); 
    this.setTitle( slots.title); 
    this.setYear( slots.year);
    if (slots.category) this.setCategory( slots.category);
    if (slots.subjectArea) this.setSubjectArea( slots.subjectArea); 
    if (slots.about) this.setAbout( slots.about); 
  }
}

We encode the checkCategory and setCategory methods for the category attribute in the following way:

Book.checkCategory = function (t) {
  if (!t) {
    return new NoConstraintViolation();
  } else {
    if (!Number.isInteger( t) || t < 1 || t > BookCategoryEL.MAX) {
      return new RangeConstraintViolation(
          "The value of category must represent a book type!");
    } else {
      return new NoConstraintViolation();
    }
  }
};
Book.prototype.setCategory = function (t) {
  var constraintViolation = null;
  if (this.category) {  // already set/assigned
    constraintViolation = new FrozenValueConstraintViolation(
        "The category cannot be changed!");
  } else {
    t = parseInt( t);
    constraintViolation = Book.checkCategory( t);
  }
  if (constraintViolation instanceof NoConstraintViolation) {
    this.category = t;
  } else {
    throw constraintViolation;
  }
};

While the setters for segment properties follow the standard pattern, their checks have to make sure that the attribute 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:

Book.checkSubjectArea = function (sa,t) {
  if (t === undefined) t = BookCategoryEL.TEXTBOOK;
  if (t === BookCategoryEL.TEXTBOOK && !sa) {
    return new MandatoryValueConstraintViolation(
        "A subject area must be provided for a textbook!");
  } else if (t !== BookCategoryEL.TEXTBOOK && sa) {
    return new OtherConstraintViolation("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:

Book.prototype.toString = function () {
  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 slot in the slots parameter) and if the new value is different from the old 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 segement 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 optional segment properties subjectArea and about in a more standard way:

Book.update = function (slots) {
  var book = Book.instances[slots.isbn],
      updatedProperties = [],
      ...;
  try {
    ...
    if ("category" in slots && book.category === undefined) {
      book.setCategory( slots.category);
      updatedProperties.push("category");
      switch (slots.category) {
      case BookCategoryEL.TEXTBOOK: 
        book.setSubjectArea( slots.subjectArea);
        updatedProperties.push("subjectArea");
        break;
      case BookCategoryEL.BIOGRAPHY: 
        book.setBiography( slots.biography);
        updatedProperties.push("biography");
        break;
      }
    }
    if ("subjectArea" in slots && "subjectArea" in book && 
            book.subjectArea !== slots.subjectArea) {
      book.setSubjectArea( slots.subjectArea);
      updatedProperties.push("subjectArea");
    }
    if ("about" in slots && "about" in book && 
            book.about !== slots.about) {
      book.setAbout( slots.about);
      updatedProperties.push("about");
    }
  } catch (e) {
    ...
  }
  ...
};

2.4. Write the View and Controller Code

The user interface (UI) consists of a start page that allows navigating to the data management pages (in our example, to books.html). Such a data management page contains 5 sections: manage books, list 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).

2.4.1. Summary

We have to take care of handling the category attribute and the subjectArea and about segment properties both in the "List all books" use case as well as in the "Create book" and "Update book" use cases by

  1. Adding a segment information column ("Special type") to the display table of the "List all books" use case in books.html.

  2. Adding a "Special type" select control, 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, and their validation event handlers set, 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".

2.4.2. Adding a segment information column in "List all books"

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>Special type</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 segment-specific information. This requires a corresponding switch statement in pl.view.books.list.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;
  }
}

2.4.3. Adding a "Special type" select control in "Create book" and "Update book"

In both use cases, we need to allow selecting a special category of book ('textbook' or 'biography') with the help of a select control, as shown in the following HTML fragment:

<p class="pure-control-group">
  <label for="creBookType">Special type: </label>
  <select id="creBookType" name="category"></select>
</p>
<p class="pure-control-group Textbook">
  <label for="creSubjectArea">Subject area: </label><input id="creSubjectArea" name="subjectArea" />
</p>
<p class="pure-control-group Biography">
  <label for="creAbout">About: </label><input id="creAbout" name="about" />
</p>

Notice that we have added "Textbook" and "Biography" as additional class values to the HTML class attributes of the p elements containing the corresponding form controls. This allows easy rendering and un-rendering of "Textbook" and "Biography" form controls, depending on the value of the category attribute (a mechanism called dynamic forms).

In the handleTypeSelectChangeEvent handler, segment property form fields are only displayed, with pl.view.app.displaySegmentFields, and their validation event handlers set, when a corresponding book category has been selected:

pl.view.books.handleTypeSelectChangeEvent = function (e) {
  var formEl = e.currentTarget.form,
      typeIndexStr = formEl.category.value,  // the array index of BookCategoryEL.names
      category=0;
  if (typeIndexStr) {
    category = parseInt( typeIndexStr) + 1;
    switch (category) {
    case BookCategoryEL.TEXTBOOK:
      formEl.subjectArea.addEventListener("input", function () {
        formEl.subjectArea.setCustomValidity( 
            Book.checkSubjectArea( formEl.subjectArea.value).message);
      });
      break;
    case BookCategoryEL.BIOGRAPHY: 
      formEl.about.addEventListener("input", function () {
        formEl.about.setCustomValidity( 
            Book.checkAbout( formEl.about.value).message);
      });
      break;
    }
    pl.view.app.displaySegmentFields( formEl, BookCategoryEL.names, category);
  } else {
    pl.view.app.undisplayAllSegmentFields( formEl, BookCategoryEL.names);
  }
};