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

2.1. Make the JS class model

We make the JS class model in 3 steps:

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

  2. Decorate all properties with a «get/set» stereotype for indicating that they have implicit getters and setters.

  3. 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:

Figure 17.1. The JS class model of the merged Book class hierarchy


2.2. New issues

Compared to the enumeration app discussed in Chapter 7, 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. Coding the enumeration type (BookCategoryEL in our example) to be used as the range of the category attribute .

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

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

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

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

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

    3. 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).

2.3. Code the model classes of the JS class model

The JS class model can be directly coded for getting the code of the model classes of our JS front-end app.

2.3.1. Summary

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

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

2.3.2. Code the enumeration type BookCategoryEL

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"]);

2.3.3. Code the model class Book

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) {
    ...
  }
  ...
};

2.4. Write the User Interface Code

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

2.4.1. Summary

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

  1. Adding a segment information column (with heading "Category") to the display table of the "Retrieve and list all books" use case in books.html.

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

2.4.2. Add a segment information column in Retrieve/List All

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;
  }
}

2.4.3. Add a category selection field in Create and Update

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.