I was discussing composition over inheritance with a friend recently and he sent me this as a way to help clarify the subject.
# Inheritance
class Animal
def eat
# eats
end
end
class Dog < Animal
end
# Composition?
module Pooping
def poop
# poops
end
end
class Cat
include 'pooping'
end
# Composition?
class Farting
def fart
# farts
end
end
class Hamster
has_a :farting
end
So, first off, one extremely common misconception is that the second scenario is OO composition. It is not. Only the third example is OO composition because OO composition is an object containing references to other objects. However in the third example the name Farting is bad because Farting is a state of being and not an thing that can be encapsulated by a hamster. Names are important for clarity.
After reading this I decided to show him what OO composition might look like for the case of modeling animal digestion.
First off you need to consider the objects involved. An animal is obviously there. There is also a system of organs in an animal for digestion called the gastrointestinal (GI) tract. For simplicity sake I will avoid modeling the individual organs of the GI tract and move on to writing some code for eating.
class Animal
def initialize
@gi_tract = GITract.new
end
def eat(food)
@gi_tract.injest(food)
end
end
class GITract
def initialize
@contents = []
end
def injest(food)
@contents << food
end
end
Great, now we have an animal that eats and successfully passes the food to its GI tract. How about pooping?
class Animal
...
def poop
@gi_tract.evacuate
end
end
class GITract
...
def evacuate
Waste.new(@contents.shift)
end
end
That is nice, but not quite right. Usually irl I get told I need to go poop by my gi_tract. We need a two way relationship between the animal and gi_tract in order to model that.
class Animal
def initialize
@gi_tract = GITract.new(owner: self)
end
...
end
class GITract
def initialize(owner:)
@owner = owner
@contents = []
@waste = []
end
def digest
@contents << nil
@waste << @contents.slice!(1..-5).compact.map{|food| Waste.new(food) }
@owner.poop if @waste.any?
end
def evacuate
@waste = []
end
...
end
Okay, that is looking good. We have now modeled an Animal eating and pooping by having a GI tract. But, what about modeling subtypes of Animal? Here is where we can introduce inheritance back in as inheritance is for modeling subtypes. Lets use Cats and Hamsters because they eat different things and have different subtypes of GI tracts and cats have a different pooping behavior.
class Cat < Animal
def initialize
@gi_tract = CarnivoreGITract.new
end
def poop
hide
squat
@gi_tract.evacuate
end
end
class Hamster < Animal
def initialize
@gi_tract = HerbivoreGITract.new
end
end
Great! Lets wrap this up. We have successfully encapsulated a lot of behavior inside of GI tract objects. By having that behavior encapsulated we can make changes with much more confidence and with fewer errors. We could from here fairly easily model a cat that is herbivorous or a carnivorous horse.
Leave comment on what you would do next!
Top comments (0)