4

If one has a choice to design things one way or the other, is it generally better (by way of performance, maintainability, readability, functionality, etc...) to do function chaining or use object property accessors for hierarchical access, and why?

Does it depend entirely on use case, or is one generally superior?

Example:

var foo = bar.get('parent').get('child').get('grandChild')...

// vs

var foo = bar['parent']['child']['grandChild']...
// or
var foo = bar.parent.child.grandChild...

Or is there a better way to do it altogether?

Note that dot notation . may not always be viable due to source minification, using property values that are not valid JavaScript identifiers, etc.

EDIT:

Since that's too generic, what about a more specific example...

Consider the following...

var animals = {
    dogs: {
        'Chloe': {
            name: 'Chloe',
            fetch: function() {},
            rollover: function() {},
            pups: {
                'Charlie': {
                    name: 'Charlie',
                    fetch: function() {},
                    rollover: function() {}
                },
                'Lily': {
                    //...
                }
            }
        },
        'Toby': {
            //...
        }
    },
    cats: {
        'Fido': {
            name: 'Fido',
            meow: function() {},
            kittens: {
                'Mia': {
                    name: 'Mia',
                    meow: function() {}
                },
                'Shadow': {
                    //...
                }
            }
        }
    }
}

Note that this is an attempt to show a heirarchy and does not show any prototypical inheritance, although it may be present (and probably would be in the example).

In the heirarchy as shown, one would use property access to get to a certain pup as follows

var myPup = animals.dogs['Chloe'].pups['Charlie'];
// or
var myPup = animals.dogs.Chloe.pups.Charlie;
// or
var myPup = animals['dogs']['Chloe']['pups']['Charlie'];

Is this ideal? Or are there significant benefits to using functions to access the pup

var myPup = animals.dogs('Chloe').pups('Charlie');
Community
  • 1
  • 1
NanoWizard
  • 2,104
  • 1
  • 21
  • 34
  • I'd say the second is cleaner, you don't have the "get" which isn't really adding any value. But I think it'd be largely a matter of preference. – Shriike Oct 01 '14 at 20:28
  • Well calling a function (probably) is more expensive than not calling a function. Using explicit getter functions like that is not idiomatic JavaScript either. – Pointy Oct 01 '14 at 20:28
  • The latter two are [already answered](http://stackoverflow.com/q/4968406/1612146). The first isn't really comparable. – George Oct 01 '14 at 20:28
  • @George My question is not asking for a comparison of the latter two, I'm asking to compare the method of property access with function calls. – NanoWizard Oct 01 '14 at 20:31
  • As stated, that isn't comparable. They're not the same thing at all. – user229044 Oct 01 '14 at 20:35
  • @meagar in general they are not comparable, but the question asks specifically for dealing with a hierarchical data set. – NanoWizard Oct 01 '14 at 20:40
  • I see your points in favor of property access, but consider this: if operations on each element in the hierarchy `return this` then you can do operations within the chain. eg `bar.get('parent').doSomething().get('child').get('grandChild')...` – NanoWizard Oct 01 '14 at 20:57
  • 5
    YES. It *does* depends entirely on use case. What are actually you trying to do? Because your first example is _not_ a compelling use case for that pattern. – Alex Wayne Oct 01 '14 at 21:37
  • 1
    @NanoWizard Your argument there is really independent of the question. There's no reason you couldn't do `animals['Chloe'].fetch().pups['Charlie'].rollover();` as well if your functions return the object itself. – James Montagne Oct 03 '14 at 15:21

1 Answers1

2

I think you are using the wrong tool for the wrong job. Deep nesting of objects like you did is a really bad choice of data modeling. Here's what your data model looks like:

+ animals
|
+--+ dogs
|  |
|  +--+ Chloe
|  |  |
|  |  +--+ Charlie
|  |  |
|  |  +--+ Lily
|  |
|  +--+ Toby
|
+--+ cats
   |
   +--+ Fido
      |
      +--+ Mia
      |
      +--+ Shadow

This kind of a data model is called the hierarchical model because the data is arranged in the form of a hierarchy.

The hierarchical data model is not a good data model because it doesn't provide access path independence. This simply means that (because of the way data is modeled) it is impossible to find a given object without knowing its access path.

For example the access path of Lily is animals.dogs.Chloe.Lily and the access path of Toby is animals.dogs.Toby. Access path independence is important because it means that you don't need to worry about where the object is located in the data model. This makes programming simpler and also ensures that every object will be accessed in the same amount of time.

In the above example it takes more time to access Lily than it takes to access Toby because it is one level deeper. To illustrate how access path dependence makes programming more difficult, consider the following code:

function findParentOf(animal) {
    var dogs = animals.dogs;

    for (var dog in dogs)
         if (dogs[dog].pups.hasOwnProperty(animal))
             return dog;

    var cats = animals.cats;

    for (var cat in cats)
         if (cats[cat].kittens.hasOwnProperty(animal))
             return cat;

    return null;
}

Now imagine you had lots of different types of animals like dogs, cats, rabbits, hamsters, etc. Can you see the amount of redundant code you would have?

In fact you data model has a lot of redundancies in it too. For example every object has a name even though its key has the same value. In addition every dog and pup has a fetch and rollover function and every cat and kitten has a meow function.

All this can be easily generalized using the correct abstraction. For example this is what I would do:

function animal(kind, parent, name) {
    return {
        kind: kind,
        parent: parent,
        name: name
    };
}

var dog = animal.bind(null, "dog", null);
var pup = animal.bind(null, "dog");

var cat = animal.bind(null, "cat", null);
var kitten = animal.bind(null, "cat");

var animals = [
    dog("Chloe"),
    pup("Chloe", "Charlie"),
    pup("Chloe", "Lily"),
    dog("Toby"),
    cat("Fido"),
    kitten("Fido", "Mia"),
    kitten("Fido", "Shadow")
];

This way of modeling data is known as the relational model. One of the best things about the relational model is that it has access path independence. Hence you can easily find objects and do other tasks. For example:

function findAnimal(name) {
    return animals.find(function (animal) {
        return animal.name === name;
    });
}

function findParentOf(name) {
    var animal = findAnimal(name);

    if (animal) {
        var parent = animal.parent;
        return parent && findAnimal(parent);
    }
}

function findChildrenOf(name) {
    return animals.filter(function (animal) {
        return animal.parent === name;
    });
}

Now that we have access path independence we can easily find objects. For example:

var lily = findAnimal("Lily");
var toby = findAnimal("Toby");
var fido = findParentOf("Shadow");
var miaAndShadow = findChildrenOf("Fido");

Finally, when it comes to the fetch, rollover and meow functions, you could define them as follows:

function fetch(animal) {
    if (animal.kind === "dog") {
        // do something
    }
}

function rollover(animal) {
    if (animal.kind === "dog") {
        // do something
    }
}

function meow(animal) {
    if (animal.kind === "cat") {
        // do something
    }
}

Or even better, put them into a namespace. Thanks to Richard Simpson for giving me the idea:

dog.fetch = function (animal) {
    if (animal.kind === "dog") {
        // do something
    }
};

dog.rollover = function (animal) {
    if (animal.kind === "dog") {
        // do something
    }
};

cat.meow = function (animal) {
    if (animal.kind === "cat") {
        // do something
    }
};

Then you could do something like:

dog.rollover(findAnimal("Charlie"));

Or with function composition:

var rolloverDog = compose(dog.rollover, findAnimal);

function compose(f, g) {
    return function (x) {
        return f(g(x));
    };
}

rolloverDog("Charlie");

Just my two cents. Hope this helps.

Community
  • 1
  • 1
Aadit M Shah
  • 72,912
  • 30
  • 168
  • 299
  • Brilliant answer. Maybe suggest having a property called `behaviours`, that could take function prototypes for the actions that could be made by the animals. – askrich Oct 03 '14 at 14:54
  • @RichardSimpson Are you suggesting putting them into a namespace so that you could have a `dog.rollover` and a `cat.rollover` function? Yes, that would be nice. – Aadit M Shah Oct 03 '14 at 15:10
  • 3
    Criticism: Bracket notation solves the "redundancy argument". Relational model isn't all fun and games. In recent years people have started debating [its problems](http://en.wikipedia.org/wiki/NoSQL) and arguing that it doesn't actually offer any more simplicity - especially since some times you really only have dogs. I also think the end result isn't simpler - You had to implement your own querying functions instead of using simple dot notation - not to mention aggregate queries. [Impedence mismatch](http://en.wikipedia.org/wiki/Object-relational_impedance_mismatch) is a real issue. – Benjamin Gruenbaum Oct 03 '14 at 15:22
  • 1
    There are certainly negatives here as well. It all depends on actual usage. If for instance there will never be a need to search within all animals for a given animal and only within its own type, you've now increased the time to find a given dog. This is the type of question that depends on the actual case and one general answer isn't really possible. – James Montagne Oct 03 '14 at 15:23
  • @BenjaminGruenbaum I always like reading your criticisms. I always learn something new. This impedance mismatch issue: it's only a problem when converting relations into objects if I am not mistaken. Does the same problem arise when you use [functional relational programming](https://github.com/papers-we-love/papers-we-love/blob/master/design/out-of-the-tar-pit.pdf)? – Aadit M Shah Oct 03 '14 at 15:34
  • @JamesMontagne You could always sort the `animals` relation according to the `kind` so that animals of the same kind are stored in contiguous locations. In a sense you are enforcing a hierarchy structure on top of the relation, but still getting the full benefits of the relational model. – Aadit M Shah Oct 03 '14 at 15:39
  • Thanks, and yes - it does. There are languages that are built around relational mapping, typically database languages (famously SQL). These languages make interacting and querying with data very easy. In object oriented languages - or more generally languages that provide nested records there is a mismatch between relational mapping and hierarchical structure - languages are all different and they change - like Erlang moving recently from a more relational model to a more hierarchical model. – Benjamin Gruenbaum Oct 03 '14 at 15:39
  • In a language like JavaScript one might argue that if OP needs complex relations - he should put them in a database. Even then, WebSQL which provided easy to do relational mapping through SQL in the browser was deprecated in favor of a more hierarchical store - IndexedDB because it ended up being simpler for people to use. – Benjamin Gruenbaum Oct 03 '14 at 15:40
  • Although SQL is based on the relational data model it doesn't do much justice to it. A better example is [datalog](http://en.wikipedia.org/wiki/Datalog) which although is not a relational language (it is a logic language), yet it allows for relational programming. Such deductive databases (like [Datomic](http://www.datomic.com/)) are far more superior than any hierarchical or object-oriented database that I have seen (including indexedDB, mongo, redis, and couch). – Aadit M Shah Oct 03 '14 at 15:46
  • Excellent answer. Relational model has drawbacks as others have said, but this is very informative. Thanks! – NanoWizard Oct 03 '14 at 22:15