Enumerations are coded in the following way with the help of the
meta-class Enumeration
:
var PublicationFormEL = new Enumeration( ["hardcover","paperback","ePub","PDF"]); var BookCategoryEL = new Enumeration( ["novel","biography","textbook","other"]); var LanguageEL = new Enumeration({"en":"English", "de":"German", "fr":"French", "es":"Spanish"});
Notice that LanguageEL
defines a code list
enumeration, while PublicationFormEL
defines a simple
enumeration.
We want to check if a new property value satisfies all constraints of a property
whenever the value of a property is set. A best practice approach for making sure that new
values are validated before assigned is to use a setter method for assigning a property, and
invoke the check in the setter. We can either define an explicit setter method (like
setIsbn
) for a property (like isbn
), or we can use JavaScript's
implicit getters and setters in combination with an internal property name (like _isbn
). We have
used explicit setters in the validation app. Now, in the Book
class definition for
the enumeration app, we use JavaScript's implicit getters and setters because they offer a more
user-friendly syntax and can be conveniently defined in an ES6 class definition.
The class Book
is coded in the form of an ES6 class
such that all its properties are defined with an internal property name
format (prefixed with _) and assigned with values from corresponding
key-value slots of a slots
parameter in the
constructor:
class Book {
constructor (slots) {
// assign default values to mandatory properties
this._isbn = ""; // string
this._title = ""; // string
...
// is constructor invoked with a non-empty slots argument?
if (typeof slots === "object" && Object.keys( slots).length > 0) {
// assign properties by invoking implicit setters
this.isbn = slots.isbn;
this.title = slots.title;
...
}
}
...
}
For each property, we define implicit getters and setters using the predefined JS keywords get
and
set
:
class Book { ... get isbn() { return this._isbn; } set isbn( n) { var validationResult = Book.checkIsbnAsId( n); if (validationResult instanceof NoConstraintViolation) { this._isbn = n; } else { throw validationResult; } } ... }
Notice that the implicit getters and setters access the corresponding internal property,
like _isbn
. This approach is based on the assumption that this internal property is
normally not accessed directly, but only via its getter or setter. Since we can normally assume
that developers comply with this rule (and that there is no malicious developer in the team),
this approach is normally safe enough. However, there is also a proposal to increase the safety
(for avoiding direct access) by generating random names for the internal properties with the
help of the ES6 Symbol
class.
Code the enumeration attribute checks in the form of class-level
('static') functions that check if the argument is a valid enumeration
index not smaller than 1 and not greater than the enumeration's MAX
value. For instance, for the checkOriginalLanguage
function
we obtain the following code:
class Book { ... static checkOriginalLanguage( ol) { if (ol === undefined || ol === "") { return new MandatoryValueConstraintViolation( "An original language must be provided!"); } else if (!util.isIntegerOrIntegerString(ol) || parseInt(ol) < 1 || parseInt(ol) > LanguageEL.MAX) { return new RangeConstraintViolation( "Invalid value for original language: "+ ol); } else { return new NoConstraintViolation(); } } ... }
For a multi-valued enumeration attribute, such as
publicationForms
, we break down the validation code into
two check functions, one for checking if a value is a valid enumeration
index (checkPublicationForm
), and another one for checking
if all members of a set of values are valid enumeration indexes
(checkPublicationForms
). The first check is coded as
follows:
static checkPublicationForm( p) {
if (p == undefined) {
return new MandatoryValueConstraintViolation(
"No publication form provided!");
} else if (!Number.isInteger( p) || p < 1 ||
p > PublicationFormEL.MAX) {
return new RangeConstraintViolation(
"Invalid value for publication form: "+ p);
} else {
return new NoConstraintViolation();
}
}
The second check first tests if the argument is a non-empty array (representing a collection with at least one element) and then checks all elements of the array in a loop:
static checkPublicationForms( pubForms) {
var i=0, validationResult=null;
if (pubForms == undefined || (Array.isArray( pubForms) &&
pubForms.length === 0)) {
return new MandatoryValueConstraintViolation(
"No publication form provided!");
} else if (!Array.isArray( pubForms)) {
return new RangeConstraintViolation(
"The value of publicationForms must be an array!");
} else {
for (i=0; i < pubForms.length; i++) {
validationResult = Book.checkPublicationForm( pubForms[i]);
if (!(validationResult instanceof NoConstraintViolation)) {
return validationResult;
}
}
return new NoConstraintViolation();
}
}
The object serialization function toString()
now needs to include the
values of enumeration attributes:
class Book {
...
toString() {
return "Book{ ISBN:"+ this.isbn +", title:"+ this.title +
", originalLanguage:"+ this.originalLanguage +
", otherAvailableLanguages:"+
this.otherAvailableLanguages.toString() +
", category:"+ this.category +
", publicationForms:"+
this.publicationForms.toString() +"}";
}
...
}
Notice that for multi-valued enumeration attributes we call the
toString()
function that is predefined for JS
arrays.
There are only two new issues in the data management operations compared to the validation app:
We have to make sure that the util.cloneObject
method, which is used in Book.update
, takes care of
copying array-valued attributes, which we didn't have before (in
the validation app).
In the Book.update
method we now have to check
if the values of array-valued attributes have changed, which
requires to test if two arrays are equal or not. For code
readability, we add an array equality test method to
Array.prototype
in
browserShims.js
, like so:
Array.prototype.isEqualTo = function (a2) {
return (this.length === a2.length) && this.every( function( el, i) {
return el === a2[i]; });
};
This allows us to express these tests in the following way:
if (!book.publicationForms.isEqualTo( slots.publicationForms)) { book.setPublicationForms( slots.publicationForms); updatedProperties.push("publicationForms"); }
In the test data records that are created by Book.createTestData()
, we now
have to provide values for single- and multi-valued enumeration attributes. For readability, we
use enumeration literals instead of enumeration
indexes:
Book.createTestData = function () { try { Book.instances["006251587X"] = new Book({isbn:"006251587X", title:"Weaving the Web", originalLanguage: LanguageEL.EN, otherAvailableLanguages: [LanguageEL.DE, LanguageEL.FR], category: BookCategoryEL.NOVEL, publicationForms: [PublicationFormEL.EPUB, PublicationFormEL.PDF] }); ... Book.saveAll(); } catch (e) { console.log( e.constructor.name + ": " + e.message); } };