POODR by Sandy Metz. Chapter 2 Designing Classes with a Single Responsibility

Feb 01, 2018

Reading time: 14 minutes

#POODR #Sandy #Metz #chapter2 #Design #Classes #Single #Responsibility #easy #Changes

This chapter will help you to decide what belongs in a single class and why desgin classes should only have one responsibility.

Classes Classes Classes

But what are? How many should I have? Their behavior? Their scope?

Ok, brick by brick, we start with the first.

"Our goal is to model our application using classes that does what supposed to do right now and are easy to change later."

The easy-to-change criteria, it's hard. To archieve this quality, we need knowledge, skills and some artistic creativity.

"Design is more the art of preserving changeability than archiveing perfection".

1. Organize code to be ready to easily change

Easy to Change:

The code we write should have: TRUE qualities

Transparent: The change should be obvious and in distant relies upon it

Reasonable: Cost of change should be proportional to the benefits the change archieves.

Usable: Existing code should be usable in new and unexpected contexts.

Exemplary: The code itself should encourage those who change it to perpetuate these qualities.

2. Creating Classes That Have a Single Responsibility

Example for a single responsibility class to show why it matters:

Bicycles and Gears

To compare different gears, bicyclists use the ratio of the numbers of their teeth, these ratios can be calculated by this Ruby script:

chainring = 52                #number of teeth
cog = 11
ratio = chainring / cog.to_f
puts ratio                    # -> 4.7272
# each time our feet push the pedals around one time, wheels will travel around almost five times

chainring = 30
cog = 27
ratio = chainring / cog.to_f
puts ratio                    # -> 1.1111
#  30 x 27 is a easier gear to pedal, but only rotate little more than once  

Reading through the description we see bicycle and gear nouns, these nouns represents the simplest candidates to be classes.

Intuition says that Bicycle should be a class but nothing in the above description lists any behavior for bicycle.

Gear however has chairings cogs and reatios  with data an behavior then it deserves to be a class.
 

class Gear
  attr_reader :chainring, :cog
  def initialize (chainring, cog)
    @chainring = chainring
    @cog       = cog
  end

  def ratio
    chainring / cog.to_f
  end
end

puts Gear.new(52, 11).ratio # -> 4.7272
puts Gear.new(30, 37).ratio # -> 1.111

We created a new Gear instance or Gear object,  providing the numbers of teeth for the chainring and cog. each Object/Instance provide three methods: chainring, cog and ratio.
Now we want to calculate the effect of the difference in wheels. In order to do it we have the next formulas.

gear  centimeters = wheel diameter * gear ratio
wheel diameter =  rim diameter + twice tire diameter

 

class Gear
  attr_reader :chainring, :cog, :rim, :tire
  def initialize (chainring, cog, rim, tire)
    @chainring = chainring
    @cog       = cog
    @rim       = rim
    @tire      = tire
  end

  def ratio
    chainring / cog.to_f
  end

  def gear_centimeters
    ratio * (rim + (tire * 2))
  end
end

puts Gear.new(52, 11, 66, 3.8).gear_centimeters # -> 347.927
puts Gear.new(52, 11, 61, 3.18).gear_centimeters # -> 318.429

Gear centimeters assumes that rime and tire sizes are given in centimeters this my not be corrrect, can be for example in inches or meters instead. And we introduced a bug, Gear.new(51, 11).ratio Gear.initialize was changed with two additional arguments.


Now, we should ask ourselves, It is the best way to organize the code? The answer, as always it is: it depends... To efficiently evolve, code must be easy to change unless Gear will be static forever.

When the Applications are easy to change the classes should as well, this is the reason we need to be sure that our class has only one responsibilty.



3. Determining a Single Responsibility class

 
  3.1 Asking questions to the class. Example: Mr Gear, what is your ratio? Mr Gear, what are your gear metters? (start to sound weird)  your tire size? (non sense question).

 

  3.2 Describe the class in one sentene.
    if we use "and" then have more than one responsibility, if we use "or" is even not related. Example: Calculate the efect that a Gear has in a bicycle... As we see tire size it is still weird inside.
    Then the  class don't have Cohesion (Single Responsibility), Then?


4 .How to make Design decisions?

Ask ourselves:

What is the future cost of doing nothing today?

When the future cost of doing nothing is the same as the current cost, postpone the decision.

When the information we have it is a must do them is time to make it. In the example we should wait for more information.



5. Writing code that Embraces Change.

