Chapter 1
Antithesis
Telling an Object What to Do
The big idea is "messaging" – that is, what the kernal [sic] of Smalltalk/Squeak is all about (and it's something that was never quite completed in our Xerox PARC phase). The Japanese have a small word – ma – for "that which is in between" – perhaps the nearest English equivalent is "interstitial." The key in making great and growable systems is much more to design how its modules communicate rather than what their internal properties and behaviors should be.
Alan Kay, (squeak-dev mailing list — http://lists.squeakfoundation.org/pipermail/squeak-dev/1998-October/017019.html)
A huge amount of complexity is wrapped up in that most common of operations: invoking an object's method. In many programming languages – C++, Java, Python, and others – this takes the form anObject.methodName(), which means "there will be a method on the class that anObject is an instance of, or some antecedent class, where the method is called methodName, please find it and run it, with the self or this value aliased to anObject." So, for example, in Java we would expect to find a (non-abstract) public void methodName() { /* ... */ } somewhere in anObject's class or parent.
This guarantee introduces a lot of coupling between the caller and the object that holds the method:
- The caller knows that the object is an instance of some class (there are so many issues bound up with inheritance that it gets its own chapter, later).
- The caller knows that the object's class, or some ancestor of it, provides a method with the given name.
- The method will run to completion in this context, then give control back to the caller (this is not particularly evident from the syntax in isolation, but nonetheless is assumed).
What would it mean to lift those assumptions? It would make the object a truly independent computer program, communicating from a distance over an agreed protocol based on message passing. What that object does, how it does it, even what programming language it's implemented in, are all private to the object. Does it collaborate with a class to find out how to respond to the message? Does that class have one parent or multiple parents?
The idea behind message-passing is exactly that arms-length separation of concerns, but even programming languages that are based on the message-passing scheme usually treat it as a special case of "look up a method," to be followed only if the usual method-resolution fails. These languages typically have a particular named method that will be run when the requested method isn't found. In Smalltalk, it's called doesNotUnderstand:, while in Ruby it's called method_missing(). Each one receives the selector (that is, the unique name of the method the caller was hoping to invoke) to decide what to do with it. This gets us a higher level of decoupling: objects can send messages to one another without having to peek at the others' implementations to discover whether they implement a method matching the message.
Why is that decoupling valuable? It lets us build our objects as truly standalone programs, considering only what their contract is with the outside world and how their implementation supports that contract. By requiring, for example, that an object will only receive a message if it is an instance of a class that contains a Java function of the same name that can be pushed onto the call stack, even if via a Java interface (a list of methods that a Java class can provide), we adopt a lot of assumptions about the implementation of the message receiver, turning them into constraints that the programmer must deal with when building the sender. We do not have independent, decoupled programs collaborating over a message interface, but a rigid system with a limited amount of modularity. Understanding one object means pulling in information about other parts of the system.
This is not merely an academic distinction, as it constrains the design of real systems. Consider an application to visualize some information about a company's staff, which is located in a key-value store. If I need every object between the view and the store to know about all of the available methods, then I either duplicate my data schema everywhere in the app by defining methods like salary() or payrollNumber(), or I provide meaningless generic interfaces like getValue(String key) that remove the useful information that I'm working with representations of people in the company.
Conversely, I could say to my Employee object "if you get a message you do not recognize, but it looks like a key in the key-value store, reply with the value you find for that key." I could say to my view object "if you get a message you do not recognize, but the Employee gives you a value in response to it, prepare that value for display and use the selector name as the label for that value." The behavior – looking up arbitrary values in the key-value store – remains the same but the message network tells us more about why the application is doing what it does.
By providing lazy resolution paths like method_missing, systems like Ruby partially lift these assumptions and provide tools to enable greater decoupling and independence of objects in the network. To fully take advantage of this, we must change the language used and the way we think about these features.
A guide to OOP in Ruby will probably tell you that methods are looked up by name, but if that fails, the class can optionally implement method_missing to supply custom behavior. This is exactly backwards: saying that objects are bags of named methods until that stops working, when they gain some autonomy.
Flip this language: an object is responsible for deciding how it handles messages, and one particular convenience is that they automatically run methods that match a received selector without any extra processing. Now your object truly is an autonomous actor responding to messages, rather than a place to store particular named routines in a procedural program.
There are object systems that expose this way of thinking about objects, a good example being the CMU Mach system. Mach is an operating system kernel that supplies communication between threads (in the same or different tasks) using message passing. A sender need know nothing about the receiver other than its port (the place to put outgoing messages) and how to arrange a message to be put in the port. The receiver knows nothing about the sender; just that a message has appeared on its port and can be acted on. The two could be in the same task, or not even on the same computer. They do not even need to be written in the same language, they just need to know what the messages are and how to put them on a port.
In the world of service-oriented architecture, a microservice is an independent program that collaborates with peers over a loosely coupled interface comprised of messages sent over some implementation-independent transport mechanism – often HTTPS or protocol buffers. This sounds a lot like OOP.
Microservice adopters are able to implement different services in different technologies, to think about changes to a given service only in terms of how they satisfy the message contract, and to independently replace individual services without disrupting the whole system. This, too, sounds a lot like OOP.