User Tools

Site Tools


inf:ruby:oop

Chapter 1. Object-Oriented Design

  • Object-oriented design is about managing dependencies. It is a set of coding techniques that arrange dependencies such that objects can tolerate change. In the absence of design, unmanaged dependencies wreak havoc because objects know too much about one another. Changing one object forces change upon its collaborators, ad infinitum. A seemingly insignificant enhancement can cause damage that radiates outward in overlapping concentric circles, ultimately leaving no code untouched.
  • SOLID represents five of the most well known principles of object-oriented design: Single Responsibility, Open-Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion. Other principles include DRY (Don't Repeat Yourself) and the Law of Demeter (LoD).
  • In addition to principles, object-oriented design involves patterns. Gang of Four (GoF) in their Design Patterns book describe patterns as “simple and elegant solutions to specific problems in object-oriented software design” that you can use to “make your own designs more flexible, modular, reusable and understandable”.
  • Agile believes that your customers can't define the software they want before seeing it, so it's best to show them sooner rather than later. If this premise is true, then it logically follows that you should build software in tiny increments, gradually iterating your way into an application that meets the customer's true need. Agile believes that the most cost-effective way to produce what customers really want is to collaborate with them, building software one small bit at a time such that each delivered bit has the opportunity to alter ideas about the next. The Agile experience is that this collaboration produces software that differs from what was initially imagined; the resulting software could not have been anticipated by any other means.
  • If Agile is correct, two other things are also true. First, there is absolutely no point in doing a Big Up Front Design (BUFD) (because it cannot possibly be correct), and second, no one can predict when the application will be done (because you don't know in advance what it will eventually do).
  • Ruby allows you to define a class that provides a blueprint for the construction of similar objects. A class defines methods (definitions of behavior) and attributes (definitions of variables). Methods get invoked in response to messages.

Chapter 2. Designing Classes with a Single Responsibility

  • 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.
inf/ruby/oop.txt · Last modified: 2021/02/16 09:56 (external edit)