-
-
Save jszmajda/1ccb901e6a632ea6d86c to your computer and use it in GitHub Desktop.
# Multiple inheritance with Modules as an alternative to injected composition | |
# from Sandi Metz's talk [Nothing is Something](http://confreaks.tv/videos/bathruby2015-nothing-is-something) | |
# Like Sandi's 'direct' DI method this has behavior outside of the base class | |
# that gets composed together. However in this gist I compose modules in class | |
# definitions instead of injecting collaborators. | |
# Tradeoffs between this and Sandi's version are that in this case the API consumer doesn't | |
# have to know how to make a RandomEchoHouse (no `house = House.new(formatter: Whatever.new)`), | |
# but also the API consumer can't make anything not already accounted for either. | |
module DefaultOrdering | |
def order(data) | |
data | |
end | |
end | |
module RandomOrdering | |
def order(data) | |
data.shuffle | |
end | |
end | |
module DefaultFormatting | |
def format(parts) | |
parts | |
end | |
end | |
module EchoedFormatting | |
def format(parts) | |
parts.zip(parts).flatten | |
end | |
end | |
class House | |
include DefaultFormatting | |
include DefaultOrdering | |
DATA = [ 'the horse and the hound and the horn that belonged to', | |
'the farmer sowing his corn that kept', | |
'the rooster that crowed in the morn that woke', | |
'the priest all shaven and shorn that married', | |
'the man all tattered and torn that kissed', | |
'the maiden all forlorn that milked', | |
'the cow with the crumpled horn that tossed', | |
'the dog that worried', | |
'the cat that killed', | |
'the rat that ate', | |
'the malt that lay in', | |
'the house that Jack built' ] | |
def initialize | |
@data = order(DATA) | |
end | |
def recite | |
(1..data.length).map {|i| line(i)}.join("\n") | |
end | |
def line(number) | |
"This is #{phrase(number)}.\n" | |
end | |
def phrase(number) | |
parts(number).join(" ") | |
end | |
def parts(number) | |
format(data.last(number)) | |
end | |
attr_reader :data # personal preference I think | |
end | |
# Inheritance is valid here IMO because we are specializing behavior | |
class RandomHouse < House | |
include RandomOrdering | |
end | |
class EchoHouse < House | |
include EchoedFormatting | |
end | |
class RandomEchoHouse < House | |
include RandomOrdering | |
include EchoedFormatting | |
end |
I've been thinking about the inheritance (which includes mixins) vs. composition question a lot lately because I've had to modify a lot of heavily mixed-in code.
I visualize it like looking for an artifact on the ocean floor. Inheritance is like looking over a square mile. You know all the code that's doing whatever it's doing is in this class, you just don't know where the code is physically located; so it may take a long time to navigate through all the modules and figure out what overrides what.
With object composition, it's like looking in a mile-deep trench: you may need to navigate through a deep object graph, but to me, the way is clearer than trying to figure out which contributed bit of functionality is causing a certain effect.
When using a deep graph of small objects, you can often get a single class in its entirety on one editor screen, or a few folds deep. Comparing that with n open files worth of modules, I find it easier to understand one "single-class" class at a time, rather than a class composed of many modules.
Class responsibility, and therefore tests, is also more focused, in my experience.
It still depends on the design of the modules/classes in question, but these techniques seem to lean in these directions.
Thanks very much for taking the time to go through this, it's awesome to be able to have discussions like this with you, I feel like I learn a lot every time we talk :)
I think your analysis is spot-on. Including the flog score is very interesting. You're naturally going to leave out the materialized concepts of
RandomEchoHouse
and friends, which will give you a lower score of course, but I don't imagine including those would add much (random_echo_house = House.new(orderer: RandomOrder.new, formatter: EchoFormatter.new)
, etc I suppose).I think interestingly you can retain the value of presenting materialized concepts to the API consumer by adding a Factory class to your implementation (
random_echo_house = HouseFactory.random_echo_house
maybe?). This adds the pro of the module-based solution while retaining all of the pros of the (more open/closed!) composition based solution.I also love your point about DI from even further outside the local context. I did that a ton when I used to write j2ee. I think people often conflate it with big nasty systems but it's a really great way to create clean code.
Thanks again for taking the time to review this, you rock! :)