DEV Community

Iteration Podcast

Single Responsibility

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_f

    puts '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_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

    • 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 our initialize 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 the tire size seems a little weird

When to Make Design Decisions

  • When we look at the Gear class, there's something off about having rim and tire 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 our gear_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
    end

    GOOD

    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

  • 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 to rim and tire 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) }
    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

Finally, the Real Wheel

  • New feature request: program should calculate bicycle wheel circumference
  • Now we can separate a Wheel class from our Gear class

see 5_gear_and_wheel.rb

Episode source