Though my Forest project is still in its infancy (a mere 150 commits in), I have spent a great deal of my time wrestling with objects and the bridges between them. The hardest parts of the design are yet to come, like when animals begin interacting with (attacking? eating?) each other. But I’ve made a few major design choices so far.
Note: this article ends up kind of being a recapitulation of David Copeland’s very well-written and informative Re-use in OO, but I come to a different conclusion.
In particular, I’ve come to final (I think!) decision on how objects obtain shared behaviors.
Here’s the problem, in the most general sense: both
Rabbit should be able to change their location. They should be able to move. The code for movement should be shared.
Module mixins are not the way
My initial solution was to include a Movable module in both
Rabbit. This is the solution for which Copeland advocates. I walked pretty far down this path, and even found solutions to some of my problems, like that private methods defined in the included module are accessible to every other included module and the parent class. I wrote a blog post about my solution, but I mention at the bottom a remaining qualm: there’s nothing preventing a module from calling another module’s public method, as in this gist. They can do so easily and inexplicitly.
I envisioned a bleak future for my project, with included modules calling each other willy nilly, in all directions, forming intractable dependencies. This problem could be worked around by carefully maintaining unidirectional dependencies, but it would be ad-hoc and easy to break, unensured against by nature of the construct.
So I asked for help. My friends Devon and Forrest, fellow Dev Bootcamp grads (RACCOONS!), helped out. Forrest showed me the Copeland article, and Devon illustrated a technique of sharing behavior through composition.
A few acceptable solutions
Decorating objects to
#move! through composition
What I’m going with is Devon’s suggestion. It has the benefit of ensuring that dependencies are explicit and travel in one direction. Additionally, instances only gain these abilities when you want them, rather than having always-on, and thus abusable, abilities.
This is an implementation of the decorator pattern, where
Movable is the decorator. I was actually already using decorators in my code to create presenters (with view-specific logic), but I didn’t at first make the connection that it could be used to share behavior.
class Movable < SimpleDelegator
def move!; ...; end
wolf = Wolf.new
movable_wolf = Movable.new(wolf)
Data, Context, Integration (DCI)
Jim Gay discusses DCI in-depth here, and I don’t understand it totally. Anyhow, it relies on modules extending objects as needed, rather than mixed into the class’s definition itself.
Note that usage is very similar to the previous solution.
def move!; ...; end
wolf = Wolf.new
Duck typing your way to movement
Another composition / duck-typing solution is as follows, where the actual moving is delegated to another object or module. This is a partial solution.
I don’t like this path as much; I intuitively think that an object should be in charge of its own movement. I could envisage plenty of cases when it would be preferable, however, like if there was a Map object in charge of collision detection. You’d have to be vigilant in preventing the Map from becoming a God Object, though.
As stated, this is a partial solution. It doesn’t solve the problem of how to ensure a
Wolf is a
Movable in the first place. One could do that through extension or composition, as in the above two examples. It just changes where the
#move! method lives, and implies that movement is the responsibility of a third object.
I like how “movable” makes semantic sense, though. And duck typing is just fun.
def self.move!(movable); ...; end
There’s more than one way to do it.
Module mixin on its own, my naive approach, seems totally untenable. It is multiple inheritance, as demonstrated by the fact that included modules are listed in a class’s
.ancestors method. And inheritance can be a mess to begin with, without dependencies being able to go in all directions.
Please let me know if I you’ve got corrections or feedback! For example, I’d love to hear more examples of when the last code example is appropriate. I feel like it’s more useful than I am realizing.