Skip to content

Immutable

Nicholas Waynik edited this page Apr 2, 2022 · 2 revisions

Immutable allows us to generate constructors that validate schema's, using Blueprint, and to render immutable objects.

im•mu•ta•ble (ĭ-myo͞oˈtə-bəl), adj. Not subject or susceptible to change.

When out objects are subject to change, side effects can cause problems. Making our objects immutable, and achieving the Open/Closed Principle in JavaScript can be verbose. polyn's Immutable makes this trivial.

NOTE: At present, the contents of arrays are not Immutable. Consider using Immutable.js for arrays, and maps.

Usage

Node

Immutable is part of the polyn package. To install it:

npm install --save polyn

Then you can require it like so:

var Immutable = require('polyn').Immutable;

Browser

Immutable is part of the polyn package. To install it, download the release folder, or:

bower install --save polyn

Then add a script tag:

<script src="polyn.min.js"></script>

Then it will be available on the window:

var Immutable = window.polyn.Immutable;

Validation

Immutable builds on Blueprint. All Blueprint validation features are avaiable to Immutables.

var Foo = new Immutable({
    num: 'number',
    str: 'string',
    arr: 'array',
    currency: 'money',
    bool: 'bool',
    date: 'datetime',
    regex: 'regexp',
    obj: 'object',
    func: {
        type: 'function',
        args: ['arg1', 'arg2']
    },
    dec: {
        type: 'decimal',
        places: 2
    },
    nullable: {
        type: 'string',
        required: false
    },
    custom: {
        validate: function (val, errors, self) {
            if (val !== 42) {
                errors.push('custom must be 42');
            }            
        }
    }
});

Given the constructor above, the following would pass validation and return an immutable object:

var foo = new Foo({
    num: 42,
    str: 'string',
    arr: [],
    currency: '42.42',
    bool: true,
    date: new Date(),
    regex: /[A-B]/,
    obj: {
        foo: 'bar'
    },
    func: function (arg1, arg2) {},
    dec: 42.42,
    custom: 42
});

However, the next example would return an exception object:

var foo = new Foo({});

Lazy validation

If for some reason, you don't want the Immutable to be validated upon construction, you can defer or omit validation, by setting the __skipValdation property:

var Person = new Immutable({
    name: 'string',
    __skipValdation: true
});

// Now this will not generate an error
var person = new Person({});

If you wish to validate the Immutable later, you can use the validate feature:

var Person = new Immutable({
    name: 'string',
    __skipValdation: true
});

var person = new Person({});

// You can validate asynchronously
Person.validate(person, function (errors, result) {
    console.log('errors:', errors); // prints errors: ['...']
    console.log('result:', result); // prints result: false
});

// Or synchronously
var result = Person.validate(person);
console.log('errors:', result.errors); // prints errors: ['...']
console.log('result:', result.result); // prints result: false

Single Property Validation

You can also check validation on single properties, using validateProperty, for when it doesn't make sense to validate an entire object.

var Person = new Immutable({
    name: 'string',
    __skipValdation: true
});

var person = new Person({});

// You can validate asynchronously
Person.validateProperty(person, 'name', function (errors, result) {
    console.log('errors:', errors); // prints errors: ['...']
    console.log('result:', result); // prints result: false
});

// Or synchronously
var result = Person.validateProperty(person, 'name');
console.log('errors:', result.errors); // prints errors: ['...']
console.log('result:', result.result); // prints result: false

Exceptions

When an error occurs, or validation fails, Immutable returns an exception object. It's easy to check for the occurrence for an error, by using the exceptions properties:

{
    type: 'InvalidArgumentException', // types vary by exception
    error: Error('hello world!'),
    messages: ['validation error 1', 'validation error 2'],
    isException: true
}

Here's an example that checks for the existence of an error:

var Person = new Immutable({
    name: 'string'
});

var person = new Person({ name: 'Andy' });

if (person.isException) {
    // do something with the exception
}

Handling Exceptions

Immutable has a hook for exceptions. By default, exceptions are written to the console. You can override this behavior with whatever you want, using configure.

Immutable.configure({
    onError(function (err) {
        throw err.error;
    });
});

var Person = new Immutable({
    name: 'string'
});

// this would throw
var person = new Person({});

Functions and Behaviors

If our models have behaviors (functions), we might not want to expose the Immutable itself, in favor of wrapping it. Here's an example:

var Person = (function () {
    var Ctor, Person;

    Ctor = new Immutable({
        firstName: 'string',
        lastName: 'string',
        sayHello: 'function'
    });

    Person = function (person) {
        person = person || {};
        person.sayHello = function () {
            var greeting = 'Hello, {{first}} {{last}}';
            console.log(greeting.replace(/{{first}}/, this.firstName).replace(/{{last}}/, this.lastName));
        };

        return new Ctor(person);
    };

    return Person;    
}());

var person = new Person({
    firstName: 'Zaphod',
    lastName: 'Beeblebrox'
});

person.sayHello();

Dealing with Mutation

Just because we have an Immutable object doesn't mean we can't change the values of properties. We just can't change them on a given reference. When we need to modify an Immutable object, we can merge it with another object, to create a new Immutable:

var Person = new Immutable({
    firstName: 'string',
    lastName: 'string',
    hasTwoHeads: 'boolean'
});

// creates an instance of Person, with the
// firstName Zaphod, lastName Beeblebrox, and hasTwoHeads equal to false
var zaphod1 = new Person({ 
    firstName: 'Zaphod',
    lastName: 'Beeblebrox',
    hasTwoHeads: false 
});

// creates another instance of Person, with the
// firstName Zaphod, lastName Beeblebrox, and hasTwoHeads equal to true
var zaphod2 = Person.merge(zaphod1, { hasTwoHeads: true });

