NestedTypes 2.0: Meet an Aggregation, and the rest of OO animals

In languages like C++ there is the distinction between passing and holding data “by value” and “by reference”, which is clear and well understood among software developers. In contrast, in languages like JavaScript there is no built-in explicit notation to express “by value” semantic, as everything which is not primitive type goes “by reference”. Is it something important, or it’s the thing we shouldn’t care about?

Let’s consider the language-neutral example. We have Car class. Every Car has its primary driver. Obviously, Car also has doors, wheels, and other parts. And when we will implement such a class using some programming language, all of these things together with driver would technically be the members of the Car object.

In OOP, however, class (as an abstract data type) is not defined by its structure, but by its behavior. And this behavior becomes particularly interesting when it should deal with the class parts somehow. Our Car, for instance, has the magical ability to clone itself. car.clone(), and we have another car, which looks exactly like the first one, but has the different identity. Suitable.

But wait, when we are cloning the car, what should happen to the driver?

Aggregation, Composition, and their meaning

In OOP terminology, there’s the difference between object’s attributes and links to other objects. All references to other objects are called associations.

And there’s special type of associations with special meaning designating “whole-part” relationship between objects. It’s called aggregation.

While driver and passengers sit inside of the car, they are not a part of the car. How would we know what is and what is not the part? Looking at the class behavior when some certain operations are performed on an object.

If something is the part of our car, it should be cloned when the car is cloned. But if it’s not — it shouldn’t.

There is a stronger form of aggregation called composition. Its distinguishing characteristic is an exclusive ownership. Which means, that with the composition nested object can be aggregated by one and only one parent object. One wheel cannot be installed in two different cars at the same time, can it? And when the parent is destroyed all its members included with composition are destroyed too.

Also, thanks to composition, all wheels and doors are also created and installed when the new car is created. And thanks to its absence in the right place, driver’s seat is empty so we could sit in the car and drive.

Despite aggregation is meant to represent the “whole-part” relationship, an only commonly agreed formal semantic behind this term is that aggregation relationships cannot form cycles. As James Rumbaugh said: “In spite of the few semantics attached to aggregation, everybody thinks it is necessary (for different reasons). Think of it as a modeling placebo”.
In this article when I’m using aggregation term, I’m referring to very strong form of aggregation (which is called composition).

As you can see, the difference between composition and plain association (reference) can be very important. Aggregated members are created by default when the parent is created, being copied when the parent object is copied, they are destroyed when the parent is destroyed. This behavior is unavoidable for object members aggregated “by value”, but it can also be implemented for selected members included “by reference”. Which is good for us, as in JavaScript we don’t have any choice since all class associations are “by reference”.

NestedTypes 2.0 RC: first-class composition support

NestedTypes 2.0 data framework introduces first-class support for the composition to ES6 classes. The definition of our cartoon Car class (assuming that Wheel, Door, and Person classes are declared elsewhere) looks like this:

import { define, Model } from 'nestedtypes'
@define
class Car extends Model {
static attributes = {
firstWheel : Wheel,
secondWheel : Wheel,
door : Door,
driver : Person.shared,
passenger : Person.shared,
odometer : Number,
nextServiceDate : Date
}
  drive( address ){ ... }
}

In order to create aggregated members when new Car is created, Car must know their names and constructor functions. That’s one of the reasons why we need attributes type spec (see that static attributes declaration?). In order to give the system the chance to process the type spec, we add @define decorator to the class definition. And the Model base class will give us an implementation of car.clone() and car.dispose() methods.

In the simplest case, attribute type spec is just the constructor function, and it means composition. Such an attributes are created, cloned, and disposed together with the car.

If you add shared modifier to the constructor, attribute becomes plain association (initialized with null, shallow cloned, and not disposed). In other aspects, attributes behaves as normal class members.

const car = new Car();
const driver = new Driver();
car.driver = driver;
car.drive( "99 South Bedford Street, Burlington, MA" );

Even better than that. They behave mostly as normal C++ class members, as attribute types are dynamically converted on assignment if they are incompatible. And that’s one of another reasons type annotations are helpful.

car.odometer = "20"; // <- Number(x)
console.assert( car.odometer === 20 );
car.odometer = "hjkhjkhfdkjs";
console.assert( isNaN( car.odometer ) );
car.nextServiceDate = "2016-09-20T04:09:27.574Z"; // <- new Date(x)
console.assert( car.nextServiceDate instanceof Date );

Car with its aggregated members forms ownership tree, which is traversable. Every object can be the part of only one ownership tree at a time. So, one wheel cannot be installed in two different cars:

car2.firstWheel = car1.firstWheel; // <- Run-time error. Nonsense.
const wheel = car1.firstWheel;
car1.firstWheel = null; // <- Now wheel have no owner.
car2.firstWheel = wheel; // <- Okay. Wheel can be mounted.
car2.firstWheel = car1.firstWheel.clone(); // This will work too.

It can be copied “by value” as well, which is a bit weird but very handy. Here we are telling to the car2.firstWheel: “you see car1’s firstWheel? Be yourself, but take the same state.”

car2.set({ firstWheel : car1.firstWheel }, { merge : true });

Shared attributes are not the part of the parent’s ownership tree, so they can be assigned freely. Thus, we can drive two cars at the same time. Suitable.

car2.driver = car1.driver = driver; // Always doing like this.

Strict ownership policy and first-class composition support are one of the distinguishing characteristics of the type system in upcoming NestedTypes 2.0 data framework release.

There are a lot of situations where an explicit handling of the composition is helpful, and the real fun starts when we consider serialization. In the next article. Stay tuned.