SOLID Design pt. 2

Inverting Dependencies and Segregating Interfaces

Welcome back to this quick demo of making breakfast with SOLID design principles. If you haven't read Part 1, check it out. It's pretty swell. Here's a quick recap:

Single Responsibility Principle: An object should only encapsulate one functionality. In this example, the Egg class' one functionality is being an egg. If I were to ask the Egg about itself, it won't tell me a list of egg recipes or over what temperature to cook it. Writing a function to do that would be simple, but that would betray the SRP.

Liskov Substitution Principle: An object should not alter the interface of any parent class from which it inherits. For variance in examples, let's consider a class MusicalInstrument. This class would have a play function, def play, as part of its public interface. For every type of musical instrument that inherits from this class, they must also have play as part of their public interface. Obviously, a trombone and a drum set would have different implementation details in their respective play functions; the point of this principle is that any object expecting to receive a MusicalInstrument to interact with shouldn't have to care about what type it receives.

All that recapping has got me hungry. Back to breakfast.


Dependency Inversion Principle: Higher level modules should dictate the implementation details of lower level modules and not the other way around. Modules should only communicate with the public interface of other modules.

To the first point, in Part 1, I mentioned that despite a Dish and a Stove having some similar functionality, a Stove should not inherit from a Dish because a dish is necessarily portable. For giggles, let's make the other decision and have Stove inherit from Dish. Because of physical restraints, Stove simply does not have the portability attribute and that violates Liskov to have a child that doesn't have its parents' attributes. To rectify that, I would have to remove portability from Dish. Having Stove, the lower level module, dictate the details of Dish, the higher level, is in violation of the Dependency Inversion Principle. Let's keep dishes and stoves as separate entities.

And the second point, here are the first draft Stove and Pan classes:

class Stove
  def initialize
    @contents = []
    @on = false
  end
  def turn_on(heat_level)
    @on = true
    @contents.heat_up(heat_level)
  end
  def add_content(new_content)
    @contents << new_content
  end
end
#####
class Pan
  def initialize
    @contents = []
    @heat_level = 0
  end
  def heat_up(heat_level)
    @heat_level = heat_level
  end
def add_content(new_content)
    @contents << new_content
  end

  def remove_content(content)
    @contents.delete_at(@contents.index(content) || @contents.length)
  end
end

The DIP is demonstrated here in Stove.turn_on. The Stove class doesn't need to know what objects are in it's contents. It could just as easily be a teapot or a hand; as long as it has a heat_up method, it will be called. It's up to the objects receiving the heat_up message to determine what to do with that.


The rest of the demonstration will require a new class, Person. This Person object we will use will have @alive = true, @able_bodied = true, and knowledge of pans, eggs, and stoves.

Moreover, this Person introduces what I believe to be the key of Object Oriented Programming. It's the starter, the highest-level module that controls the action. A Stove, Pan, or Egg would not have the ability to begin the breakfast making process, and including that ability within any of the classes would have been in violation of the Single Responsibility Principle (and common sense.)

This is the crucial concept for creating modular programs. Objects cannot instantiate themselves. Objects more than likely are inanimate and cannot alter themselves without outside causes. A controller class or run script must be used to begin any process.


Open/Close Principle and how this Person plays in:

I cheated a bit earlier by adding the add_content methods without having a way to implement them. Person will be the object sending the add_content message to Stove and Pan. In addition, the Person will need a way to see the contents of both, but as it stands, the two inanimate objects' public interfaces are not exposing their contents.

OCP states that object should be open for extension, but closed to modification. Because I know an able-bodied person should be able to access the contents of a pan and stove, I'm going to extend the current classes to include methods exposing their @contents without otherwise changing the implementation of @contents within the class in any way.

class Stove
  .
  .
  .
  def contents
    @contents
  end
end
#####
class Pan
  .
  .
  .
  def contents
    @contents
  end
