2017-11-26
- 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