This week we talk about creating classes, why single responsibility matters, and writing code that embraces change.
The foundation of an object-oriented system is the message, but the most visible organizational structure is the class
Questions to ask yourself:
A class should do the smallest possible useful thing; that is, it should have a single responsibility
Small Gears
Large Gears
harder to pedal, fast
sends you flying down those steep hills
one pedal rotation with your foot might cause the tires to rotate multiple times
Let's start with a small script and then extrapolate classes out of it:
chainring = 52
cog = 11
ratio = chainring / cog.to_f
puts 'Large Gear:'\
"\n#{chainring}-tooth chainring"\
"\n#{cog}-tooth cog"\
"\n#{ratio.round(2)} rotations"
chainring = 30
cog = 27
ratio = chainring / cog.to_f
puts "\nSmall Gear:"\
"\n#{chainring}-tooth chainring"\
"\n#{cog}-tooth cog"\
"\n#{ratio.round(2)} rotations"
Since we're talking about gears, it only makes sense that we start by creating a Gear
class based on the behavior above
see 1_gear.rb
Our Gear
class has three methods: chainring
, cog
, and ratio
Gear
is a subclass of Object
and thus inherits many other methods besides the three that we defined
What I'm trying to say is that the complete set of behavior / the total set of messages to which it can respond is fairly large
This is great and all - but what if we want to extend the functionality by taking into account the effect of the difference in wheels
Consider this formula
gear inches = wheel diameter × gear ratio
(where)
wheel diameter = rim diameter + (2 × tire diameter)
see 2_gear.rb
Gear.new(52, 11)
no longer works because we added 2 more arguments to our initialize
methodGear
class - perhaps it is doing too muchgear_inches
, which is fine - but calculating the tire
size seems a little weirdGear
class, there's something off about having rim
and tire
in there.Gear
is transparent and reasonable - this doesn't mean that we have great design. All it means is that we have no dependenciesGear
lies about its responsibilities as it has multiple responsibilities in that it has to do "wheel" calculations in our gear_inches
messageHere are some techniques that help you write code that embraces change
Hide Instance Variables
Always wrap instance variables in accessor methods instead of directly referring to variables, like the ratio
method does.
We can do this by using an attr_reader
def ratio
@chainring / @cog.to_f
end
def ratio
chainring / cog.to_f
end
If your instance variable is referred to multiple times and it suddenly needs to change, you're in for a world of hurt.
Your method that wraps your instance variable becomes the single source of truth
One drawback is that because you can wrap any instance variables in methods, its possible to obfuscate the distinction between data and objects
But the point is that you should be hiding data from yourself.
Hiding data from yourself protects code from unexpected changes
Hide Data Structures
see 3_obscuring_references.rb
Struct
class to wrap a structuresee 4_revealing_references.rb
diameters
method now has no knowledge of the internal structure of the arraydiameters
just know that it has to respond to rim
and tire
and nothing about the data structurewheelify
methodExtra Extra Responsibilities from Methods
def diameters
wheels.collect { |wheel| wheel.rim + (wheel.tire * 2) }
end
this method clearly has two responsibilities
we can separate these into two methods that each have their own responsibility
def diameters
wheels.collect { |wheel| diameter(wheel) }
end
def diameter(wheel)
wheel.rim + (wheel.tire * 2)
end
separating iteration from the action that's being performed on each element is a common case of multiple responsibilities
Wheel
class from our Gear
classsee 5_gear_and_wheel.rb