Book Notes: Practical Object-Oriented Design in Ruby


  • Good design is to plan accordingly with what is currently known in a method that is adaptable to change
    • Easy to change is defined as
      • Changes have no unexpected side effects
      • Small changes in requirements correspond with small changes in code
      • Existing code is easy to reuse
      • The easiest way to make a change is to add code that in itself is easy to change
  • Object-oriented design requires a shift from thinking of the world as predefined procedures to modeling the world as a series of messages that pass between objects
  • Object-oriented design is about managing dependencies
  • SOLID object design
    • Single responsibility - an object should do the smallest possible useful thing
    • Open-Closed - an object can be extended without modifying the object
    • Liskov Substitution - Subtypes should be suitable substitutes for their supertypes
    • Interface Segregation - no client should depend on methods it does not use - an interface should not have extraneous methods clients won’t use
    • Dependency Inversion
  • Other principles
    • DRY (Don’t repeat yourself)
    • Law of Demeter - Messages should not pass between multiple boundaries
  • Relationships
    • Use Inheritance for is-a relationships
    • Use Duck Types for behaves-like-a relationships
    • Use Composition for has-a relationships

Single Responsibility

  • Hide instance variables
  • Hide data structures
  • Depend on behavior, not data
  • Methods should not have side-effects
  • Methods should have one responsibility

Managing Dependencies

  • Coupling creates dependency, which increases brittleness - avoid when possible
  • Dependencies are introduced when:
    • Object knows the name of another class
    • Object knows the name of a message it sends to something besides self
    • Object knows the arguments a message requires
    • Object knows the order of the arguments a message requires
  • Depend on things that change less often than you do
  • How to write loosely coupled code?
    • Dependency injection
      • Pass in the dependency as a parameter in initialization or method call
      • Enables duck-typing
      • Abstracts the dependency out for future flexibility
    • Remove argument order dependency
      • Use keyword lists or hashes
    • Define defaults
      • Provides flexibility in parameters
    • Change dependency directions
      • A depends on B, but change B to depend on A
      • Follow the rule depend on things that change less often than you do
    • If dependency is required
      • Isolate instance creation in initializer or isolated method

Flexible Interfaces

  • Define by behavior, rather than nouns
    • Asking for “what” instead of telling “how”
  • Context independence
    • The more an object knows about other objects and their expected behavior, hard to test, debug, and reuse
    • Isolate the context an object knows about
      • Call generic methods on other objects, passing self, instead of calling methods on those objects directly
    • Reduces public interface
    • Aim to minimize context
  • Keep public interface small
    • Respect other public interfaces
    • Rely on private methods for independent or likely-to-change behavior
  • Follow Law of Demeter
    • If long message chains appear, this indicates
      • Dependencies
      • Context dependence
    • Reduce message calls

Duck Typing

  • Reuse public interfaces between objects
    • Objects can reuse multiple interfaces
  • Defined by behavior more than their specific class
  • Removes dependencies on hardcoded classes
  • Duck typing should be used when behavior is based on an object’s class
    • Case statements
    • kind_of? and is_a?
    • responds_to?

Behavior Through Inheritance

  • Inheritance is automatic message delegation
    • Defines a path for messages to be forwarded from one object to another
  • Open-closed
    • Open for extension while closed for modification
  • Used when an object has multiple types
    • Majority of behavior is shared, but specific instances where behavior is different
  • Multiple inheritance is permitted in Ruby
    • Promote duplicated code in subclasses to superclass
  • Misapplying inheritance
    • Subclasses have behavior that does not make sense
    • Should separate behavior that does not make sense into multiple subclasses
    • Avoid coupling between subclass and superclass
  • Template method
    • Include methods in subclasses that are called in abstract superclass (i.e., those methods don’t exist in the super class)
    • Ensures that proper data is filled
    • Can add methods to superclass, but raise error when called to explicitly define template
  • Hook methods
    • Avoid calling super in subclasses
    • Call messages implemented by the subclass in the superclass initialization method for specific behavior
    • Rather than call super in subclass, pass control back to superclass by call a method on subclass to get data

Sharing Roles with Modules

  • Roles are shared behavior between otherwise unrelated objects
  • Shared coding techniques with inheritance due to presence in method lookup chain
  • Similar to duck types, but in cases where specific behavior is needed in addition to specific message signatures
  • Use with caution - can drastically enlarge the scope of messages an object can respond to
  • Lets objects speak for themselves, rather than using a separate class
  • Modules earlier in method lookup chain than superclasses
  • extend keyword adds instance & class methods to object, rather than adding them to the method lookup chain
  • Create shallow hierarchies
    • Removes flexibility
    • Hard to understand which receiver will respond to a message

Composition

  • Larger object connected to parts via a has-a relationship
  • Class names that are composed of many objects should be plural, while individual objects are singular
  • Factory classes can be used to create composed objects
  • Difference between Inheritance and Composition
    • Inheritance provides message delegation for free when structuring a hierarchy
    • Composition allows objects to be structurally independent, but messages must be explicitly passed
    • Inheritance isn’t always transparent, but correctly modeled hierarchies are natural and easy to extend
    • Inheritance creates dependencies by nature, and can be the wrong solution to a problem (which is a high-cost decision)
    • Composition creates many small, independent objects that use a common interface
    • Composed objects must explicitly known which messages to delegate to which object

Designing Cost-Effective Tests

  • Reduces cost!
  • Testing is for
    • finding bugs
    • supplying documentation
    • deferring design decisions
    • supporting abstractions
    • exposing design flaws
  • Don’t overdo testing - once is enough
    • Test incoming messages and their outgoing results
  • Types of methods
    • Query methods - no side effects, return a value
    • Command methods - side effects, cause other behavior
  • Tests revolve around response to incoming messages (state) or passing other messages (commands)
  • Isolate the object under test
    • Removes dependencies on other objects
    • Prevents misleading failures where dependencies are in fact failing
  • Test Doubles speed tests and remove context
    • Should be used in place of objects that aren’t required for the test, decoupling code
  • Private methods should not be tested
    • If a private test is begging to be tested, it’s a good sign this could be abstracted out to a separate object with a public interface
  • Test Mocks
    • Used to verify messages are received
    • Testing commands
  • Testing Duck Types
    • Module that includes tests to assert the interface exists
    • Using same technique as Role Sharing, Ducks objects can include module to test interface
    • Tests should be used Test Doubles as well to verify their authenticity
  • Testing Inherited code
    • Module that includes tests for the common interface contract that all classes of the hierarchy enforce
    • If subclasses must implement certain behavior, use a module to test for the subclass interface contract
    • If superclasses must implement certain behavior (such as raising NotImplemented errors), use a module to test for the superclass behavior