Three enumerations are coded (within
Book.mjs
) in the following way with the help of the
meta-class Enumeration
:
const PublicationFormEL = new Enumeration( ["hardcover","paperback","ePub","PDF"]); const BookCategoryEL = new Enumeration( ["novel","biography","textbook","other"]); const LanguageEL = new Enumeration({"en":"English", "de":"German", "fr":"French", "es":"Spanish"});
Notice that LanguageEL
defines a code list
enumeration, while PublicationFormEL
and
BookCategoryEL
define simple enumerations.
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 using a setter method for assigning a property, and invoking 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
(or ES2015) class definition.
The constructor of the class Book
is defined with a
single record parameter using the ES6 syntax of function
parameter destructuring:
class Book {
constructor ({isbn, title, originalLanguage, otherAvailableLanguages,
category, publicationForms}) {
// assign properties by invoking implicit setters
this.isbn = isbn;
this.title = title;
this.originalLanguage = originalLanguage;
this.otherAvailableLanguages = otherAvailableLanguages;
this.category = category;
this.publicationForms = publicationForms;
}
...
}
Such a constructor is invoked in the following way:
const book = new Book({ isbn: "006251587X", title: "Weaving the Web", originalLanguage: LanguageEL.EN, otherAvailableLanguages: [LanguageEL.DE, LanguageEL.FR], category: BookCategoryEL.NOVEL, publicationForms: [ PublicationFormEL.EPUB, PublicationFormEL.PDF ] });
The internal properties of a class are defined using
underscore-prefixed names (like "_isbn"). 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 ES2015 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 (!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) {
if (!pubForms || (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 (const pF of pubForms) {
const validationResult = Book.checkPublicationForm( pF);
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 the 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 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( (el,i) => el===a2[i]);
};
This allows us to express these tests in the following way:
if (!book.publicationForms.isEqualTo( slots.publicationForms)) { book.publicationForms = 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.generateTestData = 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}`); } };