Or, what are classes for, anyway?
There was a bunch of noise last week about different approaches to object-oriented programming, and whether a certain approach could be considered “doing it wrong”. I remarked that this seemed to me like a problem with OOP as a whole, that we can’t even agree on what classes are for. But rather than harp on that, I think it would be useful to go through what the major models are (at least the ones that I’m familiar with) and see what they are, and how they disagree.
Classes as Semantic Hierarchy
You might consider this the rookie mistake. It’s actually used to great success in platform widget toolkits, but I’ve seen it applied a lot of places where it did more harm than good. The basic idea is you capitalize on the inheritance feature in your language, designing your system in terms of semantic abstraction through common base classes.
Extending the system involves adding classes to the hierarchy, and a primary question is where in the hierarchy a particular class should go. You find people debating the subtleties of “is-a” vs “has-a”.
Just as a sidebar, Clojure has a feature called ad hoc hierarchies which lets you do this kind of thing in a much simpler way.
Objects as Machines
This is kind of the core idea of objects. You treat an object like a machine with inputs or buttons (methods) and internal mechanisms (instance variables, private methods) which are hidden from the user. This feature is called data encapsulation. The machine model is compatible with the previous model, assuming you use classes to design your objects. (Some people double-down on this model, and argue that OOP shouldn’t be so much about classes.)
The “Law of Demeter” comes from this model. If your object is a machine that presents a defined interface, and hides its internal workings, then asking it for one of its parts, and then manipulating that part directly, seems like a bad idea. Anything you should want to do to the part should be exposed on the interface of the machine. But of course this doesn’t entirely work out because an object doesn’t physically contain its instance variables the way a machine might contain its parts. An object might have a reference to something it is connected to, which isn’t really a part of its inner workings. But this model is where the law comes from.
Objects as Data Structures
This is the contradiction of the machine model. As soon as we have data in our program, beyond simple strings and numbers, we will have to put it in some kind of structure, and the obvious thing to use in an OO language is objects. (Again, we will likely use classes to design those objects.)
Of course, this runs amuck of the whole data encapsulation thing. If we put our data inside machines, it’s really annoying when we want to just, say, look at the data. (Ever written your own pretty-printer?) You can’t do anything to the data unless the machine does it for you.
Anyway, I think there’s a legitimate approach to OOP where you want to model your data with objects, but you want that model to be as transparent and accessible as possible, because it really is just data. And I guess my point is that this kind of approach is directly in conflict with the law of Demeter, and the whole encapsulation idea.
Objects that represent database records, such as ActiveRecord models, are an example of this pattern. (These are actually kind of an extreme case where the class knows less about the data it encapsulates than everyone else in the system–such as the database schema and HTML forms.)
Classes as Workspaces
So this one may seem a little less obvious than the first 3. It is central to the design of Rails, and is kind of the antithesis of the hierarchy approach (although it isn’t as completely at odds with it as the data structure and machine approaches are with each other).
Basically you have a framework with a one or more predefined roles that a class can have in the framework, and a set of tools for building classes to fulfill those roles. Typically a base class is provided that you must subclass, although there are exceptions. At any rate, this predefined role ends up dominating the responsibilities and lifecycle of the class.
The result is that your class definition is more of a workspace for constructing a non-class entity than truly being a class definition. An example would be controllers in Rails. A controller class isn’t really used as a class at all. You aren’t expected to instantiate it or call methods on it. You can, but that isn’t how it is designed. A tell-tale sign of this approach is that method definitions are usually appropriated for some other purpose. So in a controller public methods define the actions for that controller. Many of the older unit testing frameworks took this approach as well. What really makes it a workspace rather than just an object that fulfills an interface is that the framework uses reflection to call methods that were not predefined.
Classes as Functions
This one is the issue at hand in the aforelinked Gist. You make a class with a verbish name rather than a noun name (it may or may not end in -er, but either way it is defined by a single action). You usually pass your parameters (if any) into the constructor, and then call a single method and discard the instance. This is an extreme (or perhaps, logically consistent) interpretation of the Single Responsibility Principle.
In functional languages, you usually have some way of defining helper functions within a function, and usually those helper functions can close the arguments of the containing function. (
letfn in Clojure and the
where syntax in Haskell.) This is basically the same idea as these class-functions. Because the instance captures the function parameters as instance variables, you can break up the function using private methods, which all have convenient access to those instance variables.
You can also, of course, use the instance to share some mutable state for the duration of the computation (which could be done in impure functional languages as well). One of the downsides of this pattern is that the function arguments are also in instance variables, which practically encourages you to modify them, which is a dangerous thing to do.
So I see this as an OO way to hack around not having first class functions. The people who are disgusted by it see it as contradictory to one (or all) of the other conceptual models (which it is, if used pervasively). The people who use it pervasively seem to see themselves as righteous zealots of SRP, which I suppose they are. This would almost be a viable way to do functional programming in an OO language if the class definition syntax were not so verbose.