end
#####
class Person
  def initialize(opts = {})
    # a lot of initialization
  end
  def activate(thing, level=nil)
    thing.turn_on(level) if thing.respond_to?(:turn_on)
    # other checks omitted
  end
  def add_content_to(container, content)
    container.add_content(content)
  end
  def check_contents(thing)
    thing.contents
  end
end

Lastly, the Interface Segregation Principle: each user of a module should only have to interact with the portions of its interface that pertains to what the user needs. Let's talk about eggs.

Both the Pan and the Person will interact with the Egg in different ways.

The Pan:

class Pan
  .
  .
  .
  def cook
    @contents.cook(@heat_level)
  end
end

The Person:

class Person
  .
  .
  .
  def open(thing)
    thing.open
  end
  def consume(thing)
    @energy += thing.energy
    thing.dissolve
  end
end

It's obvious that the Pan does not need to know the caloric value of the Egg whereas the Person does not need to know the process of an Egg receiving heat. The above examples represent Interface Segregation, but the concept also applies in how the objects interact. Person should not interact with Egg object through the Pan object. The program is written in such a way the the Egg can be put into the Pan's @contents and removed from the Pan's @contents without becoming pan.egg, a property of the pan. This simplifies the Pan interface.

Another aspect of Interface Segregation __ is not making users of an object specify options of the object that they do not care about. In this case, the Pan does not need to know at all the type Egg being put into its contents (or that it is an egg at all.) The Person may not care about the type of Egg, OR the Person may care a whole lot about the type of Egg. So let's build the Egg so that both cases of Person are satisfied.

class Egg
  ENERGY_LEVELS = { small: 1, med: 2, large: 3, jumbo: 4 }
  def initialize(color: 'white', size: :med)
    @color = color
    @edible = false
    @energy = ENERGY_LEVELS[size]
  end
  def cook(heat_level)
    heat_level.times { harden }
  end
  def open
    crack_shell
   @edible = true
  end
  def dissolve
    @energy = 0
  end
end

I've fleshed out the whole Egg class, but the important part for Interface Segregation is the initialize function. When a person first accesses their eggs, they will have the option to specify their favorite type of egg via the initialization options or simply not care at all and fallback to the default options of color: white, size: :med.


Recap of how to make a SOLID breakfast:

Single Responsibility Principle: The classes we modeled after inanimate entities only expose their data purely. Egg knows it can harden, but it does not know egg recipes. It can only be an egg.

Open-Closed Principle: The classes we modeled can be added to, but we must not alter their existing public interface. This is a code-specific detail. We added getters to the classes with @contents when we added the Person class as a user of those classes' instances. Importantly, this did not change how the previous users' interacted with the classes' instances.

Liskov Substitution Principle: A child class must not alter the existing interface of its parent class. A Person may upgrade their Stove to flat-top electric burners. This changes some inner, private methods of how the stove receive electricity and converts that to heat, but this does not change the public interface of the Person sending the message stove.turn_on and expecting the Stove instance to begin sending heat_up to its contents.

Interface Segregation Principle: A user of a module should only have to interact with the parts it needs. A Pan knows an Egg can receive heat. A Person knows an Egg can be consumed. Because the Person can initialize what eggs are in its inventory, they have the option to customize the type of Egg or not.

Dependency Inversion Principle: The higher level module determines the details of the lower level and not the other way around. This is another code-specific principle. For a concrete example, here is my thought process on coming up with Egg#open:

It's a bit contrived to a) call it 'open' when I mean crack_shell and b) have that render the object edible. BUT I knew I wanted the Person to have an open method for any food, e.g. bacon and bread. So I left the details to the food object to determine exactly what open means to it. For bacon and bread, open would have been to remove from its packaging render edible.

So, the higher level user, Person, determined how Egg was structured.


Happy Codings!

alexa anderson

About the Author

Alexa Anderson is an engineer and evangelist with a penchant for making products geared for progress and achievement. When not writing software, Alexa spends her time curating content and sharing her discoveries.