Friday, December 4, 2015

Simple Object Construction Pattern

Since my team is using TypeScript we have an uncomfortable focus on object constructors. We have many model objects and there are different patterns of building and using constructors. I will present the way I think constructors should be used, but first a little object C#. Here is a link to C# object constructor info. If you look at it C# has special constructs to allow very flexible object construction like so:

Foo bar = new Foo { Prop1 = "a", Prop2 = 9 }

This is seen as very flexible and desirable so they built special stuff into the compiler for this. Now let's try to write that in JavaScript.

var bar: Foo = new Foo({ Prop1 : "a", Prop2 : 9 })

Looks very similar but behaves a little differently. Now let's go over three ways to make a JavaScript constructor.

function Constructor1() {
    ...
}

var newObj = new Constructor1();
newObj.a = a;
newObj.b = b;
newObj.c = c;
newObj.d = d;
newObj.e = e;

------------------------------------------

function Constructor2(a,b,c,d,e) {
    this.a = a;
    this.b = b;
    this.c = c;
    this.d = d;
    this.e = e;
    ...
}

var newObj = new Constructor2(a,b,c,d,e);

--------------------------------------------

function Constructor3(template) {
    randomLibrary.extend(this,template);
    ....
}

var newObj = new Constructor({a:a, b:b, c:c, d:d, e:e});

Okay, so in the first case we have an empty constructor and after you call it you explicitly set all the properties you want. This has the advantage that you have a lot of control as to what properties get set, but this is also a disadvantage because you have a line for each property and this makes code verbose instead of making the constructor verbose and for frequently created objects this is bad. It also spreads out object construction instead of having a chunk of code which is clearly just making the object.

The second case is a more typical pattern. The issue is you have to explicitly set each variable during construction. You might have situations where you don't necessarily want to fill in every field. You could pass in a shortened list, but order matters so this strategy isn't very flexible. Also it is obtuse. In the first and third pattern you see what property each value is being assigned to. In the second pattern (unless you are using named parameters) this information is not revealed.

The third case is nice because you can pass in as much or as little as you want. All your lines are specifically linked with object construction. It also doesn't have a lot of boilerplate in terms of listing every property. But this has some pretty significant drawbacks. One is that you are using a library, but many libraries have extend and this isn't too big a deal although it could possibly introduce a dependency on a large library that you barely use. A more significant issue is that you are copying everything so the constructor caller can really mess up your object and put you in a weird state. Of course, the flip side of this is ridiculous flexibility.

You see the third case a lot as "options". You pass in an options object. Anything you specify in this object is used while anything you don't just comes from the defaults. 

Here is another option using some custom code:

function Constructor4(template) {
    myLibrary.extendSet(this,template,[
        'a',
        'b',
        'c',
        'd']);
    ....
}

var newObj = new Constructor({a:a, b:b, c:c, d:d, e:e});

So this is option three but you use a custom extend that takes a list of properties to extend and will not extend properties outside this set. This makes option 3 safer, but you now have to list every property like in option 1 and option 2. 

Another option is to use destructuring like so:

function Constructor5({
        a: a = defaultForA,
        b: b = defaultForB,
        c: c = defaultForC
    } = {}) {
    this.a = a;
    this.b = b;
    this.c = c;
    ....
}

var newObj = new Constructor({a:a, b:b, c:c, d:d, e:e});

This destructuring version has a few nice properties. It allows the nice flexibility of passing in whatever you want to the constructor. It also will ignore values passed in that it does know about which is also nice. The main nice thing, however, is the fact you set specify defaults right in the method signature. But now you have to list every property being assigned in the constructer twice. And while you don't have a external library dependency, you do have a dependency on the destructuring language feature. This feature tends to show up in most transpilers though.

So in ranking these I would probably say: 4, 5, 3, 1, 2. I feel like there is a strong drop off between 3, 4, and 5, and 1 and 2. The reason is because I want the constructor call to look a certain way and you tend to look at the constructor call more than the constructor function.

No comments:

Post a Comment