Skip to content

Practical Object-Oriented Design in Ruby

Metadata

  • Author: [[Sandi Metz]]

Highlights

Failures of OOD might look like failures of coding technique but they are actually failures of perspective. The first requirement for learning how to do object-oriented design is to immerse yourself in objects; once you acquire an object-oriented perspective the rest follows naturally. — location: 341 ^ref-43619


OO languages are thus open-ended. They don’t limit you to a small set of built-in types and pre-predefined operations; you can invent brand new types of your own. Each OO application gradually becomes a unique programming language that is specifically tailored to your domain. — location: 572 ^ref-6115


Design is more the art of preserving changeability than it is the act of achieving perfection. — location: 611 ^ref-49086


A class that has more than one responsibility is difficult to reuse. The various responsibilities are likely thoroughly entangled within the class. If you want to reuse some (but not all) of its behavior, it is impossible to get at only the parts you need. You are faced with two options and neither is particularly appealing. — location: 732 ^ref-47457


One way is to pretend that it’s sentient and to interrogate it. If you rephrase every one of its methods as a question, asking the question ought to make sense. For example, “Please Mr. Gear, what is your ratio?” seems perfectly reasonable, while “Please Mr. Gear, what are your gear_inches?” is on shaky ground, and “Please Mr. Gear, what is your tire (size)?” is just downright ridiculous. — location: 741 ^ref-63788


Another way to hone in on what a class is actually doing is to attempt to describe it in one sentence. Remember that a class should do the smallest possible useful thing. That thing ought to be simple to describe. If the simplest description you can devise uses the word “and,” the class likely has more than one responsibility. If it uses the word “or,” then the class has more than one responsibility and they aren’t even very related. — location: 748 ^ref-36099


In addition to behavior, objects often contain data. Data is held in an instance variable and can be anything from a simple string or a complex hash. Data can be accessed in one of two ways; you can refer directly to the instance variable or you can wrap the instance variable in an accessor method. — location: 788 ^ref-12561


However, since @data contains a complicated data structure, just hiding the instance variable is not enough. The data method merely returns the array. To do anything useful, each sender of data must have complete knowledge of what piece of data is at which index in the array. — location: 873 ^ref-5847


Separating iteration from the action that’s being performed on each element is a common case of multiple responsibility that is easy to recognize. In other cases the problem is not so obvious. — location: 946 ^ref-56848


Do these refactorings even when you do not know the ultimate design. They are needed, not because the design is clear, but because it isn’t. You do not have to know where you’re going to use good design practices to get there. Good practices reveal design. — location: 965 ^ref-33899


Summary The path to changeable and maintainable object-oriented software begins with classes that have a single responsibility. Classes that do one thing isolate that thing from the rest of your application. This isolation allows change without consequence and reuse without duplication. — location: 1072 ^ref-39253


Each message is initiated by an object to invoke some bit of behavior. All of the behavior is dispersed among the objects. Therefore, for any desired behavior, an object either knows it personally, inherits it, or knows another object who knows — location: 1079 ^ref-38517


Each of these dependencies creates a chance that Gear will be forced to change because of a change to Wheel. Some degree of dependency between these two classes is inevitable, after all, they must collaborate, but most of the dependencies listed above are unnecessary. — location: 1132 ^ref-4833


When working on an existing application you may find yourself under severe constraints about how much you can actually change. If prevented from achieving perfection, your goals should switch to improving the overall situation by leaving the code better than you found — location: 1243 ^ref-47123


The foundation of an object-oriented system is the message, but the most visible organizational structure is the class. Messages are at the core of design, but because classes are so obvious this chapter starts small and concentrates on how to decide what belongs in a class. The design emphasis will gradually shift from classes to messages over the next several chapters. — location: 590 ^ref-46914


If you define easy to change as • Changes have no unexpected side effects • Small changes in requirements require correspondingly 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. Then the code you write should have the following qualities. Code should be • Transparent The consequences of change should be obvious in the code that is changing and in distant code relies upon it • Reasonable The cost of any change should be proportional to the benefits the change achieves • Usable Existing code should be usable in new and unexpected contexts • Exemplary The code itself should encourage those who change it to perpetuate these qualities — location: 615 ^ref-42635


