What I Learned From Sandi Metz
My notes from Practical Object-Oriented Design in Ruby by Sandi Metz
More than any other coding book I’ve read, this book helped me make connections between coding and the rest of my life (music, writing, etc). If you think these notes are interesting, read the book!
Design
A well designed app will handle change more gracefully. The goal is to commit to a few things and keep those stable. If you’re not sure, you may need more info before you commit. What you don’t want to do is commit to something, have a bunch of things depend on it, and then find out later that you need to change it.
You can organize your code in multiple ways (Composition, Classes, Modules). Prefer composition to inheritance (classes) unless you can defend inheritance. Also, consider if you have one basic thing with subtypes or something that is made up of parts.
- If X is-a Y → Inheritance (class)
- If X has-a Y → Composition
- IF X behaves-like-a Y → Duck type (module)
Good design naturally progresses towards small, independent objects that rely on abstractions.
Messages
You have multiple objects that need to work together to make some larger process happen. Focus on the messages that go between them. For any desired behavior, an object has three ways to make it happen.
- It knows how to respond to the message personally
- It inherits it (either from a class or a module)
- It knows how to ask another object that will know
Like Jeff says, for any certain piece of info, there should be one object that is the authoritative source of truth, and other objects just need to know how to ask that object for the info.
When you call a method, you are asking it to respond to a message. To have have a method called “diameter” is to say “You can send me ‘diameter’ as a message.” (That would be #1 above).
Messages before classes. Think: “I need to send this message. Who should respond to it?” rather than “I know I need this class, what should it do?”
Classes / Inheritance
Pretend your classes are people. Tell them “Depend on things that change less often than you.” That’s not bad advice for people, too. Abstract classes change less than concrete classes. Thus, you want your concrete classes to depend on your abstract ones.
Class inheritance is about message delegation. An object either knows how to respond to a given message, or it passes it on to it’s parent class (automatically).
Anything that sub-classes all have in common can be moved up to the super-class. The super-class should be a documentation of what all the sub-classes have in common, and the methods that remain in a subclasses is what is different (the specializations).
Rigorously separate abstract (super-class) from concrete (sub-class). It’s easier to promote something to be abstract than it is to banish a concrete something into a sub-class.
Super-classes should use hook methods to invite sub-classes to override and add specializations (rather than using super within a sub-class). That way there can be a default for everyone, and a subclass will change it only if it’s different.
Using classes, modules, and duck types, is about answering the question: “How are these objects the same (abstraction) and how are they different (specializations)?” Both classes and modules are about automatic message delegation.
If you’re using a variable like “type” or “category” to determine what message to send to self, then those should probably be subclasses of an abstract class.
Insist on the abstraction! (if there is one) What is common to ALL that either are some type (class) or share some role (module)? Once you find everything that they have in common, you can put all that code in one place, and then you need only write the specialization code for what makes them different. Also if you to change what they have in common, you can do it in that one place instead of in all of them.
Subclasses must be substitutable for their superclasses (Liskov). Otherwise, are they really a type or kind of that superclass? Keep heirarchies are shallow as possible (subclasses having subclasses can get complicated quickly).
Public Interfaces
Each class should have a clear set of methods it expects other objects to use. This is the public interface. It will show the the primary responsibilities and it shouldn’t change (therefore it’s reliable). Private is the opposite. No one is depending on it, so it can change.
Public interfaces are not always explicit, but you should make yours explicit so that it’s clear. That way, when you’re coming back to something later, you know what the public interface is, and you know that you should be slow to change it because other things depend on it.
An object should not need to know HOW another object does something, just WHAT to ask for and WHAT to expect as an answer. HOW might change, but WHAT should stay the same, and is thus more reliable. The less it knows about HOW the better.
All an object needs to know is “The object I’m sending this message to will know what to do with it.” Another way of saying it: “I know what I want and I trust you to do your part. #BlindTrust” Be able to operate with minimum context.
Duck Types
These aren’t scary. They’re fascinating. Every class has a public interface. Treat your objects as if they were defined by their behavior (their public interface) rather than their class. This again is empasizing messages over classes.
A duck type is an extension of this idea. It’s a public interface that doesn’t have a class at all! It’s not tied to any specific class. Multiple classes can be part of it though, because all they need to do is to respond to the same message (or set of messages). Put another way, they need to agree upon and share a public interface. This is why you need to make the public interface explicit, so what’s shared and agreed on (and depended on) is documented.
To find a duck type, look for times where a method asks for things from several different classes, and ask yourself, “What does it really want?” Make a new method out of the answer to that question, and have all the classes respond to that message.
If you’re checking the class of a receiving object to determine what message to send, you have overlooked a duck type. Figure out what the sending object is really asking for, and which different objects it needs to ask, and create a public interface that all those objects will respond to.
Modules
Modules are about objects not in the same class that nevertheless all perform a similar role. Duck types and modules are connected. If there is a “preparer” duck type, then there is probably a “preparable” role, which is an opportunity for a module. Is it right to say that a module is the explict documentation of what it means to be a member of a specific duck type?
The simplest way to get started is to write concrete code that works for one concrete class. Then, refactor intot abstract that will work for many classes. Objects should be able to speak for themselves. You shouldn’t have to ask Object B for info about Object A).
A role is not an objects main responsibility. Many otherwise unrelated objects can share the same role.
Composition
A larger more complex object can be made up of small simple objects, which play a role and have an established interface. The composed object depends on the interface of the ROLE.
Aggregation is like composition (both are A has-a B) but in an aggregation B has a life outside of A.
- Composition: University has departments (without the university, the departments cease to exist)
- Aggregation: Department has professors (without the department, the professors still exist)
A factory is an object that creates other objects.
Tests
Good tests prove that you refactored everything correctly: you made design advances, but it still does what it is supposed to do (you didn’t break it). They can also help to document the public interfaces of your objects.
Tests can expose design flaws:
- Painful setup → expecting too much context
- Dragging other objects in → too many dependencies
Test goals: be loosely coupled and test only the most important things. Tests should depend on the public interfaces of objects.
Two types of messages:
- Query: just asking for something
- Command: responsible for creating some larger change
Query messages don’t need to be tested by the sender whether or not they were sent. Command messages do (using a mock). With both, the receiving object is the one that should be testing how IT responds to the message.
Obj A will test whether Obj A sent a message, only if it’s a command, but Obj B will test whether obj B responds to ANY message appropriately, because that is a question of Obj B’s public interface, which is the responsibility of Obj B.
You should have tests that make the public interface for duck types explicit. It should respond to THIS message. You can do the same thing for classes. You are documenting, making explicit, codifying the expectations of the public interface for a class or duck type. It should respond to THESE messages.