Implementing
Information Management Concepts and TechniquesWarning: This tutorial manuscript may still contain errors and may still be incomplete in certain respects. Please report any issue to Gerd Wagner at [email protected]
This tutorial is also available in the following formats: PDF. You may run the example app from our server, or download it as a ZIP archive file. See also our Web Engineering project page.
Copyright © 2014-2020 Gerd Wagner
This tutorial article, along with any associated source code, is licensed under The Code Project Open License (CPOL), implying that the associated code is provided "as-is", can be modified to create derivative works, can be redistributed, and can be used in commercial applications, but the article must not be distributed or republished without the author's consent.
2020-05-18
Table of Contents
List of Figures
Movie
and Person
.List of Tables
Publisher
Book
Author
This tutorial is Part 5 of our series of six tutorials
about model-based development of front-end web applications with plain JavaScript.
It shows how to build a web app that takes care of the object
types Author
, Publisher
and Book
as well as the
bidirectional associations between Book
and Author
and between
Book
and Publisher
.
The app supports the four standard data management operations (Create/Read/Update/Delete). It extends the example app of part 3 by adding code for handling derived inverse reference properties. The other parts of the tutorial are:
Part 1: Building a minimal app.
Part 2: Handling constraint validation.
Part 3: Dealing with enumerations.
Part 4: Managing unidirectional associations, such as the associations between books and publishers, assigning a publisher to a book, and between books and authors, assigning authors to a book.
Part 6: Handling subtype (inheritance) relationships between object types.
You may also want to take a look at our open access book Building Front-End Web Apps with Plain JavaScript, which includes all parts of the tutorial in one document, dealing with multiple object types ("books", "publishers" and "authors") and taking care of constraint validation, enumeration attributes, associations and subtypes/inheritance.
Table of Contents
In OO modeling and programming, a bidirectional association is an association that is represented as a pair of mutually inverse reference properties, which allow `navigation´ (object access) in both directions.
The model shown in Figure 1.1 below (about publishers, books and their authors) serves as our running example in all other parts of the tutorial. Notice that it contains two bidirectional associations, as indicated by the ownership dots at both association ends.
For being able to easily retrieve the committees that are chaired or co-chaired by a club
member, we add two reference properties to our Committee-ClubMember example model: the property of a club member to be the chair of
a committee (ClubMember
::chairedCommittee
) and the property of a club
member to be the co-chair of a committee
(ClubMember
::coChairedCommittee
). We assume that any club member may
chair or co-chair at most one committee (where the disjunction is non-exclusive). So, we get the
following model:
Notice that there is a close correspondence between the two reference properties
Committee
::chair
and
ClubMember
::chairedCommittee
. They are the inverse of each other: when the club member
Tom is the chair of the budget committee, expressed by the tuple ("budget committee", "Tom"), then the budget committee is the committee chaired
by the club member Tom, expressed by the inverse tuple ("Tom", "budget
committee"). For expressing this inverse correspondence in the diagram, we append
an inverse property constraint, inverse of chair
, in curly braces to the
declaration of the property ClubMember
::chairedCommittee
, and a
similar one to the property Committee
::chair
, as shown in the
following diagram:
Using the reference path notation of OOP languages, with c referencing a Committee
object, we obtain the equation:
Or, the other way around, with m referencing a
ClubMember
object, we obtain the equation:
Notice that when a property p2 is
the inverse of a property p1, this
implies that, the other way around, p1
is the inverse of p2. Therefore, when
we declare the property ClubMember
::chairedCommittee
to be the inverse
of Committee
::chair
, then, implicitly,
Committee
::chair
is the inverse of
ClubMember
::chairedCommittee
. We therefore call
Committee
::chair
and
ClubMember
::chairedCommittee
a pair of mutually inverse reference properties. Having such
a pair in a model implies redundancy because each of the two involved reference properties can be
derived from the other by inversion. This type of redundancy implies data
storage overhead and update overhead, which is the
price to pay for the bidirectional navigability that supports efficient object access in both
directions.
For maintaining the duplicate information of a mutually inverse reference property pair, it
is common to treat one of the two involved properties as the master, and the other one as the slave, and take
this distinction into consideration in the code of the change methods (such as the property
setters) of the affected model classes. We indicate the slave of an inverse reference property
pair in a model diagram by declaring the slave property to be a derived property using the UML notation of a slash
(/
) as a prefix of the property name as shown in the following diagram:
The property chairedCommittee
in ClubMember
is now derived (as indicated by its slash prefix). Its annotation {inverse of
chair}
defines a derivation rule according to which it
is derived by inverting the property Committee
::chair
.
There are two ways how to realize the derivation of a property: it may be derived on read via a read-time computation of its value, or it may be derived on update via an update-time computation performed whenever one of the variables in the derivation expression (typically, another property) changes its value. The latter case corresponds to a materialized view in a database. While a reference property that is derived on read may not guarantee efficient navigation, because the on-read computation may create unacceptable latencies, a reference property that is derived on update does provide efficient navigation.
When we designate an inverse reference property as derived by prefixing its name with a
slash (/
), we indicate that it is derived on update. For instance, the property
/chairedCommittee
in the example above is derived on update from the property
chair
.
In the case of a derived reference property, we have to deal with life cycle dependencies between the affected model classes requiring special change management mechanisms based on the functionality type of the represented association (either one-to-one, many-to-one or many-to-many).
In our example of the derived inverse reference property
ClubMember
::chairedCommittee
, which is single-valued and optional,
this means that
whenever a new committee object is created (with a mandatory chair
assignment), the corresponding ClubMember
::chairedCommittee
property has to be assigned accordingly;
whenever the chair
property is updated (that is, a new chair is assigned to
a committee), the corresponding ClubMember
::chairedCommittee
property has to be unset for the club member who was the previous chair and set for the one
being the new chair;
whenever a committee object is destroyed, the corresponding
ClubMember
::chairedCommittee
property has to be unset.
In the case of a derived inverse reference property that is multi-valued while its inverse
base property is single-valued (like Publisher
::publishedBooks
in
Figure 1.4 below being derived from
Book
::publisher
), the life cycle dependencies imply that
whenever a new 'base object' (such as a book) is created, the corresponding inverse
property has to be updated by adding a reference to the new base object to its value set
(like adding a reference to the new book object to
Publisher
::publishedBooks
);
whenever the base property is updated (e.g., a new publisher is assigned to a book),
the corresponding inverse property (in our example,
Publisher
::publishedBooks
) has to be updated as well by
removing the old object reference from its value set and adding the new one;
whenever a base object (such as a book) is destroyed, the corresponding inverse
property has to be updated by removing the reference to the base object from its value
set (like removing a reference to the book object to be destroyed from
Publisher
::publishedBooks
).
Notice that from a purely computational point of view, we are free to choose either of the
two mutually inverse reference properties (like Book
::authors
and
Author
::authoredBooks
) to be the master. However, in many cases,
associations represent asymmetrical ontological existence dependencies that dictate which of the
two mutually inverse reference properties is the master. For instance, the authorship association
between the classes Book
and Author
represents an ontological existence
dependency of books on their authors. A book existentially depends on its author(s), while an
author does not existentially depend on any of her books. Consequently, the corresponding object lifecycle dependency between Book
and Author
implies that their bidirectional association is maintained by maintaining Author
references in Book
::authors
as the natural choice of master property, while Author
::authoredBooks
is the slave property, which is derived from Book
::authors
.
Since classical OO programming languages do not support explicit associations as first class citizens, but only classes with reference properties representing implicit associations, we have to eliminate all explicit associations for obtaining an OO class model.
The starting point of our association elimination procedure is an information design model with various kinds of unidirectional and bidirectional associations, such as the model shown in Figure 1.1 above. If the model still contains any non-directed associations, we first have to turn them into directed ones by making a decision on the ownership of their ends, which is typically based on navigability requirements.
Notice that both associations in the Publisher-Book-Author information design model, publisher-publishedBooks and authoredBooks-authors (or Authorship), are bidirectional as indicated by the ownership dots at both association ends. For eliminating all explicit associations from an information design model, we have to perform the following steps:
Eliminate unidirectional associations, connecting a source with a target class, by replacing them with a reference property in the source class such that the target class is its range.
Eliminate bidirectional associations by replacing them with a pair of mutually inverse reference properties.
A unidirectional association connecting a source with a target class is replaced with a corresponding reference property in its source class having the target class as its range. Its multiplicity is the same as the multiplicity of the target association end. Its name is the name of the association end, if there is any, otherwise it is set to the name of the target class (possibly pluralized, if the reference property is multi-valued).
A bidirectional association, such as the authorship association between the classes
Book
and Author
in the model shown in Figure 1.1 above, is replaced with a
pair of mutually inverse reference properties, such as Book
::authors
and Author
::authoredBooks
. Since both reference properties represent
the same information (the same set of binary relationships), it's an option to consider one of
them being the "master" and the other one the "slave", which is derived from the master. We
discuss the two cases of a one-to-one and a many-to-many association
In the case of a bidirectional one-to-one association, this leads to a pair of mutually inverse single-valued reference properties, one in each of the two associated classes. Since both of them represent essentially the same information (the same collection of links/relationships), one has to choose which of them is considered the master property, such that the other one is the slave property, which is derived from the master property by inversion. In the class diagram, the slave property is designated as a derived property that is automatically updated whenever 1) a new master object is created, 2) the master reference property is updated, or 3) a master object is destroyed.
Figure 1.2. Turn a bidirectional one-to-one association into a master-slave pair of mutually inverse single-valued reference properties
|
|
|
A bidirectional many-to-many association is mapped to a pair of mutually inverse multi-valued reference properties, one in each of the two classes participating in the association. Again, in one of the two classes, the multi-valued reference property representing the (inverse) association is designated as a derived property that is automatically updated whenever the corresponding property in the other class (where the association is maintained) is updated.
Figure 1.3. Turn a bidirectional many-to-many association into a master-slave pair of mutually inverse multi-valued reference properties
|
|
|
After replacing both bidirectional associations with reference properties, we obtain the following OO class model:
Since books are entities that existentially depend on authors and possibly on publishers,
and not the other way around, it's natural to maintain the master references in book objects,
and consider the inverse references in publisher and author objects as derived (or slave) data.
Therefore, we define publishedBooks
and authoredBooks
as derived
inverse reference properties, which is indicated by their slash prefix in the OO class
model.
Table of Contents
In this chapter of our tutorial, we show
how to derive a JS class model from an OO class model with derived inverse reference properties,
how to code the JS class model in the form of JavaScript model classes,
how to write the view and controller code based on the model code.
The starting point for making our JS class model is an OO class model with derived inverse reference properties like the following one:
Notice that the model contains two derived inverse reference properties:
Publisher
::/publishedBooks
and
Author
::/authoredBooks
. Each of them is linked to a master property,
from which it is derived. Consequently, each of them represents a pair of mutually inverse
reference properties corresponding to a bidirectional association.
The meaning of the OO class model can be illustrated by a sample data population for the three model classes involved:
Table 2.1. Sample data for Publisher
Name | Address | Published books |
---|---|---|
Bantam Books | New York, USA | 0553345842 |
Basic Books | New York, USA | 0465030793 |
Table 2.2. Sample data for Book
ISBN | Title | Year | Authors | Publisher |
---|---|---|---|---|
0553345842 | The Mind's I | 1982 | 1, 2 | Bantam Books |
1463794762 | The Critique of Pure Reason | 2011 | 3 | |
1928565379 | The Critique of Practical Reason | 2009 | 3 | |
0465030793 | I Am A Strange Loop | 2000 | 2 | Basic Books |
Table 2.3. Sample data for Author
Author ID | Name | Authored books |
---|---|---|
1 | Daniel Dennett | 0553345842 |
2 | Douglas Hofstadter | 0553345842, 0465030793 |
3 | Immanuel Kant | 1463794762, 1928565379 |
Notice how Book
records reference Publisher
and Author
records, and, vice versa, Publisher
and Author
records reference Book
records.
Compared to making JS class models with unidirectional associations, the only new issue is:
Add a «get» stereotype to all derived inverse reference properties, implying that they have an implicit getter, but no setter. They are implicitly set whenever their inverse master reference property is updated.
This concerns the two derived inverse reference properties
Publisher
::/publishedBooks
and
Author
::/authoredBooks
. Thus, we get the following JavaScript class
model:
The JS class model can be directly coded for getting the code of the model layer of our JavaScript frontend app.
Compared to the unidirectional association app discussed in a previous tutorial, we have to deal with a number of new technical issues:
We define the derived inverse reference properties, like
Publisher
::/publishedBooks
, without a check operation and without a set
operation.
We also have to take care of maintaining the derived inverse reference properties by maintaining the derived (sets of) inverse references that form the collection value of a derived inverse reference property. This requires in particular that
whenever the value of a single-valued master reference property is initialized or updated with the help of a
setter (such as assigning a reference to a Publisher
instance p
to b.publisher
for a Book
instance b
), an inverse
reference has to be assigned or added to the corresponding value of the derived inverse
reference property (such as adding b
to p.publishedBooks
); when
the value of the master reference property is updated
and the derived inverse reference property is multi-valued, then the obsolete inverse reference to the previous value of
the single-valued master reference property has to be deleted;
whenever the value of an optional single-valued master reference property is unset (e.g. by assigning
null
to b.publisher
for a Book
instance
b
), the inverse reference has to be removed from the corresponding value of
the derived inverse reference property (such as removing b
from
p.publishedBooks
), if the derived inverse reference property is
multi-valued, otherwise the corresponding value of the derived inverse reference property
has to be unset or updated;
whenever a reference is added to the value of a multi-valued master reference property with the help
of an add method (such as adding an Author
reference a
to
b.authors
for a Book
instance b
), an inverse
reference has to be assigned or added to the corresponding value of the derived inverse
reference property (such as adding b
to a.authoredBooks
);
whenever a reference is removed from the value of a multi-valued master reference property with the help
of a remove
method (such as removing a reference to an Author
instance a
from b.authors
for a Book
instance
b
), the inverse reference has to be removed from the corresponding value of
the derived inverse reference property (such as removing b
from
a.authoredBooks
), if the derived inverse reference property is multi-valued,
otherwise the corresponding value of the derived inverse reference property has to be
unset or updated;
whenever an object with a single reference or with multiple references as the value
of a master reference property is destroyed (e.g., when a Book
instance b
with a single reference b.publisher
to a Publisher
instance
p
is destroyed), the derived inverse references have to be removed first
(e.g., by removing b
from p.publishedBooks
).
Notice that when a new object is created with a single reference or with multiple
references as the value of a master reference property (e.g., a new Book
instance b
with a single reference b.publisher
), its setter
or add method will be invoked and will take care of creating the derived inverse references.
Code each class of the JS class model as an ES2015 class with implicit getters and setters (or, alternatively, as an ES5 constructor function with explicit setters):
Code the property checks in the form of class-level ('static') methods. Take care that all constraints of a property as specified in the JS class model are properly coded in the property checks.
For each single-valued property, code the specified getter and setter:
In each setter, the corresponding property check is invoked and the property is only set/unset, if the check does not detect any constraint violation.
If the concerned property is the inverse of a derived reference property (representing a bidirectional association), make sure that the setter also assigns/unsets (or adds/removes) the corresponding inverse reference to/from (the collection value of) the inverse property.
For each multi-valued property, code its add and remove operations, as well as the specified get/set operations:
Code the add/remove operations as (instance-level) methods that invoke the corresponding property checks.
Code the setter such that it invokes the add operation for each item of the collection to be assigned.
If the concerned property is the inverse of a derived reference property (representing a bidirectional association), make sure that the add/remove methods also assign/unset (or add/remove) the corresponding inverse reference to/from (the collection value of) the inverse property.
Write the code of the serialization function toString()
and the
object-to-storage conversion function toRecord()
. In the object-to-storage
conversion of a publisher or author object with toRecord()
, the derived
properties publishedBooks
and authoredBooks
are not included since
their information is redundant (it is derived from the publisher
and
authors
properties of books).
Take care of deletion dependencies in the destroy
method. Make sure that when an object with a single reference or with multiple references
as the value of a master reference property is destroyed, the derived inverse references are
unset (or removed) first.
These steps are discussed in more detail in the following sections.
For instance, the Publisher
class from the JS class model is coded
in the following way:
class Publisher {
constructor ({name, address}) {
this.name = name;
this.address = address;
// derived inverse reference property (inverse of Book::publisher)
this._publishedBooks = {}; // initialize as an empty map of Book objects
}
get name() {...}
static checkName( n) {...}
static checkNameAsId( n) {...}
static checkNameAsIdRef( n) {...}
set name( n) {...}
get address() {...}
static checkAddress( a) {...}
set address( a) {...}
toString() {...}
toRecord() {...}
}
Notice that we have added the (derived) multi-valued reference property
publishedBooks
, but we do not assign it in the constructor function because it is
a read-only property that is assigned implicitly when its inverse master reference property
Book::publisher
is assigned.
Any setter for a reference property that is coupled to a derived inverse reference property
(implementing a bidirectional association), now also needs to assign/add/remove inverse
references to/from the corresponding collection value of the inverse reference property. An
example of such a setter is publisher
in the Book
class:
set publisher( p) { if (!p) { // the publisher reference is to be deleted // delete the corresponding inverse reference from Publisher::publishedBooks delete this._publisher.publishedBooks[ this._isbn]; // unset the publisher property delete this._publisher; } else { // p can be an ID reference or an object reference const publisher_id = (typeof p !== "object") ? p : p.name; const constraintViolation = Book.checkPublisher( publisher_id); if (constraintViolation instanceof NoConstraintViolation) { if (this._publisher) { // delete the obsolete inverse reference in Publisher::publishedBooks delete this._publisher.publishedBooks[ this._isbn]; } // create the new publisher reference this._publisher = Publisher.instances[ publisher_id]; // add the new inverse reference to Publisher::publishedBooks this._publisher.publishedBooks[ this._isbn] = this; } else { throw constraintViolation; } } }
For any multi-valued reference property that is coupled to a derived inverse reference property, both the add and the remove method also have to assign/add/remove corresponding references to/from (the value set of) the inverse property.
For instance, for the multi-valued reference property Book::authors
that is
coupled to the derived inverse reference property Author:authoredBooks
for
implementing the bidirectional authorship association between Book
and
Author
, the Book::addAuthor
method is coded in the following
way:
addAuthor( a) { // a can be an ID reference or an object reference const author_id = (typeof a !== "object") ? parseInt( a) : a.authorId; const validationResult = Book.checkAuthor( author_id); if (author_id && validationResult instanceof NoConstraintViolation) { // add the new author reference this._authors[author_id] = Author.instances[author_id]; // automatically add the derived inverse reference this._authors[author_id].authoredBooks[this._isbn] = this; } else { throw validationResult; } }
For the remove operation removeAuthor
we obtain the following code:
removeAuthor( a) { // a can be an ID reference or an object reference const author_id = (typeof a !== "object") ? parseInt( a) : a.authorId; const validationResult = Book.checkAuthor( author_id); if (validationResult instanceof NoConstraintViolation) { // automatically delete the derived inverse reference delete this._authors[author_id].authoredBooks[this._isbn]; // delete the author reference delete this._authors[author_id]; } else { throw validationResult; } }
In the object-to-storage conversion of a publisher or author object with
toRecord()
, the derived properties publishedBooks
and
authoredBooks
are not included since their information is redundant. It is derived
from the publisher
and authors
properties of books.
toRecord() { var rec = {}; // loop over all Author properties for (let p of Object.keys( this)) { // keep underscore-prefixed properties except "_authoredBooks" if (p.charAt(0) === "_" && p !== "_authoredBooks") { // remove underscore prefix rec[p.substr(1)] = this[p]; } }; return rec; }
When a Book
instance b
, with a single reference
b.publisher
to a Publisher
instance p
and multiple
references b.authors
to Author
instances, is destroyed, the derived
inverse references have to be removed first (e.g., by removing b
from
p.publishedBooks)
:
Book.destroy = function (isbn) { const book = Book.instances[isbn]; if (book) { console.log( book.toString() + " deleted!"); if (book.publisher) { // remove inverse reference from book.publisher delete book.publisher.publishedBooks[isbn]; } // remove inverse references from all book.authors for (let authorID of Object.keys( book.authors)) { delete book.authors[authorID].authoredBooks[isbn]; } // finally, delete book from Book.instances delete Book.instances[isbn]; } else { console.log("There is no book with ISBN " + isbn + " in the database!"); } };
In the UI code we can now exploit the inverse reference properties for more efficiently creating a list of inversely associated objects in the Retrieve/List All use case. For instance, we can more efficiently create a list of all published books for each publisher. However, we do not allow updating the set of inversely associated objects in the update object use case (e.g. updating the set of published books in the update publisher use case). Rather, such an update has to be done via updating the master objects (in our example, the books) concerned.
For showing information about published books in the Retrieve/List All
publishers use case, we can now exploit the derived inverse reference property
publishedBooks
:
pl.v.publishers.retrieveAndListAll = {
setupUserInterface: function () {
const tableBodyEl = document.querySelector("section#Publisher-R>table>tbody");
tableBodyEl.innerHTML = "";
for (let key of Object.keys( Publisher.instances)) {
const publisher = Publisher.instances[key];
const row = tableBodyEl.insertRow(-1);
// create list of books published by this publisher
const listEl = util.createListFromMap( publisher.publishedBooks, "title");
row.insertCell(-1).textContent = publisher.name;
row.insertCell(-1).textContent = publisher.address;
row.insertCell(-1).appendChild( listEl);
}
document.getElementById("Publisher-M").style.display = "none";
document.getElementById("Publisher-R").style.display = "block";
}
};
This project is based on the information design model below. The app from the previous
assignment is to be extended by adding derived
inverse reference properties for implementing the bidirectional
associations. This is achieved by adding the multi-valued reference properties
directedMovies
and playedMovies
to the model class
Person
, both with range Movie
.
This project includes the following tasks:
Make an OO design model derived from the given information design model.
Make a JavaScript class model derived from the OO class model.
Code your JS class model, following the guidelines of the tutorial.
You can use the following sample data for testing your app:
Table 2.4. Movies
Movie ID | Title | Release date | Director | Actors |
---|---|---|---|---|
1 | Pulp Fiction | 1994-05-12 | 1 | 5, 6 |
2 | Star Wars | 1977-05-25 | 2 | 7, 8 |
3 | Inglourious Basterds | 2009-05-20 | 1 | 9, 1 |
4 | The Godfather | 1972-03-15 | 4 | 11, 12 |
Table 2.5. People
Person ID | Name | Directed movies | Played movies |
---|---|---|---|
1 | Quentin Tarantino | 1, 3 | 3 |
2 | George Lucas | 2 | |
4 | Francis Ford Coppola | 4 | |
5 | Uma Thurman | 1 | |
6 | John Travolta | 1 | |
7 | Ewan McGregor | 2 | |
8 | Natalie Portman | 2 | |
9 | Brad Pitt | 3 | |
11 | Marlon Brando | 4 | |
12 | Al Pacino | 4 |
Make sure that your pages comply with the XML syntax of HTML5, and that your JavaScript code complies with our Coding Guidelines and is checked with JSHint.
If you have any questions about how to carry out this project, you can ask them on our discussion forum.