Think of every dependency as an alien bacterium that’s trying to infect your class. Give your class a vigorous immune system; quarantine each dependency. Dependencies are foreign invaders that represent vulnerabilities, and they should be concise, explicit, and isolated. — location: 1248 ^ref-18409


The way you manage dependencies on external class names has profound effects on your application. If you are mindful of dependencies and develop a habit of routinely injecting them, your classes will naturally be loosely coupled. — location: 1294 ^ref-40061


Now that you’ve isolated references to external class names it’s time to turn your attention to external messages, that is, messages that are “sent to someone other than self.” — location: 1299 ^ref-13611


This technique becomes necessary when a class contains embedded references to a message that is likely to change. Isolating the reference provides some insurance against being affected by that change. Although not every external method is a candidate for this preemptive isolation, it’s worth examining your code, looking for and wrapping the most vulnerable dependencies. — location: 1334 ^ref-12199


An alternative way to eliminate these side effects is to avoid the problem from the very beginning by reversing the direction of the dependency. — location: 1337 ^ref-45202


If you are working on a method whose parameter list is lengthy and wildly unstable, in a framework that is intended to be used by others, it will likely lower overall costs if you specify arguments in a hash. — location: 1394 ^ref-18489


The classes in your application should depend on code that you own; use a wrapping method to isolate external dependencies. — location: 1456 ^ref-35309


protect yourself by wrapping each in a method that is owned by your own application. — location: 1500 ^ref-57936


interesting thing about GearWrapper is that its sole purpose is to create instances of some other class. Object-oriented designers have a word for objects like this; they call them factories. — location: 1495 ^ref-24878


Choosing Dependency Direction Pretend for a moment that your classes are people. If you were to give them advice about how to behave you would tell them to depend on things that change less often than you do. — location: 1547 ^ref-23934


This short statement belies the sophistication of the idea, which is based on three simple truths about code: • Some classes are more likely than others to have changes in requirements. • Concrete classes are more likely to change than abstract classes. • Changing a class that has many dependents will result in widespread consequences. — location: 1549 ^ref-31640


Dependency management is core to creating future-proof applications. Injecting dependencies creates loosely coupled objects that can be reused in novel ways. Isolating dependencies allows objects to quickly adapt to unexpected changes. Depending on abstractions decreases the likelihood of facing these changes. The key to managing dependencies is to control their direction. The road to maintenance nirvana is paved with classes that depend on things that change less often than they do. — location: 1621 ^ref-8289


Design, therefore, must be concerned with the messages that pass between objects. It deals not only with what objects know (their responsibilities) and who they know (their dependencies), but how they talk to one another. The conversation between objects takes place using their interfaces; this chapter explores creating flexible interfaces that allow applications to grow and to change. — location: 1629 ^ref-13608


When your classes use the public methods of others, you trust those methods to be stable. When you decide to depend on the private methods of others, you understand that you are relying on something that is inherently unstable and are thus increasing the risk of being affected by a distant and unrelated change. — location: 1694 ^ref-65096


Domain objects are easy to find but they are not at the design center of your application. Instead, they are a trap for the unwary. If you fixate on domain objects you will tend to coerce behavior into them. 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. — location: 1725 ^ref-58251


“Should this receiver be responsible for responding to this message?” Therein lies the value of sequence diagrams. They explicitly specify the messages that pass between objects, and because objects should only communicate using public interfaces, sequence diagrams are a vehicle for exposing, experimenting with, and ultimately defining those interfaces. — location: 1763 ^ref-4807


This transition from class-based design to message-based design is a turning point in your design career. The message-based perspective yields more flexible applications than does the class-based perspective. — location: 1769 ^ref-52472


You don’t send messages because you have objects, you have objects because you send messages. — location: 1772 ^ref-21042


The problem in Figure 4.4 is that Moe not only knows what he wants, he also knows how other objects should collaborate to provide it. The Customer class has become the owner of the application rules that assess trip suitability. — location: 1790 ^ref-60123


Asking for “What” Instead of Telling “How” The distinction between a message that asks for what the sender wants and a message that tells the receiver how to behave may seem subtle but the consequences are significant. Understanding this difference is a key part of creating reusable classes with well-defined public interfaces. — location: 1794 ^ref-48725


