Casey Brant

⬅︎ back to blog index

Notes from Practical Object-Oriented Design in Ruby

March 20, 2017

This is a braindump of the notes I took while reading this excellent book by Sandi Metz. No attempt was made to structure or summarize beyond what I personally found useful as reference material. I'm publishing it here in case someone else finds it useful, but really, you owe it to yourself to just read the whole book!

1. Object-Oriented Design

  • Design is the art of arranging code in such a way that it is easy to change.
  • Dependencies, mismanaged, are what make code hard to change.
    • When objects know too much about one another, changes ripple through a whole system.
  • The Tools of Design
    • Principles are guidelines like Law of Demeter, SOLID, etc., that are suggestions laid down by experienced practitioners.
    • Patterns are names given to specific arrangements of code that keep popping up. These are powerful because they give us a common language to discuss things.
  • Design as late as possible! Postpone decisions until you have the most information. Code in a way that lets you postpone.
  • Agile and OOD are closely related. Since an agile process guarantees frequent change, well-designed—that is, easy to change—code is the only way to achieve agility.
  • Design is judged not on aesthetic merits but on how well it helped the project achieve its goals. Implied by this is the notion that sometimes you will intentionally take on technical debt (that you have to pay down later) in order to meet, e.g., a sales deadline.

2. Designing Classes With a Single Responsibility

  • What does it mean to be “easy” to change?
    • changes have no unexpected side effects
    • the amount of code change is proportional to the size of the requirement
    • existing code lends itself to reuse
    • the easiest way to make a given change is to add more easy-to-change code
    • Easy-to-change code is TRUE
      • Transparent: easy to understand what it does
      • Reasonable: the cost of a change is proportional to its benefits
      • Usable: existing code can be reused outside its original context
      • Exemplary: it demonstrates the style that new code ought to follow
  • Single Responsibility Principle
    • Why it matters
      • If an object has many jobs, it can break on you when changed for some random reason
      • Unfocused objects are hard to test
      • Unfocused objects encourage duplication because they discourage reuse (or more accurately, you are likely to want to reuse just one part of an object but not the rest of it)
      • SRP promotes isolation, which in turn translates to protection from change.
    • Determining if a class has a single responsibility, that is, if it is cohesive
      • Interrogate it like a person. “Hi, Gear, what is your gear_ratio?” makes sense, whereas “Hi, Gear, what’s your tire_size?” does not.
      • Describe it in a single sentence without “and” or “or”.
      • (these are heuristics; passing does not guarantee cohesion and failing does not deny it)
  • Writing Code that Embraces Changes
    • Depend on behavior, not data
      • prefer methods over instance variables. Even if it’s just a simple value, use an attr_reader to reference it outside the constructor rather than a var. This lets you replace it later with a more complex method without updating call sites.
      • DRY up understanding of incoming data. e.g. if you have to take in a complex Hash, unpack it in one place into semantic objects/vars.
        • Structs in Ruby are a great way to do this, because they can be promoted to classes later with little trouble.
    • Enforce SRP everywhere (not just classes)
      • Make sure your methods only do one thing. If message passing causes performance concerns, address that when it happens; better to design for changeability now.
      • Iterating over a collection is one responsibility!
        • e.g. outer loop goes in one method, which calls another method for each item in the loop

3. Managing Dependencies

  • Sloppy dependency management is the primary culprit that causes a system to get harder and harder to change over time.
  • 4 ways that an object can depend on another
    1. It knows the name of a concrete class
    2. If knows the name of a message to send to another object
    3. It knows what arguments to pass with a message
    4. It knows the order of arguments to pass with a message
  • Objects are tightly coupled when a change in one usually necessitates a change in the other, and/or when they always come with one another (you can’t reuse them alone).
  • Writing Loosely Coupled Code
    • Inject Dependencies
      • Dependency Injection is not a bad word! It’s just passing an object into another rather than instantiating it within that object.
      • Code to interface instead of specific concrete class. Duck typing! DI is one of the things that enables duck typing.
      • Of course, somebody still has to know how to create those objects and pass them in. This is one of the challenges of design.
    • Isolate Dependencies
      • Inside a dependent class, keep knowledge of messages going to the dependency in a small number of methods. Don’t call the same dependency method in more than one method of the dependent class.
      • This applies to instantiation as well. Try to keep knowledge of how to create a given object in one spot.
    • Remove Argument-Order Dependencies
      • Use an args hash rather than many positional args. This protects against change and is easily extensible. It also makes the args more self-documenting at the call site.
      • For obvious stuff, positional args are still better (like, add(a, b) would be silly with an args hash).
  • Choosing Dependency Direction
    • You can reverse dependencies. Consider carefully which way they go.
    • In general, an object should depend on things that are more stable than it.
    • This is one reason coding to the interface via duck typing is so powerful: abstract interfaces are more stable than concrete classes.

