Matt Willard


Practical Object-Oriented Design in Ruby

Software under this paradigm is a series of messages that pass between black boxes called objects.
It’s about managing dependencies. If objects know too much about each other, they’re not adaptable and flexible. Inevitable change will mess it up.
(Remember: a dependency is basically: object A needs object/thing B to work.)
Ideally, you want to get to a juncture where an object doesn’t need to know how other objects work in order to get what it wants.

Design lets you do design LATER and reduces the cost and ardor of needed change, breaking less shit when you add something new.

Design software with an Agile approach: do it as you go and adjust based on feedback. But start with something up front, of course. Design DOES pay off in the long run however.

CLASSES NEED A SINGLE RESPONSIBILITY
Classes should be designed with a single responsibility that makes them easy to change over time. In fact, they should have a SINGLE reason to change.
-changes have no unexpected side effects
-small changes = smal changes in code
-easy to reuse
-easiest way to make a change is to add code that is also easy to change

Consequences of changing code should be obvious.
Costy of change = proportional to benefits
Existing code is usuable

For example: changing the number of required arguments for an object, in a big program, would be a huge cost having to change things everywhere.

Do this:

  • try to describe the class in one sentence: if there’s an “and” or an “or” in that description, chances are pretty good the class has more than one responsbility.
  • rephrase its methods as a question: “Mr. Gear what is your ratio?” This question makes sense. “Mr. Gear, what is your tire size?” This less so - a Gear doesn’t have a tire size, so it seems like it’s doing more than one thing.

    You have to consider the cost of changing a muddled class today versus the cost of changing it in the future. Every design decision has a choice. So the ideal thing to do is arrange the code in case it needs to be changed, even if you don’t know it.

    do this:
  • Classes should depend on behavior, not data. Good classes keep behavior in one place only.
    -Use getters and setters so you only need to change instance variables in one place. Hide it even from the class that has it.
    -Hide data structures too. Write wrapper methods that decipher the structure and work within it. It’s better to send a message to wheel.tire and get something back as opposed to send a message to like cell[1].
  • Everything should have single responsibility: classes, methods, etc. Ask what they do, try to describe them in a single sentence. Seperate where needed.


    DEPENDENCIES
    Object-oriented design is all about a series of messages that pass between objects.
    Sooner or later, one object must talk to each other, have that behavior, or inherit it.

    Remember: a dependency is when object A has to know something about object B.
    If object B has to change, and object A then MIGHT have to change, that’s a dependency.
    Some dependecies (arguments) are inevitable, but we can remove a lot of them. The goal is: reduce dependencies to the fewest possible to do its job.

    Objects have a dependency when:
  • they know the name of another class. (Gear expects Wheel to exist.)
  • they know the name of a message that it intends to send to someone. (Gear expects Wheel to response to diameter.)
  • the arguments a message requires. (Gear knows that new Wheel needs rim and tire.)
  • the order of those arguments.

    Here’s how to write loosely coupled (way less dependencies) code:
  • inject dependencies where possible. For example, in Gear, instead of using a hard-cided reference to the Wheel class, introduce a Wheel-or-other-object instance (instance_of_wheel) as an argument, then refer to that argument instead. This way, Gear knows less.
  • Seek to isolate dependencies where possible. For example, if you ABSOLUTELY need a new Wheel instance inside of the Gear class, put it in the constructor. You can also put it in a def wheel method to use that one instead. Better than nothing.\
  • You can do this with dependent messages, too, putting it inside their own method. (These are for messages not neccessarily intended for self, like calling wheel.diameter)
  • For depeendent arguments, try using a data structure/hash for the single argument with the guts of what you need. (args has chainring, cog, and wheel, as an example) But better to do this with more complicated argument needs.
  • Also, if you absolutely need a fixed order of arguments, you can use a wrapper to set the initial instance anyway with args.
  • Speaking of arguments, set explicit defaults for arguments in case they’re otherwise not available.

    Consider the direction of your dependencies to lower change consequences -
    depend on the things that change less often than you do.
    In basic terms: your classes that are likely to change often and/or their changes have broad effects should ideally pull their dependencies from more abstract classes (consider Java’s innate String, for example) that are unlikely to change and/or have few side effects.
    You want to try to avoid, however, classes that are likely to change a LOT and have a lot of depndencies. (Keep an eye on ANY class that has a lot of dependencies.)


    INTERFACES
    Remember that an OOP application is made up of classes but defined by their messages.
    Via interfaces, you can work with how objects send messages to each other.

    For this path of dialogue, it’s best to shoot for classes that reveal as little about themselves, and know as little about others, as possible. The path of these message trails should be clear, a series of pluggable islands with a few bridges connecting them.

    Exposed methods in a class are called the public interface.

    Here’s an example. You go to the Counter and place an Order. That Order goes back to the Kitchen, that makes the Order and returns it.
    The Counter is like the public interface. It sends a message to the kitchen: “make this Order”. Doesn’t care what Kitchen does or how it makes the Order. All ti does is “give this Order” and Kitchen returns Order using its private messages in its private interface.

    Public interfaces are public, expect to be invoked by others, and are usually very stable.
    Private interfaces handle implementation details, can and will change, and stay in the object.


    DESIGNING
    Focus your design around the messages that pass between objects, not just the obvious domain objects with data and behavior.
    You want to focus on asking for what you want, instead of how it’s done.
    You want to consider the intention: the objects and the messages that need to pass between them.

    The fundamental design question to start off: “I need to send this message, who should respond to it?” Work on message-based design between objects, and not objects that just happen to send messages. You don’t send messages because you have objects, you have objects because you send messages.

    A great thing to do is to use sequence diagrams to design the flow of messages between objects. This allows you to evaluate each message and see more accurately what objects should be responding to which messages.

    Ask for WHAT instead of telling HOW. A Trip shouldn’t tell a Mechanic “do this, then this, then this” for each bicycle. It should just be like “give prepared bicycles” and MEchanic does it all and returns it. EVEN BETTER: it can remove context and just be like “prepare trip”. Making context independent from the object helps with this. (Try injecting dependencies like the Trip itself, or even an array of objects into prepare_trip.)

    Sequence diagraming is cool because it allows you to discover objects you need with minimal drawback. If Customer wants to find suitable trips, he can send the message to a TripFinder interface that does the dirty work for him, without Customer having to specify. (Remember, diagrams can change as you go, that’s the point and it’s fine.)

    => Here’s how to write good interfaces for these sorts of designs:
  • make them explicit, aboput what than how, with names you don’t expect to change, which takes a hash as a parameter
  • You should really try to use public interfaces over private methods. Getters and setters, for example. If you absolutely must use something private, try to isolate that dependency.
  • Remember to minimize context. Try to have public methods get WHAT they want without knowing HOW the other class does it. If you need to, you can set up a public interface with a new public method, a wrapper class, or a wrapper method.

    The Law of Demeter: rule of thumb, but most of the time, try to talk to your neighboring classes (use only one dot, like bicycle.wheel). This isn’t ALWAYS true and some violations make sense (Hash.keys.short.join(’, ‘)). But typically, more context = more violation.

    A chain like this though (customer.bicycle.wheel.rotate) is another HOW expression. “I know what I want but also HOW to navigate through it to get there.” Chains => more context => more cost, usually, when needing to be changed as well as limits on reuse. Focusing on a message based design will help here, passed along public interfaces.


    DUCK TYPING
    If an object walks and talks like a duck, it’s a duck.
    Specifically, duck types are public interfaces defined by their behavior and aren’t tied to a specific class. They’re flexible, easy to change, and reduces design costs. It’s like abstracting out the concept. You don’t pass in a Mechanic/Coordinator/Driver, you pass in Preparers, and the resulting interface can accept any preparer and work accordingly.

    Think of it like this: a method thinks it needs a mechanic to prepare bicycles. But if it changes and we need to pass in a driver too, you’ll have to change that methods behavior, add other methods, etc. More dependent classes needed = greater change cost. Switching to Preparers makes it more abstract and a little harder to understand but the tradeoff is flexibility.

    (As a bonus, this is a great way to achieve polymorphism. One message (poly) has many forms (morphs).)

    It’s important to trust your duck types - we feel wired to get specific but we need to trust them to be whatever they have to do and to behave correctly.

    Here’s some ways to determine where you could install a duck type:
  • If there’s an if/case statement that switches on class name, find the common ground and abstract it out to a duck type.
  • kindof and instanceof methods check class and can be duck typed often. Same thing with respondsto.
    In terms of working with static typed languages, duck typing is built on accepting the use of dynamic typing.


    INHERITANCE
    Think of classical inheritance (Cat extends Animal) as a pre-determined message path. The subclass, Cat, automatically forwards messages to the superclass, Animal.

    The usual thing to do is to create a base abstract class and then have subclasses extend from it, implementing the core while adding things unique to that class. (Both Cat and Bird extend Animal, but Bird adds flight, while Cat has lands_on_feet exclusive to itself, etc.) This hellps reduce change costs.

    Here’s some rules of thumb when applying inheritance:
  • find the abstraction. Subclasses are specializations of their superclasses, so the superclass needs to contain common behavior shared among all specifications. The subclasses themselves will implement anytyhing specialized to them.
  • don’t do inheritance if there’s only one subclass needed.
  • when setting up an abstraction, keep the code in a subclass at first, then promote code to an abstract superclass where needed for other subclasses. It’s easier to promote up than demote downwards. This creates a more trustworthy hierarchy with what you need.

    Since you also need to manage coupling between sub and superclasses, focus on using hooks to send messages over having the subclasses receive and know about the super() constructor. Sending it makes a dependency. One thing to do is to send empty implementations of methods that subclasses will then override for their own implementations.


    MODULES AND SHARING ROLE BEHAVIOR
    Roles use modules to define common behavior as an alternative to inheritance. Remember, inheritance does have a cost when it comes to changing, especially if subclasses ultimately need to be combined.

    When objects begin to play a common role together, it creates relationships with other objects and thus dependencies. By defining a role, you can make code to share among all players and minimize dependencies. (For example, a Preparer interface is often a role with a pair such as Preparable.)

    As always, you want to consider the messages being sent over the details of knowing what’s in other classes. By removing unneeded depdencies, and making sure objects only contain/manage their own behavior, you can improve responsibilities. (You can do this with duck types; for example, checking if something is “schedulable”)

    Let objects speak for themselves. Many times, you don’t need a third party to manage something a class can do on its own.

    Here’s some rules of thumb when implementing role behavior as models.
  • Implement duck types using a concrete class at first.
  • Then, extract the abstraction.

    And if you want to write good inheritable code to keep them more useful and maintainable, follow these tips.
  • Avoid antipatterns like using category/type variables to determne the type of message to send. Also, avoid having the sending object check for the type: set up a dock type for these.
  • Insist on abstractions - all code in the superclass should apply to EVERY subclass. Workarounds for this are bad hacks for weird behavior. If you can’t find an abstraction, don’t make one and don’t do inheritance for it.
  • Honor the contract. Subclasses need to take the same kind of inputs and return the same kind of outputs across the board.
  • Aim to pre-emptively decouple classes - use hooks that don’t require inheritors to send super.
  • Aim for a shallow, narrow hierarchy of superclass and subclasses. Really try to avoid having subclasses inherit from subclasses, it’s harder to work with in the long run. Shallow, wide pyramids that inherit from one superclass and easier to use and extend. Shallow and wide is better, but deep narrow and deep wide hierarchies get harder to work with.

    COMPOSITION
    Composition is about combining parts into a whole. In software, this is combining smaller objects into one big one.

    Inheritance has an is-a relationship: a mountain bike is a bicycle.
    Composition has a has-a relationship: a bicycle has a pedal, a wheel, etc.
    Aggregation, in turn, is basically composition, but the objects inside of the bigger object have lives of their own. (A university can have professors and these professors can be in departments, but if the department is destroyed, the professors still exist and do things on their own.)

    A Bicycle could have Parts. It could ask its Parts for specific pieces, and in Parts, you can add Parts to make a mountain bike, or different Prts for a Road bike, etc. The Parts object could have a bunch of Part objects, in turn.

    However, you need to be mindful of when polling for certain things returns different things than expected. In this situation you have to consider the pros and cons of certain approaches when polling for information.

    In situations like this, where you’re making a lot of Parts based on criteria, you can use a PartsFactory to take in arguments and solely focus on creating Parts. This is a design pattern reused a lot on object-oriented design. This way, you can often reduce dependecies and simplify the code, as well as promoting composition.

    While compositon can be effective, it shouldn’t be used all the time.
    It’s important to consider the pros and cons VS composition and inheritance.

    INHERITANCE
    PROS: small code changes can do big chances in behavior; easier to understand; open for extending and closed for modifying and it’s easy to do
    CONS: if you screw up the hierarchy it can be trouble to work with; if the model isn’t correct you’ll need to clone or restructure code; more dependencies; those broad changes in code can backfire

    COMPOSITION
    PROS: easy to understand small objects that make up bigger, adding new parts is easy and high tolerance to change, easy to extend
    CONS: more automatic message delegation and often identical, whole object can be more complicated

    Consider the relationships you need:
  • When you have an is-a relationships, and the new class mostly has another one’s functionality and only has a small, specialized bit of new code, consider inheritance.
  • If you have a behaves-like-a relationships, where many objects play a common role, use a duck type. (Example: an appointment is schedulable. A trip is schedulable. Both of these have the Schedulable role, and thus can use a duck type interface for this common role.)
  • Has-a relationships benefit best from composition. A Bicycle can be built from many Parts, but the entire Bicycle object has behavior seperate and in addition to these Parts. (However, many Parts may share similar functionality, and these would benefit from inheritance.)
    Practice these design techniques and do your best to learn them and improve with them. No shame. Experience will help.


    COST-EFFECTIVE TESTS
    Good, changeable code depends on:
    -the code being well-designed
    -refactoring being applied skillfully and carefully
    -the ability to write high-value tests

    Good design stays flexible by delaying design decisions as much as possible until specific requirements are gathered.
    Good refactoring will then morph this code into what’s needed for the new requirements. This will improve your design skills.

    The main reason to have tests: above all else, they reduce costs for change. Reducing bugs, providing documentation, and improving application design are good side bonuses.
    The best thing to do is to know what, when, and how to test. Then, the learning curve will pay off.

    -Finding bugs: with tests, they’re found earlier rather than later, which lowers change costs.
    -Documentation: help yourself remember design choices
    -Defer decisions: allows you to function without requiring specific designs until you get more info; by testing through interfaces, you can make changes without too much hassle
    -Support abstractions: since good design relies on small independent objects that need abstraction, tests record what you’re doing so you can understand the whole bheavior of the design
    -Expose design flaws: bad designs usually mean painful tests like too much context or too many depdencies; good tests root these out

    -WHAT TO TEST-
    Write fewer tests that hit everything just once.
    Remember: object-oriented applications are a series of messages passing between objects that are treated like black boxes. Think of it this way to limit what’s exposed and what others are permitted to know.
    It’s ideal to deal with objects as if they’re only and exactly the messages to which they respond. This makes the app more changeable, and allows you to write high-value tests at minimum cost.
    Tests have the same issue - you need to limit couplings to other classes and write these tests to public interfaces to reduce change costs and keep out of boundaries. They should, in turn, focus mainly on incoming or outgoing messages.

    Incoming messages should be tested for state.
    Outgoing messages with side effects (file is written, data table is saved) should be tested.
    Outgoing messages that DON’T do this don’t need to be tested.

    -WHEN TO TEST-
    Write tests FIRST, where it makes sense. It won’t promise a well-designed application but it’s still an improvement, lowering overall costs. Use the above design skills to write testable code first that’s loosely coupled and reusuable.

    -HOW TO TEST-
    Err towards frameworks with the best, recent support.
    As for doing TDD or BDD, depends on the situation, but both are fine, with pros and cons. (BDD is outside-in, starting with objects, and TDD is inside-out, starting with tests and building around this.)
    Write tests that only know about the messages that come and go.

    Here’s some general testing tips:
    -delete code that has no value,especially for interfaces without dependents
    -prove that the public interface returns the correct value in every possible situation
    -test the object in isolation and make sure it doesn’t rely on code from other objects
    -interject dependencies, classes or roles, as needed because then tests will break when they should which is important
    -use clone double objects for this for steady testing

    Test private methods by:
    -focusing mainly on testing the public methods that invoke these only

    test outgoing messages by:
    -ignoring messages sent to self and outgoing query messages
    -use mocks to prove command messages; easily substituted when injecting dependencies

    test duck types:
    make sure they’re set up and test that they implement the duck type interface
    -validate double objects with role tests

    test inherited code:
    -write shared test for common contract and include test in every object to make sure contract holds up
    -use a test to confirm that each subclass meets the requirements of the superclass
    -test to confirm superclass will enforce this and raise errors

    -make sure to test concrete subclass behavior and abstract superclass behavior.\