DEV Community

Kelly Stannard
Kelly Stannard

Posted on • Edited on

Composition and Inheritance

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)