Skip to content

Instantly share code, notes, and snippets.

@speric
Last active March 25, 2025 01:06
Show Gist options
  • Save speric/31ae0987d21eac1d4f87 to your computer and use it in GitHub Desktop.
Save speric/31ae0987d21eac1d4f87 to your computer and use it in GitHub Desktop.
Notes From "Practical Object-Oriented Design In Ruby" by Sandi Metz

Chapter 1 - Object Oriented Design

The purpose of design is to allow you to do design later, and it's primary goal is to reduce the cost of change.

SOLID Design:

  • Single Responsibility Principle: a class should have only a single responsibility
  • Open-Closed Principle: Software entities should be open for extension, but closed for modification (inherit instead of modifying existing classes).
  • Liskov Substitution: Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.
  • Interface Segregation: Many client-specific interfaces are better than one general-purpose interface.
  • Dependency Inversion: Depend upon Abstractions. Do not depend upon concretions. See Dependency Injection

Other principles include:

  • Do Not Repeat Yourself: Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.
  • Law of Demeter: A given object should assume as little as possible about the structure or properties of anything else.

Chapter 2 - Designing Classes with a Single Responsibility

A class should do the smallest possible useful thing.

Applications that are easy to change consist of classes that are easy to resue.

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 it's methods as a question, asking the question ought to make sense. For example, asking "Gear, what is your ratio?" seems perfectly reasonable..."Gear, what is your tire size?" is just downright ridiculous.

Depend On Behavior, Not Data

Hide instance variables. [Code example]

Hide data structures. [Code example]

Chapter 3 - Managing Dependencies

An object depends on another object if, when one object changes, the other might be forced to change in turn.

Recognizing Dependencies

An object has a dependency when it knows:

  • The name of another class
  • The name of a message that it intends to send to someone other than self (methods on other objects).
  • The arguments that a message requires.
  • The order of those arguments.

Your design challenge is to manage dependencies so that each class has the fewest possible; a class should know just enough to do it's job and not one thing more.

The more one class knows about another, the more tightly it is coupled.

Test-to-code over-coupling has the same consequence as code-to-code over-coupling.

Factory: an object whose purpose is to create other objects.

Depend on things that change less often than you do.

  • Some classes are more likely than others to have changes in requirements
  • Concrete classes are more likely to change than abstract classes
  • Changing a class that has many dependents will result in widespread consequences

Abstraction in Ruby = duck typing; depending on an interface (objects will respond to methods) rather than a concrete class implementation.

Chapter 4 - Creating Flexible Interfaces

Domain objects are easy to find but they are not at the design center of your application. They are a trap for the unwary. 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.

Using a kitchen analogy: your objects should "order off a menu" instead of "cooking in the kitchen".

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.

Ask for "what" instead of telling "how".

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

Context: the things that an object knows about other objects. Objects that have a simple context are easy to test, objects with a complicated context are more difficult to test.

The best possible situation is for an object to be completely independent of it's context (dependency injection). I know what I want and I trust you to do your part.

Law of Demeter

Only talk to your immediate neighbors

It's a "law" in the sense of a guideline, not a hard and fast rule.

Balance the likelihood and cost of change against the cost of removing the violation.

...Demeter is more subtle than it appears. It's fixed rules are not an end in themselves; like every design principle, it exists in service of your overall goals. Certain "violations" of Demeter reduce your application's flexibility and maintainability, while others make perfect sense.

The problem with Demeter violations (like customer.bicycle.wheel.rotate) is that they show that code (customer) knows too much about how other code works. It's a manifestation of tight coupling.

The train wrecks of Demeter violations are clues that there are objects whose public interfaces are lacking.

Chapter 5 - Reducing Costs with Duck Typing

Duck types = public interfaces not tied to any specific class

It's not what an object is that matters, it's what it does.

Concrete code is easy to understand, but costly to extend. Abstract code may initially seem more obscure but, once understood, is far easier to change.

Once you begin to treat your objects as if they are defined by their behavior rather than by their class, you enter a new realm of expressive design.

Recognizing Hidden Ducks

Case statements that switch on class, kind_of?, is_a?, and responds_to? are potential ducks.

####Chapter 6 - Acquiring Behavior Through Inheritance

Inheritance is, at it's core, a mechanism for automatic message delegation. It defines a forward path for not-understood messages.

Subclasses are specializations of their superclasses

Everything the parent class is, plus more.

Template Method pattern

Any class that implements the template method pattern must supply an implementation for every message it sends, even if the only reasonable implementation in the sending class looks like:

class Bicycle
  #...
  def default_tire_size
    raise NotImplementedError
  end
end

See https://github.com/skmetz/poodr/blob/master/chapter_6.rb#L358 for an example of refactoring a base class and two subclasses with the template method pattern

Chapter 7 - Sharing Role Behavior With Modules

Combining the qualities of two existing subclasses is something Ruby cannot do (multiple inheritance)

Because no design technique is free, creating the most cost-effective application requires making informed tradeoffs between the relative cost and likely benefits of alternatives

Classical inheritance vs module inclusion can be thought of as is a? vs behaves like.

Method Lookup Flow

  • Including a module inserts it's method "above" the superclass of the including class (before Object), in the object hierarchy.
  • Therefore, if a method exists anywhere in the hierarchy between subclass and superclass, and also in an included module, the superclass method wins out.

When a single class includes several different modules, the modules are placed in the method lookup path in the reverse order of module inclusion. Thus, the methods of the last included module are encountered first in the lookup path.

_Note: Sandi mentions that the above image could in some cases be more complicated, but for most programmers it's ok to think of the object hierarchy in this way_

Writing Inheritable Code

...when a sending object checks the class of a received object to determine what message to send, you have overlooked a duck type. This is another maintenance nightmare; the code must change everytime you introduce a new class of receiver. In this situation all of the possible receiving objects play a common role. They should be codified as a duck type and receivers should implement the duck type's interface. Once they do, the original object can send one single message to every receiver, confident that because each receiver plays the role it will understand the common message.

Insist On The Abstraction

If you cannot correctly identify the abstraction there may not be one, and if no common abstraction exists then inheritance is not the solution to your design problem.

Copy link

ghost commented Oct 18, 2015

However I will only update this page once,

@lchanmann
Copy link

Great summary. One little typo "ibjects" in Chapter 8 but still a great job.

@tucq88
Copy link

tucq88 commented Feb 24, 2017

Some images are broken

@mdkalish
Copy link

resue :D

@keitheous
Copy link

resue =)

Thanks for this mate

@elissonmichael
Copy link

Thanks for sharing this.

@amuhle
Copy link

amuhle commented Feb 13, 2019

Thanks for this!! I loved this book but I didn't take the time to make a summary like this. Super useful!!

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