4. Creating Flexible Interfaces

  • Applications are made of classes and defined by the messages they send. Good designers listen closely to the messages their application needs to send.
  • Understanding Interfaces
    • An interface is the set of messages to which an object responds.
    • Broad interfaces can lead to tightly interwoven sets of objects that cannot be easily used or changed independently.
    • Narrow interfaces lead to clusters of small objects that communicate simply and change easily.
  • Defining Interfaces
    • public methods should be slow-changing and well-tested
    • private methods should be tested implicitly through the public interface. They can change around a lot with no effect on the outside world.
  • Domain Objects
    • Objects that map directly to business concepts.
    • Notice these! But design doesn’t stop with them. There are probably other objects in your system too, not directly expressed in the domain, if you want small, single-responsibility classes.
    • Thing about the messages that need to be passed around in your system, then decide what objects they belong to. Not the other way around!
  • Seeking Context Independence
    • Objects are more reusable and testable when they know as little as possible about the outside world.
    • Dependency Injection is one of the best tools for decreasing context.
    • Passing self to collaborators rather than specific attributes can also help decrease context.
  • Trusting Other Objects
    • Send the right message to collaborators and trust that they know what to do with it. Don’t interrogate them about their state first.
    • Sometimes, if it’s hard to trust your collaborators, there might be another object in the system that hasn’t been expressed yet. Design!
  • The Law of Demeter
    • a.b is OK, a.b.c means a knows too much about b.
    • Seeing multiple dots doesn’t always mean it’s a bad design. For example, collection.sort.uniq isn’t an LoD violation, or at least not one that’s bad.
    • Bad violations of LoD violation the TRUE principles:
      • T: a failure in the middle of the train is surprising, and therefore not Transparent
      • R: changes far downstream can require change at the call site, which is not Reasonable
      • U: knowing so much about the internals of collaborators makes you less Usable
      • E: train wrecks encourage more train wrecks. Not Exemplary.

5. Reducing Costs With Duck Typing

  • Duck Types are implicit interfaces that any class can implement just by responding to the right messages.
  • It doesn’t matter what you are, only what you do.
  • Writing Code That Relies on Ducks
    • Caring about the class of your collaborators is usually a bad idea. It leads to tight coupling and difficult changes. Caring instead about duck types means you can swap things out or add new ones easily!
    • Likewise, is_a? and kind_of? and responds_to? are almost surefire giveaways you’re missing a duck.
    • Pay attention to how you’re talking to your collaborators. There might be a duck type interface waiting to be expressed that could reduce coupling with a small amount of change in the other classes.
    • If you care too much about the internals of your collaborator instead of trusting it to handle things based on messages, you’re increasing coupling and worsening the design (and therefore changeability) of your system.
  • Potential Downsides of Duck Typing
    • Duck types are more abstract than concrete classes.
    • Abstractness can be harder to understand at a glance.
    • Good designers are comfortable finding the right amount of abstractness in their designs.
    • By definition, these interfaces are not codified anywhere, so it’s up to you. Tests are the best place to do this.

NOTE

Paradoxically, I got the most out of chapters 1–5. 6–9 covered material I was already more comfortable with, and so I didn’t take detailed notes on them. This doesn’t mean they aren’t insight-packed, incredibly valuable chapters! They’re excellent.

6. Acquiring Behavior Through Inheritance

7. Sharing Role Behavior with Modules

8. Combining Objects with Composition

9. Designing Cost-Effective Tests