We want to do an easy-to-change code even with uncertainty, and this is the way to do it:


  5.1 Write DRY code (Don't repeat yourself).

  5.2 Hide inheritance variables: wraping instance variables in methods(att_reader) and encapsulate them. Then we change the method from data to behavior.
        Data is when the instance variable with the code information is everywhere

        Behavior is when we encapsulate inside the method and we only define it once(only the method know what it is) and we use it everywhere. Then in the future it will be very easy to adjust when
        the data have new additions or
even some complex structures.

Bad

class Gear
  def initialize(chainring, cog)
    @chainrig = chainring
    @cog      = cog
  end

  def ratio
   @chainrig / @cog.to_f # <-?!  (╯°□°)╯︵ ┻━┻ 
  end
end

* Imagine that we use @cog 10 times inside the code. Then we need to adjust the code 10 times.

Good

class Gear
  attr_reader :chainring, :cog
  def initialize(chainring, cog)
    @chainrig = chainring
    @cog      = cog
  end

  def ratio
    chainrig / cog.to_f # <- (☞゚ヮ゚)☞ ᕕ( ᐛ )ᕗ
  end
end # ┬──┬◡ノ(° -°ノ) 

the cog method (encapsulated in attr_reader as :cog) is the only place in the code that understands what cog means, then, we chage cog from data (bad example was referenced all over) to behavior ( good example which is defined once).

In the last example we can reimplement the changes inside the method in one place and in this places is the only place that understands :cog behavior changes!

def cog
  @cog * new_easy_or_complex_adjustment # (ノ◕ヮ◕)ノ*:・゚✧
end

Implementing this method changes itslef from data(which is refferenced all over) to behavior (which is defined once).
 

Very nice! But wait! Now our data is kind of an object that uderstands messages, and this can be dangerous and generate confusion. The public method :cog can be accesed by  other objects in the application. any other object can ssend now cog method to a Gear class. *We hould create a private wrapping method *(It is covered in chapter 4 ).

Another more complex problem: Because we can wrap the instance variables in a method and use it as another object the distintion of data and regular object starts to be gone.  (⊙_☉)

In any case, we should hide data from ourselves, then we protect the code from unexpected changes that we neather can know. It is a good practice to send messages to acces variabeles, even if we think of them as data.

5.2 Hide Data Structures: if we store rim and tires in just @data with an att_reader :data and we have @data = [ [633, 18] [662, 17]... ] we will have a nightmare in case of change when the structur of the array changes.

How to separate the structur from maning? Struct does the job, by definition "Struct convert the way to bundle a number of attributes together using accessor methods without having to write an explicit class".

class IncredibleRevelation
  attr_reader :wheels
  def initialize(data)
    @wheels = wheelify(data)
  end
  
  def diameters
    wheels.collect { |wheel| wheel.rim + ( wheel.tire * 2)}
  end

  Wheel = Struct.new(:rim, :tire)
  def wheelify(data)
    data.collect { |cell| Wheel.new(cell[0], cell[1])}
  end
end # ◉_◉

The diameters method don't have knowledge about the internal structure of the array. All knowledege from the struture is encapsulated in the wheelify method, this method isolates the messy structural information and DRYs the code make it easier to changes. Now we have an array of Structs. Don't forget.

Hide the mess even from yourself

5.3 Enforce Single Responsibility Everywhere

 5.3.1 Extract Extra Responsibilities from Methods: In the diameter method avobe we can see that has more than one responsibility. Iteration over the wheels and diameter calculation.

def diameters
  wheels.collect { |wheel| diameter(wheel)}
end

def diameter(wheel)
  wheel.rim + ( wheel.tire * 2)
end

Doing these refactorings are needed because because the design is not clear. "Good practices reveal design".

Benefits of single  responsibility methods:
- Expose previously hidden qualities

- Avoid the need of comments
- Encourage reuse
- Are easy to move to another class

 5.3.2 Isolate Extra Responsibilities in Classes 

As we write changeable code it is better to postpone decisions until we are absolutely forced to make them

Don't guess, preserve your hability to make a decision later

In the next example we can see how to remove the responsibility for calculating tire and diameter from Gear with no need of a new class. We will use Wheel Sruct with a block that add a method to calculate the diameter.

class Gear
  attr_reader :chainring, :cog, :tire
  def initialize(chainring, cog, rim, tire)
    @chainring = chainring
    @cog       = cog
    @wheel     = Wheel.new(rim, tire)
  end

  def ratio
    chainrig / cog.to_f
  end

  def gear_inches
    ratio * wheel.diameter
  end

  Wheel = Struct.new(:rim, :tire) do
    def diamteter
      rim + (tire * 2)
    end
  end
end

If we find extra responsibilities that we cannot yet remove, isolate them.

The future is here and our friend told you that she needs the bycicle wheel circunference. We need to add a new method for that. And furthermore, our application has a explicit need for a Wheel class that we can use independently from Gear. As we carefully isolated Wheel  inside Wheel Struct this change is painless.
 

class Gear
  attr_reader :chainring, :cog, :wheel
  def initialize(chainring, cog, rim, wheel = nil)
    @chainring = chainring
    @cog = cog
    @wheel = wheel
  end

  def ratio
    chainrig / cog.to_f
  end

  def gear_inches
    ratio * wheel.diameter
  end
end

class Wheel
  attr_reader :rim, :tire
  def initialize( rim, tire)
    @rim = rim
    @tire = tire
  end

  def diameter
    rim + (tire *2 )
  end

  def circunference
    diameter * Math::PI
  end
end

@wheel = Wheel.new(26, 1.5)
puts @wheel.cincurference
# -> 91.106

puts Gear.new(52,11, @wheel).gear_inches
# -> 137.09

puts Gear.new(52, 11).ratio
# -> 4.7272

 

 

<<