// Or asynchronously
Person.merge(zaphod1, { hasTwoHeads: true }, function (err, zaphod2) {
    if (!err) {
        console.log(zaphod2);
    }
});

This way, any code that references zaphod1 will experience no side-effects when we change values. We end up with two separate objects.

Getting Plain Old Objects

Sometimes we need a plain old Object. Perhaps we need to update several properties of an Immutable. To do this, we can use the toObject feature:

var Person = new Immutable({
    firstName: 'string',
    lastName: 'string',
    hasTwoHeads: 'boolean'
});

// creates an instance of Person, with the
// firstName Zaphod, lastName Beeblebrox, and hasTwoHeads equal to false
var zaphod1 = new Person({ 
    firstName: 'Zaphod',
    lastName: 'Beeblebrox',
    hasTwoHeads: false 
});

// creates a plain old JavaScript Object with the
// Immutable's properties and values
var updated = Person.toObject(zaphod1);
updated.hasTwoHeads = true;
var zaphod2 = new Person(updated);

// OR asynchronously
Person.toObject(zaphod1, function (err, updated) {
    if (!err) {
        updated.hasTwoHeads = true;
        var zaphod2 = new Person(updated);        
    }
});

Debugging, and Logging to the Console

When logging Immutables to the console, the property values are displayed as [Getter/Setter]. This is not terribly helpful when you're debugging. When you need to see the actual values that are set, use the log feature:

var Person = new Immutable({
    name: 'string'
});

var person = new Person({ name: 'Andy' });

// Prints { name: 'Andy' } to the console
Person.log(person);

It can be difficult to tell where our error messages are coming from, if the property names aren't unique within our app. We can use the __blueprintId to help with that. By setting it to something we understand, all validation error messages will indicate what Blueprint/Immutable they were generated by.

var Person = new Immutable({
    __blueprintId: 'Person',
    name: 'string'
});

Nested Immutables

Like Blueprints, Immutables can be nested. Child objects are converted to Immutables automatically, although you can define them yourself, if you wish.

var Person = new Immutable({
    name: 'string',
    address: {
        street1: 'string',
        street2: { type: 'string', required: false },
        city: 'string',
        stateOrProvince: 'string',
        postalCode: 'string',
        country: 'string'
    }
});
var Person = new Immutable({
    name: 'string',
    address: new Immutable({
        street1: 'string',
        street2: { type: 'string', required: false },
        city: 'string',
        stateOrProvince: 'string',
        postalCode: 'string',
        country: 'string'
    })
});

Nested Immutables can be accessed by the PascalCased version of their property name:

var Person = new Immutable({
    name: 'string',
    address: {
        street1: 'string'
    }
});

var person = new Person({
    name: 'Trillian',
    address: {
        street1: 'Earth'
    }
});

Person.Address.log(person.address);

Schemas

With Immutables, and Blueprints, the schema is the object that we pass to the Immutable or Blueprint constructor. It's the object that tells us that a given property is a string, and another is a number.

Immutables expose their schema through the getSchema function. The schema that is returned is a copy, not a reference, so it is safe to manipulate it.

They also expose their Blueprint through the blueprint property. Note that the blueprint, and it's properties are themselves immutable.

var Person = new Immutable({
    firstName: 'string',
    lastName: 'string'
});

// prints { firstName: 'string', lastName: 'string' }
console.log(Person.getSchema());

// Or asynchronously
Person.getSchema(function (err, schema) {
    console.log(schema);
});

// prints { firstName: [Getter/Setter] ('string'), lastName: [Getter/Setter] ('string') }
console.log(Person.blueprint.props);

Using Schema's for Inheritance

The schema's can be used to achieve sub-type polymorphism. In the following example, we create a Person constructor. Then, by extending the Person schema, we create an Author constructor, which essentially inherits the schema of Person.

var Person = new Immutable({
    firstName: 'string',
    lastName: 'string'
});

var authorSchema = Person.getSchema();
authorSchema.books = 'array';

var Author = new Immutable(authorSchema);

If our constructor should export behaviors (functions), we might want to approach this a different way. There are many ways to approach this. This example wraps the Immutable constructors, so they can

var people = (function () {
    var PersonCtor, authorSchema, AuthorCtor, BasePerson, Person, Author;

    PersonCtor = new Immutable({
        firstName: 'string',
        lastName: 'string',
        type: 'string',
        sayHello: 'function'
    });

    authorSchema = PersonCtor.getSchema();
    authorSchema.books = 'array';

    var AuthorCtor = new Immutable(authorSchema);

    BasePerson = function (person, type, Ctor) {
        person = person || {};
        person.type = type;
        person.sayHello = function () {
            var greeting = 'Hello, {{first}} {{last}}';
            console.log(greeting.replace(/{{first}}/, this.firstName).replace(/{{last}}/, this.lastName));
        };

        return new Ctor(person);
    };

    Person = function (person) {
        return BasePerson(person, 'PERSON', PersonCtor);
    }

    Author = function (author) {
        return BasePerson(author, 'AUTHOR', AuthorCtor);
    }

    return {
        Person: Person,
        Author: Author
    };    
}());

var person, author;

person = new people.Person({
    firstName: 'Zaphod',
    lastName: 'Beeblebrox'
});

author = new people.Author({
    firstName: 'Zaphod',
    lastName: 'Beeblebrox',
    books: ['The Hitchhiker\'s Guide to the Galaxy']
});

person.sayHello();
author.sayHello();

Performance

This package has not yet undergone performance analysis. It's usually fine for small data structures. If your app needs large arrays or maps of immutable data, consider using Immutable.js for that.