These notes discuss the second chapter of Sandi Metz's book with the code snippets in Ruby. If you're new to the Ruby language, you may want to check out this simple tutorial.
If you missed chapter 1, here it is.
I've included quotes from the book and important notes. All Ruby codes here are taken from the book or slightly changed.
Let's start chapter 2 now...
Your goal is to model your application, using classes, such that it does what it is supposed to do right now and it is also easy to change later.
Sometimes you know how to write code but not where to put it. Once you know where to put that code, you can decide what belongs in a class.
Organizing Code to Allow for Easy Changes
The idea of easy is too broad; you need concrete definitions of easiness and specific criteria by which to judge code. If you define easy to change as:
- Changes have no unexpected side effects
- Small changes in requirements require correspondingly small changes in code
- Existing code is easy to reuse
- The easiest way to make a change is to add code that in itself is easy to change
Then the code you write should have the following qualities. Code should be:
- Transparent The consequences of change should be obvious in the code that is changing and in distant code that relies upon it
- Reasonable The cost of any change should be proportional to the benefits the change achieves
- 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
Code that is Transparent, Reasonable, Usable, and Exemplary (TRUE) not only meets today's needs but can also be changed to meet the needs of the future. The first step in creating code that is TRUE is to ensure that each class has a single, well-defined responsibility.
Creating Classes That Have a Single Responsibility
When you create a class, put this in your mind: Class always does the smallest possible useful thing. It shouldn't do two things. That is to say, it should have a single responsibility.
An Example Application: Bicycles and Gears
Let's design a class and try to apply single responsibility by solving a problem. Say, you want to buy a bike and you'd like to make sure what bike would be a better choice. That choice can depend on how long the bike will move once you push the pedal. Bicyclists compare that by comparing the teeth of two gears; the one at the front (chainring) and the rear one (cog).
The gear combining a 52-teeth chainring and an 11-teeth cog has a ratio of ~ 4.73 which means the rotational motion is converted to translational 5 times that round your feet do when they push the pedal.
Here is the simple class design quoted from the book:
Gear class is a subclass of
Object it inherits all its methods like
new so we pass the arguments of chainring and cog teeth to it. calling the ratio method calculates the ratio of both teeth dividing chainring by the cog.
Let's iterate on this class if you want to consider the wheel size.
Cyclists (at least in the US) use something called gear inches to compare bicycles that differ in both gearing and wheel size. The formula follows: gear inches = wheel diameter * gear ratio
where wheel diameter = rim diameter + twice tire diameter
Let's add this to our
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_inches ratio * (rim + (2 * tire)) end end puts Gear.new(52, 11, 26, 1.5).gear_inches # 137.0909090909091 puts Gear.new(52, 11, 24, 1.25).gear_inches # 125.27272727272728
There is a problem with this design because
ratio does not work anymore:
Let's ignore this bug for now and see if this class has a single responsibility or not.
Determining if a Class has a Single Responsibility
One way to determine if a class has a single responsibility or not is to ask the methods questions. For example, a question like Please Mr. Gear, what is your ratio? makes perfect sense, while Please Mr. Gear, what is your gear_inches? is on shaky ground, and Please Mr. Gear, what is your tire (size)? seems ridiculous.
Another way is to be able to describe the class in one sentence. When you use the word 'and' or 'or', the class likely has more than one responsibility and you need to avoid that.
Determining When to Make Design Decisions
Ask yourself, is this class really a Gear? How it has rims and tires?! Perhaps Gear should be Bicycle? or maybe there is a Wheel in here?
This "improve it now" versus "improve it later" tension always exists. Applications are never perfectly designed. Every choice has a price. A good designer understands this tension and minimizes costs by making informed tradeoffs between the needs of the present and the possibilities of the future.
Writing Code That Embraces Change
You want to create a code that depends on behavior, not data. A behavior is captured by methods. The "Don't Repeat Yourself" (DRY) phrase is a shortcut to writing a class that has a single responsibility.
One way to make your code changeable is to make hide instance variables just like we did with
cog for example. We used
attr_reader which causes Ruby to create a wrapper method for the variables. It virtually created this method for
so that you don't have to call the instance variable
@cog each time you need it.
Also, to make your code changeable you should hide data structure as in the following example:
Remember when we had the equation inside the
gear_inches which has
rim + (2 * tire). The above example shows that we can do that by defining a data structure of 2d array
data which explicitly shows that if you iterate over
data, you'll find rims are at the index 0 and tires are at the index 1.
This simple example is bad enough; imagine the consequences if
datareturned an array of hashes that were referenced in many places. A change to its structure would cascade through your code; each change represents an opportunity to create a bug so stealthy that your attempts to find it will make you cry.
We can solve this issue by applying the following:
wheelify method contains the only bit of code that understands the structure of the incoming array and the
diameters method only knows that the message
wheels returns an enumerable and that each enumerated thing responds to
Enforce Single Responsibility Everywhere
Single responsibility is not only applied to classes but also methods.
Looking at the
diameters method, we can see it has two responsibilities: it iterates over the
wheel and calculates the diameters:
What we need to do is to separate the two responsibilities into two different methods:
This way, you can use the
diameter method in other places inside your code. It's not overdesigned, it's just simplifying things for the future.
this method has two responsibilities that enforce us to isolate them:
here we calculated the wheel
diameter in a separate method and then the output of it is used to determine
This simple refactoring makes the problem obvious.
Gearis responsible for calculating
Gearshould not be calculating wheel diameter.
Gear class code further by adding
class Gear attr_reader :chainring, :cog, :wheel def initialize chainring, cog, rim, tire @chainring = chainring @cog = cog @wheel = Wheel.new(rim, tire) end def ratio chainring / cog.to_f end def gear_inches ratio * wheel.diameter end Wheel = Struct.new(:rim, :tire) do def diameter rim + (tire * 2) end end end
Finally, the Real Wheel
There is a feature request coming your way to calculate the circumference of the wheel. It's simple, right? It is, but the real change here is that little change forces us to make a new design decision which is to make a separate class for
Wheel independent of
Adding a new class in our case is simple because what you just need to do is to convert the structure data type that you made previously and add a circumference method to it.
Each class now has a single responsibility. The code is not perfect though, but it is good enough.
In this chapter, we've been introduced to the single responsibility and how changeable and maintainable object-oriented software is applied through classes that do one thing. Each class should be isolated from the rest of your application to make your code easy to change.
Note: Please note that these notes are not sufficient to get all the info from the book, these are my notes on the book. Sometimes I skip stuff and sometimes I rephrase statements based on my understandings and other times I write my own thoughts. If you want to get the complete knowledge behind the book, you've got to get the book and read it :)
See you in chapter 3 notes!
Get Practical Object-Oriented Design in Ruby now! (if you haven't already)
or this second edition (both links are affiliates)