Sharing behavior among and between objects requires a balanced decision. Will other objects use this behavior? Which ones? Why? Should I make a new class to handle what I need?
Determining answers led me to dive into exactly what delegation is and inevitably led to more research to get a better understanding of inheritance.
Prefer delegation over inheritance goes the standard advice.
It seems that many developers think about inheritance in terms of classes. Classes define everything about your objects so it must be the only way to handle inheritance. We tend to think about delegation in terms of instances of classes as well.
This mental model mostly works well. But then there's always someone who points out a seemingly contradictory phrase...
Delegation is Inheritance
Lynn Andrea Stein argued that Delegation is Inheritance in her 1987 OOPSLA paper of the same name. It's a fantastic read and an important paper in describing the behavior of Object-oriented systems and what the costs and benefits are to different inheritance schemes.
But this title seems to go against a simple mental model of what inheritance and delegation are.
Classes and Prototypes
First and foremost, object-oriented programs tend to have either class-based or prototype-based inheritance.
Sometimes you'll see comments around the web like this
a problem that Javascript is having is people wanting to force OOP on the language, when fundamentally it’s a prototype-based language
It's certainly a good thing to understand the way a language was designed, but there is no dichotomy between OOP and prototypes.
Prototypes are an object-oriented paradigm.
Classes are a paradigm which groups objects by type. Classes generate instances whose abilities are defined by their class. This is a rigid approach to sharing behavior and is the one provided in Ruby.
Prototypes on the other hand share behavior among individual objects. An object's prototype defines behaviors that may be used on the object itself.
Perhaps a problem that people see in JS development is instead forcing a class-based paradigm on their programs. Perhaps not. I'm not venturing into that debate but recognize that prototypes are OO.
Inheritance Through Delegation
Many (most?) OO programmers are familiar with class-based inheritance systems.
You create a new instance of Thing
and that thing
instance has all of the abilities defined in its class. This concept is easy to grasp.
Prototype-based inheritance, however, seems much more complex. In order to understand the behavior of one object you also need to understand the behavior provided by its prototype which is just another object. A prototype doesn't necessarily act as an umbrella of classification like classes.
It's harder to think in generalizations outside of a class system. The type of an object is a simple mental model but with prototypes, the possibilities are myriad.
But getting back to the point of this section, class-based inheritance uses delegation to share behavior. A message sent to the instance thing
is delegated to the class Thing
. All of the behaviors are defined in the class, but self
is bound to the object that received the message. The behavior is evaluated in the context of the object. That is: self
refers to the instance, not the class (where the behavior is defined).
class Thing
def hello
self.inspect
end
end
thing = Thing.new
thing.hello # <-- delegation to its class which defines the behavior
In a case where two objects exist in a prototype-based system, the same rules apply. The keyword self
is bound to the object that received the message and any delegated behavior is defined in another object.
Ruby delegates through classes and as a result caches its methods based upon the classes of objects. This class-based approach means that adding new behavior to an object leads to the cache being cleared because as an instance of a particular class the object's behavior changes and needs to be reflected in the method lookup (defined by its class). Charlie Somerville has a great rundown of things that clear Ruby's method cache.
Our class-colored lenses skew our understanding of delegation.
Inheritance Does Not Exist
This brings me around to why reading Stein's argument about delegation being inheritance is important. Most (or maybe just many) programmers seem to misunderstand what delegation is.
Stein's paper made the argument that delegation is inheritance. Her paper is based upon the definition of delegation citing Henry Lieberman's 1986 paper.
Lynn Stein was recently very helpful to me and suggested that the original understanding of delegation may have come from "Delegation in Message Passing Proceedings of First International Conference on Distributed Systems Huntsville, AL. October 1979". Alas, I can't find that paper online but it is related to the application of the Actor pattern by Carl Hewitt, Beppe Attardi, and Henry Lieberman.
Stein's work was built against the understanding that delegation has a specific meaning. When methods are delegated, they are bound to the object that received the message. The self
or this
keywords (depending on the language) refer to the original object that received the message.
Many commenters on my delegation article (and on other forums) claimed that the "delegation" term has changed meaning over the years. Lieberman, critics say, may have coined the term but he doesn't own the meaning.
Some have argued that delegation no longer means what Lieberman, Stein, and other OO pioneers meant. If this is true and delegation means 2 objects forwarding messages, and if delegation is inheritance, then effectively every object sending a message is inheritance. Either that or inheritance does not exist.
Why Care?
I've been working around the limitations of implementing DCI in Ruby and the behavior of objects is a central concern.
In DCI our bare data objects gain behavior inside a context relevant to some business goal. Typically this is either explained through the use of extend
to add a module to an object's inheritance lookup:
thing.extend(SomeModule)
thing.method_from_module
The problem in Ruby is that the object's inheritance tree remains the same for the life of the object.
# Before
thing.singleton_class.ancestors #=> [Thing, Object, Kernel, BasicObject]
thing.extend(SomeModule)
# After
thing.singleton_class.ancestors #=> [SomeModule, Thing, Object, Kernel, BasicObject]
The thing
object is forever tied to it's new set of ancestors. This may affect your program if you expect your object to lose behavior after executing some procedure inside a DCI context.
There are projects which hack Ruby to add unextend
or other like methods, but they are additions to Ruby typically only in one interpreter (like MRI).
What Can Ruby Do?
A new feature of Ruby 2.0 is exciting in the abilities we'll have to build libraries that aid in delegation.
Methods defined in modules may now be bound to objects regardless of their class ancestry.
SomeModule.instance_method(:method_from_module).bind(thing).call
That code is a mouthful, but it shows (in Ruby 2.0) that we can add behavior to objects without extending them.
If we want to implement a delegation technique we could hide all this inside a method.
def delegate(method_name, mod)
mod.instance_method(method_name).bind(self).call
end
This is an approach I built into casting, a gem that helps add behavior to objects, preserves the binding of self
inside shared methods and leaves the object in it's original state.
It's easy to use, but it takes a different view of what delegation is compared to how most Ruby developers seem to think. The delegated method here will preserve the self
reference to the thing
object and behavior is added without extending the object.
class Thing
include Casting::Client
end
thing.delegate('some_method', SomeModule)
It's a work in progress and has given me a great reason to explore what Ruby can and can't do.
The Best of Both Worlds
Stein's paper suggested that a hybrid system of both class-based and prototype-based inheritance would provide both rigidity and flexibility when needed.
As I've worked with my code more and researched more for Clean Ruby I've had the desire for a hybrid system. Stein's words ring true:
In order to take full advantage of the potential of such a system, objects must be treated simply as objects, rather than as classes or instances.
Indeed, DCI requires a system where an object gains and loses behavior based upon the context. The behaviors are localized to the context and a hybrid system like the one Stein described would benefit from the advancement of DCI.
This is a challenge to implement in Ruby but I'll be working more on casting in the future.
For now, it's useful to consider the contextual behavior of our objects and think less about the classes that define them and more about the message passing among them.
Focus on the behavior of your system first, use delegation and worry about classes later.