POODR by Sandy Metz. Chapter 2 Designing Classes with a Single Responsibility
#POODR #Sandy #Metz #chapter2 #Design #Classes #Single #Responsibility #easy #Changes
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