Skip to content

Instantly share code, notes, and snippets.

@rickpeyton
Last active November 23, 2017 02:02
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rickpeyton/1d84daaf8d4dbc487fa1baa14cee2480 to your computer and use it in GitHub Desktop.
Save rickpeyton/1d84daaf8d4dbc487fa1baa14cee2480 to your computer and use it in GitHub Desktop.
Notes from POODR - Practical Object Oriented Design in Ruby

Notes from Practical Object Oriented Design in Ruby

Chapter 1

The Problem Design Solves

Something will change. It always does. The customers didn’t know what they wanted, they didn’t say what they meant. You didn’t understand their needs, you’ve learned how to do something better. Even applications that are perfect in every way are not stable. The application was a huge success, now everyone wants more. Change is unavoidable. It is ubiquitous, omnipresent, and inevitable

Changing requirements are the programming equivalent of friction and gravity. They introduce forces that apply sudden and unexpected pressures that work against the best-laid plans. It is the need for change that makes design matter.

Object-oriented design is about managing dependencies. It is a set of coding tech- niques 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.

You must not only write code for the feature you plan to deliver today, you must also cre- ate code that is amenable to being changed later.

Practical design does not anticipate what will happen to your application, it merely accepts that something will and that, in the present, you cannot know what. It doesn’t guess the future; it preserves your options for accommodating the future. It doesn’t choose; it leaves you room to move.

In 2001, Laing and Coleman exam- ined several NASA Goddard Space Flight Center applications (rocket science) with the express intention of finding “a way to produce cheaper and higher quality software.”3 They examined three applications of varying quality, one of which had 1,617 classes and more than 500,000 lines of code. Their research supports the earlier studies and further confirms that design principles matter.

Even if you never read these studies you can be assured of their conclusions. The principles of good design represent measurable truths and following them will im- prove your code.

Laing and Coleman NASA Study Power Point

Laing and Coleman NASA PDF

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).

BUFD inevitably leads to an adversarial relationship between customers and pro- grammers. Because any big design created in advance of working software cannot be correct, to write the application as specified guarantees that it will not meet the cus- tomer’s needs. Customers discover this when they attempt to use it. They then request changes. Programmers resist these changes because they have a schedule to meet, one that they are very likely already behind. The project gradually becomes doomed as participants switch from working to make it succeed to striving to avoid being blamed for its failure.

Agile works because it acknowledges that certainty is unattainable in advance of the applica- tion’s existence; Agile’s acceptance of this truth allows it to provide strategies to over- come the handicap of developing software while knowing neither the target nor the timeline.

Class-based OO languages like Ruby allow 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. The same method name can be defined by many different objects; it’s up to Ruby to find and invoke the right method of the correct object for any sent message.

OO languages are thus open-ended. They don’t limit you to a small set of built-in types and pre-predefined operations; you can invent brand new types of your own. Each OO application gradually becomes a unique programming language that is specifically tailored to your domain.

Notes from Practical Object Oriented Design in Ruby

Chapter 2

Designing Classes with a Single Responsibility

Your goal is to model your application, using classes, such that it does what it is supposed to do right now and is also easy to change later.

Define easy to change as

  • Changes have no unexpected side effects
  • Small changes in requirements require correspondingly small changes in code
  • Existing code is easy to reuse
  • The easiest way to make a change is to add code that in itself is easy to change

Code should be

  • Transparent The consequences of change should be obvious in the code that is changing and in distant code that relies upon it
  • Reasonable The cost of any change should be proportional to the benefits the change achieves
  • Usable Existing code should be usable in new and unexpected contexts
  • Exemplary The code itself should encourage those who change it to perpetuate these qualities

Code that is Transparent, Reasonable, Usable, and Exemplary (TRUE) not only meets today’s needs but can also be changed to meet the needs of the future. The first step in creating code that is TRUE is to ensure that each class has a single, well-defined responsibility.

How can you determine if the Gear class contains behavior that belongs somewhere else? One way is to pretend that it’s sentient and to interrogate it. If you rephrase every one of its methods as a question, asking the question ought to make sense. 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.

