6 
Objects

Imagine that the hardware store in your town was sold to a new owner. The previous owner had arranged the store with similar items grouped together: the paint over here, hand tools over there, and raw materials some place or other. It wasn’t always obvious where things were, and it always seemed that if you were only buying two things for the same project, they were on opposite sides of the store, but if you knew where a certain type of merchandise was stored, you could find similar things nearby.

Now imagine that the new owner chose to reorganize the layout based on tasks. They might reason that somebody building a chair needs wood, a saw, screws, a screw gun, and paint. They group all these things together in one area and call it the “make a chair area.” The people planning this would spend a while thinking through various activities they needed to support and assigning their inventory to different categories.

If you went to the store and wanted to make a chair, this would be great, since it’s all in one place, nice and convenient. But what if you wanted to build a chair and you needed something that the designer of the “make a chair area” hadn’t anticipated? Now you have to go hunting for the item, trying to guess what activity the new layout specialists had bucketed your missing item into.

And more fundamentally, you’re trying to build a chair. Your shopping experience at the hardware store could be slightly improved or worsened by changing the layout of the store, but in the greater scheme of things, the hard part is figuring out what parts you need and actually building the chair, and that doesn’t change.

To the owners of the store, though, the layout would feel logical, and they would feel they had done something clever and forward thinking. In fact, they might decide that they had revolutionized the art of hardware store design. They might even write a book and go on a speaking tour, explaining to other hardware store owners how significant their changes were. Naturally they would find a few chair builders who preferred the new layout and gather positive quotes about the experience.

The quest to “design in” software quality eventually evolved to a similar point, although the beginnings were more humble.

Wirth, creator of Pascal, published a book in 1976 titled Algorithms + Data Structures = Programs.1 The title is accurate; a program consists of algorithms—the code that runs—and data structures—the data that it operates on. Both are important, and a programmer who wants to learn how a program operates must understand both of them. The “structured programming” push was aimed at clarity of algorithms, as expressed in code, but clarity of data structures received much less attention.

Data structures are built up from the fundamental data types: numbers and strings. In many early languages, there was no way to group such data together: if you wanted to store both a person’s name and age, you defined two separate variables, without any visible connection between them, except whatever could be shoehorned into the names of the variables. Only by reading the code that used them could another programmer intuit that they were related. To continue my hardware store analogy, it’s as if different power tools were stored randomly all around the store, because nobody had thought of a way to put them together.

The one grouping construct that existed was arrays, which were designed to hold multiple instances of the same sort of value, such as a top-ten high score list. Sometimes, for lack of any other way, programmers would use arrays for grouping different pieces of data; for instance, a BASIC program wanting to store somebody’s height and weight might create an array of two integers, and put the height in the first and weight in the second. This connected them, but it was not obvious, from the declaration of a single array, what the specific meaning of each element was, and it was easy to get them backward in the code. The Hockey game in BASIC Computer Games used an array of seven strings, storing the names of the six players in the first six elements, and the name of the team in the seventh element. This made it a bit confusing to read, which wasn’t helped by the fact that the array in question was named A$.2

Languages such as Pascal and C avoided this clunky feeling by letting you bundle related data into a larger entity, called a record in Pascal and a struct in C. For example, a C struct could hold information about a person:

struct person {
   int age;
   char name[64];
};

and if you had a variable p of type person, you could refer to the individual elements as p.age and p.name, but also refer to an entire person, such as in a function parameter. This was an underappreciated step forward in program clarity since it addressed the second part of Wirth’s equation.

One of Wirth’s points is that algorithms and data structures are closely related: “The choice of structure for the underlying data profoundly influences the algorithms that perform a given task.” Furthermore, neither one will likely remain unchanged during the writing of a program: “In the process of program construction the data representation is gradually refined—in step with the refinement of the algorithm—to comply more and more with the constraints imposed.”3

The person struct shown above looks reasonable—you are storing the age and name—but while working on a program with such a data structure, you may see opportunities for “refinement.” You realize that the age of a person needs to be updated every year, even though their birthday will never change, so you decide to store their birthday instead of their age; this means you have to recalculate their age if you need it, but that’s not too hard. Or you might notice that several times in your code, you have had to split the name into first and last names. Again, this isn’t hard—hopefully the second time you did it, you moved that code into a separate API so you could call it from anywhere as needed—but it seems easier to store the first and last name separately. These are the kinds of details you might not think of when first sketching out your data structures; it’s the normal evolution of a program’s design.

At a mechanical level, however, this sort of change can be a pain when working in a procedural language—the type of language we have seen thus far, and a category that includes Fortran, BASIC, Pascal, and C. Although there is clearly a logical connection between your data structures and algorithms, they tend to be separated in the source code; the data structures defined near the top and implementation of the algorithms further down, or in a separate file. So when making improvements like this, which involve modifying both the definition of your data structures and code that uses them, you wind up moving back and forth in your source code. More important, if you wanted to figure out, say, which functions operated on a person or used the soon-to-be-replaced person.age, you would need to scan the parameter lists of all your functions to find those that took a person, and then scan the code to find any use of age.

Starting in the 1960s, the idea emerged of grouping data structures together with the code that operates on that data, in a construct known as a class. A class is actually a blueprint for such a collection; an instance of the data in a class, in the memory of a computer running a program, is called an object. As a result, this approach is labeled object-oriented programming.