If objects were human and could describe their own relationships, in Figure 4.5 Trip would be telling Mechanic: “I know what I want and I know how you do it;” in Figure 4.6: “I know what I want and I know what you do” and in Figure 4.7: “I know what I want and I trust you to do your part.” — location: 1883 ^ref-64885


Armed with knowledge about the distinction between what and how, and the importance of context and trust, — location: 1888 ^ref-8476


(Inter)Face Forward The clarity of your interfaces reveals your design skills and reflects your self-discipline. — location: 1920 ^ref-35634


The clarity of your interfaces reveals your design skills and reflects your self-discipline. Because design skills are always improving but never perfected, and because even today’s beautiful design may look ugly in light of tomorrow’s requirement, it is difficult to create perfect interfaces. — location: 1921 ^ref-29872


The following section contains rules-of-thumb for creating interfaces. Create Explicit Interfaces — location: 1926 ^ref-54053


When you depend on a private interface you increase the risk of being forced to change. When that private interface is part of an external framework that undergoes periodic releases, this dependency is like a time bomb that will go off at the worst possible moment. — location: 1969 ^ref-20872


Summary Object-oriented applications are defined by the messages that pass between objects. This message passing takes place along “public” interfaces; well-defined public interfaces consist of stable methods that expose the responsibilities of their underlying classes and provide maximal benefit at minimal cost. Focusing on messages reveals objects that might otherwise be overlooked. When messages are trusting and ask for what the sender wants instead of telling the receiver how to behave, objects naturally evolve public interfaces that are flexible and reusable in novel and unexpected ways. — location: 2058 ^ref-57895


Message chains like customer.bicycle.wheel.rotate occur when your design thoughts are unduly influenced by objects you already know. Your familiarity with the public interfaces of known objects may lead you to string together long message chains to get at distant behavior. — location: 2039 ^ref-43449


The purpose of object-oriented design is to reduce the cost of change. Now that you know messages are at the design center of your application, and now that you are committed to the construction of rigorously defined public interfaces, you can combine these two ideas into a powerful design technique that further reduces your costs. — location: 2064 ^ref-63428


Duck types are public interfaces that are not tied to any specific class. These across-class interfaces add enormous flexibility to your application by replacing costly dependencies on class with more forgiving dependencies on messages. — location: 2067 ^ref-26855


Every sequence diagram thus far — location: 2185 ^ref-18657


Sequence diagrams should always be simpler than the code they represent; when they are not, something is wrong with the design. — location: 2186 ^ref-34596


This tension between the costs of concretion and the costs of abstraction is fundamental to object-oriented design. Concrete code is easy to understand but costly to extend. Abstract code may initially seem more obscure but, once understood, is far easier to change. — location: 2247 ^ref-49055


Polymorphism in OOP refers to the ability of many different objects to respond to the same message. Senders of the message need not care about the class of the receiver; receivers supply their own specific version of the behavior. — location: 2257 ^ref-46144


You can replace the following with ducks: • Case statements that switch on class • kind_of? and is_a? • responds_to? — location: 2270 ^ref-12120


it controls rather than trusts other objects. — location: 2323 ^ref-41794


Flexible applications are built on objects that operate on trust; it is your job to make your objects trustworthy. When you see these code patterns, concentrate on the offending code’s expectations and use those expectations to find the duck type. Once you have a duck type in mind, define its interface, implement that interface where necessary, and then trust those implementers to behave correctly. — location: 2330 ^ref-726


The major difference between this example and the previous ones is the stability of the classes that are being checked. When first depends on Integer and Hash, it is depending on core Ruby classes that are far more stable than it — location: 2365 ^ref-6853


Summary Messages are at the center of object-oriented applications and they pass among objects along public interfaces. Duck typing detaches these public interfaces from specific classes, creating virtual types that are defined by what they do instead of by who they are. Duck typing reveals underlying abstractions that might otherwise be invisible. Depending on these abstractions reduces risk and increases flexibility, making your application cheaper to maintain and easier to change. — location: 2446 ^ref-41148


