Since Flatiron School is over, I’m committing myself to reading a new programming book each week and blogging about the important key points from each chapter. I’ve decided to start with Practical Object-Oriented Design in Ruby (POODIR) by Sandi Metz. Reading POODIR helped clarify and reinforce programming concepts:
- What is object orientation?
- What should go in a class?
- Design patterns and practices that allow code to change easily
- What does it mean when objects know too much about each other? (And why that is bad!)
- How to write reuseable code (think: duck typing, modules, superclasses, inheritance)
What is Design?
- Design: how you arrange code.
- Design your applications to be flexible because eventually specs or requirements will change.
- “Arranging code to efficiently accomodate change is a matter of design.”
What is Object Orientation?
- Object-oriented programs consist of “objects and the messages that pass between them.”
- All objects have certain types of behavior, which are defined in methods.
- Methods are invoked by messages.
- Objects can also store data (think strings, arrays, hashes) using variables.
- Object attributes are defined by variables.
- Classes are the blueprint for certain types of objects. For example, the Fizzbuzz class contains all the details of behaviors of fizzbuzz objects, so that when new instances of fizzbuzz are created, they use the same methods but may have different attributes.
Desinging Classes (and Methods) with a Single Responsibility
What goes in a class?
- Classes should have a single responsibility because classes that have more than one responsibility are difficult to reuse.
- To figure out whether a particular method ought to go in a certain class, rephrase that method as a question and see if that question makes sense. For example, should gear objects be responsible for details about a tire’s size? No,
- Describe each class in one sentence. If your sentence has the word “and,” then the class probably has more than one responsibility. If your sentence includes the word “or,” then the class probably has unrelated responsibilities.
- Cohesion: descriptor for single responsibility concept (e.g. “Highly cohesive” classes have a single responsibility.)
Coding Techniques that Embrace Change
Isolate Data and Data Structures
- Objects contain Data and Behavior.
- “Behavior is captured in methods and invoked by sending messages.””
- Define behavior once. When it changes, you only have to change it in once place.
- Bottom Line: Hide Data away. For example, use attribute readers instead of instance variables (referenced all over your application) so that you reference methods (defined once). If the Data changes, you just have to edit it once versus all over your app where you’ve used the instance variable.
- Also hide Data Structures away through methods that isolate messy structural information and keep your code DRY.
Single Responsibility Everything
- Major Takeaway: Methods should also have a single responsibility.
- “[Refactorings of methods from multiple to single responsibility] are needed, not become 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.”
- Single Responsibility Methods: expose previously hidden qualities, avoid the need for comments, encourage reuse, and are easy to move to other classes.
- Separate responsibilities into different classes. “Concentrate on the primary class. If you identify extra responsibilities that you cannot yet remove, isolate them.”
How to tell when objects know too much about each other?
- You know if your class has dependencies if you see another class name in it (e.g. another class name, name of methods sent to other classes other than self, arguments of those methods, or order of those arguments).
- Classes that are tightly coupled together are more likely to cause problems when you need to make a changes. A change in an object creates changes in another dependent object. When you test one object, you’ll likely be testing the other object simultaneously.
- Agh: Dependncies are especially destructive when chained (e.g. Class A knows things about Class B, which knows things about Class C).
How to Decouple Code and Reduce Dependencies
- Dependency Injection allows us to reorgnize code so that classes don’t have any explicit dependencies on other classes. In other words, objects can talk to each other without knowing what they do.
- Isolate instance creation so that you explicitily expose the dependency
- Isolate references to classes other than self (e.g. make a new method that makes the external call)
- Let methods take in a hash of arguments rather than a specific fixed-order
- Pattern: few fixed-order arguments followed by an options hash
- Cool Tip: To define default values of arguments of boolean values, instead of using || method, try fetch method or merging a defaults hash.
- Use a Wrapper so you DRY out the creation of instances that require you make calls to external interfaces
- Depend on objects that are less likely to changes as often as self
- Depend on abstraction rather than concretion because abstraction is more stable
- “Depend on things that change less often than you do”
Creating Flexible Interfaces
What vs. How - Think of methods as messages being sent between objects. The messages should speak to the “what” and not “how” of what the objects want. - Objects should know as little as possible about how other objects are responding to messages. That way if an object changes, it’s less likely to cause changes for other dependent objects. - Messages between objects should reflect “I know what I want from you and I trust you to give me that.”
- Duck typing allows you to define ojects by what they do instead of by what they are.
- “Duck types are public interfaces that are not tied to any specific class.” In other words, “ducks are virtual types that are defined by what they do instead of by who they are.”
- Duck typing allows your code to be more flexible and easier to maintain/change.
- Avoid code that expresses “I know who you are and because of that I know what you do.” When you see If/Else Statements or Case Statements that rely on knowing what types of objects it’s speaking to, you know you have an opportunity for abstraction. Examine the code’s expectations to find the duck type.
- Metz’s code example really helps illustrate duck types
Inheritance & Modules
Abstracting Behavior to Superclasses, Defining Concretions in Subclasses
- “Subclasses are specializations of their superclasses.” You probably wouldn’t design a superclass that only has one subclass.
- Promote abstract behavior. Promoting means taking behavior from subclasses and moving them up to superclasses because other subclasses share that behavior.
- Promotion failures have low consequences (the worst that can happen is you fail to find the abstraction).
- Demotion failures can have sever and widespread consequences (like breaking dependencies).
- General Refactoring Rule: Arrange code so you can promote abstractions and not demote concretions.
Template Method Pattern - Template Method allows you to “define a basic structure in the superclass and send messages to acquire subclass-specific contributions.” (good example of template method implemented on pg. 126) - “Always document template method requirements by implementing matching methods that raise useful errors.” (good example of how to implement matching method that raises useful error) - “Hook methods allow subclasses to contribute specializations without knowing the abstract algorithm. They reduce coupling between layers of hierarchy and increase its tolerance for change.”
- Modules are used when different objects share common behavior
- Think of modules as defining shared roles among objects (versus superclasses which are like a genus and subclasses which are like species)
- Behaviors defined in modules can be added to any object, classes, instances of classes, or other modules
- Methods from modules and methods acquired via inheritance get put into the same lookup path
- Use Template Method Pattern: Objects that use the module should supply the specialization behavior
- Use Hook Methods to avoid forcing includers to send super (objects that use the module should not have knowledge of the algorithm)
Follow Up Links:
- If you’re learning Ruby, read it: Practical Object-Oriented Design in Ruby
- In Chapter 2, Metz mentions the Struct Ruby class when discussing how to isolate responsibilities in a model when you’re not yet ready to separate a responsibility into an entirely new model. Structs are described as a solution for when you need a “lightweight object.”
- In Chapter 4, Metz mentions the Ruby delegate method.
- When Avi started his lectures on Object Orientation, he showed us this video of Sandi Metz’s awesome presentation at the GuRoKu conference.
- While writing this post, I listened to this super awesome mix by Ryan Hemsworth for Diplo & Friends