The JS class model shown on the right hand side in Figure 5.1 can be coded step by step for getting the code of the model layer of our JavaScript front-end app. These steps are summarized in the following section.
Code the model class as a JavaScript constructor function.
Code the check
functions, such as checkIsbn
or
checkTitle
, in the form of class-level ('static')
methods. Take care that all constraints, as specified in the JS
class model, are properly coded in the check functions.
Code the setter
operations, such as setIsbn
or
setTitle
, as (instance-level) methods. In the setter,
the corresponding check operation is invoked and the property is
only set, if the check does not detect any constraint
violation.
Code the add and remove operations, if there are any, as instance-level methods.
These steps are discussed in more detail in the following sections.
The class Book
is coded as a corresponding constructor function with the same name
Book
such that all its (non-derived) properties are
supplied with values from corresponding key-value slots of a
slots
parameter.
function Book( slots) {
// assign default values
this.isbn = ""; // string
this.title = ""; // string
this.year = 0; // number (int)
// assign properties only if the constructor is invoked with an argument
if (arguments.length > 0) {
this.setIsbn( slots.isbn);
this.setTitle( slots.title);
this.setYear( slots.year);
// optional property
if (slots.edition) this.setEdition( slots.edition);
}
};
In the constructor body, we first assign default values to the class properties. These values will be used when the constructor is invoked as a default constructor (without arguments), or when it is invoked with only some arguments. It is helpful to indicate the range of a property in a comment. This requires to map the platform-independent datatypes of the information design model to the corresponding implicit JavaScript datatypes according to the following table.
Table 5.1. Datatype mapping
Platform-independent datatype | JavaScript datatype | SQL |
---|---|---|
String | string | CHAR(n) or VARCHAR(n) |
Integer | number (int) | INTEGER |
Decimal | number (float) | REAL, DOUBLE PRECISION or DECIMAL(p,s) |
Boolean | boolean | BOOLEAN |
Date | Date | DATE |
Since the setters may throw constraint violation errors, the constructor function, and any setter, should be called in a try-catch block where the catch clause takes care of processing errors (at least logging suitable error messages).
As in the minimal app, we add a class-level property
Book.instances
representing the collection of all
Book
instances managed by the app in the form of an entity
table:
Book.instances = {};
Code the property check functions in the form of class-level
('static') methods. In JavaScript, this means to define them as method
slots of the constructor, as in Book.checkIsbn
(recall that
a constructor is a JS object, since in JavaScript, functions are
objects, and as an object, it can have slots).
Take care that all constraints of a property as specified in the
class model are properly coded in its check function. This concerns, in
particular, the mandatory value and
uniqueness constraints implied by the
standard identifier declaration (with
{id}
), and the mandatory
value constraints for all properties with multiplicity 1,
which is the default when no multiplicity is shown. If any constraint is
violated, an error object instantiating one of the error classes listed
above in Section 5.1 and defined in the file
errorTypes.mjs
is returned.
For instance, for the checkIsbn
operation we obtain
the following code:
Book.checkIsbn = function (id) { if (!id) { return new NoConstraintViolation(); } else if (typeof id !== "string" || id.trim() === "") { return new RangeConstraintViolation( "The ISBN must be a non-empty string!"); } else if (!/\b\d{9}(\d|X)\b/.test( id)) { return new PatternConstraintViolation( "The ISBN must be a 10-digit string or "+ " a 9-digit string followed by 'X'!"); } else { return new NoConstraintViolation(); } };
Notice that, since isbn
is the standard identifier
attribute of Book
, we only check the syntactic constraints
in checkIsbn
, but we check the mandatory value and uniqueness constraints in
checkIsbnAsId
, which itself first invokes
checkIsbn
:
Book.checkIsbnAsId = function (id) { var constraintViolation = Book.checkIsbn( id); if ((constraintViolation instanceof NoConstraintViolation)) { if (!id) { constraintViolation = new MandatoryValueConstraintViolation( "A value for the ISBN must be provided!"); } else if (Book.instances[id]) { constraintViolation = new UniquenessConstraintViolation( "There is already a book record with this ISBN!"); } else { constraintViolation = new NoConstraintViolation(); } } return constraintViolation; };
We assume that all check functions and setters can deal both with proper data values (that are of the attribute's range type) and also with string values that are supposed to represent proper data values, but have not yet been converted to the attribute's range type. We take this approach for avoiding datatype conversions in the user interface ("view") code. Notice that all data entered by a user in an HTML form field is of type String and must be converted (or de-serialized) before its validity can be checked and it can be assigned to the corresponding property. It is preferable to perform these type conversions in the model code, and not in the user interface code..
For instance, in our example app, we have the integer-valued
attribute year
. When the user has entered a value for this
attribute in a corresponding form field, in the Create or Update user interface, the form field holds a
string value. This value is passed to the Book.add
or
Book.update
method, which invokes the setYear
and checkYear
methods. Only after being validated, this
string value is converted to an integer and assigned to the
year
attribute.
Code the setter operations as (instance-level) methods. In the
setter, the corresponding check function is invoked and the property is
only set, if the check does not detect any constraint violation.
Otherwise, the constraint violation
error object returned by the check function is thrown. For instance, the
setIsbn
operation is coded in the following way:
Book.prototype.setIsbn = function (id) {
const validationResult = Book.checkIsbnAsId( id);
if (validationResult instanceof NoConstraintViolation) {
this.isbn = id;
} else {
throw validationResult;
}
};
There are similar setters for the other properties
(title
, year
and edition
).
It is helpful to have an object serialization function tailored to
the structure of an object (as defined by its class) such that the
result of serializing an object is a human-readable string
representation of the object showing all relevant information items of
it. By convention, these functions are called toString()
.
In the case of the Book
class, we use the following code:
Book.prototype.toString = function () {
var bookStr = `Book{ ISBN: ${this.isbn}, title: ${this.title}, year: ${this.year}`;
if (this.edition) bookStr += `, edition: ${this.edition}`;
return bookStr;
};
In addition to defining the model class in the form of a
constructor function with property definitions, checks and setters, as
well as a toString()
serialization function, we also need
to define the following data management operations as class-level
methods of the model class:
Book.convertRec2Obj
and
Book.retrieveAll
for loading all managed Book
instances from the persistent data store.
Book.saveAll
for saving all managed Book
instances to the persistent data store.
Book.add
for creating a new Book instance and
adding it to the collection of all Book instances.
Book.update
for updating an existing Book
instance.
Book.destroy
for deleting a Book
instance.
Book.createTestData
for creating a few sample
book records to be used as test data.
Book.clearData
for clearing the book data
store.
All of these methods essentially have the same code as in our minimal app discussed in Part 1, except that now
we may have to catch constraint violations in suitable
try-catch blocks in the Book
procedures convertRec2Obj
, add
,
update
and createTestData
;
we create more informative status and error log messages for better observing what's going on; and
we can use the toString()
function for
serializing an object in status and error messages.
Notice that for the change operations add
(create)
and update
, we need to implement an all-or-nothing policy:
whenever there is a constraint violation for a property, no new object
must be created and no (partial) update of the affected object must be
performed.
When a constraint violation is detected in one of the setters
called when new Book(...)
is invoked in
Book.add
, the object creation attempt fails, and instead a
constraint violation error message is created. Otherwise, the new book
object is added to Book.instances
and a status message is
created, as shown in the following program listing:
Book.add = function (slots) {
var book = null;
try {
book = new Book( slots);
} catch (e) {
console.log( `${e.constructor.name}: ${e.message}`);
book = null;
}
if (book) {
Book.instances[book.isbn] = book;
console.log( `${book.toString()} created!`);
}
};
When an object of a model class is to be updated, we first create a clone of it for being able to restore it if the update attempt fails. In the object update attempt, we only assign those properties of the object the value of which has changed, and we report this in a status log.
Normally, all properties defined by a model class, except the standard identifier attribute, can be updated. It is, however, possible to also allow updating the standard identifier attribute. This requires special care for making sure that all references to the given object via its old standard identifier are updated as well.
When a constraint violation is detected in one of the setters
invoked in Book.update
, the object update attempt fails,
and instead the error message of the constraint violation object thrown
by the setter and caught in the update
method is shown, and
the previous state of the object is restored. Otherwise, a status
message is created, as shown in the following program listing:
Book.update = function (slots) {
var noConstraintViolated = true,
updatedProperties = [];
const book = Book.instances[slots.isbn],
objectBeforeUpdate = cloneObject( book);
try {
if (book.title !== slots.title) {
book.setTitle( slots.title);
updatedProperties.push("title");
}
if (book.year !== parseInt( slots.year)) {
book.setYear( slots.year);
updatedProperties.push("year");
}
if (slots.edition && slots.edition !== book.edition) {
// slots.edition has a non-empty value that is new
book.setEdition( slots.edition);
updatedProperties.push("edition");
} else if (!slots.edition && book.edition !== undefined) {
// slots.edition has an empty value that is new
delete book.edition; // unset the property "edition"
updatedProperties.push("edition");
}
} catch (e) {...}
...
};
Notice that optional properties, like edition
, need
to be treated in a special way. If the user doesn't enter any value for
them in a Create or Update user interface, the form field's value
is the empty string ""
. In the case of an optional
property, this means that the property is not assigned a value in the
add use case, or that it is unset if it has had a value in the update use case. This is different from the
case of a mandatory property, where the empty string value obtained from
an empty form field may or may not be an admissible value.
If there is a constraint violation exception, an error message is written to the log and the object concerned is reset to its previous state:
Book.update = function (slots) { ... try { ... } catch (e) { console.log( e.constructor.name +": "+ e.message); noConstraintViolated = false; // restore object to its state before updating Book.instances[slots.isbn] = objectBeforeUpdate; } if (noConstraintViolated) { if (updatedProperties.length > 0) { console.log(`Properties ${updatedProperties.toString()}` + `modified for book ${slots.isbn}`); } else { console.log(`No property value changed for book ${slots.isbn}!`); } } };