Practical Object-Oriented Design in Ruby - Notes
Notes by: Sergio Rodriguez / Book by: Sandi Metz
These are some notes I took while reading the book. Feel free to send me a pull request if you want to make an improvement.
Chapter 1 - Object Oriented Design
Why Design?
- Changes in applications are unavoidable.
- Change is hard because of the dependencies between objects
- The sender of the message knows things about the receiver
- Tests assume too much about how objects are built
- Good design gives you room to move in the future
- The purpose of design is to allow you to do design later, and its primary goal is to reduce the cost of change.
- It does not anticipate the future (this almost always goes badly)
The Tools of Design
Design Principles
- SOLID Design
- Single Responsibility Principle: a class should have only a single responsibility. (See Ch2).
- Open-Closed: 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.
- Don't 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.
Design Patterns
Simple and elegant solutions to specific problems in OOP that you can use to make your own designs more flexible, modular reusable and understandable.
This book is not about patterns. However, it will help you understand them and choose between them.
The Act of Design
How Design Fails (When Design Fails)
Design fails when:
- Principles are applied inappropriately.
- Patterns are misapplied (see and use patterns where none exist).
- The act of design is separated from the act of programming.
- Follow Agile, NOT Big Up Front Design (BUFD)
When to Design
- Do Agile Software development.
- Don't do Big Up Front Design (BUFD).
- Designs in BUFD cannot possibly be correct as many things will change during the act of programming.
- BUFD inevitable leads to an adversarial relationship between customers and programmers.
- Make design decisions only when you must with the information you have at that time (postpone decisions until you are absolutely forced to make them).
- Any decision you make in advance on an explicit requirement is just a guess. Preserve your ability to make a decision later.
Judging Design
There are multiple metrics to help you measure how well your code follows OOD principles. Take into account the following:
- Bad OOD metrics are an indisputable sign of bad design (code that scores poorly will be hard to change).
- Good scores don't guarantee that the next change you make will be easy.
- Applications may be anticipating the wrong future (Don't try to anticipate the future).
- Applications may be doing the wrong thing in the right way.
- Metrics are proxies for a deeper measurement.
- How much design you do depends on two things: 1) Your skills, 2) Your timeframe.
- There is a tradeoff between the amount of time spent designing and the amount of time this design saves in the future (and there is a break-even point).
- With experience you will learn how to apply design in the right time and in the right amount.
Chapter 2 - Designing Classes with a Single Responsibility
- A class should do the smallest possible useful thing.
- Your goal is to make classes that do what they need to do right now and are easy to change later.
- The code you write should have these qualities:
- Changes have no unexpected side effects.
- Small changes in requirements = small changes in code.
- Easy to reuse.
- The easiest way to make a change is to add code that in itself is easy to change (Exemplary code).
Creating classes with single responsibility
- A class must have data and behavior (methods). If one of these is missing, the code doesn't belong to a class.
Determining if a class has a single responsibility
- Technique 1: Ask questions for each of it's methods.
- "Please Mr. Gear, what is your ratio?" - Makes sense = ok
- "Please Mr. Gear, what is your tire size?" - Doesn't make sense = does not belong here
- Technique 2: Describe the class in one sentence.
- The description contains the words "and" or "or" = the class has more than one responsibility
- If the description is concise but the class does much more than the description = the class is doing too much
- Example: "Calculate the effect that a gear has on a bicycle"
Writing Code that Embraces Change
Depend on behavior (methods), Not Data
Hide Instance Variables
Never call @variables inside methods = user wrapper methods instead. Wrong Code Example / Right Code Example
Hide Data Structures
If the class uses complex data structures = Write wrapper methods that decipher the structure and depend on those methods. Wrong Code Example / Right Code Example
Enforce Single Responsibility Everywhere
Extract Extra Responsibilities from Methods
- Methods with single responsibility have these benefits:
- Clarify what the class does.
- Avoid the need for comments.
- Encourage reuse.
- Easy to move to another class (if needed).
- Same techniques as for classes work (See techniques).
- Separate iteration from action (common case of single responsibility violation in methods).
Isolate Extra Responsibilities in Classes
If you are not sure if you will need another class but have identified a class with an extra responsibility, isolate it (Example using Struct.)
Chapter 3 - Managing Dependencies
Recognizing Dependencies
An object has a dependency when it knows (See example code):
- The name of another class. (Gear expects a class named Wheet to exist.)
- Solution strategy 1: Inject Dependencies
- Solution strategy 2: Isolate Instance Creation
- The name of the message that it intends to send to someone other than self. (Gear expects a Wheel instance to respond to diameter).
- Solution strategy 1: Reversing Dependencies
- Avoids the problem from the beginning, but it is not always possible.
- Solution strategy 2: Isolate Vulnerable External Messages
- Solution strategy 1: Reversing Dependencies
- The arguments that a message requires. (Gear knows that Wheel.new requires rim and title.)
- Mostly unavoidable dependency.
- In some cases default values of arguments might help.
- The order of those arguments. (Gear knows the first argument to Wheel.new should be 'rim', 'tire' second.)
- Solution Strategy 1: Use Hashes for initialization arguments
- Solution Strategy 2: Use Default Values
- Solution Strategy 3: Isolate Multiparameter Initialization
- When you can't change the original method (e.g when using an external interface).
*You may combine solution strategies if it makes sense.
Some degree of dependency is inevitable, however most dependencies are unnecessary.
Solution Strategies
Inject Dependencies
Instead of explicitly calling another class' name inside a method, pass the instance of the other class as an argument to the method. Wrong Code Example / Right Code Example
Isolate Instance Creation
Use this if you can't get rid of "the name of another class" dependency type through dependency injection.
- Technique 1: Move external class name call to the initialize method. (Example code)
- Technique 2: Isolate external class call in explicit defined method. (Example code)
Isolate Vulnerable External Messages
For messages sent to someone other than self.
Not every external method is a candidate for isolation. External methods become candidates when the dependency becomes dangerous. For example:
- The external method is buried inside other complex code.
- There are multiple calls to the external methods inside the class.
- The method is part of the private interface of another class.
Wrong Code Example / Right Code Example
Use Hashes for Initialization Arguments
- You will be changing the argument order dependency for an argument name dependency.
- Not a problem: argument name is more stable and provides explicit documentation of arguments.
- The use of these technique depends on the case:
- For very simple methods you are better off accepting the argument order dependency.
- For complex method signatures, hashes are best.
- There are many cases in between where some arguments are required as stable (dependent on order) and some are less stable or optional (dependent on names by hash). This is fine.
Wrong Code Example / Right Code Example
Explicitly Define Defaults
- Simple non-boolean defaults: '||' (Code Sample)
- Problem: you can't set an attribute to nil or false because the fallback value will take over.
- Hash as argument with simple defaults: fetch (Code Sample)
- Fetch depends on the existence of the key. If the key is not present, it returns the fallback value.
- This means that attributes can be set to nil and false, without the fallback value taking over.
- Fetch depends on the existence of the key. If the key is not present, it returns the fallback value.
- Hash as argument with complex defaults: defaults method + merge (Code Sample)
- The defaults method is an independent method that handles the complex logic for defaults and returns a hash. This hash is then merged to the actual arguments hash.
Isolate Multiparameter Initialization
For methods where you can't change the order of arguments (e.g external interfaces).
- Wrap the external interface in a module whose sole purpose is to create objects from the external dependency. (Code Sample)
- FYI: objects whose purpose is to create other objects are called factories.
- Use a module, not a Class because you don't expect to create instances of the module.
Reversing dependencies
Imagine the case where KlassA depends on KlassB. This is where KlassA instantiates KlassB or calls methods from KlassB.
You could write a version of the code were KlassB depends con KlassA. Gear depends on Wheel Sample / Wheel depends on Gear Sample
Choosing Dependency Direction
Depend on things that change less often than you do.
- Some classes are more likely than others to have changes in requirements.
- You can rank the likelihood of change of any classes you are using regardless of their origin (internal or external). This will help you to make decisions.
- Concrete classes are more likely to change than abstract classes.
- Abstract Class: disassociated from any specific instance.
- Changing a class that has many dependents will result in widespread consequences.
- A class that if changed causes a catastrophe has enormous pressure to never change.
- Your app may be forever handicapped because of having such types of classes.
Finding Dependencies that Matter
Not all dependencies are harmful. Use the following framework to organize your thoughts and help you find which of your classes are dangerous.
Chapter 4 - Creating Flexible Interfaces
Flexible interfaces: message based design, not class based design
The conversation between objects takes place using their public interfaces.
Bad vs Good Interfaces
(Original: Understanding Interfaces)
- Bad Interface structure:
- Objects expose too much of themselves.
- Objects know too much about neighbors.
- Result: They do only the thing they are able to do right now.
This design issue is not necessarily a failure of dependency injection or single responsibility. Those techniques, while necessary, are not enough to prevent the construction of an application whose design causes you pain. The roots of this new problem lie not in what each class does but with what it reveals.
- Good Interface structure:
- Objects reveals as little of themselves as possible.
- Objects know as little of their neighbors as possible.
- Result: plug-able, component-like objects.
Defining interfaces
On a restaurant the kitchen does many things but does not, expose them all to its customers. It has a public interface that customers are expected to use: the menu. Within the kitchen many things happen, many other messages get passed, but these messages are private and thus invisible to customers. Even though they may have ordered it, customers are not welcome to come in and stir the soup.
The menu lets customers ask for what they want without knowing anything about how the kitchen makes it.
Public Interfaces
- Reveals the class' primary responsibility.
- The public interface should correspond to the class' responsibility. A single responsibility may require multiple public methods. However, too many loosely related public methods can be a sign of single responsibility violation.
- Are expected to be invoked by others.
- Will not change on a whim.
- Are safe for other to depend on.
- (Depend on less changeable things)
- Are thoroughly documented in tests.
Private Interfaces
- Handle implementation details (utility methods only meant to be used internally).
- Are not expected to be sent by other objects.
- Can change for any reason whatsoever
- (And it's safe for them to change as the public interface should remain stable).
- Are unsafe for others to depend on.
- May not even be referenced in tests.
Finding a good public interface
(Original: Finding the public interface)
Focus on messages, NOT domain objects (classes)
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.
Step 1: Using Sequence Diagrams
- Lightweight way of acquiring a design intention.
- Low cost object arrangement and message passing (public interface) experiments.
- Helpful for communicating ideas.
- Keep agile: use them for exerimenting and communicating. Do NOT do big up-front design.
- Value of these diagrams:
- Should this receiver be responsible for responding to this message?
- I need to send this message, who should respond to it?
Step 2: Asking for 'What' instead of telling 'How'
Better explained through an example: compare novice vs intermediate.
Step 3: Seeking Context Independence
Context: The things that a class knows about other objects. In the intermediate design example, Trip has a single responsibility but expects to be holding onto a Mechanic capable of responding to prepare bicyble.
- (+) context = (-) reusability = (-) testability ease
- Use dependency injection to seek context independence.
- Better explained through an example: compare intermediate vs experienced.
Step 4: Trusting Other Objects
I know what I want and I trust your to do your part.
Better explained through an example: compare intermediate vs experienced.
Using Messages to Discover Objects
A message based approach (following these steps) can help you to discover not so obvious, but important objects. See next example
A message based approach also helps you to find the first thing to assert in a test.
Writing Code That Puts Its Best (Inter)Face Forward
Think about interfaces. Create them intentionally. It is your interfaces, more than all of your tests and any of your code, that define your application and determine it’s future.
The following are rules-of-thumb for creating interfaces:
R1. Create Explicit Interfaces
- Method 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
- Do not test private methods. If you must, segregate those test from the ones of the public methods
- In ruby: public, private, protected keywords
- Use them if you like but take into account that ruby has mechanisims to circumvent them
- You are better off using comments or a naming convention for public and private methods than using the keywords
- Rails uses a leading '_' for private methods
R2. Honor the Public Interfaces of Others
- If your design forces the use of a private method in another class, re-think your design (try very hard to find an alternative)
- A dependency on a private method of an external framework is a form of technical debt
R3. Exercise Caution When Depending on Private Interfaces
- If you must depend on a private interface, isolate the dependency
- This will prevent calls from multiple places
R4. Minimize Context
- Create public methods that allow senders to get what they want without knowing how your class does it
- If you face a class with an ill-defined public interface you have these options (depending on the case):
- Option 1. Define a new well-defined method for that class' public interface.
- Option 2. Create a wrapper class with a well defined public interface.
- Option 3. Create a single wrapping method and put it in your own class.
The Law of Demeter
Only talk to your immediate neighbors.
This is not an absolute law. Certain “violations” of Demeter reduce your application’s flexibility and maintainability, while others make perfect sense. Additionally, violations typically lead to objects that require a lot of context.
The definition "only use one dot" is not always right. There are cases that use multiple dots that do not violate Demeter.
Examples:
- customer.bicycle.wheel.tire
- Type: returns a distant attribute
- There is debate on how firmly Demeter applies. It may be cheapest in your specific case to reach through intermediate objects than to go around.
- customer.bicycle.wheel.rotate
- Type: invokes distant behavior
- Cost is high. Remove this type of violation.
- hash.keys.sort.join(', ')
- No violation
- See? The "use only one dot" definition is not always right.
Wrong approach to comply with Demeter: Delegation
Delegation removes visible violations but ignores Demeter's spirit. Using delegation to hide tight coupling is not the same as decoupling code.
- Delegation in Ruby: delegate.rb or forwardable.rb
- Delegation in Rails: the delegate method
How to comply with Demeter
- Demeter violations are clues of missing objects whose public interface you have not yet discovered..
- It is easy to comply with Demeter if you use a message-based perspective in your design.
Chapter 5 - Reducing Costs with Duck Typing
Undestranding Duck Typing
- Duck types are public interfaces that are not tied to any specific Class.
- Duck types are abstractions that share the public interface's name.
- Different objects respond to the same message.
- Senders of the message do not care about the class of the receiver.
- Receivers supply their own specific version of the behavior.
- Duck types are abstractions that share the public interface's name.
- Class is just one way for an object to acquire a public interface (it is one of several public interfaces it can contain).
- It is not what an object is that matters, it's what it does.
Design in need of a Duck (Concretion) - Wrong
What is wrong with this approach:
- Explosion of dependencies (explicit name of classes, name of messages each class understands, arguments those messages require).
- This style of code propagates itself. To add another preparer you need to create a dependency.
- Sequence diagrams should always be simpler than the code they represent; when they are not, something is wrong with the design.
Design with Duck (Abstraction) - Right
What is right with this approach:
- The prepare method trusts all of its arguments to do their part.
- Objects that implement prepare_trip are Preparers (this is the Duck Type abstraction).
- This makes it very easy to change the code (add or remove preparers without the need to change Trip at all).
Things to consider:
- Cost of Concretion VS Cost of Abstraction
- Concrete code: easy to understand, costly to extend.
- Abstract code: initially harder to understand, far easier to change.
Writing code that Relies on Ducks
It is relatively easy to implement a duck type; your design challenge is to notice that you need one and to abstract its interface.
Recognizing Hidden Ducks
The following coding styles are indications that you are missing a Duck:
- Case Statements that switch on class / If Statements with .class == "KlassName" (Example)
- kind_of? and is_a? (Example)
- responds_to? (Example)
Documenting Duck Types
Tests are the best documentation. You only need to write the tests.
Sharing Code Between Ducks
Ducks share the interface (method names) and may share some code in the implementation inside the shared methods:
- Share interface name but NOT code in methods: strategy described in this chapter.
- Share interface name AND some code in methods: See Ch7
Choosing Your Ducks Wisely
Some times Ducks can exist but may not be needed. Here is an example from the Rails Framework.
Takeaways about this example:
- This code is depending on Ruby's Integer and Hash classes. They are far more stable than this method is (this is why ignoring the Duck isn't much of a deal).
- There is probably a hiding Duck here.
- The implementation of a Duck will probably not reduce the cost of the application.
- The implementation of a Duck requires to monkey patch Ruby.
- Feel free to monkey patch Ruby if needed. However, you need to be able to defend the decision.
What about Duck Typing in Statically Typed Languages?
(Conquering a Fear of Duck Typing)
The author compares both types of languages and makes an argument in favor of dynamically typed languages. Here are the takeaways from her discussion:
- Duck Typing is not possible on static typed languages.
- Metaprogramming is much easier in dynamic typed languages (strong argument in favor of dynamic typed languages).
- When a dynamically typed application cannot be tuned to run quickly enough, static typing is the alternative. (If you must, you must).
- The compiler cannot save you from accidental type errors (This notion of safety is an illusion).
- Any language that allows casting a variable into a new type is vulnerable.
Chapter 6 - Acquiring Behavior Through Inheritance
Inheritance is for specialization, NOT for sharing code.
Understanding Classical Inheritance
Classical: Inheritance of classes
No matter how complicated the code, the receiving object ultimately handles any message in one of two ways. It either responds directly or it passes the message on to some other object for a response.
- Defines a forwarding path for non-understood messages.
Recognizing Where to Use Inheritance
(Recognizing when you have a problem that inheritance solves)
The problem that inheritance solves: highly related types that share common behavior but differ along some dimension (single class with several different but related types)
Here is a typical progression for problems that inheritance solves:
-
1) Your code starts with a Concrete Class
-
2) Then you start embedding multiple types into that Class
- Here is an example of the Bicycle Class with the embedded road style bike.
- Objects holding onto an instance of Bicycle may be tempted to check style before sending a message (creating a dependency).
- Spot the Antipattern: an if statetment that checks an attribute that holds the category of self to determine what message to send to self.
- This pattern indicates a missing subclass.
-
3) Then you find the embedded types in your class
- Be on the lookout for variables/attributes that denote different types. Typical names for these variables are: type, category, style
Some extra details about inheritance
- Multiple Inheritance: Gets complicated quickly. Ruby does NOT do this.
- Single Inheritance: a subclass is only allowed one parent superclass (Ruby does this).
- Duck Types cut across classes. They do not use classical inheritance; they share common behavior via Ruby modules.
- Subclasses are specializations of their Superclasses.
Misapplying Inheritance
You should never inherit from a concrete Class. Always inherit from abstract Classes.
Abstract Class: disassociated from any specific instance.
Wrong Code Example
Properly Applying Inheritance
(Finding the Abstraction)
Subclasses are everything their Superclasses are, plus more. Any object that expect Bicycle should be able to interact with a Mountain Bike in blissful ignorance of its actual Class.
Two things are required for inheritance to work:
- There is a generalization-specialization relationship in the objects you are modelling.
- Correct coding techniques are used.
Here is a typical process on how to build a proper inheritance strategy:
- 1) Creating an Abstract Superclass and Pushing Down Everything to a Concrete Class
- Abstract Superclass: Disassociated from any specific instance.
- e.g You won't expect to have instances of Bicycle
- Try to postpone the design of the inheritance until you are required to handle 3+ specializations.
- e.g Until you are asked to deal with 3+ types of bikes.
- Two: wait if you can. Three: will help you find the right abstraction.
- It almost never makes sense to create an abstract superclass with only 1 subclass.
- Push down all code from the original class with mixed types (soon your abstract superclass) into one of the concrete classes.
- Pushing down all code to one concrete class will probably break the other concrete class. This will be fixed next.
- Result of this step:
- Abstract Superclass: Disassociated from any specific instance.
-
2) Promoting abstract behavior while separating the abstract from the concrete
- Identify behavior that is common to all specializations and promote it to the abstract superclass. Example of promotion
- This could even requiere splitting methods that have both abstract and concrete behavior inside.
- On this example spares is a candidate for promotion but it tape color is only applicable for the RoadBike specialization. Hence, we need to separate it and promote only the abstract (shared code).
- This could even requiere splitting methods that have both abstract and concrete behavior inside.
- Why push down and then promote?
- Consequences of promotion failures are low.
- Consequences of wrong demotion (leaving concrete code on the superclass) are high and difficult to solve.
- Identify behavior that is common to all specializations and promote it to the abstract superclass. Example of promotion
-
3) Invite Inheritors to Supply Specializations Using the Template Method Pattern
- Template method pattern is a technique where a superclass implements and calls methods that can be overriden by the subclasses to supply specialized behaviour by implementing them.
- Bicycle's initilize method relies on the default_chain and default_tire_size methods. Any specialization can implement those methods to set their own defaults.
- Another example of specialization with the template method pattern with the post_initialize method.
- Avoid problems downstream by implementing and documenting every template method
- On this example the Bicycle superclass hasn't implemented the default tire size method. A programmer is asked to create a new specialization subclass (RecumbentBike) but he expects Bicycle's default tire size method to hande the default. As the method is not implemented anywhere and it is not documented with a useful error, a cryptic error is raised.
- Any superclass that uses the template method pattern must supply an implementation for every message it sends. Even if the implementation is rasing a useful error that documents the pattern.
- Template method pattern is a technique where a superclass implements and calls methods that can be overriden by the subclasses to supply specialized behaviour by implementing them.
Managing Coupling Between Superclasses and Subclasses
Abstract superclasses use the template method pattern to invite inheritors to supply specializations, and use hook methods to allow these inheritors to contribute these specializations without being forced to send super.
The way to manage coupling is illustrated using the implementation of the spares method as example. Two implementations will be shown:
- (1) Coupled solution using super (wrong)
- (2) Decoupled solution using hooks (right).
Understanding Coupling
(Coupled approach using super - Wrong)
Take a look at this solution and notice the following:
- Subclasses rely on super.
- This means the subclass knows the algorithm. It depends on this knowledge.
- Both Subclasses know things their superclass.
- They know that their superclass responds to initialize. (They send super on their initialize methods).
- They know that their superclass implements spares and that it returns a hash.
- Pattern: know things about themselves and about their superclass
- This pattern requires that sublasses know how to interact with their superclasses.
- Forcing a subclass to know how to interact with their superclass can cause many problems
Decoupling Subclasses Using Hook Messages
(Decoupled approach using hooks - Right)
Control should be on the Superclass, NOT the Subclasses
- Hook Example 1 - post_initialize.
- Removes the initialize method completely from the subclass.
- Eliminates super.
- Hook Example 2 - local_spares.
- RoadBike no longer knows that Bicycle implements a spares method.
- Eliminates super.
- This is the final implementation of Bicyle and its Subclasses and this is how easy it is to create a new subclass.
Well-designed inheritance hierarchies are easy to extend with new subclasses, even for programmers who know very little about the application.
Chapter 7a - Sharing Role Behavior with Modules
Classical inheritance is not the best solution strategy for all problems. Other inheritance strategies such as sharing role behavior with modules may be handy in those cases. Look here for some guidelines on when to use each strategy
Creation of a recumbent mountain bike subclass requires combining the qualities of two existing subclasses, something that inheritance cannot readily accommodate. Even more distressing is the fact that this failure illustrates just one of several ways in which inheritance can go wrong.
The use of classical inheritance is optional; every problem that it solves can be solved another way.
Understanding Roles
- Roles are for sharing behavior and/or some method names in the public interface among unrelated objects.
- If objects share only some public method names, Duck typing of method names can be enough (no modules required).
- If objects share the public method names and the behaviour inside those methods, you should organize that code in a module.
- When objects begin to play a role they enter in a relationship with the objects for whom they play the role
- Using a role creates dependencies that need to be taken into account when deciding among design options.
Here is a typical process to create a proper role strategy. The design strategy is improved incrementally:
-
1) Finding Roles
- Duck types are roles.
- Roles often come in pairs (if there is a
Preparer
role, there will also be aPreparable
role).Preparable
implements an interface with all methods that aPreparer
might send to it.
-
2) Check if Responsibilites are Right (Organizing Responsibilites)
- This section shows an example of a wrong decision of responsibilites to help you spot some anti-patterns.
- The following sequence diagram shows a wrong organization of responsibilites:
-
3) Solving Bad Responsibilites (Removing Unnecessary Dependencies)
- 3.1) Discovering the Schedulable Duck Type (Role)
- The following diagram proposes and improvement but still has some improvement opportunities.
- 3.2) Letting Objects Speak for Themselves
- 3.1) Discovering the Schedulable Duck Type (Role)
Objects should manage themselves; they should contain their own behavior. If your interest is in object B, you should not be forced to know about object A if your only use of it is to find things out about B.
Extreme example to illustrate the idea:
Imagine a StringUtils class that implements utility methods for managing strings. You can ask StringUtils if a string is empty by sending StringUtils.empty?(some_string), but this you are involving a third party for something that String should be able to do alone.
- 4) There are 2 decisions to deal with when implementing role behavior with modules.
- 4.1) What the code does (Writing the Concrete Code)
- Pick an arbitrary concrete class (as opposed to an abstract class) and implement the duck
- i.e Type the duck directly into the concrete class. You will worry about where the code lives later.
- This code and the following diagram show an example of writing the duck directly on the concrete class.
- Pick an arbitrary concrete class (as opposed to an abstract class) and implement the duck
- 4.1) What the code does (Writing the Concrete Code)
- 4.2) Where the code lives (Extracting the Abstraction)
- Bicycle is not the only thing that is schedulable. How to rearrange the code so that it can be shared among objects of different classes?
- This Code shows how to extract the abstraction from the previous step into a module.
- Notice that:
- The dependency on
Schedule
has been moved to theSchedulable
module, isolating it. - The module implements the
lead_days
hook to follow the template method pattern. Thelead_days
hook is overridable by any includer of the module.- Just as with classical inheritance, modules must implement every template method pattern (even if it only raises an error).
- Other objects can play the
Schedulable
role by including the module without duplicating the code. The current implemantation looks as follows:
- The dependency on
- Notice that:
Chapter 7b - How does Ruby Method Look-Up Works?
(Looking Up Methods)
Include vs Extend
- Include: affects all instances of the class where the module was included (behave like instance methods).
- Extend: adds the module's behavior directly into the single object.
- Extending a class with a module creates class methods (A class is an object).
- Extending an instance of a class with a module creates instance methods in that instance.
How missing methods are handled in Ruby
If all attempts to find a suitable method fail, you might expect the search to stop, but many languages make a second attempt to resolve the message.
Ruby gives the original receiver a second chance by sending it new message, method_missing, and passing :spares as an argument. Attempts to resolve this new message restart the search along the same path, except now the search is for method_missing rather than spares.
Chapter 7c - Writting Inheritable Code
Applies for Chapter 5, Chapter 6 and Chapter 7.1
With classical inheritance and sharing roles with modules you can write very convoluted and difficult to debug code. The intention of this chapter is to show you the specific coding techniques used to write quality inheritance strategies.
Coding Technique 1: Recognize the Antipatterns
- Antipattern 1: objects that use variable names like
type
orcategory
to determine what message to send toself
- Solution: Classican Inheritance
- Antipattern 2: when a sending object checks the type of the receiving object to determine what message to send.
- You have overlooked a duck type.
- Solution: Implement a duck type interface on all recieving objects.
- If duck types also share behaviour (not only the interface), place that code in a module and include it on every duck.
- Extra info: When choosing between classical inheritance or roles (duck types) think about this:
- is-a (classical) versus behaves-like-a (roles)
Coding Technique 2: Insist on the Abstraction
- Rule: All of the code in an abstract superclass should apply to every class that inherits it / The code in a module must apply to all who use it.
- Consequences of breaking the rule: inheriting objects obtain incorrect behaviour. Programmes start to do awful hacks to get around this weird behaviour.
- Symptoms of breaking the rule: Subclasses or objects that include a module that override a method to raise an exception like 'does not implements this method'.
- Common pitfalls when working with abstractions:
- Creating an abstraction where it doesn't exist. (If you cannot indentify it correctly, there may not be one.)
- If no common abstraction exists, then inheritance is not the solution to the problem.
Coding Technique 3: Good Practices on Superclasses & Subclasses
(Honor the Contract)
Contract: All Subclasses
must be suitable to substitute their Superclass
without breaking anything.
Subclasses must:
- Conform to their
Superclass
interface- Respond to every message in that interface, taking the same kinds of inputs and returning the same kind of outputs.
- They are not permitted to do anything that forces others to check their type in order to know how to treat them.
Coding Technique 4: Use the Template Method Pattern
See Properly Applying Inheritance for more information.
Coding Technique 5: Preemptively Decouple Classes
Avoid writing code that requires its inheritors to send super; instead use hook messages to allow subclasses to participate while absolving them of responsibility for knowing the abstract algorithm.
See Decoupling Subclasses Using Hook Messages
Warning: Hook methods only solve the problem of sending super
for adjacent levels of the hierarchy. That is why coding technique is important.
Coding Technique 6: Create Shallow Hierarchies _______________________________________________________________________________
Chapter 8.1 - Combining Objects with Composition
Combining different parts into a complex whole such that te whole becomes more than the sum of it's parts.
In composition the larger object is connected to its parts via a has-a relationship (A Bicycle has parts). Part is a role and bicycles are happy to collaborate with any object that plays the role.
This chapter shows how to replace gradually an inheritance design with composition.
Parts
into a Bicycle
Step 1) Compose - If you create an object to hold all of a bicycle's parts (i.e a
Parts
object), you could delegate the spares message to that new object (See this line of code). - This code shows how to turn a Bicycle into a composed object.
- Bicycle is now responsible for 3 things: knowing it's size, holding on to it's
Parts
and answering spares.
- Bicycle is now responsible for 3 things: knowing it's size, holding on to it's
Parts
class
Step 2) Moving the parts logic into the - This first coding approach is temporal as it still relies on inheritance to work.
- Pros: made obvious how little
Bicycle
specific code there was. - Cons: still uses inheritance for the specialization of
Parts
. - The following diagram depicts the design strategy up to this point.
Parts
object with Part
objects
Step 3) Composing the There will be a
Parts
object and it will contain manyPart
objects.
The following diagram illustrates the final strategy that is going to get built. Notice that inheritance dissappears.
Part
object
Step 3.1) Creating a This code shows the creation of the new Part
class and the corresponding refactor on the Parts
class.
Here you can see how the previous code can be used to create Part
objects, sets of Part
objects for each bicycle configuration and Bicycle
objects.
Avoid this pitfall
While it may be tempting to think of these objects as instances of
Part
, composition tells you to think of them as objects that play thePart
role. They don’t have to be a kind-of thePart
class, they just have to act like one; that is, they must respond toname
,description
, andneeds_spare
.
-
Cons:
- The
Bicycle
's methodsspares
andparts
behave weird because they return different sort of things.
mountain_bike.spares # returns an array of Part objects mountain_bike.parts # returns a Parts object
- This causes weird behaviour when using them.
- The
Parts
Object More Like an Array
Step 3.2) Making the This chapter will explore 4 different approaches to deal with the aforementioned weird behavior.
-
Approach 1: Leave as is and accept the lack of array-like behavior
- Pros: As simple as it gets.
- Cons: Limited use.
-
Approach 2: Emulate the array-like behavior that is needed by adding methods to the
Parts
Class- Pros: Simple solution if you need limited array-like behavior.
- Cons: Slippery slope path. Soon you will be adding
each
andsort
(and more array behavior).
-
Approach 3: Subclass Array
- Pros: Straight forward solution that adds all array-like behavior.
- Use this if you are certain that you will never encounter confusing errors.
- Cons: Confusing errors can arise from
Array
methods that return arrays instead of the subclassedParts
object.- Many methods in the
Àrray
class return arrays. - In approach 1 a
Parts
object could not respond tosize
. In this approach the addition of toParts
objects cannot respond tospares
.
- Many methods in the
- Pros: Straight forward solution that adds all array-like behavior.
-
Approach 4: Use Delegation and Enumerable
- Forwardable: is a ruby module that allows you to forward a message to a designated object. More info in the Ruby Doc.
- For example,
def_delegators :@parts, :size, :each
means that wheneversize
oreach
is sent to aParts
object, the message will be forwared to it's@parts
(i.e@parts.size
and@parts.each
). - Classes are usually extended with
Forwardable
.
- For example,
- Enumerable: is a ruby module that when mixed into a collection class, provides it's instances (e.g a
Parts
object) several transversal, searching and sorting methods. More info in the Ruby Doc or on this link.- Enumerable is usually included into collection classes (e.g the
Parts
class). - The class that includes
Enumerable
must implement aneach
that yields successive members of the collection. - On this example
each
in theParts
class is 'implemented' by forwarding it to it's@parts
attribute.
- Enumerable is usually included into collection classes (e.g the
- This example shows that both
spares
and aParts
object respond tosize
. - Pros:
- Middle ground between complexity and usability.
- Responds to all
Enumerable
methods. - Raises errors when a
Parts
object is treated like an array.
- Cons:
- The code may be complex for new developers.
- Forwardable: is a ruby module that allows you to forward a message to a designated object. More info in the Ruby Doc.
Step 4) Manufacturing Parts (with Factories)
Problem
Look at these lines. These 4 lines represent a big knowledge dependency on how to create the appropiate Part
objects for a specific Bicycle. This dependency can spread through your app.
The solution is given incrementally on the following steps.
Part
objects. Centralize that knowledge in one place.
Step 4.1) There are only a few valid combination of PartsFactory
Step 4.2) Create the A factory is an object whose only purpose is to manufacture other objects.
This code shows a new PartsFactory module. Its job is to take an array like one of those listed above and manufacture a Parts object. Along the way it may well create Part objects, but this action is private. Its public responsibility is to create a Parts.
- The factory takes 3 arguments:
- 2 & 3) Name of the classes to be used for creating
Part
objects and theParts
object.
- Pros:
- Creating a
Parts
object with proper configuration for a specific bike is easy. - Your knowledge is centralized. You should always create new
Parts
objects using the factory.
- Creating a
- Cons:
- Although all the code up to this point works perfectly, the
Part
class has become so simple after all this refactoring that it may not be necessary at all.
- Although all the code up to this point works perfectly, the
PartsFactory
to remove the Part
class
Step 4.2) Leveraging the If the
PartsFactory
created every part, thePart
class would not be necessary. This would simplify the code.
- The
Part
class can be replaced by anOpenStruct
.OpenStruct
is a lot likeStruct
. It provides a convenient way to bundle a number of attributes into an object (without creatin a class).OpenStruct
takes a hash for initialization whileStruct
takes position order initialiation arguments.
- This code shows a refactored version of the
PartsFactory
where thePart
creation was moved into the factory usingOpenStruct
and thePart
class was deleted.- This code is the only place in the app where
need_spares
defaults to true. - The
PartsFactory
should be the only responsible for manufacturingParts
. - Here is how this version of the factory works.
- This code is the only place in the app where
Step 5) Wrapping Up- The Composed Bicycle Overview
This section shows how all the code written from step 1 to step 4 works together. No new code is introduced.
- This 54 lines of code replace the 66 lines required for the inheritance strategy. Important thinks to keep in mind:
Bicycle
has-aParts
, which in turnshas-a
collection ofPart
objects.Parts
andPart
may exist as classes. However the important thing is that someone plays theParts
andPart
role (think of them as roles).- In this example
Parts
class plays theParts
role (it implements spares.) The role ofPart
is played by anOpenStruct
(implementsname, description
andneed_spares
).
- In this example
- Here is how you use this code to create a specific type of bike.
- Here is how you create a completely new type of bike (with 3 lines!).
- You only need to describe the new type of bike's parts
Chapter 8.2 - A more strict definition of composition
Aggregation: A Special Kind of Composition
-
Broad definition of composition
- has-a relationship.
- Meals have appetizers, departments have professors.
- Meals and departments are composed objects.
- Appetizers and professors are roles.
- Composed objects depend on the interface of the role.
- New objects that want to act as appetizers only need to implement the appetizer interface.
-
Strict definition of composition
- has-a relationship where the contained object has NO life independent of its container (the composed object).
- e.g when the meal is eaten, the appetizer is also gone.
-
Strict definition of aggregation
- has-a relationship where the contained object can exist independent of its container.
- e.g departments have professors. When the department is gone, the professors continue to exist.
Chapter 8.3 - Composition vs Inheritance
Short Answer
If you cannot explicitly defend inheritance as a better solution, use composition. Composition contains far fewer built-in dependencies than inheritance; it is very often the best choice.
Inheritance is a better solution when its use provides high rewards for low risk.
Long Answer
Take into account the pros and cons of each strategy to help you decide.
Pros and Cons of Inheritance
Pros
- Big changes in behavior can be achieved via small changes in code.
- If you change the methods that are defined at the top of the hierarchy.
- Inheritance hierarchies are open-closed (open for extension and closed for modification.)
- You can easily create new subclasses to accomodate new variants.
- Inheritance hierarchies are easy to follow (exemplary) for other developers as they are very explicit.
Cons
- Small changes can break everything.
- If you change the methods that are defined at the top of the hierarchy.
- You can't extend behavior when a new subclass is a mixture of existing types (e.g you can't model a recumbent mountain bike from a mountain bike and a recumbent bike).
- Chaos arises when novice programmers attempt to extend incorrectly modeled hierarchies.
- Inheritance by definition comes with a deeply embedded set of dependencies.
- The cost of being wrong when designing an inheritance hierarchy is very high. (ask yourself What will happen if I'm wrong?)
Tips when deciding if inheritance is right
- Your decission should be influenced by the expectations of the population that will use your code.
- If in-house app team & You are familiar with the domain -> you may be able to predict the future well-enough to be confident that your design problem is one for which inheritance is a cost-effective solution.
- If you write code for a wider audience -> suitability of inheritance goes down.
- Avoid writing framewors that require users of your code to subclass your objects in order to gain your behavior. Their apps may already use inheritance so inheriting from your framework may not be possible.
Pros and Cons of Composition
Pros
- Composition produces small, structurally independent objects with single responsibilites and well-defined interfaces.
- Also, objects specify their own behavior (Leading to code that is easy to understand).
- Objects are easily pluggable and interchanged.
- Composed objects are independent from a hierarchy.
- Objects are are generally inmune from suffering side effects derived from changes to other objects.
- Because composed objects deal with their parts via an interface, adding a new kind of part is a simple matter of plugging in a new object that honors the interface.
Cons
- A composed object relies on its many parts. Even if each part is small and easily understood, the combined operation of the whole may be less than obvious.
- The benefits of structural independence are gained at the cost of automatic message delegation. The composed object must explicitly know which messages to delegate and to whom.
- Identical delegation code many be needed by many different objects; composition provides no way to share this code.
- Not suitable for arranging code for a collection of parts that are very nearly identical.
Chapter 8.4 - Tips on how to choose between design strategies
Applies for chapters 5 through 8.
The trick to lowering your application costs is to apply each technique to the right problem.
Use Inheritance for is-a Relationships
Example: Imagine your are modelling an app where users can buy six different types of shocks for their bikes.
Different shocks are much mure alike than they are different and they are certainly all shocks.
Shocks can be modelled using a shallow and narrow hierarchy.
If requirements change such that there is an explosion in the kinds of shocks, reassess this design decision. Perhaps it still holds, perhaps not. If modeling a bevy of new shocks requires dramatically expanding the hierarchy, or if the new shocks don’t conveniently fit into the existing code, reconsider alternatives at that time.
Use Duck Types for behaves-like-a Relationships
- 2 keys for recognizing the existence of a role
-
- Although an object plays it, the role is not the object’s main responsibility.
- A bicycle behaves-like-a schedulable but it is-a bicycle
-
- The need is widespread; many otherwise unrelated objects share a desire to play the same role.
-
- Some roles consist only of their interface, others share common behavior. Define the com- mon behavior in a Ruby module to allow objects to play the role without duplicating the code.
Use Composition for has-a Relationships
- When objects have numerous parts but are more than the sum of those parts.
- The is-a versus has-a distinction is at the core of deciding between inheritance and composition.
- The more parts an object has, the more likely it is that it should be modeled with composition.
- The deeper you drill down into individual parts, the more likely it is that you’ll discover a specific part that has a few specialized variants and is thus a reasonable candidate for inheritance (see the shocks example.)