Another way to hone in on what a class is actually doing is to attempt to describe it in one sentence. Remember that a class should do the smallest possible useful thing. That thing ought to be simple to describe. 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. That everything the class does be highly related to its purpose.

When the future cost of doing nothing is the same as the current cost, postpone the decision (whether to address the class design). Make the decision only when you must with the information you have at that time.

Behavior is captured in methods and invoked by sending messages. When you create classes that have a single responsibility, every tiny bit of behavior lives in one and only one place. The phrase “Don’t Repeat Yourself” (DRY) is a shortcut for this idea. DRY code tolerates change because any change in behavior can be made by changing code in just one place.

Always wrap instance variables in accessor methods instead of directly referring to variables, like the ratio method does below:

class Gear
  def initialize(chainring, cog)
    @chainring = chainring
    @cog = cog
  end

  def ratio
    @chainring / @cog.to_f  # <-- road to ruin
  end
end

Hide the variables, even from the class that defines them, by wrapping them in methods. Ruby provides attr_reader as an easy way to create the encapsulating methods:

class Gear
  attr_reader :chainring, :cog # <-------

  def initialize(chainring, cog)
    @chainring = chainring
    @cog = cog
  end

  def ratio
    chainring / cog.to_f
  end
end

attr_reader :cog is equivalent to

def cog
  @cog
end

This cog method is now the only place in the code that understands what cog means. Cog becomes the result of a message send. Implementing this method changes cog from data (which is referenced all over) to behavior (which is defined once).

When you have data in an array it’s not long before you have refer- ences to the array’s structure all over. The references are leaky. They escape encapsu- lation and insinuate themselves throughout the code. They are not DRY.

Direct references into complicated structures are confusing, because they obscure what the data really is, and they are a maintenance nightmare, because every reference will need to be changed when the structure of the array changes.

Just as you can use a method to wrap an instance variable, you can use the Ruby Struct class to wrap a structure.

Page 28 Struct Example

class RevealingReferences
  attr_reader :wheels
  def initialize(data)
    @wheels = wheelify(data)
  end

  def diameters
    wheels.collect do |wheel|
      wheel.rim + (wheel.tire * 2)
    end
  end
  # ... now everyone can send rim/tire to wheel

  Wheel = Struct.new(:rim, :tire)
  def wheelify(data)
    data.collect { |cell| Wheel.new(cell[0], cell[1]) }
  end
end