We saw an illustration of this in chapter 3, since C# is an object-oriented language. The string class defines both data (the characters in the string) and methods (the currently preferred term for an API in object-oriented programming) that operate on the string, such as ToUpper() in our examples. You use dot notation to join the object and method name:

upperstring = mystring.ToUpper();

We are calling the method ToUpper() on the object mystring, which is an instance of the string class. The code inside the implementation of ToUpper() will be operating on the class data of that mystring object.

In a procedural language like C, you call functions directly rather than on an object; any function that wants access to data needs to have it passed as one of its parameters.4 It doesn’t look much different:

upperstring = ToUpper(mystring);

This is sometimes stated as “Rather than calling a function to uppercase the string, you ask the string object to uppercase itself,” as if the objects have become self-aware and relieved their human overlords of mundane coding tasks. In reality somebody still has to write the code, but things get cleaner: all the methods that operate on the data in the class are grouped together in one place in the code, next to the definition of the data itself.

The first language to introduce classes was Simula, developed at a research lab in Norway in the 1960s (one of its authors was Ole-Johan Dahl, later the coauthor of one of the Structured Programming books). Simula was a general-purpose programming language, but designed with the goal of writing simulations, such as for cars on a highway or people waiting in line at a bank. The object model works quite well for simulating real-world objects: the object representing a car will have certain pieces of data associated with it, like speed and position, and will have certain operations that it can perform, such as accelerating or turning. Grouping them together in a class makes this clear to somebody reading the code.

In Simula, you would create a new object of a given class (which is also known as instantiating an instance of that class) using the keyword NEW followed by the name of the class, like this:

MyObject :- new MyClass;

where :- is known as the reference assignment operator (modern eyes should read it as an equal sign), and you could also specify that the creation of an object could take what were called class declaration parameters, like this:

MyPerson :- new Person(name, age);

where the Person class included initialization code, run every time that a Person object was created, that could use the name and age parameters as it saw fit. In this example, it would presumably store them in the class data so they could later be used in class procedures (which is what Simula called methods). This code that runs when an object is created is generally known as a constructor, although Simula did not use that term.

Classes can also have subclasses. This concept is now known as inheritance, although again, Simula did not call it that (the term inheritance, “Object B inherits from Object A,” implies a more anthropomorphic view of code than the businesslike Simula phrasing “Object B is a subclass of Object A”). A subclass has all the data and procedures of the superclass, plus whatever other ones it adds. This again fits well with the simulation focus: you can have classes for animal, vegetable, and mineral, and then subclasses of animal for specific animals; you could layer it as deep as you would like.5 A pointer that referenced an object of a specific animal class could also be used in a context (such as a procedure parameter) where the more general animal class was expected (you could also switch the pointer between referencing the specific and general classes using a keyword with the lovely name QUA, which sadly has not been picked up by any modern languages). This let you reuse common data and procedures in the animal class, but allow the specific animal classes to have their own added data and procedures. If you had a class Animal and a subclass Dog, Animal could have a procedure GetName(), which applies to all animals, and Dog could have a procedure GetBreed(), which makes sense for dogs but not most other animals.

Simula also introduced the important object-oriented notion of a virtual procedure. You want your code to be able to refer to all animals using the Animal class, which makes it nice and generic, but perhaps you want the implementation of GetName() to be provided by Dog, since that code would be aware of any dog-specific details. The solution is to declare GetName() as a virtual procedure in Animal. Animal can implement GetName(), but Dog can provide its own more specific implementation; Simula would look, at runtime, for the innermost class (that is, the deepest level subclass) with an implementation of GetName(), and call that one.6

When Simula first appeared, objects were seen as a notational convenience, not a major breakthrough in how software was written. The 1973 book SIMULA Begin, written by the authors of the language, doesn’t use the term object-oriented programming at all, and presents classes and objects as just one useful feature in the language.7 R. J. Pooley’s An Introduction to Programming in SIMULA, which came out in 1987, doesn’t get to the concept of classes or use the term object-oriented programming until chapter 9, although it does use objects in examples in earlier chapters. Pooley states, “One major advantage of this approach is that, given a sensible choice of names, we will have a much more readable program. Complicated detail is moved from the main part of the program to the procedure[s] … of the class and replaced by meaningful procedure names.”8

The language that popularized object-oriented programming was C++, created by a Danish computer scientist at Bell Labs named Bjarne Stroustrup starting in 1979—just a decade after C, the language on which it was based, was invented. The name is a reference to the ++ operator in C, which increments the value of a variable; Stroustrup states that the name had “nice interpretations,” although he mentions that ++ can also be read as next and successor, which sounds a bit nicer then the “incremental” interpretation.9

Stroustrup helpfully wrote the book The Design and Evolution of C++, in which he explained his thinking in designing the language. Although he cleaned up a few things he disliked about C, the primary goal was certainly to bring over the class idea from Simula; his initial name for his language was “C with Classes.” He beefed up Simula’s class support, such as by allowing multiple constructors for a class, as long as each constructor had a unique parameter signature so the compiler could tell them apart. For that matter he came up with the term constructor, after first trying out new-function. Stroustrup used the terms derived class and base class instead of subclass and superclass because he thought they were clearer, with the potential confusion being that a subclass is an expansion of the superclass, not the other way around as one would expect from the way the word subset is used in mathematics. He also referred to the data and functions of a class as members, a term that has since become standard (C++ did not use the term method but instead called them member functions).10

More important, Stroustrup made all class members default to being private rather than public. If a variable in a class was declared as private, it was hidden from code that used that class (known as calling code or a caller); only the code inside the class itself, implementing class member functions, could access it (member functions themselves could be similarly hidden, which restricted them to being called only by other member functions). In the first version of Simula all members were public; in the early 1970s the notion of private members was added, but the default was still public—accessible directly by any caller unless you explicitly marked members as private in your class declaration.11 C++ made the opposite choice, with members being private unless otherwise indicated. This encouraged abstraction of the implementation from callers; implementation details can change without affecting code that calls the class, as long as the only changes are to private data and functions, and the public surface remains the same.

David Parnas is a computer science professor who wrote the first papers on “information hiding,” as he called abstraction: the idea that keeping different modules from knowing the internal details of each other’s data structures made them more robust. As he observed, “It was information distribution that made systems ‘dirty’ by establishing almost invisible connections between supposedly independent modules.”12 In his original 1971 paper on information hiding, Parnas wrote about the connections between modules:

Many assume that the “connections” are control transfer points, passed parameters, and shared data. … Such a definition of “connection” is a highly dangerous oversimplification which results in misleading structure definitions. The connections between modules are the assumptions which the modules make about each other. In most systems we find that these connections are much more extensive than the calling sequence and control block formats usually shown in system structure descriptions. …

We now consider making a change in the completed system. We ask, “What changes can be made to one module without involving changes to other modules?” We may make only those changes which do not violate the assumptions made by other modules about the module being changed. In other words, a single module may be changed only as long as the “connections” still “fit.” …

The most difficult decisions to change are usually the earliest. The last piece of code inserted may be changed easily, but a piece of code inserted several months earlier may have “wormed” itself into the program and become difficult to extract.13

In a 1972 paper, discussing the division into modules of a larger program, Parnas summarizes the approach: “Its interface or definition was chosen to reveal as little as possible about its inner working.”14 This gave the owner of a module the maximum flexibility to rework it without having to modify all the code that called its API.

Based on my experience in college working on programs involving a maximum of two programmers, I was unaware of these finer points and would not have understood why they mattered. Nonetheless, C++ began to catch on as the next logical successor to C. I believe the first I ever heard of the language was from a classmate at Princeton, probably in early 1988, describing a program they wanted to write and saying something along the lines of, “A few years ago I would have written it in C, now of course I would write it in C++.” He was probably just showing off, but still it demonstrates how the language was gradually intruding into the consciousness of programmers as the new thing. Nevertheless, the original name, C with Classes, is indicative of what Stroustrup was aiming for: take the language C and add the useful feature of classes, as opposed to changing the world through object-oriented programming. In the preface of the original C++ Programming Language, he states (using the term type, where a class is a user-defined type), “In addition to the facilities provided by C, C++ provides flexible and efficient facilities for defining new types. A programmer can partition an application into manageable pieces by defining new types that closely match the concept of the application. … When used well, these techniques result in shorter, easier to understand, and easier to maintain programs.”15 Stroustrup doesn’t get to an in-depth discussion of classes until chapter 5, toward the middle of the book.

There was another object-oriented language floating around at the time called Smalltalk, which had been developed at Xerox’s Palo Alto Research Center (PARC) during the 1970s. The first general release was called Smalltalk-80, after the year in which it was appeared. The book Smalltalk-80: The Language, coauthored by Adele Goldberg, one of the designers of the language, and David Robson, explains, “The Smalltalk-80 system is based on ideas gleaned from the Simula language and from the visions of Alan Kay.”16 Kay is a good source for visions. If you have heard of Xerox PARC, it is likely as the place that invented graphical windowing environments, later appropriated by Apple and Microsoft; Kay was one of the leaders on that project, in addition to coining the term object-oriented programming.

Stroustrup notes that he had heard of Smalltalk when he was designing C with Classes, but doesn’t list it as a primary influence (Smalltalk-80 was the fifth version of the language; the language predates Stroustrup’s work adding classes to C).17 In Smalltalk you don’t call methods, you send messages to an object, which that object may choose to process by calling a method (so a method is an internal implementation detail in a class, not the public surface; another effect of this is that all class members are necessarily private). Smalltalk is a “pure” object-oriented language; even language constructs are based on objects. Instead of an IF/ELSE statement, as in most languages, you have an expression that evaluates to a Boolean object (an object that stores a value that is either true or false), which is then sent a message with two other objects containing blocks of code (because code itself is also an object), one of which should be run if the Boolean is true, and the other which should be run if the Boolean is false, like this:18

number > 0
   ifTrue: [positive ← 1]
   ifFalse: [positive ← 0]

This is described in Smalltalk terminology as sending a message with the selector ifTrue:ifFalse:, meaning that the message has two arguments, named ifTrue: and ifFalse: (true camel casing, you will observe). The message is sent to the Boolean object that results from evaluating the expression number > 0. This is the rough equivalent of calling a method on the Boolean that takes two parameters, except note that the parameters are identified by a name, not a position in an argument list, which also means that the syntax easily supports making them optional (if there is no ifFalse: argument, for example, it is like having no ELSE block on an IF statement). Named versus positional parameters is a minor detail at this point in our story, but keep it in mind for later.

It’s all quite mind expanding, if a bit hard to read for somebody used to almost any other language. Smalltalk is proud and unapologetic in its stance; Smalltalk-80: The Language jumps right into objects, messages, classes, instances, and methods in the first chapter. To its credit, it avoids ascribing hyperbolic benefits to this system, merely stating, similar to what Stroustrup said about C++, that “an important part of designing Smalltalk-80 programs is determining which kinds of objects should be described and which message names provide a useful vocabulary of interaction among these objects.”19

Given that C++ was emerging at around the same time as Smalltalk-80, with its similarity to C making it appealing to people who had been seduced by that language, it is understandable that Smalltalk, whose syntax was unusual for anybody familiar with the Algol-Pascal-C language family (which is to say, almost everybody), never gained as much mainstream traction as C++.

Programmers began playing around with C++, especially C programmers who wanted to try something slightly edgier; they happily divided their class variables into public and private, enjoying the excitement of playing with their new object-oriented toys.

In 1986, the first Object-Oriented Programming, Systems, Languages, and Applications (OOPSLA) conference was held in Portland, Oregon, under the aegis of the Association for Computing Machinery (ACM). Goldberg was one of the organizers. OOPSLA is one of a series of specialized conferences that the ACM puts on (in 2010, the conference was merged into the Systems, Programming, Languages, and Applications: Software for Humanity [SPLASH] conference). Stroustrup describes the first OOPSLA conference as the “start of the OO [object-oriented] hype.”20

OOPSLA arrived at the beginning of the third act of an important arc in programming language design. Although IBM was behind the creation of Fortran and PL/I, many of the first computer languages were developed at universities: BASIC was invented by two Dartmouth professors, Kemeny and Kurtz, Pascal by Wirth at the ETH in Zurich, and Algol by a committee of computer scientists. These were simpler days, where languages and the problems they solved were much less complex than they are today, and a better match to the capacity of a college professor. After that we moved into an era where languages emerged from research labs, either private ones like the Norwegian lab that came up with Simula or more commonly research labs within larger hardware companies: C and C++ came from Bell Labs, and Smalltalk from Xerox PARC. These languages were invented, in various degrees, to support the company’s business, but they were a by-product and not the end goal.

In the 1980s, there began to emerge languages designed by companies that were an end unto themselves: the company’s business was the language, and the success of the company depended on programmers adopting a new language—always a difficult sell to programmers. Two of the first of these were Objective-C, invented by Brad Cox and Tom Love at a company called StepStone, and Eiffel, invented by Bertrand Meyer at a company called Eiffel Software. Both of these were object-oriented languages.

I am not impugning the motives behind Objective-C and Eiffel; the object-oriented approach was genuinely seen as a road to software that was both higher quality and easier to write. Nonetheless, one can appreciate that if the foundation of your business involves convincing programmers, enamored of C, to switch to an object-oriented language, you need to present object-oriented languages as a bit more than the mere notational convenience that Simula and C++ were aiming for.

Cox’s book Object-Oriented Programming: An Evolutionary Approach was published in 1986. Despite the subtitle, his approach was evolutionary only in the sense that his language, Objective-C, was based on C rather than being completely new. In the preface, Cox gets right to it: “It is time for a revolution in how we build software, comparable to the ones that hardware engineers now routinely expect about every five years. The revolution is object-oriented programming.” He does dial down the rhetoric in the body of the book; Objective-C uses message passing instead of method calls, the same as Smalltalk, which does give it more flexibility than C++ in how objects can choose to support a message. Cox at one point calls this “the only substantive difference between conventional programming and object-oriented programming.”21

Such restraint was missing in the writings of Meyer (who presented the paper “Genericity versus Inheritance” at the first OOPSLA).22 He starts his 1988 book Object-Oriented Software Construction with this paragraph, which rivals anything Dijkstra ever wrote for sheer awesomeness:

Born in the ice-blue waters of the festooned Norwegian coast, amplified (by an aberration of world currents, for which marine geographers have yet to find a suitable explanation) along the much grayer range of the Californian Pacific; viewed by some as a typhoon, by some as a tsunami, and by some as a storm in a teacup—a tidal wave is reaching the shores of the computing world.23

Following on to the oblique references to Simula and Smalltalk, the next paragraph acknowledges that the reader may have heard this sort of thing before: “‘Object-oriented’ is the latest in term, complementing or perhaps even replacing ‘structured’ as the high-tech version of ‘good.’ … Let’s make it clear right away, lest the reader think the author takes a half-hearted approach to this topic: I do not think object-oriented design is a mere fad.” Meyer then throws down the gauntlet: “I believe it is not only different from but even, to a certain extent, incompatible with the software design methods that most people use today—including some of the principles taught in most programming textbooks. I further believe that object-oriented design has the potential for significantly improving the quality of software, and that it is here to stay.”24

He states that he will show

how, by reversing the traditional focus of software design, one may get more flexible architectures, furthering the goals of reusability and extendibility. … When laying out the architecture of a system, the software designer is confronted with a fundamental choice: should the structure be based on the actions or on the data? In the answer to this question lies the difference between traditional design methods and the object-oriented approach.25

He presents the top-down functional approach as the old way, in which you start with the overall goal of a program and then break it down into smaller pieces of functionality; this is what structured programming was about. This, he argues, tends to lock the software into a certain functionality, making it hard to modify when user requirements (inevitably) change, and preventing reusability, since the pieces that the software is broken down into will be specific to the overall function (which is ironic, since SIMULA Begin presents both top-down and bottom-up routes to the same goal, and in An Introduction to Programming in SIMULA, the authors talk about “how object-oriented programming makes top-down design easier,” because you can rough out your object interfaces without worrying about implementation details).26

Meyer then explains how object-oriented design avoids these problems, because your data tends to change less than your functionality and can more likely be reused in other components. He states the mantra, “Ask not first what the system does; Ask WHAT it does it to!” and then posits, “For many programmers, this change in viewpoint is as much of a shock as may have been for some people, in another time, the idea of the earth orbiting around the sun rather than the reverse.”27

As with structured programming, object-oriented programming could be viewed as either a process to arrive at an object-oriented program or the resulting program itself. The initial discussion, in the days of Simula and Smalltalk, was about the resulting program: it had objects, so it was object oriented. Meyer is instead talking about object-oriented design—a process that begins before you start writing the code, thereby representing a more fundamental shift in approach.

Indeed in his view, object-oriented design does not need language support; his book includes a section on how to write object-oriented code in existing languages, including a particularly forced attempt to do it in Fortran, although he washes his hands of Pascal.28 Nonetheless, Meyer is clearly claiming that object-oriented design turns out better when programming in an object-oriented language, and a lot of what he talks about, such as allowing classes to be extensible (which is done through inheritance), does require language support. His basic claim is that bottom-up design, in which you start by defining your classes and then stitch them together into a program, produces designs superior to those produced by top-down design.

The way in which the design of a program actually evolves is not as simple as Meyer implies. You create a class, defining your best guess as to what the methods should be. Later you might realize that the code that calls those methods needs the data in a different format or needs access to details about an object that you haven’t exposed; alternately, you realize that a method you have defined isn’t being called by anybody, so isn’t needed (for now, anyway—let’s see what your code reviewer thinks). This is not bad; it’s the way program design evolves. But it’s the sort of driven-from-above change that object-oriented design was supposed to avoid, per Meyer.

A 2010 paper by Linden Ball, Balder Onarheim, and Bo Christensen compared breadth-first design (proceeding from the top down in an ordered way) to depth-first design (digging into specific areas, such as a single class). It summed up a point made in an earlier study, co-authored by Ball, this way:

experts will often tend to mix breadth-first and depth-first design. … [T]he preferred strategy of expert designers is a top-down, breadth-first one, but they will switch to depth-first design to deal strategically with situations where their knowledge is stretched. Thus, depth-first design is a response to factors such as problem complexity and design uncertainty, with in-depth exploration of solution ideas allowing designers to assess the viability of uncertain concepts and gain confidence in their potential applicability.

The authors then verified this by analyzing video recordings of three different teams of actual programmers at work:

All design teams rapidly produced an initial “first-pass” solution … indicative of breadth-first solution development. … High-complexity requirements were subsequently dealt with much earlier in the transcripts in comparison to intermediate- and low-complexity requirements. … This finding was generalized across all three design teams and suggested the use of a depth-first strategy to handle high-complexity requirements and a breadth-first strategy to deal with low- and intermediate-complexity requirements. … Overall, these findings point to a sophisticated interplay between structured breadth-first and depth-first development in software design.29

In other words, programmers can go back and forth, identifying broad areas and then digging into the details when they recognize that the solution for a given area is unclear. But even for simple classes, the design does not radiate outward from the class definition in a brilliant beam of clarity; it is a dance between the providers (the class) and consumers (the callers of the class) until it settles on something that seems reasonable. It’s similar to the dance between algorithm and data structures that Wirth was describing in the quote at the beginning of this chapter. To paraphrase a military saying, “No plan survives contact with the callers of your class methods.” The unfortunate fact is, it’s hard to know if a design is “good” until the whole program is written and working. There is no reliable process, top down or bottom up, to arrive at this situation in a deterministic way, and even if nirvana is reached, it is only a temporary respite until the requirements of the program change.

Remember the discussion of the importance of API design in chapter 3? A class is providing an API, and its design needs careful attention, like any other API. As Joshua Bloch recommended in a talk on the subject of API design, “Write to your API early and often.”30 In other words, you need to consider an API you are providing from the caller’s view before deciding that it is well designed. As the caller of methods in object-oriented code, you still wind up being ruled by whatever the author of the object chose to expose. If your notion of how an API should be structured is in sync with the person who wrote the API, then it will appear obvious and intuitive, object oriented or not. And if you’re not in sync, it will be puzzling and hard to work with.

At one point at Microsoft, I worked on a product that supplied an API to programmers outside Microsoft. Somebody on the team commented that the API looked reasonable, but we couldn’t be sure until a lot of people had used it. My instinctive reaction was, “It’s a good API. What do we care what other people think?” … but I now see the error of my ways. Henry Baird, one of my on-loan-from-Bell-Labs professors at Princeton, points out that API design requires social skills, because you have to be aware of what assumptions your callers may make. Such empathy can be rare in programmers, who, to quote Baird again, “are strongly attracted to the idea of going into a room alone with the machine and getting something beautiful”—beautiful, of course, in their own eyes.31

As Stroustrup cautions, “Remember that much programming can be simply and clearly done using only primitive types, data structures, plain functions, and a few classes from a standard library. The whole apparatus involved in defining new types should not be used except when there is a real need.”32 Rushing to define your own classes doesn’t have much point if you don’t even need any new classes, but of course if you define your classes first, you might never realize that they are unnecessary.

Meyer at one point states, “We have seen that continuity provides the most convincing argument: over time, data structures, at least if viewed at a sufficient level of abstraction, are the really stable aspects of a system.”33 The problem is that the sufficient level of abstraction needed for this to be true is so high level that it only applies to the basic broad-brush design of a system: “We’ll need a place to store the data,” or “The images should be accessible in a standard way.” Once you start diving in, things become much less straightforward, and you won’t know your class design is right until you have written the code for all the actions you need to perform.

If you read through papers presented at the OOPSLA conferences, they are all full of interesting ideas about object-oriented programming—many of them claiming to be in the service of writing software that demonstrates desirable attributes, such as modularity, composability, reusability, and so on. They rarely, however, have research to back up these claims; they simply state that such-and-such arrangement of objects is pleasing to the eye. There is little side-by-side investigation of the “old way” and “new way,” and no metrics to evaluate if arranging your objects in a certain way produces fewer bugs or more maintainable code. To the extent that there were papers of this sort, they came from academia or corporate research labs, especially Hewlett-Packard and IBM.

The fact that it was old-school hardware companies doing the empirical studies is not surprising. First of all, they had the capacity to do so; there were no large software-only companies in the 1980s (Microsoft Research was founded in 1991). Second, in the world of hardware, which is driven by research-based science, dramatic improvements do come from research labs. Back in 1965, Gordon Moore, one of the cofounders of Intel, predicted that the capacity of integrated circuits—the basic building blocks of computers—would double every year.34 The timeline for doubling has since moved back to two years, more or less, but the law has proved remarkably durable for half a century. These advances were based on scientific research into the materials and processes used to fabricate integrated circuits. It would be reasonable for a hardware company, observing the term software engineering, to think that similar advances could be made on that side; software companies would be too pessimistic to try. In the late 1980s, John Young, the CEO of Hewlett-Packard, announced that “software quality and productivity had to rise by a factor of ten in five years.”35

Trying to impose a Moore’s law equivalent for software turned out to be impossible, but it did drive some good research (Brooks, who managed hardware teams at IBM before moving over to software, points out that proximity to hardware gives software a bad rap, like an extremely attractive friend who makes you feel inadequate: “The anomaly is not that software progress is so slow but that computer hardware progress is so fast. No other technology since civilization began has seen six orders of magnitude price-performance gain in 30 years”).36 Hewlett-Packard did make improvements in software productivity, as documented in Robert Grady’s book Successful Software Process Improvement. The goal was a factor-of-ten reduction in postrelease defect density as well as the total number of open serious and critical defects. They did reach one-sixth of the previous defect density, short of the goal but still an impressive result. Unfortunately the amount of software in their products grew so much that the other metric, open and serious critical defects, remained level.37

The academic papers acknowledge that they report on only one study, and that more study is needed, although oftentimes this subtlety is lost. Consider the Law of Demeter, a well-known object-oriented rule that was first presented by Karl Lieberherr, Ian Holland, and Arthur Riel, professors from Northeastern University, at OOPSLA in 1988.38 It is aimed at reducing coupling between classes—that is, how much any class knows about another class—in support of the oft-desired goal of making it easier to rework a class without breaking other code that uses that class. Specifically, the approach is to reduce the number of classes that any given class knows anything at all about (Demeter is the Greek goddess of the harvest; the law was not directly named after her, but took the name from a tool, also called Demeter, that was used for formally specifying class definitions—a rich bounty of class definitions, presumably—and was also developed at Northeastern). The Law of Demeter states that if a certain class C is using an object A, it should be oblivious about any objects returned by methods on Aoblivious, in this case, meaning that if code in C calls a method on A that returns an object of class B, then C should not call any methods (or access any public data members, if such exist) on B. The most C can do is hand B back to another method on A if it is needed. The law can be paraphrased as “don’t talk to strangers.”

The effect of this is that if B changes in any way, even in the names or parameters of its public methods, C won’t need to change, because it is treating B as a black box. C only leverages knowledge of A, not B. This is stronger than saying “C only accesses public members of B,” the basic coupling reducer in object-oriented programming. It’s saying that C doesn’t access anything in B at all.

This certainly does reduce coupling at the class level: class C is only sensitive to changes in class A, not class B. The problem is, what if C wants to accomplish something that is best handled by a method on B—you call a method on A, it returns an object of class B, now you want to use that B object to do something else? The answer, per the Law of Demeter, is that A should add a new method that provides that functionality, which it (presumably) implements by turning around and calling B internally; C is allowed to call this new method on A, since it is already coupled to A.

This is following the letter of the law, but not the spirit. C is still not coupled to B, but it is now more tightly coupled to A because it is now calling another method on A. And A is now slightly more coupled to B because it is now providing a method that it likely implements by calling B. Yes, technically, A could continue to support this new method without calling B, since this is an internal implementation detail, but that would likely be more work.

The authors of the original OOPSLA paper had been using the law in a large programming project with their students, so they had experience to back up their proposal. They do avoid extravagant claims, and acknowledge, as you would expect academics to do, that there are potential issues. The method count on a class may increase: “In this case the abstraction may be less comprehensible, and implementation and maintenance more difficult.” The authors continue in the same vein: “We have seen that there is a price to pay. The greater the level of data hiding, the greater the penalties are in terms of the number of methods, speed of execution, number of arguments to methods and sometimes readability of their code.”39 They end with a recommendation for further investigation.

I don’t doubt that the Law of Demeter is helpful in certain situations. What is not known, because it has not been studied formally, is what those situations are: what it is about the programming task at hand, the size of the team, the likelihood of future changes, and so on, that makes applying the Law of Demeter a net benefit. Yet, in the time since the original paper was published, the Law of Demeter has been picked up and is now presented as universally applicable object-oriented canon; I was taught it as such in an object-oriented programming class I took. I suppose “The Possibly Useful Idea of Demeter” doesn’t sound as appetizing.

Some OOPSLA papers did involve solid research. In a paper presented at OOPSLA 1989, Mary Beth Rosson and Eric Gold from IBM Research point out,

A widely held belief about object-oriented design (OOD) is that it allows designers to model directly the entities and structures of the problem domain. … This is an inherently psychological claim, with psychological consequences: a design approach that more directly captures the real world should ease the cognitive process of mapping from the problem to a solution, and it should produce design solutions that are more comprehensible in terms of the problem domain. Surprisingly, though, virtually no psychological analyses of such claims exist.40

The authors then proceed to compare actual object-oriented programmers to procedural programmers as they analyze a problem and talk through a solution. It’s good stuff, but unfortunately rare for an OOPSLA paper.

Even better is a 1991 paper titled “An Empirical Study of the Object-Oriented Paradigm and Software Reuse.” It starts with the obligatory group mea culpa:

While little or no empirical validation exists for many of software engineering’s basic assumptions, the need for scientific experimentation remains clear. … The use of precise, repeatable experiments to validate any claim is the hallmark of a mature scientific or engineering discipline. Far too often, claims made by software engineers remain unsubstantiated because they are inherently difficult to validate or because their intuitive appeal seems to dismiss the need for scientific confirmation.41

The study, though, is great; done by John Lewis, Sallie Henry, Dennis Kufara, and Robert Schulman, all professors at Virginia Tech, it involved a carefully planned experiment comparing reuse between procedural and object-oriented languages, using students as the test subjects. The professors had a control group. They balanced out the skill set among the students. One of the professors was a statistician! They conclude that object-oriented languages promote software reuse more than procedural ones; I could push back on the grounds that the languages used were C++ and Pascal, which isn’t a fair fight, but that would be a quibble.42 Instead, I will salute this paper as a shining beacon of engineering research.

Most of the rest, however, is anecdotal reporting on what the authors accomplished. As Marvin Zelkowitz wrote in 2013,

The typical conference proceedings today in software engineering contains numerous papers of the form

How <my acronym>, using <this new theory of mine>,

is useful for the testing of <application domain>

and is able to find <class of errors> better than existing tools.43

Since every software project is unique, and problems tend to show up as the group of programmers changes or the software evolves over the years, the successful completion of a project is not an indicator that the methodology and language chosen were the best ones possible. I successfully wrote games in IBM PC BASIC; that doesn’t mean that it’s a great language. And the wide universe of software makes it easy to construct specific examples where a given arrangement of classes in an object-oriented program works well; that doesn’t mean the advice can automatically be generalized. When writing a game on the IBM PC, a lot of the code involves changing the image displayed on the screen. In this environment, making the screen globally accessible from anywhere in the program rather than requiring a screen handle or screen object is a convenience; it avoids having to pass around a parameter that will always be the same. I could have written a paper on “The Effective Use of Global Variables to Optimize Interactivity”; that doesn’t mean that global variables are always good. There are many other areas, even in the code for the same game, where they make the code hard to read, not to mention hard to modify without breaking anything.

Certainly OOPSLA had a lot of “hype,” as Stroustrup called it. In the abstract for a panel on “OOP in the Real World” at OOPSLA 1990, a description of a troubled project included this:

These were by and large, failures of management and many of them were quite independent of the use of an OOPL [Object-Oriented Programming Language]. Nonetheless, the fact that we were using an OOPL was important because it contributed to an attitude that would not otherwise have existed. It was very true and is still somewhat true that OOP protagonists are true believers. The very real benefits of using OOP are presented in a very one-sided fashion which too often leads to the view that OOP is a panacea. This better than life outlook induced a euphoria in management which caused suspension of the normal procedures and judgment criteria.44

It’s not that these object-oriented ideas are bad; many of them are good. They may produce more readable, maintainable programs. Maybe all of them do! But more evidence would be helpful in knowing what actually is better, as opposed to just sounding better, and in figuring out when a particular approach will work best. I think it comes back to the fact that many programmers are self-taught; they are used to their own experiences being all the evidence they need that an idea is worthwhile. As with any situation where code is calling an API defined by somebody else, it’s similar to the hardware store redesign: if your task lines up with what the designer had in mind, it can be nice, but otherwise it can make your work difficult.

