2

While learning D3.js, I have come across the blog post explaining the main design-pattern behind it's reusable units of code. I have reproduced the relevant bit of code below. The way the pattern is presented below is exactly the way it is used in D3 codebase and plug-ins (example).

My one issue with this code is that it has so much copy-paste for the properties. JavaScript being a functional language, I thought I'd be able to re-factor the boilerplate code out, but I can't think of a way to do it. The arguments and value parameters are easy to pass to a common function, but I can't find a way to preserve a reference to the width and height properties.

function chart() {
  var width = 720, // default width
      height = 80; // default height

  function my() {
    // generate chart here, using `width` and `height`
  }

  my.width = function(value) {
    if (!arguments.length) return width;
    width = value;
    return my;
  };

  my.height = function(value) {
    if (!arguments.length) return height;
    height = value;
    return my;
  };

  return my;
}

The fact that this is how it's done in the actual D3 codebase, makes me wonder if re-factoring is possible, but I'm hoping that it's simply a matter of this not being a high priority issue (and new contributors are doing it this way, because that's how it was done before).

What I'm looking for is basically replacing every accessor's body with:

my.height = function(value) {
  return getSet(arguments, value, whatever);
};

There is still some boilerplate for the call, but at least the logic is centralized and can be updated in only one place, if needed.

VividD
  • 10,456
  • 6
  • 64
  • 111
Alex K
  • 10,835
  • 8
  • 29
  • 34
  • What exactly are you looking for? Sounds like you want something like php's "magic" getter/setters: a single function that would take care or getting/setting any property. Is that the case? – bfavaretto Feb 18 '13 at 15:55
  • @bfavaretto updated my question to clarify. – Alex K Feb 18 '13 at 16:00
  • Hm, you'll probably need something like [this](http://stackoverflow.com/a/543820/825789). – bfavaretto Feb 18 '13 at 16:15

2 Answers2

1

If you define getSet in the scope of chart, it will have access to the closed over variables too. The problem is, you can't access those variables by name string (unless you use some kind of eval).

You could avoid that by wrapping all your private variables in an object untested:

function chart() {
    var props = {
        width: 720, // default width
        height: 80 // default height
    }

    function my() {
        // generate chart here, using `width` and `height`
    }

    my.height = function(value) {
        // Call getSet with current my instance as this, 
        // 'height' as the first argument, then value
        return getSet.apply(this, arguments.slice().unshift('height'));
    };

    // Works just like your current accessors, on the props object
    function getSet(property, value) {
        if (arguments.length > 1) return props[property];
        props[property] = value;
        return this;
    }

    return my;
}

The problem is this is not much shorter than writing several similar accessors for each property. Your current accessors make the private variables virtually public, so why not just ditch them and use public variables instead?

bfavaretto
  • 71,580
  • 16
  • 111
  • 150
  • the reason they aren't simply public vars, is that the whole point of this pattern is to enable method chaining. With public vars you'd have something like chart.height=33, which is going to break the chaining flow. Also, args.slice() part doesn't work (no such method) and I'm not sure what your intent there was. – Alex K Feb 24 '13 at 14:03
  • I now see there are some mistakes in my code. However, I'm not familiar with D3, so I'm not sure what to suggest now. Could you point me to an example of how it's done on the D3 codebase? – bfavaretto Feb 25 '13 at 14:32
  • there is an example link in OP, the bottom part of that doc is full of functions like "rollup.whateverXYZ" format. As you can see they are all just copy-paste, that's what I want to fix. – Alex K Feb 26 '13 at 22:50
0

An alternative solution to that already provide is to define a 'property' function which returns a function e.g.

function property (v) {
    return function (_) {
    if(!arguments.length)
            return v;
        v = _;
    return this;
};
}

So you can say:

function chart() {
    chart.width = property.call(chart, 500);
    chart.height = property.call(chart, 500);

    chart.render = function() {
    //render logic goes here
    };

    return chart;
}

I cannot take full credit for this - I think the source was actually Mike Bostock (but I cannot find a link to the original post)

I use this 'property' function in many of my modules - it saves a lot of annoying typing. You can easily augment it to emit events when the incoming value changes.