The official Ruby documentation (http://ruby-doc.org/core/classes/Struct.html) defines Struct as “a convenient way to bundle a number of attributes together, using accessor methods, without having to write an explicit class.” This is exactly what wheelify does; it creates little lightweight objects that respond to rim and tire.

Single Responsibility Methods

Methods, like classes, should have a single responsibility. All of the same reasons apply; having just one responsibility makes them easy to change and easy to reuse. All the same design techniques work; ask them questions about what they do and try to describe their responsibilities in a single sentence.

The following method clearly has two responsibilities: it iterates over the wheels and it calculates the diameter of each wheel.

def diameters
  wheels.collect { |wheel|
    wheel.rim + (wheel.tire * 2) }
end

Break that into two methods

# first - iterate over the array
def diameters
  wheels.collect { |wheel| diameter(wheel) }
end

# second - calculate diameter of ONE wheel
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 responsibility that is easy to recognize.

This

def gear_inches
  ration * diameter
end

def diameter
  rim + (tire * 2)
end

Not

def gear_inches
  ratio * (rim + (tire * 2))
end

Do these refactorings even when you do not know the ultimate design. They are needed, not because the design is clear, but because it isn’t. You do not have to know where you’re going to use good design practices to get there. Good practices reveal design.

In this example you have a wheel diameter that was being calculated inside of a gear class. It would make sense to go ahead and create a wheel class, but if want to delay that decision you can calculate the diapeter inside of the struct for now.

class Gear
  attr_reader :chainring, :cog, :wheel
  def initialize(chainring, cog, rim, tire)
    @chainring = chainring
    @cog = cog
    @wheel = Wheel.new(rim, tire)
  end

  def ratio
    chainring / cog.to_f
  end

  def gear_inches
    ratio * wheel.diameter
  end

  Wheel = Struct.new(:rim, :tire) do
    def diameter
      rim + (tire * 2)
    end
  end
end

Notes from Practical Object Oriented Design in Ruby

Chapter 3

Recognizing Dependencies

An object has a dependency when

  • The object knows the name of another class. Book example showed a how a Gear expects a class named Wheel to exist.
  • The object knows the name of a message (method) it itends to send to an an instance other than self. Book example was a Gear that expects a Wheel instance to respond to diameter.
  • The objects knows the arguments that a message (method) on another class requires. Example was that Gear knows that Wheel.new requires both a rim and a tire.
  • The object knows the order of the arguments required by another class. Example was that Gear knows the first argument to Wheel.new should be rim and the second should be tire.

Each of those dependencies creates a chance that Gear will be forced to change because of a change to Wheel.

Your design challenge is to manage dependencies so that each class has the fewest possible dependencies. A class should know just enough to do its job and not one thing more.

Coupling Between Objects (CBO)

Dependencies couple objects together. The more Gear knows about Wheel, the more tightly coupled they are. And the more they know about one another the more they behave like a single entity.

** When objects are tightly coupled like Gear and Wheel you are probably going to have to change Wheel any time you change Gear. If you want to reuse Gear, Wheel comes along for the ride. And if you want to test Gear you are going to have to test Wheel too. **

Law of Demeter

  • An object should have only limited knowledge about other objects and only of objects closely related to the current object
  • An object should only talk to its friends; don't talk to strangers
  • Only talk to your immediate friends
  • Object A can request an instance method of object B, but object A should not reach through object B to access yet another object.

** This may result in having to write many wrapper methods to propagate calls to components **

When Gear hard-codes a reference to Wheel deep inside its gear_inches method, it is explicitly declaring that it is only willing to calculate gear inches for instances of Wheel. Gear refuses to collaborate with any other kind of object, even if that object has a diameter and uses gears.

Gear does not care and should not know about the class of that object. It is not necessary for Gear to know about the existence of the Wheel class in order to calculate gear_inches. It doesn’t need to know that Wheel expects to be initialized with a rim and then a tire; it just needs an object that knows diameter.

Think of every dependency as an alien bacterium that‘s trying to infect your class. Give your class a vigorous immune system; quarantine each dependency. Dependencies are foreign invaders that represent vulnerabilities, and they should be concise, explicit, and isolated.

Isolate Dependencies

If you are so constrained that you cannot change the code to inject a Wheel into a Gear, you should isolate the creation of a new Wheel inside the Gear class. The intent is to explicitly expose the dependency while reducing its reach into your class.

One option is to put the creation of the Wheel object into the initialize method. This publicly exposes the dependency. This also unconditionally creates a new wheel each time a new Gear is created.

The other option is to create a wheel method that lazily (via the ||= operator) creates a new instance of Wheel when requested.

Both of these examples leave Gear knowing too much about Wheel (it still requires a rim and tire in initialization), but it is an improvment over creating a wheel in the gear_inches method.

If you are mindful of dependencies and develop a habit of rou- tinely injecting them, your classes will naturally be loosely coupled.

Even something as simple as

def gear_inches
  ratio * wheel.diameter
end

Is a dependency. And that can be cleaned up by moving wheel.diameter into its own class. That is probably overkill in this case, but if there was more going on in that method you would want to do this to preemptively DRY up your code and remove the hidden dependency.

n the original code, gear_inches knew that wheel had a diameter. This knowledge is a dangerous dependency that couples gear_inches to an external object and one of its methods. After this change, gear_inches is more abstract. Gear now isolates wheel.diameter in a separate method and gear_inches can depend on a message sent to self.

Remove Argument-Order Dependencies

def initialize(chainring, cog, wheel) has three dependencies every time new is called:

  1. It requires three arguments
  2. It requires that they be in a specific order
  3. It requires that they specify a value. There is no default provided

** Use Hashes for Initialization Arguments to reduce dependency **

** Use the fetch methods when you have a required argument. It expects the key you're fetching to be in the hash and provides options for handling missing keys. **

Example:

def initialize(args)
  @chainring = args[:chainring] || 40
end

This means that @chainring can not be nil or false it has to have a value.

If you want to handle nil or false differently use fetch

def initialize(args)
  @chainring = args.fetch(:chainring, 40)
end

Now if :chainring is not found it can be nil or false and if it is found it can default to 40

You can also completely remove the defaults from initialize and isolate them inside of a separate wrapping method. The defaults method below defines a second hash that is merged into the options hash during initialization. In this case, merge has the same effect as fetch; the defaults will get merged only if their keys are not in the hash.

def initialize(args)
  args = defaults.merge(args)
  @chainring = args[:chainring]
  ...
end

def defaults
  { :chainring: 40, cog: 18 }
end

This isolation technique is perfectly reasonable for the case above, but it is especially useful when the defaults are more complicated. If your defaults are more than simple numbers or strings, implement a defaults method.

Isolate Multiparameter Initialization (external dependencies)

The the case of a class where the initialization method requires a fixed-order of arguments and that method is outside your control (part of the framework, introduced by a third party library, etc) you can DRY up this process by using a single method to wrap the external interface and isolate the external dependencies.

In this example, the SomeFramework::Gear class is not owned by your application; it is part of an external framework. Its initialization method requires fixed-order argu- ments. The GearWrapper module was created to avoid having multiple dependencies on the order of those arguments. GearWrapper isolates all knowledge of the external interface in one place and, equally importantly, it provides an improved interface for your application.

# When Gear is part of an external interface
module SomeFramework
  class Gear
    attr_reader :chainring, :cog, :wheel
    def initialize(chainring, cog, wheel)
      @chainring = chainring
      @cog       = cog
      @wheel     = wheel
    end
    
    # blah
  end
end

# wrap the interface to protect yourself from changes
module GearWrapper
  def self.gear(args)
    SomeFramework::Gear.new(args[:chainring],
                            args[:cog],
                            args[:wheel])
  end
end

# Now you can create a new Gear using an arguments hash.
GearWrapper.gear(
  chainring: 52,
  cog:       11,
  wheel:     Wheel.new(26, 1.5)).gear_inches

There are two things to note about GearWrapper. First, it is a Ruby module instead of a class. GearWrapper is responsible for creating new instances of SomeFramework::Gear. Using a module here lets you define a separate and distinct object to which you can send the gear message (line 24) while simultaneously con- veying the idea that you don’t expect to have instances of GearWrapper. You may already have experience with including modules into classes; in the example above GearWrapper is not meant to be included in another class, it’s meant to directly respond to the gear message.

The other interesting thing about GearWrapper is that its sole purpose is to create instances of some other class. Object-oriented designers have a word for objects like this; they call them factories.

The above technique for substituting an options hash for a list of fixed-order arguments is perfect for cases where you are forced to depend on external interfaces that you cannot change. Do not allow these kinds of external dependencies to permeate your code; protect yourself by wrapping each in a method that is owned by your own application.

Classes that have many dependencies should be abstract interfaces. They can have more dependencies because they are abstractions and are less likely to change than the less abstract classes they abstract from. Those classes should have few dependencies because they are more likely to change.

Depend on things that change less often than you do is a heuristic that stands in for all the ideas in this section. The zones are a useful way to organize your thoughts but in the fog of development it may not be obvious which classes go where. Very often you are exploring your way to a design and at any given moment the future is unclear. Following this simple rule of thumb at every opportunity will cause your application to evolve a healthy design.

** A class should only depend on classes that change less often that it does**

Chapter 4

Creating Flexible Interfaces

Public Interfaces

  • Reveal the methods primary responsibility
  • Are expect to be invoked by others
  • Will not change on a whim
  • Are safe for others to depend on
  • Are throroughly documented in tests

Private Interfaces

  • Handle implementation details
  • Are not expected to be send by other objects
  • Can change for any reason whatsoever
  • Are unsafe for others to depend on
  • May not even be referenced in the tests

Public methods should read like a description of responsibilities. It is the contract that articulates the responsibilities of your class.

Constructing the Design

If you fixate on domain objects you will tend to coerce behavior into them. Design experts notice domain objects without concentrating on them; they focus not on these objects but on the messages that pass between them. These messages are guides that lead you to discover other objects, ones that are just as necessary but far less obvious.

Using Sequence Diagrams

There is a perfect, low-cost way to experiment with objects and messages: sequence diagrams.

This transition from class-based design to message-based design is a turning point in your design career. The message-based perspective yields more flexible applications than does the class-based perspective. Changing the fundamental design question from “I know I need this class, what should it do?” to “I need to send this message, who should respond to it?” is the first step in that direction.

You don’t send messages because you have objects, you have objects because you send messages.

Asking for "What" Instead of Telling "How"

The distinction between a message that asks for what the sender wants and a message that tells the receiver how to behave may seem subtle but the consequences are significant. Understanding this difference is a key part of creating reusable classes with well-defined public interfaces.

If objects were human and could describe their own relationships, in Figure 4.5 Trip would be telling Mechanic: “I know what I want and I know how you do it;” in Figure 4.6: “I know what I want and I know what you do” and in Figure 4.7: “I know what I want and I trust you to do your part.” This blind trust is a keystone of object-oriented design. It allows objects to collab- orate without binding themselves to context and is necessary in any application that expects to grow and change.

Create Explicit Interfaces

Your goal is to write code that works today, that can easily be reused, and that can be adapted for unexpected use in the future. Other people will invoke your methods; it is your obligation to communicate which ones are dependable.

Every time you create a class, declare its interfaces. Methods in the public interface should

  • Be explicitly identified as such
  • Be more about what than how
  • Have names that, insofar as you can anticipate, will not change
  • Take a hash as an options parameter

Protected, and Private Keywords

The private keyword denotes the least stable kind of method and provides the most restricted visibility. Private methods must be called with an implicit receiver, or, inversely, may never be called with an explicit receiver.

The protected keyword also indicates an unstable method, but one with slightly different visibility restrictions. Protected methods allow explicit receivers as long as the receiver is self or an instance of the same class or subclass of self.

The Law of Demeter

The Law of Demeter (LoD) is a set of coding rules that results in loosely coupled objects. Loose coupling is nearly always a virtue but is just one component of design and must be balanced against competing needs. Some Demeter violations are harmless, but others expose a failure to correctly identify and define public interfaces.

Demeter restricts the set of objects to which a method may send messages; it prohibits routing a message to a third object via a second object of a different type. Demeter is often paraphrased as "only talk to your immediate neighbors" or "use only one dot."

customer.bicycle.wheel.tire
customer.bicycle.wheel.rotate
hash.keys.sort.join(', ')

Each line is a message chain containing a number of dots (periods). These chains are colloquially referred to as train wrecks; each method name represents a train car and the dots are the connections between them. These trains are an indication that you might be violating Demeter.

An aside: The Ruby Delegate Method

You can use delete in place of method chaining, but be wary of the Law of Demeter.

You should only use this to access methods and not ever to change behavior.

Demeter is trying to tell you something and it isn’t “use more delegation.”

class Post
  belongs_to :user
end

class User
  has_many :posts
end

class Post
  belongs_to :user
  delegate :name, :to => :user, :allow_nil => true
end

class Post
  belongs_to :user

  delegate :name, :to => :user, :prefix => true
  # post.user_name

  delegate :name, :to => :user, :prefix => "author"
  # post.author_name
end

Reaching across disparate objects to invoke distant behavior is tantamount to saying, “there’s some behavior way over there that I need right here and I know how to go get it.” The code knows not only what it wants (to rotate) but how to navigate through a bunch of intermediate objects to reach the desired behavior. Just as Trip, earlier, knew how Mechanic should prepare a bike and so was tightly coupled to Mechanic, here the depart method knows how to navigate through a series of objects to make a wheel rotate and therefore is tightly coupled to your overall object structure.

Summary

Object-oriented applications are defined by the messages that pass between objects. This message passing takes place along “public” interfaces; well-defined public inter- faces consist of stable methods that expose the responsibilities of their underlying classes and provide maximal benefit at minimal cost. Focusing on messages reveals objects that might otherwise be overlooked. When messages are trusting and ask for what the sender wants instead of telling the receiver how to behave, objects naturally evolve public interfaces that are flexible and reusable in novel and unexpected ways.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment