Chapter 1. Object-Oriented Design

The world is procedural. Time flows forward and events, one by one, pass by. Your morning procedure may be to get up, brush your teeth, make coffee, dress, and then get to work. These activities can be modeled using procedural software; because you know the order of events you can write code to do each thing and then quite deliberately string the things together, one after another.

The world is also object-oriented. The objects with which you interact might include a spouse and a cat, or an old car and a pile of bike parts in the garage, or your ticking heart and the exercise plan you use to keep it healthy. Each of these objects comes equipped with its own behavior and, while some of the interactions between them might be predictable, it is entirely possible for your spouse to unexpectedly step on the cat, causing a reaction that rapidly raises everyone’s heart rate and gives you new appreciation for your exercise regimen.

In a world of objects, new arrangements of behavior emerge naturally. You don’t have to explicitly write code for the spouse_steps_on_cat procedure, all you need is a spouse object that takes steps and a cat object that does not like being stepped on. Put these two objects into a room together and unanticipated combinations of behavior will appear.

This book is about designing object-oriented software, and it views the world as a series of spontaneous interactions between objects. Object-oriented design (OOD) requires that you shift from thinking of the world as a collection of predefined procedures to modeling the world as a series of messages that pass between objects. 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.

This book guides you through the immersion process. This chapter starts with a general discussion of OOD. It argues the case for design and then proceeds to describe when to do it and how to judge it. The chapter ends with a brief overview of object-oriented programming that defines the terms used throughout the book.

In Praise of Design

Software gets built for a reason. The target application—whether a trivial game or a program to guide radiation therapy—is the entire point. If painful programming were the most cost-effective way to produce working software, programmers would be morally obligated to suffer stoically or to find other jobs.

Fortunately, you do not have to choose between pleasure and productivity. The programming techniques that make code a joy to write overlap with those that most efficiently produce software. The techniques of object-oriented design solve both the moral and the technical dilemmas of programming; following them produces cost-effective software using code that is also a pleasure to work on.

The Problem Design Solves

Imagine writing a new application. Imagine that this application comes equipped with a complete and correct set of requirements. And if you will, imagine one more thing: once written, this application need never change.

For this case, design does not matter. Like a circus performer spinning plates in a world without friction or gravity, you could program the application into motion and then stand back proudly and watch it run forever. No matter how wobbly, the plates of code would rotate on and on, teetering round and round but never quite falling.

As long as nothing changed.

Unfortunately, something will change. It always does. The customers didn’t know what they wanted, they didn’t say what they meant. You didn’t understand their needs, you’ve learned how to do something better. Even applications that are perfect in every way are not stable. The application was a huge success, now everyone wants more. Change is unavoidable. It is ubiquitous, omnipresent, and inevitable.

Changing requirements are the programming equivalent of friction and gravity. They introduce forces that apply sudden and unexpected pressures that work against the best-laid plans. It is the need for change that makes design matter.

Applications that are easy to change are a pleasure to write and a joy to extend. They’re flexible and adaptable. Applications that resist change are just the opposite; every change is expensive and each makes the next cost more. Few difficult-to-change applications are pleasant to work on. The worst of them gradually become personal horror films where you star as a hapless programmer, running madly from one spinning plate to the next, trying to stave off the sound of crashing crockery.

Why Change Is Hard

Object-oriented applications are made up of parts that interact to produce the behavior of the whole. The parts are objects; interactions are embodied in the messages that pass between them. Getting the right message to the correct target object requires that the sender of the message know things about the receiver. This knowledge creates dependencies between the two and these dependencies stand in the way of change.

Object-oriented design is about managing dependencies. It is a set of coding techniques that arrange dependencies such that objects can tolerate change. In the absence of design, unmanaged dependencies wreak havoc because objects know too much about one another. Changing one object forces change upon its collaborators, which in turn, forces change upon its collaborators, ad infinitum. A seemingly insignificant enhancement can cause damage that radiates outward in overlapping concentric circles, ultimately leaving no code untouched.

When objects know too much they have many expectations about the world in which they reside. They’re picky, they need things to be “just so.” These expectations constrain them. The objects resist being reused in different contexts; they are painful to test and susceptible to being duplicated.

In a small application, poor design is survivable. Even if everything is connected to everything else, if you can hold it all in your head at once you can still improve the application. The problem with poorly designed small applications is that if they are successful they grow up to be poorly designed big applications. They gradually become tar pits in which you fear to tread lest you sink without a trace. Changes that should be simple may cascade around the application, breaking code everywhere and requiring extensive rewriting. Tests are caught in the crossfire and begin to feel like a hindrance rather than a help.

A Practical Definition of Design

Every application is a collection of code; the code’s arrangement is the design. Two isolated programmers, even when they share common ideas about design, can be relied upon to solve the same problem by arranging code in different ways. Design is not an assembly line where similarly trained workers construct identical widgets; it’s a studio where like-minded artists sculpt custom applications. Design is thus an art, the art of arranging code.

Part of the difficulty of design is that every problem has two components. You must not only write code for the feature you plan to deliver today, you must also create code that is amenable to being changed later. For any period of time that extends past initial delivery of the beta, the cost of change will eventually eclipse the original cost of the application. Because design principles overlap and every problem involves a shifting timeframe, design challenges can have a bewildering number of possible solutions. Your job is one of synthesis; you must combine an overall understanding of your application’s requirements with knowledge of the costs and benefits of design alternatives and then devise an arrangement of code that is cost effective in the present and will continue to be so in the future.

Taking the future into consideration might seem to introduce a need for psychic abilities normally considered outside the realm of programming. Not so. The future that design considers is not one in which you anticipate unknown requirements and preemptively choose one from among them to implement in the present. Programmers are not psychics. Designs that anticipate specific future requirements almost always end badly. Practical design does not anticipate what will happen to your application, it merely accepts that something will and that, in the present, you cannot know what. It doesn’t guess the future; it preserves your options for accommodating the future. It doesn’t choose; it leaves you room to move.

The purpose of design is to allow you to do design later and its primary goal is to reduce the cost of change.

The Tools of Design

Design is not the act of following a fixed set of rules, it’s a journey along a branching path wherein earlier choices close off some options and open access to others. During design you wander through a maze of requirements where every juncture represents a decision point that has consequences for the future.

Just as a sculptor has chisels and files, an object-oriented designer has tools—principles and patterns.

Design Principles

The SOLID acronym, coined by Michael Feathers and popularized by Robert Martin, represents five of the most well known principles of object-oriented design: Single Responsibility, Open-Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion. Other principles include Andy Hunt and Dave Thomas’s DRY (Don’t Repeat Yourself) and the Law of Demeter (LoD) from the Demeter project at Northeastern University.

The principles themselves will be dealt with throughout this book; the question for now is “Where on earth did they come from?” Is there empirical proof that these principles have value or are they merely someone’s opinion that you may freely discount? In essence, who says?

All of these principles got their start as choices someone made while writing code. Early OO programmers noticed that some code arrangements made their lives easier while others made them harder. These experiences led them to develop opinions about how to write good code.

Academics eventually got involved and, needing to write dissertations, decided to quantify “goodness.” This desire is laudable. If we could count things, that is, compute metrics about our code and correlate these metrics to high- or low-quality applications (for which we also need an objective measure), we could do more of the things that lower costs and fewer of things that raise them. Being able to measure quality would change OO design from infinitely disputed opinion into measurable science.

In the 1990s Chidamber and Kemerer1 and Basili2 did exactly this. They took object-oriented applications and tried to quantify the code. They named and measured things like the overall size of classes, the entanglements that classes have with one another, the depth and breadth of inheritance hierarchies, and the number of methods that get invoked as a result of any message sent. They picked code arrangements they thought might matter, devised formulas to count them, and then correlated the resulting metrics to the quality of the enclosing applications. Their research shows a definite correlation between use of these techniques and high-quality code.

While these studies seem to prove the validity of the design principles, they come, for any seasoned programmer, with a caveat. These early studies examined very small applications written by graduate students; this alone is enough to justify viewing the conclusions with caution. The code in these applications may not be representative of real-world OO applications.

However, it turns out caution is unnecessary. In 2001, Laing and Coleman examined several NASA Goddard Space Flight Center applications (rocket science) with the express intention of finding “a way to produce cheaper and higher quality software.”3 They examined three applications of varying quality, one of which had 1,617 classes and more than 500,000 lines of code. Their research supports the earlier studies and further confirms that design principles matter.

Even if you never read these studies you can be assured of their conclusions. The principles of good design represent measurable truths and following them will improve your code.

Design Patterns

In addition to principles, object-oriented design involves patterns. The so-called Gang of Four (Gof), Erich Gamma, Richard Helm, Ralph Johnson, and Jon Vlissides, wrote the seminal work on patterns in 1995. Their Design Patterns book describes patterns as “simple and elegant solutions to specific problems in object-oriented software design” that you can use to “make your own designs more flexible, modular, reusable and understandable.”4

The notion of design patterns is incredibly powerful. To name common problems and to solve the problems in common ways brings the fuzzy into focus. Design Patterns gave an entire generation of programmers the means to communicate and collaborate.

Patterns have a place in every designer’s toolbox. Each well-known pattern is a near perfect open-source solution for the problem it solves. However, the popularity of patterns led to a kind of pattern abuse by novice programmers, who, in an excess of well-meaning zeal, applied perfectly good patterns to the wrong problems. Pattern misapplication results in complicated and confusing code but this result is not the fault of the pattern itself. A tool cannot be faulted for its use, the user must master the tool.

This book is not about patterns; however, it will prepare you to understand them and give you the knowledge to choose and use them appropriately.

The Act of Design

With the discovery and propagation of common design principles and patterns, all OOD problems would appear to have been solved. Now that the underlying rules are known, how hard can designing object-oriented software be?

Pretty hard, it turns out. If you think of software as custom furniture, then principles and patterns are like woodworking tools. Knowing how software should look when it’s done does not cause it to build itself; applications come into existence because some programmer applied the tools. The end result, be it a beautiful cabinet or a rickety chair, reflects its programmer’s experience with the tools of design.

How Design Fails

The first way design fails is due to lack of it. Programmers initially know little about design. This is not a deterrent, however, as it is possible to produce working applications without knowing the first thing about design.

This is true of any OO language but some languages are more susceptible than others and an approachable language like Ruby is especially vulnerable. Ruby is very friendly; the language permits nearly anyone to create scripts to automate repetitive tasks, and an opinionated framework like Ruby on Rails puts web applications within every programmer’s reach. The syntax of the Ruby language is so gentle that anyone blessed with the ability to string thoughts into logical order can produce working applications. Programmers who know nothing about object-oriented design can be very successful in Ruby.

However, successful but undesigned applications carry the seeds of their own destruction; they are easy to write but gradually become impossible to change. A programmer’s past experience does not predict the future. The early promise of painless development gradually fails and optimism turns to despair as programmers begin to greet every change request with “Yes, I can add that feature, but it will break everything.

Slightly more experienced programmers encounter different design failures. These programmers are aware of OO design techniques but do not yet understand how to apply them. With the best of intentions, these programmers fall into the trap of overdesign. A little bit of knowledge is dangerous; as their knowledge increases and hope returns, they design relentlessly. In an excess of enthusiasm they apply principles inappropriately and see patterns where none exist. They construct complicated, beautiful castles of code and then are distressed to find themselves hemmed in by stone walls. You can recognize these programmers because they begin to greet change requests with “No, I can’t add that feature; it wasn’t designed to do that.

Finally, object-oriented software fails when the act of design is separated from the act of programming. Design is a process of progressive discovery that relies on a feedback loop. This feedback loop should be timely and incremental; the iterative techniques of the Agile software movement (http://agilemanifesto.org/) are thus perfectly suited to the creation of well-designed OO applications. The iterative nature of Agile development allows design to adjust regularly and to evolve naturally. When design is dictated from afar none of the necessary adjustments can occur and early failures of understanding get cemented into the code. Programmers who are forced to write applications that were designed by isolated experts begin to say, “Well, I can certainly write this, but it’s not what you really want and you will eventually be sorry.

When to Design

Agile believes that your customers can’t define the software they want before seeing it, so it’s best to show them sooner rather than later. If this premise is true, then it logically follows that you should build software in tiny increments, gradually iterating your way into an application that meets the customer’s true need. Agile believes that the most cost-effective way to produce what customers really want is to collaborate with them, building software one small bit at a time such that each delivered bit has the opportunity to alter ideas about the next. The Agile experience is that this collaboration produces software that differs from what was initially imagined; the resulting software could not have been anticipated by any other means.

If Agile is correct, two other things are also true. First, there is absolutely no point in doing a Big Up Front Design (BUFD) (because it cannot possibly be correct), and second, no one can predict when the application will be done (because you don’t know in advance what it will eventually do).

It should come as no surprise that some people are uncomfortable with Agile. “We don’t know what we’re doing” and “We don’t know when we’ll be done” can be a difficult sell. The desire for BUFD persists because, for some, it provides a feeling of control that would otherwise be lacking. Comforting though this feeling may be, it is a temporary illusion that will not survive the act of writing the application.

BUFD inevitably leads to an adversarial relationship between customers and programmers. Because any big design created in advance of working software cannot be correct, to write the application as specified guarantees that it will not meet the customer’s needs. Customers discover this when they attempt to use it. They then request changes. Programmers resist these changes because they have a schedule to meet, one that they are very likely already behind. The project gradually becomes doomed as participants switch from working to make it succeed to striving to avoid being blamed for its failure.

The rules of this engagement are clear to all. When a project misses its delivery deadline, even if this happened because of changes to the specification, the programmers are at fault. If, however, it is delivered on time but doesn’t fulfill the actual need, the specification must have been wrong, so the customer gets the blame. The design documents of BUFD start out as roadmaps for application development but gradually become the focus of dissent. They do not produce quality software, instead they supply fiercely parsed words that will be invoked in the final, scrambling defense against being the person who ends up holding the hot potato of blame.

If insanity is doing the same thing over and over again and expecting different results, the Agile Manifesto was where we collectively began to regain our senses. Agile works because it acknowledges that certainty is unattainable in advance of the application’s existence; Agile’s acceptance of this truth allows it to provide strategies to overcome the handicap of developing software while knowing neither the target nor the timeline.

However, just because Agile says “don’t do a big up front design” doesn’t mean it tells you to do no design at all. The word design when used in BUFD has a different meaning than when used in OOD. BUFD is about completely specifying and totally documenting the anticipated future inner workings of all of the features of the proposed application. If there’s a software architect involved this may extend to deciding, in advance, how to arrange all of the code. OOD is concerned with a much narrower domain. It is about arranging what code you have so that it will be easy to change.

Agile processes guarantee change and your ability to make these changes depends on your application’s design. If you cannot write well-designed code you’ll have to rewrite your application during every iteration.

Agile thus does not prohibit design, it requires it. Not only does it require design, it requires really good design. It needs your best work. Its success relies on simple, flexible, and malleable code.

Judging Design

In the days of yore, programmers were sometimes judged by the number of lines of code (referred to as source lines of code or SLOC) they produced. It’s obvious how this metric came to be; any boss who thinks of programming as an assembly line where similarly trained workers labor to construct identical widgets can easily develop a belief that individual productivity can be judged by simply weighing output. For managers in desperate need of a reliable way to compare programmers and evaluate software, SLOC, for all its obvious problems, was far better than nothing; it was at least a reproducible measure of something.

This metric was clearly not developed by programmers. While SLOC may provide a yardstick by which to measure individual effort and application complexity, it says nothing about overall quality. It penalizes the efficient programmer while rewarding the verbose and is ripe to be gamed by the expert to the detriment of the underlying application. If you know that the novice programmer sitting next to you will be thought more productive because he or she writes a lot of code to produce a feature that you could produce with far fewer lines, what is your response? This metric alters the reward structure in ways that harm quality.

In the modern world, SLOC is a historical curiosity that has largely been replaced by newer metrics. There are numerous Ruby gems (a google search on ruby metrics will turn up the most recent) that assess how well your code follows OOD principles. Metrics software works by scanning source code and counting things that predict quality. Running a metrics suite against your own code can be illuminating, humbling, and sometimes alarming. Seemingly well-designed applications can rack up impressive numbers of OOD violations.

Bad OOD metrics are indisputably a sign of bad design; code that scores poorly will be hard to change. Unfortunately, good scores don’t prove the opposite, that is, they don’t guarantee that the next change you make will be easy or cheap. The problem is that it is possible to create beautiful designs that over-anticipate the future. While these designs may generate very good OOD metrics, if they anticipate the wrong future they will be expensive to fix when the real future finally arrives. OOD metrics cannot identify designs that do the wrong thing in the right way.

The cautionary tale about SLOC gone wrong therefore extends to OOD metrics. Take them with a grain of salt. Metrics are useful because they are unbiased and produce numbers from which you can infer something about software; however, they are not direct indicators of quality, but are proxies for a deeper measurement. The ultimate software metric would be cost per feature over the time interval that matters, but this is not easy to calculate. Cost, feature, and time are individually difficult to define, track, and measure.

Even if you could isolate an individual feature and track all of its associated costs, the time interval that matters effects how code should be judged. Sometimes the value of having the feature right now is so great that it outweighs any future increase in costs. If lack of a feature will force you out of business today it doesn’t matter how much it will cost to deal with the code tomorrow; you must do the best you can in the time you have. Making this kind of design compromise is like borrowing time from the future and is known as taking on technical debt. This is a loan that will eventually need to be repayed, quite likely with interest.

Even when you are not intentionally taking on technical debt, design takes time and therefore costs money. Because your goal is to write software with the lowest cost per feature, your decision about how much design to do depends on two things: your skills and your timeframe. If design takes half your time this month and does not start returning dividends for a year, it may not be worth it. When the act of design prevents software from being delivered on time, you have lost. Delivering half of a well-designed application might be the same as delivering no application at all. However, if design takes half of your time this morning, pays that time back this afternoon, and then continues to provide benefits for the lifetime of the application, you get a kind of daily compounding interest on your time; this design effort pays off forever.

The break-even point for design depends on the programmer. Inexperienced programmers who do a lot of anticipatory design may never reach a point where their earlier design efforts pay off. Skilled designers who write carefully crafted code this morning may save money this afternoon. Your experience likely lies somewhere between these extremes, and the remainder of this book teaches skills you can use to shift the break-even point in your favor.

A Brief Introduction to Object-Oriented Programming

Object-oriented applications are made up of objects and the messages that pass between them. Messages will turn out to be the more important of the two, but in this brief introduction (and in the first few chapters of the book) the concepts will get equal weight.

Procedural Languages

Object-oriented programming is object-oriented relative to non object-oriented, or procedural, programming. It’s instructive to think of these two styles in terms of their differences. Imagine a generic procedural programming language, one in which you create simple scripts. In this language you can define variables, that is, make up names and associate those names with bits of data. Once assigned, the associated data can be accessed by referring to the variables.

Like all procedural languages, this one knows about a small, fixed set of different kinds of data, things like strings, numbers, arrays, files, and so on. These different kinds of data are known as data types. Each data type describes a very specific kind of thing. The string data type is different from the file data type. The syntax of the language contains built-in operations to do reasonable things to the various data types. For example, it can concatenate strings and read files.

Because you create variables, you know what kind of thing each holds. Your expectations about which operations you can use are based on your knowledge of a variable’s data type. You know that you can append to strings, do math with numbers, index into arrays, read files, and so on.

Every possible data type and operation already exists; these things are built into the syntax of the language. The language might let you create functions (group some of the predefined operations together under a new name) or define complex data structures (assemble some of the predefined data types into a named arrangement), but you can’t make up wholly new operations or brand new data types. What you see is all you get.

In this language, as in all procedural languages, there is a chasm between data and behavior. Data is one thing, behavior is something completely different. Data gets packaged up into variables and then passed around to behavior, which could, frankly, do anything to it. Data is like a child that behavior sends off to school every morning; there is no way of knowing what actually happens while it is out of sight. The influences on data can be unpredictable and largely untraceable.

Object-Oriented Languages

Now imagine a different kind of programming language, a class-based object-oriented one like Ruby. Instead of dividing data and behavior into two separate, never-the-twain-shall-meet spheres, Ruby combines them together into a single thing, an object. Objects have behavior and may contain data, data to which they alone control access. Objects invoke one another’s behavior by sending each other messages.

Ruby has a string object instead of a string data type. The operations that work with strings are built into the string objects themselves instead of into the syntax of the language. String objects differ in that each contains its own personal string of data, but are similar in that each behaves like the others. Each string encapsulates, or hides, its data from the world. Every object decides for itself how much, or how little, of its data to expose.

Because string objects supply their own operations, Ruby doesn’t have to know anything in particular about the string data type; it need only provide a general way for objects to send messages. For example, if strings understand the concat message, Ruby doesn’t have to contain syntax to concatenate strings, it just has to provide a way for one object to send concat to another.

Even the simplest application will probably need more than one string or number or file or array. As a matter of fact, while it’s true that you may occasionally need a unique, individual snowflake of an object, it’s far more common to desire to manufacture a bunch of objects that have identical behavior but encapsulate different data.

Class-based OO languages like Ruby allow you to define a class that provides a blueprint for the construction of similar objects. A class defines methods (definitions of behavior) and attributes (definitions of variables). Methods get invoked in response to messages. The same method name can be defined by many different objects; it’s up to Ruby to find and invoke the right method of the correct object for any sent message.

Once the String class exists it can be used to repeatedly instantiate, or create, new instances of a string object. Every newly instantiated String implements the same methods and uses the same attribute names but each contains its own personal data. They share the same methods so they all behave like Strings; they contain different data so they represent different ones.

The String class defines a type that is more than mere data. Knowing an object’s type lets you have expectations about how it will behave. In a procedural language variables have a single data type; knowledge of this data type lets you have expectations about which operations are valid. In Ruby an object may have many types, one of which will always come from its class. Knowledge of an object’s type(s) therefore lets you have expectations about the messages to which it responds.

Ruby comes with a number of predefined classes. The most immediately recognizable are those that overlap the data types used by procedural languages. For example, the String class defines strings, the Fixnum class, integers. There’s a pre-existing class for every data type that you would expect a programming language to supply. However, object-oriented languages are themselves built using objects and here’s where things begin to get interesting.

The String class, that is, the blueprint for new string objects, is itself an object; it’s an instance of the Class class. Just as every string object is a data-specific instance of the String class, every class object (String, Fixnum, ad infinitum) is a data-specific instance of the Class class. The String class manufactures new strings, the Class class manufactures new classes.

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.

Whether this language ultimately brings you pleasure or gives you pain is a matter of design and the concern of this book.

Summary

If an application lives long enough, that is, if it succeeds, its biggest problem will become that of dealing with change. Arranging code to efficiently accommodate change is a matter of design. The most visible elements of design are principles and patterns, but unfortunately even applying principles correctly and using patterns appropriately does not guarantee the creation of an easy-to-change application.

OO metrics expose how well an application follows OO design principles. Bad metrics strongly imply future difficulties; however, good metrics are less helpful. A design that does the wrong thing might produce great metrics but may still be costly to change.

The trick to getting the most bang for your design buck is to acquire an understanding of the theories of design and to apply these theories appropriately, at the right time, and in the right amounts. Design relies on your ability to translate theory into practice.

What is the difference between theory and practice?

In theory, there is none. If theory were practice you could learn the rules of OOD, apply them consistently, and create perfect code from this day forward; your work here would be done.

However, no matter how deeply theory believes this to be true, practice knows better. Unlike theory, practice gets its hands dirty. It is practice that lays bricks, builds bridges, and writes code. Practice lives in the real world of change, confusion, and uncertainty. It faces competing choices and, grimacing, chooses the lesser evil; it dodges, it hedges, it robs Peter to pay Paul. It makes a living by doing the best it can with what it has.

Theory is useful and necessary and has been the focus of this chapter. But enough already; it’s time for practice.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.174.253