Despite the importance of correctly grouping methods into classes, at this early stage of your project you cannot possibly get it right. You will never know less than you know right now.
A class should do the smallest possible useful thing; that is, it should have a single responsibility.
If you read through the description above looking for nouns that represent objects in the domain you'll see words like bicycle and gear. These nouns represent 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, so, as yet, it does not qualify. Gear however, has chainrings, cogs, and rations, that is, it has both data and behavior. 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
puts Gear.new(30, 27).ratio
Enhancement: I have two bicycles; they have the same gearing but different wheel sizes. Add possibility to calculate the effect of the difference in wheels.
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 + (tire * 2))
end
end
puts Gear.new(52, 11, 26, 1.5).gear_inches
puts Gear.new(52, 11, 26, 1.25).gear_inches
This however introduced a bug:
puts Gear.new(52, 11).ratio # ArgumentError
Gear.initialize
was changed to require two additional arguments, and this broke all existing callers of the method.
Applications that are easy to change consist of classes that are easy to reuse. Reusable classes are pluggable units of well-defined behavior that have few entanglements. An application that is easy to change is like a box of building blocks; you can select just the pieces you need and assemble them in unanticipated ways.
A class that has more than one responsibility is difficult to reuse. The various responsibilities are likely thoroughly entangled within the class. If you want to reuse some (but not all) of its behavior, it is impossible to get at only the parts youn need. You are faced with two options and neither is particularly appealing.
How can you determine if the Gear
class contains behavior that belongs somewhere else? One way is to pretend that it's entient and to interrogate it. If you rephrase every one of its methods as a question, asking the question ought to make sens. For example, “Please Mr. Gear, what is your ratio?” seems perfectly reasonable, while “Please Mr. Gear, what are your gear_inches?” is on shaky ground, and “Please Mr. Gear, what is your tire (size)?” is just downright ridiculous.
Don't resist the idea that “what is your tire?” is a question that can legitimately be asked. From inside the Gear
class, tire
may feel like a different kind of thing than ratio
or gear_inches
, but that means nothing. From the point of view of every other object, anything that Gear
can respond to is just another message. If Gear
responds to it, someone will send it, and that sender may be in for a rude surprise when Gear
changes.
If the simplest description you can devise uses the word “and,” the class likely has more than one responsibility. If it uses the word “or,” then the class has more than one responsibility and they aren't even very related.
When everything in a class is related to its central purpose, the class is said to be highly cohesive or to have a single responsibility. The Single Responsibility Principle (SRP) doesn't require that a class do only one very narrow thing or that it change for only a single nitpicky reason, instead SRP requires that a class be cohesive - that everything the class does be highly related to its purpose.
Always wrap instance variables in accessor methods instead of directly referring to variables:
class Gear
def initialize(chainring, cog)
@chainring = chainring
@cog = cog
end
def ratio
@chainring / @cog.to_f # <- road to ruin
end
end
If the @cog
instance variable is referred to ten times and it suddenly needs to be adjusted, the code will need many changes. However, if @cog
is wrapped in a method, you can change what cog means by implementing your own version of the method.
Wrapping the @cog
instance variable in a public cog
method exposes this variable to the other objects in your application; any other object can now send cog
to a Gear
. It would have been just as easy to create a private wrapping method, one that turns the data into behavior without exposing that behavior to the entire application.
Because it's possible to wrap every instance variable in a mathod and to therefore treat any variable as it it's just another object, the distinction between data and a regular object begins to disappear. While it's sometimes expedient to think of parts of your application as behavior-less data, most things are better thought of as plain old objects.