Nikita Kazakov
Nikita Kazakov
5 min read

Tags

I’ve been listening to more of Sandi Metz presentations and reviewing notes from her book Practical Object Oriented Designs.

I reflected back on my own code I wrote in Ruby on Rails a few years ago. I wasn’t shy about making ‘fat models’. I created a God class that did everything and was hard to test.

Here’s how I’d go about breaking up class that has too many responsibilities into several objects that do what they need to do.

Let’s go through an example of a Car object in Ruby.

class Car
  attr_reader :running

  def initialize
    @running = false
  end

  def engine_on
    @running = true
  end

  def engine_off
    @running = false
  end
end

We can instantiate a car object car = Car.new. When we get inside a car, we stick our key in and we turn on the engine, right?

We add two methods called engine_on and engine_off.

Engine doesn’t belong in a car.

Notice that we added two methods that start with a noun engine and end with a behavior on / off. These methods are inside a Car class. Why the heck is Car responsible for telling an engine how to turn on?

When you start writing methods inside a class with nouns (engine) that don’t belong to that class (Car), that’s an object that’s screaming to be created.

What can an Engine do? It can be turned on and off. Let’s create a simple Engine Object.

class Engine
  def initialize
    @on = false
  end
  
  def on
    @on = true
  end
  
  def off
    @on = false
  end
end

How do we bring the Engine back into the Car? One way of doing that is to instantiate it from the Car object.

Note that we can now change the method names from engine_on (noun + behavior) to a full behavior turn_on. Same goes for turn_off.

class Car
  def initialize
    @engine = Engine.new # Instantiate the engine from here.
  end

  def turn_on
    @engine.on
  end

  def turn_off
    @engine.on
  end
end

Above is an improvement from before but the Car still knows too much about the Engine. It knows an Engine class exists and that you can instantiate a new engine object from it.

A Car should be modular. When you buy a car and it is built, you can specify what engine or color you’d prefer, right? A car doesn’t make that decision itself. You do.

To make car a bit more loosely coupled, we can use Dependency Injection to bring in the Engine like this:

class Car
  # Dependency Injection
  def initialize(engine: Engine.new)
    @engine = engine 
  end

  def turn_on
    @engine.on
  end

  def turn_off
    @engine.on
  end
end

Let’s instantiate a car object and turn it on.

car = Car.new(engine: Engine.new)
# or the car by default injects Engine.new

car = Car.new
# <Car:0x00007fde7d070e50 @engine=#<Engine:0x00007fde7d070d60 @on=false>>

car.turn_on #true
car
# #<Car:0x00007fde7d070e50 @engine=#<Engine:0x00007fde7d070d60 @on=true>>
# Note that the engine turned on! @on=true

Okay, so where’s the advantage? Cars have different engines. A Diesel engine needs to be warmed up in the winter before it can start while a gas engine doesn’t care.

Antipattern Example

Let’s look at an inefficient way to take into account a diesel and a gasoline engine and turn the car on.

class Car
  def initialize(engine = Engine.new)
    @engine = engine
  end

  def turn_on
    # The start up is different based on the type of engine you're using.
    if @engine.kind == 'Diesel'
      @engine.warm_up
      @engine.on
    else
      @engine.on
    end
  end
end

Notice that the turn_on method just became complicated with a conditional. The car now has to know how to start an engine properly. It’s doing too much. An engine should be able to take a message and know how to start itself.

Also take a look at what we had to do to the engine class:

class Engine
  attr_reader :kind
  def initialize(kind: 'Gasoline')
    @on = false
    # We had to add two extra instance variable to keep track of
    # the kind of engine it is and whether it's warm.
    # Warm is only relevant for a Diesel engine, not a gasoline one.
    @warm = false
    @kind = kind
  end

  def on
    @on = true
  end

  def off
    @on = false
  end

  # warm_up method is only relevant to the Diesel Engine.
  # It's not needed at all for a gasoline engine. We're polluting this class.
  def warm_up
    @warm = true
  end
end

Refactor

Don’t make the Car object thing about how each engine should start. The car object should only worry about sending a on message to an engine and the engine should do whatever it has to do to start itself.

The hint we should have taken was that there are two types of engines. Two nouns. Two engine objects? Yes! A GasolineEngine and a DieselEngine.

class GasolineEngine
  def initialize
    @on = false
  end

  def on
    @on = true
  end
end

class DieselEngine
  def initialize
    @on = false
    @warm = false
  end

  def on
    warm_up
    @on = true
  end

  private
  def warm_up
    @warm = true
  end
end

Notice that the DieselEngine is a bit more complex. It has a @warm instance variable to track whether it has been warmed up. A GasolineEngine does not need that and doesn’t have it.

The important part is that both Engine classes can respond to on. They both have a on method. However, both have a different procedure to follow to turn on. That’s key. Let each do it’s own thing. Don’t allow the Car object to micromanage and tell and the engine how to turn on.

Here’s how the Car class looks like:

class Car
  def initialize(engine: GasolineEngine.new)
    @engine = engine
  end

  def turn_on
    @engine.on
  end

  def turn_off
    @engine.on
  end
end

Let’s instantiate.

car = Car.new
car 
#<Car:0x00007fbfa882bda0 @engine=#<GasolineEngine:0x00007fbfa882bd78 @on=false>>
# A Gasoline Engine is passed in by default.  @on = false.
car.turn_on # Car sends a message to the engine to turn on.
car
#<Car:0x00007fbfa882bda0 @engine=#<GasolineEngine:0x00007fbfa882bd78 @on=true>>
# It turned on without the car worrying about how to do it.

# Let's try it with a Diesel Engine.
car = Car.new(engine: DieselEngine.new)
car
#<Car:0x00007fbfa70dbdf8 @engine=#<DieselEngine:0x00007fbfa70dbe48 @on=false, @warm=false>>
car.turn_on
car
#<Car:0x00007fbfa70dbdf8 @engine=#<DieselEngine:0x00007fbfa70dbe48 @on=true, @warm=true>>
# DieselEngine took care of warming itself up and starting the car!

The Car object only has to command an engine to turn on. We managed to get rid of the conditional complexity we had before. The code is easy to read and each object does it’s own job.

That’s the power of dependency injection and breaking a God class into different objects.