Object-oriented programming has been discussed enough that the term has filtered into the public’s consciousness as something that programmers do, although to the extent that it is covered in the mainstream press, it’s not about improved design but rather reusability. In particular, it concerns the idea that now that you have these objects, you can easily glue them together to make programs. This perception is not surprising. First of all, the word object makes it sound like you can do that. Second, it’s a convenient way to explain to a nonprogrammer what is new about objects compared with the old way; they are standardized components that can then be used to assemble larger pieces. Most important, object-oriented proponents repeatedly made this claim. Cox wrote that programmers would “produce reusable software components by assembling components of other programmers. These components are called Software-ICs to emphasize their similarity with the integrated silicon chip, a similar innovation that has revolutionized the computer hardware industry.”45

This ignores a couple of problems. You could do that with procedural programming (all programs, object oriented or not, are built up in layers that have to connect with each other and interact cleanly, and “reuse” just means that somebody else wrote some of the layers, which is independent of object-oriented programming).46 The reality is that you can slap objects together with other objects if they were designed to do that or happen to mesh together well, and you can’t if they weren’t, and that is also the same as procedural programming.

It turns out that small problems can trip up the combining of objects. Hewlett-Packard researcher Lucy Berlin’s paper at OOPSLA 1990 titled “When Objects Collide” observed that “pairs of independently sensible pragmatic decisions can cause fundamental incompatibilities among components.”47 In other words, the designers of the code that is calling a class, and the designers of that class itself, can each make completely rational, sensible decisions about how their code works, in out-of-the-spotlight areas such as how they handle errors and how objects are initialized, which turn out to be fundamentally opposed and make piecing together objects impossible.

There has been one instance when the “stick the blocks of code next to each other and it will work” approach was successful, and it predates object-oriented programming. Back in the 1970s, the UNIX operating system introduced the notion of a pipeline: taking the output of one program and sending it to another program as the input. This is most accessible to users when they are using the command-line interface, which is what DOS looked like: the operating system displays a prompt and blinking cursor, the user types a command and hits enter, the output of the command scrolls past, and then the computer prompts for the next command. Although they are somewhat hidden, command prompts still exist in both Windows and macOS (not to mention Linux) because they allow certain complicated commands to be typed easily. It’s not just programmers who appreciate this; in 1999, the science fiction author Neal Stephenson wrote a long paean to the power of the command line titled In the Beginning … Was the Command Line, which was later reprinted as a book.48 Kernighan and Plauger’s book Software Tools is primarily about the usefulness of command-line tools to programmers.

Using simple syntax on the UNIX command line, you could print the contents of a file, extract data from it, rearrange that data as needed, and build useful larger “programs” out of smaller building blocks without needing to modify the underlying code—the putative benefit of object-oriented programming. You put together a series of commands using the vertical bar as the pipeline symbol, like this (which I’ve split to fit on the page, but in reality would be typed on one line):

cat filename.txt | grep total | cut -d , -f 5 |
      tr a-z A-Z | sort | uniq

This says “take the contents of filename.txt, grab only the rows that contain the word ‘total,’ interpret it as a comma-separated list and take the fifth column, uppercase the values, sort them, and remove duplicate rows.” This is quite powerful for a lot of simple manipulation of data. It’s part of the “UNIX Philosophy,” which is described in Brian Kernighan and Rob Pike’s 1984 book The UNIX Programming Environment:

Even though the UNIX system introduces a number of innovative programs and techniques, no single program or idea makes it work well. Instead, what makes it effective is an approach to programming, a philosophy of using the computer. Although that philosophy can’t be written down in a single sentence, at its heart is the idea that the power of the system comes more from the relationship among programs than from the programs themselves. Many UNIX programs do quite trivial tasks in isolation, but, combined with other programs, become general and useful tools.49

This is a different UNIX-related notion from the “an incremental approach to program improvement is better than a heavy-handed process” one that I discussed in the previous chapter, although it could be viewed as a different side of the same coin: many small things are better than one big thing.

The reason you could stitch the command-line tools together so well was that when transferring data between them, they broke the data down into the simplest, most portable format—everything became text strings—so the result was not particularly fast, and it required the user to have knowledge of the data format. But the user could easily modify the data, precisely because it was just text strings; in fact, a lot of the command-line tools existed solely to manipulate the data so it could be successfully passed into the next tool, such as cut and tr in the example above. If one tool output the data separated by commas but you needed it separated by tabs, or you needed to sort the output or remove duplicate lines, simple tools were available for that. When linking objects together you run into problems, because the output of one method can’t always be easily fed into the next method; the UNIX command-line environment let you first notice the equivalent problems by visually inspecting the output of the first tool, and then fix it up as needed by inserting commands into the pipeline before feeding it into the second tool. Of course all this was slow, with all the conversion to strings and back, so stitching together commands this way wasn’t considered “real” programming, but it did work.

But so far, this is the only case where the “objects as building blocks” idea works. Early object-oriented writers recognized this. Cox called UNIX command-line pipelines “one of the most potent reusability technologies known today.”50 Meyer mentions them when talking about composability, although for him that is just one of the criteria for good design: “This criterion reflects an old dream: transforming the software design process into a construction box activity, whereby programs would be built by combinations of existing standard elements.”51

Still, in most cases objects can’t be arbitrarily glued together. What can they be used for? In fact, there is a situation in which objects are unquestionably a step forward, but it’s not the elegant designs that Meyer had in mind; it’s more mundane, yet also more useful.

Notes