Skip to content

Notes for Sandi Metz's OOD book: "Practical Object-Oriented Design in Ruby."

Notifications You must be signed in to change notification settings

serodriguez68/poodr-notes

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

74 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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):

  1. The name of another class. (Gear expects a class named Wheel to exist.)
  2. The name of the message that it intends to send to someone other than self. (Gear expects a Wheel instance to respond to diameter).
  3. 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.
  4. The order of those arguments. (Gear knows the first argument to Wheel.new should be 'rim', 'tire' second.)

*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.
  • 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.

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.
  • 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

Wrong Code Example

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

Right Code Example

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

  • 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:

  1. There is a generalization-specialization relationship in the objects you are modelling.
  2. 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.

  • 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).
    • 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.
  • 3) Invite Inheritors to Supply Specializations Using the Template Method Pattern

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

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 a Preparable role).
      • Preparableimplements an interface with all methods that a Preparermight 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

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.

  • 4.2) Where the code lives (Extracting the Abstraction)

_______________________________________________________________________________

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 or category to determine what message to send to self
  • 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.

Step 1) Compose Parts into a Bicycle

  • 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.

Step 2) Moving the parts logic into the Partsclass

  • 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.

Step 3) Composing the Partsobject with Partobjects

There will be a Parts object and it will contain many Part objects.

The following diagram illustrates the final strategy that is going to get built. Notice that inheritance dissappears.

Step 3.1) Creating a Part object

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 the Part role. They don’t have to be a kind-of the Part class, they just have to act like one; that is, they must respond to name, description, and needs_spare.

  • Cons:

    • The Bicycle's methods spares and parts 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

Step 3.2) Making the Parts Object More Like an Array

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 and sort (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 subclassed Parts object.
      • Many methods in the Àrray class return arrays.
      • In approach 1 a Parts object could not respond to size. In this approach the addition of to Parts objects cannot respond to spares.
  • 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 whenever size or each is sent to a Parts object, the message will be forwared to it's @parts (i.e @parts.size and @parts.each).
      • Classes are usually extended with Forwardable.
    • 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 an each that yields successive members of the collection.
      • On this example each in the Parts class is 'implemented' by forwarding it to it's @parts attribute.
    • This example shows that both spares and a Parts object respond to size.
    • Pros:
    • Cons:
      • The code may be complex for new developers.

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.

Step 4.1) There are only a few valid combination of Part objects. Centralize that knowledge in one place.

Step 4.2) Create the PartsFactory

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:
      1. Config array
    • 2 & 3) Name of the classes to be used for creating Part objects and the Parts 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.
  • 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.

Step 4.2) Leveraging the PartsFactory to remove the Part class

If the PartsFactory created every part, the Part class would not be necessary. This would simplify the code.

  • The Part class can be replaced by an OpenStruct.
    • OpenStruct is a lot like Struct. It provides a convenient way to bundle a number of attributes into an object (without creatin a class).
      • OpenStruct takes a hash for initialization while Struct takes position order initialiation arguments.
  • This code shows a refactored version of the PartsFactory where the Part creation was moved into the factory using OpenStruct and the Part class was deleted.

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.

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
      1. 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
      1. 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.)

About

Notes for Sandi Metz's OOD book: "Practical Object-Oriented Design in Ruby."

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages