Go offline with the Player FM app!
Single Responsibility
Manage episode 222982210 series 1900125
Chapter 2: Designing Classes with a Single Responsibility
The foundation of an object-oriented system is the message, but the most visible organizational structure is the class
Questions to ask yourself:
- What are your classes?
- How many should you have?
- What behavior will they implement?
- How much do they know about other classes?
- How much of themselves should they expose?
Creating Classes That Have a Single Responsibility
A class should do the smallest possible useful thing; that is, it should have a single responsibility
An Example Application: Bicycles and Gears
- Let's take a look at bikes. Consider the types of gears that bikes use
Small Gears
- easy to pedal, not as fast
- takes many pedals just to make the tires rotate once
- can help you creep along steep hills
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:
Large Gear
chainring = 52
cog = 11
ratio = chainring / cog.to_fputs 'Large Gear:'\
"\n#{chainring}-tooth chainring"\
"\n#{cog}-tooth cog"\
"\n#{ratio.round(2)} rotations"Small Gear
chainring = 30
cog = 27
ratio = chainring / cog.to_fputs "\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
, andratio
Gear
is a subclass ofObject
and thus inherits many other methods besides the three that we definedWhat 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
- Bigger wheels travel much farther during each wheel rotation versus smaller wheels
Consider this formula
gear inches = wheel diameter × gear ratio
(where)
wheel diameter = rim diameter + (2 × tire diameter)
see 2_gear.rb
- This new code is great except our old call to
Gear.new(52, 11)
no longer works because we added 2 more arguments to ourinitialize
method
Why Single Responsibility matters
- Applications that are easy to change consist of classes that are easy to reuse. [...] A class that has more than one responsibility are difficult to reuse
Determining If a Class Has a Single Responsibility
- How can you tell if your class is only doing a single thing? Try describing what it does in a single sentence. You'll find out very quickly
- Remember that a class should do the smallest possible useful thing
- When we look at our
Gear
class - perhaps it is doing too much - We are calculating
gear_inches
, which is fine - but calculating thetire
size seems a little weird
When to Make Design Decisions
- When we look at the
Gear
class, there's something off about havingrim
andtire
in there. - Right now the code in
Gear
is transparent and reasonable - this doesn't mean that we have great design. All it means is that we have no dependencies - Right now,
Gear
lies about its responsibilities as it has multiple responsibilities in that it has to do "wheel" calculations in ourgear_inches
message
Write Code That Embraces Change
Here are some techniques that help you write code that embraces change
Depend on Behavior, Not Data
- Behavior is captured in methods and invoked by sending messages
- Objects also contain data (not just behavior)
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
BAD
def ratio
@chainring / @cog.to_f
endGOOD
def ratio
chainring / cog.to_f
endIf 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
- Depending on a complicated data structure can also lead to a world of hurt
- For instance, if you create a method that expects the data structure is being passed to it to be an array of arrays with two items in each array - you create a dependency
see 3_obscuring_references.rb
- Ruby makes it easy to separate structure from meaning
- You can use a Ruby
Struct
class to wrap a structure
see 4_revealing_references.rb
- the
diameters
method now has no knowledge of the internal structure of the array diameters
just know that it has to respond torim
andtire
and nothing about the data structure- Knowledge of the incoming array is encapsulated in our
wheelify
method
Enforce Single Responsibility Everywhere
Extra Extra Responsibilities from Methods
def diameters wheels.collect { |wheel| wheel.rim + (wheel.tire * 2) } end
this method clearly has two responsibilities
- iterate over wheels
- calculate the diameter of each wheel
we can separate these into two methods that each have their own responsibility
def diameters
wheels.collect { |wheel| diameter(wheel) }
enddef diameter(wheel)
wheel.rim + (wheel.tire * 2)
endseparating iteration from the action that's being performed on each element is a common case of multiple responsibilities
Finally, the Real Wheel
- New feature request: program should calculate bicycle wheel circumference
- Now we can separate a
Wheel
class from ourGear
class
see 5_gear_and_wheel.rb
78 episodes
Manage episode 222982210 series 1900125
Chapter 2: Designing Classes with a Single Responsibility
The foundation of an object-oriented system is the message, but the most visible organizational structure is the class
Questions to ask yourself:
- What are your classes?
- How many should you have?
- What behavior will they implement?
- How much do they know about other classes?
- How much of themselves should they expose?
Creating Classes That Have a Single Responsibility
A class should do the smallest possible useful thing; that is, it should have a single responsibility
An Example Application: Bicycles and Gears
- Let's take a look at bikes. Consider the types of gears that bikes use
Small Gears
- easy to pedal, not as fast
- takes many pedals just to make the tires rotate once
- can help you creep along steep hills
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:
Large Gear
chainring = 52
cog = 11
ratio = chainring / cog.to_fputs 'Large Gear:'\
"\n#{chainring}-tooth chainring"\
"\n#{cog}-tooth cog"\
"\n#{ratio.round(2)} rotations"Small Gear
chainring = 30
cog = 27
ratio = chainring / cog.to_fputs "\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
, andratio
Gear
is a subclass ofObject
and thus inherits many other methods besides the three that we definedWhat 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
- Bigger wheels travel much farther during each wheel rotation versus smaller wheels
Consider this formula
gear inches = wheel diameter × gear ratio
(where)
wheel diameter = rim diameter + (2 × tire diameter)
see 2_gear.rb
- This new code is great except our old call to
Gear.new(52, 11)
no longer works because we added 2 more arguments to ourinitialize
method
Why Single Responsibility matters
- Applications that are easy to change consist of classes that are easy to reuse. [...] A class that has more than one responsibility are difficult to reuse
Determining If a Class Has a Single Responsibility
- How can you tell if your class is only doing a single thing? Try describing what it does in a single sentence. You'll find out very quickly
- Remember that a class should do the smallest possible useful thing
- When we look at our
Gear
class - perhaps it is doing too much - We are calculating
gear_inches
, which is fine - but calculating thetire
size seems a little weird
When to Make Design Decisions
- When we look at the
Gear
class, there's something off about havingrim
andtire
in there. - Right now the code in
Gear
is transparent and reasonable - this doesn't mean that we have great design. All it means is that we have no dependencies - Right now,
Gear
lies about its responsibilities as it has multiple responsibilities in that it has to do "wheel" calculations in ourgear_inches
message
Write Code That Embraces Change
Here are some techniques that help you write code that embraces change
Depend on Behavior, Not Data
- Behavior is captured in methods and invoked by sending messages
- Objects also contain data (not just behavior)
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
BAD
def ratio
@chainring / @cog.to_f
endGOOD
def ratio
chainring / cog.to_f
endIf 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
- Depending on a complicated data structure can also lead to a world of hurt
- For instance, if you create a method that expects the data structure is being passed to it to be an array of arrays with two items in each array - you create a dependency
see 3_obscuring_references.rb
- Ruby makes it easy to separate structure from meaning
- You can use a Ruby
Struct
class to wrap a structure
see 4_revealing_references.rb
- the
diameters
method now has no knowledge of the internal structure of the array diameters
just know that it has to respond torim
andtire
and nothing about the data structure- Knowledge of the incoming array is encapsulated in our
wheelify
method
Enforce Single Responsibility Everywhere
Extra Extra Responsibilities from Methods
def diameters wheels.collect { |wheel| wheel.rim + (wheel.tire * 2) } end
this method clearly has two responsibilities
- iterate over wheels
- calculate the diameter of each wheel
we can separate these into two methods that each have their own responsibility
def diameters
wheels.collect { |wheel| diameter(wheel) }
enddef diameter(wheel)
wheel.rim + (wheel.tire * 2)
endseparating iteration from the action that's being performed on each element is a common case of multiple responsibilities
Finally, the Real Wheel
- New feature request: program should calculate bicycle wheel circumference
- Now we can separate a
Wheel
class from ourGear
class
see 5_gear_and_wheel.rb
78 episodes
All episodes
×Welcome to Player FM!
Player FM is scanning the web for high-quality podcasts for you to enjoy right now. It's the best podcast app and works on Android, iPhone, and the web. Signup to sync subscriptions across devices.