How am I supposed to do my cargo cult encapsulation in AngularJS?

A computer science degree and years of engineering best practices have given me a very specific anxiety disorder. Whenever I see a public member variable, I feel intense distress. Something like:

public class Coordinate {
    double x;
    double y;
}

makes me physically ill. You know that study where the MIT Professor asked people to do mean things to Furbies?

...in a small experiment conducted for the radio show Radiolab in 2011, Freedom Baird of MIT asked children to hold upside down a Barbie doll, a hamster and a Furby robot for as long as they felt comfortable. While the children held the doll upside down until their arms got tired, they soon stopped torturing the wriggling hamster, and after a little while, the Furby too. They were old enough to know the Furby was a toy, but couldn’t stand the way it was programmed to cry and say “Me scared”.

-- Would you murder a robot?

That's how I feel about making things public that can be made private. Stop doing that to that poor class! Stop it!

Most of my work now is in AngularJS. If you're not familiar, the 101 of it is that if you want to display something, or have a form, you use a special syntax in a template and all the changes will be magically wired together and appear.

<div ng-controller="MadeUpCtrl">
 <form>
   <input type="text" ng-model="MadeUpCtrl.text">
 </form>
 <div>
   {{MadeUpCtrl.text}}
 </div>
</div>

When you type things in the form, they display in the div below without any real effort. You do have to have a variable named text in the controller, even if it has no value. (You can do {{text}} but there a lot of reasons not that I find too boring to explain now).

But wait, you conveniently ask, isn't that mucking around with some other object's internal variables? Yes, yes it is. "text" is a public member variable and we are doing terrible, terrible things to it.

The problem is that there's no real way around this. The usual getText() setText() monstrosity won't work -- it has to be a value, not a function that returns a value. How does Angular know getText() will return something different? Well, you'd have to tell it.

That creates a disaster like this. Template:

<div ng-controller="MadeUpCtrl2">
 <form>
   <input type="text" ng-model="MadeUpCtrl2.exportText">
 </form>
 <div>
   {{MadeUpCtrl2.exportText}}
 </div>
</div>

Controller:

MadeUpCtrl2 = function($scope) {
  this.text_ = "";
  this.exportText = "";
  var self = this;
  // this fires when exportText changes
  $scope.$watch(this.exportText, function(exportText) {
    self.setText(exportText);
  });
}
 
MadeUpCtrl2.prototype.setText(text) {
  this.text_ = text;
}
 
MadeUpCtrl.prototype.getText() {
  return this.text_;
}

The problem is that this is awful. If you want to do anything interesting in setText, let's say escape some html, you're going to have to push it back to exportText to make it visible, and then that's going to trigger another $watch...

Looks like I just have to live with it.

  • Alistair Davidson

    Use ES6

    class MyController {
    private _text = '';
    get text() { return this._text; }
    set text(newVaue) { return this._text = newValue }
    }

  • That you called it "cargo cult encapsulation" makes me think you already believe it to be suspect, but I'll throw in my 2 cents anyway.

    JavaScript isn't your typical $FAVORITE_OO_LANGUAGE (or is that OOLanguageFactoryImpl.getFavoriteLanguageInstance()?). Forcing it to work like one leads to frustration in a lot of cases.

    That bit aside, getters and setters seem like the ultimate "cargo cult" application here. They're nice when your class wants to hide its inner details from the outside world to avoid coupling. But this is a view's controller, and its associated view. They're basically coupled by definition 🙂

    • Nicholas Johnson

      Upvote on that. JavaScript "objects" are what other languages call Hashmaps. Trying to force a hashmap to keep some of it's keys private makes no sense.

      This said, you don't have to live with it, look up ES6 getters and setters. They work natively in all current evergreen browsers, and would solve your problem.