Sunday, April 25, 2010

Knuckly interfaces

I loved Marcus Phillipstalk at the san francisco javascript meetup on Friday. He's got a great presentation style and he clearly knows his stuff.

In this case, that stuff was the implementation of OOP patterns in javascript, and he distinguished among the competing inheritance styles, arguing for "functional inheritance". I hadn't heard that name for that pattern before, but I did wonder: with a name so close to a whole different programming paradigm, why not just take the leap over to the dark side? When I asked Marcus about this, he voiced his support for encapsulationI've been critical of OOP for a long time, but Marcus' talk made me think about why.

In the realm of human interactions, I prefer the warm, if moist, hand slap over the apocalypto-hygenic fist bump. But with computer programming, I try to keep the interfaces knuckly. I think we've been trained to think of the interfaces in classical OOP systems, like Java, as hygienic and loosely coupled, since they distinguish between publicly accessible state and private. But stepping back, the core OOP coupling, the coupling between data and methods, actually causes a lot of problems.

The first problem that encapsulation causes is inheritance. Many of us think of inheritance as a feature, but I think we need to ask ourselves: what do we get from inheritance? How often do we really encounter the "perfect-subset" relation in a domain model? It's rare. I think that this kind of relation is actually a by-product of years of reliance of SQL data stores, where data can only be modeled as a set. If you're using a document data store, you get used to thinking more in terms of capabilities than classifications.

Encapsulation causes problems around serialization and transportability. The private nature of some state in the OOP pattern is precisely what makes them so hard to scale. The flip side of this is that OOP is a good pattern for exposing shared mutable state. While there's this movement afoot towards actors , I, for one, think that some abstractions are still best modeled in OOP terms, like global queues and hash tables. But I think there's a consensus building that these kinds of objects are the exception, and not the rule. Where possible, components should not encapsulate any state.

Going back to the example that Marcus gives, with his preferred "functional inheritance" pattern:
// parent class maker function
var animal = function (location){
  var result = {'location' : location};
  result.move = function (){
    this.location += 1;
  };
  return result;
};


// child class maker function
var person = function (location, carSpeed){
  var result = animal(location);
  result.carSpeed = carSpeed;
  result.drive = function (){
    this.location += carSpeed;
  };
  return result;
};

Recast in functional terms, we simply extract the state variables:
var benji = {'location' : POUND};
var carolyn = {'location' : DOWNTOWN, 'speed': FAST};

var move = function (animal){
    animal.location += 1;
}

var drive = function (person){
    person.location += person.speed;
}

move(benji);
drive(carolyn);

At this point the type-safety types (I see you Python people) complain about what happens in the functional paradigm if we pass in invalid data:
//functional style
//data-loss error!
drive(benji);

It's true that this slightly worse than the corresponding error if we employ encapsulation:
//OOP style
//NPE!
benji.drive();

Of course, the functional version of "drive" could always be written to validate its inputs. And in general, it seems like we could use better tools for schema contracts in javascript. But the bigger question is: how often does data cross these kinds of API boundaries, where the input data types are unchecked and unknown?

I would venture that we can sidestep these issues in functional systems, since they can be much more strict about the allowed couplings. In an OOP interface, any public method can call any other. But in msjs, for instance, functional dependencies are fixed at construction time. This makes it easy to track the impact of a change to the kind of data emitted by a given a function.

And the advantages of this approach are tremendous. The main benefit is that the entire system state is cacheable, since there's no private data. I keep finding that, in the places where I used OOP, a functional approach makes for simpler, more portable code that performs better.

There's a place for OOP, but for the average programmer building generic application and UI logic, I think it's overkill.

No comments:

Post a Comment