This code contains an if statement that checks an attribute that holds the category of self to determine what message to send to self. This should bring back memories of a pattern discussed in the previous chapter on duck typing, where you saw an if statement that checked the class of an object to determine what message to send to that object. — location: 2580 ^ref-61854


Variables with these kinds of names are your cue to notice the underlying pattern. Type and category are words perilously similar to those you would use when describing a class. After all, what is a class if not a category or type? — location: 2591 ^ref-5353


These are the rules of inheritance; break them at your peril. For inheritance to work, two things must always be true. First, the objects that you are modeling must truly have a generalization–specialization relationship. Second, you must use the correct coding techniques. — location: 2698 ^ref-2643


Abstract classes exist to be subclassed. This is their sole purpose. They provide a common repository for behavior that is shared across a set of subclasses—subclasses that in turn supply specializations. — location: 2718 ^ref-33958


Even though you now have a requirement for two kinds of bikes, this still may not be the right moment to commit to inheritance. Creating a hierarchy has costs; the best way to minimize these costs is to maximize your chance of getting the abstraction right before allowing subclasses to depend on it. — location: 2723 ^ref-25793


If a third bike is imminent, it may be best to duplicate the code and wait for better information. However, if the duplicated code would need to change every day, it may be cheaper to go ahead and create the hierarchy. You should wait, if you can, but don’t fear to move forward based on two concrete cases if this seems best. — location: 2730 ^ref-21623


However, if you start by moving every bit of the Bicycle code to RoadBike, you can then carefully identify and promote the abstract parts without fear of leaving concrete artifacts. — location: 2825 ^ref-35106


but this push-everything-down-and-then-pull-some-things-up strategy is an important part of this refactoring. — location: 2821 ^ref-4952

Follow push everything down then pull it up strategy


The general rule for refactoring into a new inheritance hierarchy is to arrange code so that you can promote abstractions rather than demote concretions. — location: 2841 ^ref-63852


This technique of defining a basic structure in the superclass and sending messages to acquire subclass-specific contributions is known as the template method pattern. — location: 2907 ^ref-54836


Always document template method requirements by implementing matching methods that raise useful errors. — location: 3008 ^ref-5599


It illustrates the strength and value of inheritance; when the hierarchy is correct, anyone can successfully create a new subclass. — location: 3294 ^ref-14589


RecumbentBike is transparently obvious and is so regular and predictable that it might have come off of an assembly line. It illustrates the strength and value of inheritance; when the hierarchy is correct, anyone can successfully create a new subclass. — location: 3293 ^ref-63774


The best way to create an abstract superclass is by pushing code up from concrete subclasses. Identifying the correct abstraction is easiest if you have access to at least three existing concrete classes. — location: 3298 ^ref-22819


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. Hook methods allow subclasses to contribute specializations without knowing the abstract algorithm. They remove the need for subclasses to send super and therefore reduce the coupling between layers of the hierarchy and increase its tolerance for change. — location: 3300 ^ref-36463


To reap benefits from using inheritance you must understand not only how to write inheritable code but also when it makes sense to do so. Use of classical inheritance is always optional; — location: 3312 ^ref-26682


This chapter explores an alternative that uses the techniques of inheritance to share a role. It begins with an example that uses a Ruby module to define a common role and then proceeds to give practical advice about how to write all inheritable code. — location: 3315 ^ref-1904


Modules thus provide a perfect way to allow objects of different classes to play a common role using a single set of code. — location: 3342 ^ref-27339


You’ve seen the pattern of checking class to know what message to send; here the Schedule checks class to know what value to use. In both cases Schedule knows too much. This knowledge doesn’t belong in Schedule, it belongs in the classes whose names Schedule is checking. This implementation cries out for a simple and obvious improvement, one suggested by the pattern of the code. Instead of knowing details about other classes, the Schedule should send them messages. — location: 3382 ^ref-52755


The Schedule expects its target to behave like something that understands lead_days, that is, like something that is “schedulable.” You have discovered a duck type. — location: 3402 ^ref-21265


Using a separate class to manage strings is patently redundant; strings are objects, they have their own behavior, they manage themselves. Requiring that other objects know about a third party, StringUtils, to get behavior from a string complicates the code by adding an unnecessary dependency. This specific example illustrates the general idea that 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. — location: 3411 ^ref-14996