8.14.0.4

V Programming Basics🔗

    18 Clarity of Code

    19 Comments are Needed in a Small Number of Places

    20 The Very Basics

      20.1 Names Matter

      20.2 The Size of Functions and Methods

      20.3 A Short Note on Conditionals and Loops

    21 Code Inspections, the Basics

      21.1 Inspecting Code for Coding Flaws

      21.2 A Code Inspection Memo

      21.3 Reacting to a Code Inspection

      21.4 Inspecting Code for Errors and Test Coverage Problems

      21.5 Pushing Back During a Code Inspection

      21.6 Inspecting Code for Design Mistakes

    22 Systematic Design, the Basics

      22.1 Developing Data Representations for Information

        22.1.1 Information that is Considered Atomic

        22.1.2 Structured Information: Don’t Use Strings to Represent It

        22.1.3 Structured Information: Use Structs, Records, Objects

        22.1.4 From Information to Data Representations, And Back

        22.1.5 From Information to Classes via Doodling

        22.1.6 Choosing from Several Alternatives

      22.2 Developing Data Representations for Composition

      22.3 Methods vs Functions

      22.4 Designing Functions, Systematically

      22.5 Atomic vs Composite: A Tiny Case Study

      22.6 Designing Methods, Systematically

        22.6.1 Favor Functions, Favor Immutability

      22.7 A Second Look at the Tiny Case Study

      22.8 Why Systematic Design is Socially Responsible

    23 Code Inspections, Systematically

      23.1 The Labyrinth Game: A Problem Statement

      23.2 Presenting Systematically Designed Code

      23.3 Inspecting Systematically Designed Code

      23.4 A General Guide to Code Inspections

        23.4.1 README Files

        23.4.2 Presenting

        23.4.3 Inspecting

 

 

What is programming to a software developer?

In the context of software development, programming refers to the creation of code that realizes the planned components. And, critically, the artifact that many other people will look at again and again. These people will have to read the code, and they will have to understand how it achieves its purpose, meaning both its functionality and its basic performance.

Sometimes all of this is easy to infer from code. Equally often, some aspects remain hidden because programming is the result of turning invisible thoughts into written code. The creator of some code may recall why a variable contains values of a certain shape; the problem is that some distant piece of code in a large module or class creates these values—and there is no manifest relationship between these pieces of code. Similarly, a creator may know that a call to one method is only valid if some other method on the same object has returned a certain kind of value; yet, most languages don’t even have a mechanism for expressing or checking such constraints directly. In short, comprehending code often means reconstructing the thoughts that went into its creation, including those that are not locally visible or those that aren’t written down anywhere.

Given this situation, the goal is to create code that allows maintainers to reconstruct the original thoughts quickly. Positively put, code must express a creator’s thoughts as directly, as clearly, and as comprehensively as possible in the chosen programming language. As the preceding three chapters suggest, the clarity Over time, programming languages improve or new languages come about that allow developers to express more of their thinking directly. But this change is slow, and so we must focus on improving code per se. of code depends on a developer’s proper understanding of the nature of software as the sum of proper mindset, plans, and code, plus an awareness of the “big picture” surrounding a software project. This chapter presents ideas on how systematic programming can create easily comprehensible code and how inspections may help programmers recognize problems before they become a big cost factor.

18 Clarity of Code🔗

When panelists consider some piece of code incomprehensible during an inspection, a common reaction is to promise explanatory comments. This reaction completely ignores the nature of comments. As a language construct, they are just a mechanism for adding unchecked words to code. As unchecked words, comments are often out of sync with the surrounding code. The cure is to use comments in a limited number of cases, where they are truly needed and where they tend to remain partially valid.

As for code itself, a range of factors affects its nature as an expression of thoughts. One truly basic factor is the naming of classes, fields, methods, parameters, and even local variables. At the large-scale level, individual developers may be able to influence the design of classes and relationships among classes or the partitioning of a sub-system into modules.

Experience also tells us that choosing a well-suited data representation is a key factor in clarifying the purpose of code. At almost every level of programming, a developer must make a choice, namely, how to represent domain information in the data sub-language of the chosen programming language. Once the choice is made, the respective code must convey how a piece of data is to be interpreted in the domain and how any piece of information is to be represented as data. Indeed, to get this part right, a comment next to data definition is almost always necessary.

The chosen data representation should induce the organization of methods, This is often called encapsulation.specifically their allocation to specific components. Conversely, the design of functions should mirror the data representation. Imagine a function that consumes a union data type with n variants; if the function’s body consists of a conditional with n corresponding branches, a maintainer can easy connect a branch with a variant and make changes accordingly.

Next, code benefits from some abstraction but not too much. Repetitions of code call for the creation of an abstraction and a good name that tells a future reader what the abstraction accomplishes. Then again, a developer should notComplex protocols indicate a gap in a language’s expressive power. abstract in anticipation of repetitions or abstract in ways that call for complex call-back protocols or deep inheritance chains.

Finally, as explained in Principles, in-person inspections are an amazing tool for simulating the hand-over of code from one developer to another. In the spirit of social responsibility, presenting and inspecting code aren’t chores but ways to simplify the lives of others—including older versions of the current team members. Getting code inspections right isn’t easy; but practice makes perfect or at least good enough.

19 Comments are Needed in a Small Number of Places🔗

Every programming language supplies a mechanism for adding informal, natural-language phrases to code. The idea is that comments can expose relationships among pieces of code that are difficult or impossible to express in the programming language itself. Sadly, due to their informal nature, commenting isn’t often practiced in university courses, and, as a result, students tend to think of them as a nuisance to be added once the coding is done. Comments have thus become the most abused language construct in the world of software, yielding over-commented, mis-commented, and under-commented software repositories.

Useless Comments The main problem with comments is that they are often out of sync with the code and thus misleading to a reader. When a comment is first added, it probably describes the code under consideration in a correct or close to correct manner. But, as others—including older versions of the original developer—modify the code, they forget to change the comments in accordance with the code changes. If this happens repeatedly, the comments become worthless.

A second well-known problem is that comments add nothing to the code. Instead they pollute it and pose an obstacle to reader. Consider the following, infamous example from the world of COBOL programming:

  ADD 1 to X // adds 1 to x

It illustrates over-commenting better than any abstract argument.

Where Comments are Useful Any language mechanism, including comments, must come with a policy of how to use it. One useful policy for adding comments consists of the following three rules:This policy does not cover the addition of README files, which help readers navigate a large code base.
  • to interpret a data representation, that is, how data relates to information in the problem domain and vice versa;

  • to state the purpose of a unit of code, meaning what it computes; and

  • to explain how a complex loop or a generative-recursive method accomplish their purpose.

The following paragraphs illustrate these rules.

Data definitions need interpretations. People like to have their rooms at a certain temperature. With a heater, an air conditioner, and a thermostat, it is possible to keep the temperature in a range around this desired temperature. A thermostat consists of a thermometer, which measures the temperature, and an actuator, which calls for warm or cold air from a heater or air conditioner, respectively. Programming a thermostat calls for a data representation of temperatures and differences in temperature. While temperatures in this problem domain are positive real numbers, common programming languages support only floats (double, single) and integers. In this situation, a developer may represent the crucial numbers from this domain with three integers, like thus:

  int current;

  int heatSetting;

  int acSetting;

Stop! Don’t proceed until you have worked through the following exercise.

Exercise 12. In this problem domain, a thermometer may measure a temperature at 17.1C. How is this temperature represented given the above data representation? Conversely, if current is 781, what temperature does the thermometer display?

What this exercise points out is that a developer tasked with updating this code gets no clue from reading it as to how information about temperatures is represented. The exercise also reminds us that some countries want thermostats that use Celsius while the United States is still using the Fahrenheit scale.

Once the data representation is equipped with comments, solving the exercise becomes possible:

  int current;     // in 1/10 of Celsius degrees

  int heatSetting; // call for heat when `current` falls below

  int acSetting;   // ... for cold air when `current` is above

Stop! Imagine a developer reading this code snippet. Is it now clear what is represented and how?

This simple example is illustrative of why data definitions—the result of choosing a data representation—deserve a comment. In general, representing information as data is a far more complex affair than this example indicates. See Developing Data Representations for Information for some additional examples.

Classes and methods need purpose statements. Whenever the thermometer deposits a new temperature measurement, the thermostat’s control software needs to decide whether it is time to call for heat or cold air. In large buildings only one or the other is available during certain months of the year, which may additionally depend on the hemisphere. Assuming that the code is enclosed in a program container, the following is a sketch of what this may look like:

  program Thermostat () {

      ...

      int current;     // in 1/10 of Celsius degrees

      int heatSetting; // call for heat when ...

      int acSetting;   // ... for cold air when ...

  

      IDecision decide(int current) {

        this.current = current;

  

        if ((current <= heatSetting) && this.heatAvailable()) {

          return new CallForHeat();

        } else if ((current >= acSetting) && this.acAvailable()) {

          return new CallForAC();

        } else {

         return new Nothing();

        }

      }

  }

Now consider a maintainer reading this code. Even though this code is somewhat simple, question yourself whether it controls all of the thermostat, that is, the thermometer, the actuator, and the external connectivity to the A/C and heater. Or, take a look at decide. What does this method compute? It consists of merely nine lines of code, so it is easy to read. But, should a developer read nine lines to understand a method’s purpose? If a developer has to understand the purpose of ten methods, it means reading and comprehending 90 lines of code.

A program, a class, or a method make up a unit of code. On rare occasions, or for extremely small units, the name of the unit brings across what the purpose of the unit is. In many more cases, though, such a unit deserves a one-line purpose statement for the developer who needs just a basic idea of whatnot howthe unit serves the overall objective. For the running example, one-liners do suffice to answer the above questions:Only the book’s type setting makes it two lines.

  // implements the controller software for the thermostat

  program Thermostat () {

      ...

      // whether `this` should call for heat or ac,

      // depending on the time of the year

      IDecision decide(int current) {

      ...

      }

  }

Once again, this example is simple, and a developer with some understand of the context may have figured out the purpose of either unit with a single glance. In reality, software is large and developers don’t always have the context at their finger tips. These one-liners cost the creator of the program unit little, but create the focus needed to realize a unit properly and help readers quite a bit. Designing Functions, Systematically presents some additional examples and explains a bit more how such purpose statements help.

Recursive methods, complex loops demand explanations. While many methods are straightforward, some implement complex algorithms that rely on intricate logical invariants. Sorting algorithms, graph traversals, and context-sensitive tree traversals are some examples. They may use generative recursion or loop-based worklist designs. In both cases, it may not be obvious how the algorithm computes its result or why it terminates. Hence, both kinds of recursive functions deserve one-line comments, explaining the how and the why in addition to the what. In case the developer avoids recursion in favor of a worklist, the comment is best placed with the looping construct (while, repeat), and it is best formulated as an accumulator property or a loop invariant. Finally, if an algorithm demands deep insights into the domain—think Simplex for solving linear programming problems in applied mathematics—a comment pointing to a journal paper or an on-line tutorial serves as a good warning to a maintainer; additionally, some in-lined comments should connect pieces of the implementation to the key ideas of the algorithm description.

In sum, here is the policy concerning comments in one line

Don’t comment code except for the three situations discussed here.

To recall, comments interpret data definitions, state the purpose of every (small or large) unit of code, and elucidate complex loops.

20 The Very Basics🔗

Making code self-explanatory starts with small things: names; the size of functions and methods;Style Guides conditionals and loops. Imagine the following scenarios as someone who is tasked with modifying a method:
  • finding the method and noticing it is some 100 lines long;

  • reading the variable x in its body versus the variable distance; or

  • encountering a conditional whose then branch is 10 lines long and comes with a one-line else branch.

At first glance, each of these problems is small. But, as these small problems pile up, a reader is easily overwhelmed because each of them demands a resolution. Since the code of surviving software is read many more times than it is created, the creator must put in the extra effort to eliminate such mental obstacle courses for future maintainers.

20.1 Names Matter🔗

Names matter in many contexts. First, literal constants need names. Second, the names of classes help navigate a code base. Third, good names for methods and functions, plus their parameters, can summarize many lines of code as a single line. Finally, introducing well-named, local variables for intermediate results helps readers comprehend the underlying computational process.

Most constants need names. A name conveys meaning, usually a reference to the application domain of the software system. Think of a reader who encounters

  100 * 200

versus one who looks at

  windowWidth * windowHeight

where the two names stand for the two constants, respectively.

In addition to conveying meaning, names for constants also facilitate the adjustment of values. For example, nowadays end-users sit in front of significantly larger monitors than a decade ago. It’s therefore acceptable to increase the width and height of the imagined window. Doing so with named constants is easy; finding all occurrences of 100 and 200 makes such an adjustment extremely difficult.

Exercise 13. List some constants that don’t need names no matter where they show up in code. What do they have in common?

Methods and their parameters need names that reflect their purpose. Here is a method header from a software system for an assisted-driving vehicle:

  int moveRight(int distance) {

    .. }

While the choice of moveRight depends on the context, the choice of distance is an entirely local affair. Instead of the plain word, the variable name could convey that it is the shortest distance between this car Some programming languages support notations for units and unit-checking, which is superior to naming conventions. But most don’t. (which is running the software) and the car in the next lane. A different use might call for bringing across that it is a distance measured in centimeters or inches. Of course, it is possible that both aspects of the name are critical, which may yield a name like this:

  int moveRight(int shortestDistanceToCarOnTheLeftInCM) {

    .. }

which is quite long. When IDEs allow developers to complete names with a couple of keystrokes,Racket’s implementation comes with variable names that are over 80 characters long. their lengths should not matter too much. What does matter, though, is readability.

Names must satisfy a compromise that takes into account (precision of) meaning and readability. Here “readability” means parsing the name: discerning its pieces and understanding how they hang together. The renamed parameter of moveRight is really a short sentence:

shortest distance to the car on the left in centimeters

A reader must discern these distinct words on every encounter.

To avoid this effort, a compromise is needed. One approach is to put the most important aspect of the variable into its name, and document the rest with an interpretation statement; see Comments are Needed in a Small Number of Places. In this spirit, here is a variant of the moveRight method that emphasizes the unit of measurement (cm):

  // `distanceCM` denotes the shortest distance to

  // an obstacle to the left of `this` car

  int moveRight(int distanceCM) {

    .. a conversion to inches for the next call .. }

The author of the code may think that adding the unit of measurement is more important than its precise meaning because the method calls a function that needs the same distance measured in inches.

Exercise 14. Reformulate the moveRight method header imagining that it is all about confirming a reaction to a distance measurement. Ask a peer to review your reformulation and comment on it.

Names for intermediate results explain computations. Here is a function from a game program that needs to compute whether players have placed the mouse inside of some area within a surrounding context:

  ;; represents a positioned square

  class SquareShape:

    Posn center;  // of `this` shape

    int width;    // in pixels of `this` shape

  

    ;; is `p` inside `this` square shape

    boolean onTarget(Posn p) {

      return

        this.isInside(

          Geometry.vectorFromTo(

            p,

            Geometry.shift(this.center,(width/2))));

    }

Now imagine you’re assigned a “bug ticket” related to this method. Stop for a moment and try to explain how the function computes its result—not what it computes; its purpose statement properly documents the goal.

Even though the developer has nicely indented the nested expression and aligned the arguments of the static method calls, it is still a challenge to understand the steps of this computation. Breaking up such a large expression into small ones and giving names to the intermediate results greatly improves readability as the following revision shows:

  ...

    ;; is `p` inside `this` square shape

    boolean onTarget(Posn p) {

     int halfWidth  = width / 2;

     Posn nuOrigin = Geometry.shift(this.center,halfWidth);

     Posn pRelative2Center = Geometry.vectorFromTo(p,nuOrigin);

     return this.isInside(pRelative2Center);

   }

  ...

This variant does not break the expression into atomic pieces—the first definition uses two operations—but it breaks it into a sequence that explains the procedure in sequential terms.

Like in the case of plain names, a sense of compromise is needed here. Some formulas are so well known that breaking them up is unnecessary. Some operations, say division by 2, are so obvious that they don’t need a separate name. Worse, breaking up expressions inside of a function may lengthen it so much that it is no longer an easy read. And that brings us to the next important basic point about code: keeping units of code sufficiently small.

20.2 The Size of Functions and Methods🔗

Functions and methods are the fundamental building blocks of programs, similar to the bricks and two-by-fours of houses. When developers must comprehend code with the goal of modifying it, they must comprehend functions and methods above all. Keeping functions and methods comprehensive is thus imperative, and this usually means keeping them short.

Squeak is a great example of a large system with short, clean methods. It is a modern version of Smalltalk, the first major object-oriented programming language. A reasonably recent inspection counted between 500,000 and 600,000 lines of code. The number of methods came in at around 100,000. In other words, the developers kept the average method to about six (yes, 6) lines of code. If these methods also come with sensible and meaningful names, a developer can comprehend the essence of each with a single quick reading. Each of these methods can be understood in a short time and with little cost.

The question is how developers keep methods and functions so small.

The key is to recognize two distinct classes of functions and methods:
  • An atomic function depends on the language, some imported functions, and possibly itself.

  • A composite function composes atomic and other composite functions, using mathematical function composition, ordinary sequencing, iteration, comprehension, and other composition mechanisms of the programming language.

    The onTarget method of the preceding section composes two functions with a local method, and it thus represents an example of a composite method.

So, to keep methods and functions concise and still comprehensible, developers should start the design for each of them with the question of whether it is atomic or composite; conversely, they must avoid methods that consist of blocks of atomic work and compose others.

Organizing code according to this guideline rests on two ideas:
  1. Don’t be afraid of function or method calls.

  2. Consider turning a method into a composite whenever it grows large.

The twoItem No. 1 applies to (almost) all popular languages and their implementations. Generally speaking, a developer must understand the nature of the implementations of the chosen language. ideas are two sides of the same coin. The first point is really about the chosen programming language; it enables the second point, which is about good habits for the programming-is-a-message idea.

Figure 33: Recognizing separate functionality

The ideas say “break up any large function” and turn it into a composition of several small functions, that is, a composite function. The performance cost is negligible. And the gains in comprehensibility are large. Creating a composite function means recognizing blocks of code in an existing function as a separate, meaningful task. The second step is to move this code into a function by itself, to give this function a name that describes the task properly, and to replace the code with a call to this function.

Figure 33 displays a somewhat longish function. It certainly exceeds the already-mentioned six lines per method metric of Squeak. The key is to recognize the lack of balance in the three branches of the (match) conditional. Specifically, the second, boxed branch consists of six lines while the first one requires two and the last on is just a function call. These counts alone suggest that the method is a mix of atomic computation and a composition of helper functions.

The next step calls for factoring out pieces of code as separate functions or methods. This means identifying all those variables that are defined in the existing function and needed in the code block. These variables become the arguments and parameters, respectively, to the factored-out functions. When a developer considers factoring out one branch of a conditional, it is often best to factor out all other branches that consist of more than a line.

  

  #; {World Message -> World}

  ;; process `msg` from server in `current` state of the world

  (define (receive-update current msg)

    (match msg

      [(posn-msg posn dir opponents)

       (deal-with-positioning-msg current posn dir opponents)]

      [(fight-msg updated-fighters outcomes)

       (deal-with-fight-msg current updated-fighters outcomes)]

      [_  (stop-with (format "can't deal with ~v yet" msg))]))

Figure 34: Breaking up the function in figure 33

In the running example, the existing function pattern-matches the received msg against structs named posn-msg and fight-msg, respectively. This matching defines several variables locally. Since the code in the two branches also refers to currentthe first parameter of receive-updateit also becomes an argument to the factored-out functions.

Figure 34 displays the result of this re-factoring step. The re-factored variant consists of a conditional with three branches, each a singe function call. Comparing the two variants should immediately clarify how the re-factoring and the choice of function names helps a developer navigate this code base. If, for example, a glitch is visible during the set-up phase of the game—when each player receives information about where its fighter is located and going—the name of the function in the first branch directs the developer to the correct code. Nothing else is needed. Likewise, if the result of fights is broken, the second branch calls for attention.

The factored-out deal-with-posn-msg function is an example of an atomic function. Atomic because it use nothing by struct operations (e.g. world-I, fighter-update) and imported functions (e.g. map).

20.3 A Short Note on Conditionals and Loops🔗

After identifier references, conditionals and loops are the most frequently used forms in methods. They deserve some basic care.

A conditional identifies distinct cases of computation, and it sets up distinct blocks of code for each case. Distinguishing cases helps developers focus. By subjecting a statement to a condition, a developer can focus on just this special one form of data and ignore all others.

The following guidelines govern the basics of conditionals:
  • all conditions and branches should use a uniform style;

  • all cases of a situation are covered by a single conditional; and

  • all branches should be of roughly the same size and complexity; or always place the shorter branches ahead of the longer ones.

Take a second look at the code in figure 33, specifically at the conditional in the function body. It distinguishes three input cases for msg. The conditions are formulated in a uniform style. While the first two pattern-match and deconstruct a struct into its pieces, clearly distinguishing two cases, the last one is a catch-all clause. By contrast, and as the preceding section points out, the branches themselves violate the first and third guideline with the second one standing out by size alone. The second branch also contains sophisticated pattern-matching definitions, (simple) function calls, and a nested conditional (distinguishing two different result situations). The refactoring of the preceding section creates a function whose conditional satisfies the guidelines.

As for the second guideline, consider the following code snippet:

  /* @param `state`  the current game state

   * @param `action` the player's chosen action

   * @return

   *   is `target` reachable after applying `action` to `state`

   * @effect may modify `state`

   */

  

  private static boolean reachable(State state, Action action) {

    Position target = action.getTarget();

    state.applyAction(action);

    if (!state.canActivePlayerReachTile(target)) {

      state.undoAction(action);

      return false;

    };

    return true;

  }

Although both return statements belong to the same situation—whether the target position is reachable or not—the method body does not signal this symmetry.

The violation is easily eliminated with an else branch. Adding it exposes that the two branches are of unequal length and the (expected) one is at the end. Removing the negation from the condition fixes this violation of the third guideline:

  private static boolean reachable(State state, Action action) {

    Position target = action.getTarget();

    state.applyAction(action);

    if (state.canActivePlayerReachTile(target)) {

      return true;

    } else {

      state.undoAction(action);

      return false;

    };

  }

Stop! Contrast the purpose statement of this method with the first and second organization of the method body. Which organization matches the purpose statement best?

  /* @param `dir` the desired direction of an action

   * @param `ind` the row or column to be shifted

   * @return whether `ind` is a valid index given `dir` */

  private static validIndex(Direction dir, Index ind) {

    if (dir.isVertical()) {

      if (!state.getBoard().isMovableCol(ind)) {

        return false;

      }

    } else if (!state.getBoard().isMovableRow(ind)) {

      return false;

    }

  }

Figure 35: Comprehending conditionals

Exercise 15. Figure 35 displays a snippet from a rule-checking class for a game whose board comes with movable rows and columns. Does the conditional satisfy all guidelines? Why? If so, explain. Why not? If not, rewrite the method. Work with a partner.

A for loop combines a conditional with a function. In its most basic form, the purpose of a for loop is to traverse some data structure and apply the body to each component of the data structure. In some languages, the enumeration of elements is an explicit process, and in this case, a termination condition is stated explicitly. As for the function part, nobody ever speaks of a loop body as a function, but that’s what it is. A while loop is similar to a for loop. It differs in two ways. First, the components of a data structure are computed explicitly, step by step. Second, the generation of components—and the termination of the loop—is governed by an explicit condition.

The key is to write clear conditions (if needed) and to keep the loop body comprehensible. Like for an ordinary function, the loop body should not exceed an easily comprehensible size, about the same size as a function. Conversely, if the loop body gets too large, identify atomic tasks and factor them out into functions; if necessary, turn the entire loop body into a function with a suggestive name.

Consider the following two code snippets. Read the one on the left first:

  void echo(String fn) {

    try {

      Reader br = open(fn);

      int  iIn;

      while ((iIn = br.read()) != -1) {

        char cIn = (char) iIn;

        System.out.print(cIn);

      }

    }

    catch ...

  }

               

  void echo(String fn) {

    try {

      Reader br = open(fn);

      while (br.ready()) {

        echoChar(br);

      }

    }

    catch ...

  }

While such a short snippet is easy to understand for an experienced code reader, it illustrates two problems. First, the loop condition combines an assignment statement with a comparison to a magic constant (-1). What does this mean? Second, the loop body consists of two lines whose purpose a code reader must also infer. Imagine a loop body of 10 or 20 lines.

Now look at the code on the right. It uses a ready method to bring across that the termination condition is about finding the end of the readable input. The loop body is just a function call to a well-named function: echoChar. Every code reader can immediately discern what this loop body computes.

21 Code Inspections, the Basics🔗

A code inspection calls for the discovery of three kinds of problems: coding flaws, errors, and design mistakes.

Coding flaws make the code difficult to understand. These flaws are common, easily spotted, and equally easily corrected. The most basic coding flaws violate the basic suggestions of the preceding section. Others conflict with specific coding rules that a team has adopted but aren’t enforced with linting tools.

Errors make code inconsistent with expected behavior. Most pieces of code come with informal specifications. The panelists must understand these specifications and what the reviewed functionality is to accomplish, though they may not see how to realize it. When panelists discover discrepancies between the code and the specification, everyone involved must rejoice; it is a bug not deployed. One way to search for errors is to inspect the unit tests for a piece of functionality. Panelists may notice that the tests don’t cover some aspects of the desired functionality; they may then propose that a test case be added and pay close attention to the uncovered aspects.

Design mistakes may make the code useless. In some cases, developers choose a code organization—a division of functionality—that overlooks essential aspects of the problem or specification: functionality specifications, performance constraints, contextual peculiarities, etc. Spotting such design flaws requires a good amount of experience, because they are about relationships of the reviewed code to other parts of the system or they call for a radically different choice of data representation and algorithm.

Panelists can discover design flaws at two stages: during an inspection of the specification architecture or during an inspection of the code itself. For the first kind, review the discussion in Interface Inspections, Systematically; it illustrates how panelists can question a relationship between different system components, specifically the choice of one communication protocol over another between two components. This early discovery avoids the creation of code with serious design flaws, but not all design flaws manifest themselves at this stage. For the second kind, panelists must continuously question whether they are agreeing to a presenter’s explanations of the code too easily. In many cases, they must insist that the present explicate implicit assumptions about the context of the presented code—and when this is done, they may see where the development went off the rails.

Using the guidelines from the previous section, an inspection targeting coding flaws is straightforward. Simple instances of the other two are also quite easy to spot.

Figure 36: Inspecting for coding flaws

21.1 Inspecting Code for Coding Flaws🔗

Imagine yourself as a panelist that is about to review the code in figure 36. This function is an extract from a QOI library. For our purposes, it does not matter what QOI is about other than that the figure displays a single function from this library. The question is where and how this code violates the basic guidelines from the preceding section. Stop! List those violations before reading on. Thanks to Guillaume Savanton who wrote most of this code originally and who gave permission to use it for this illustration here. The imaginary dialog is loosely based on a conversation between the creator and the author.

An actual code inspection may proceed as follows:

Here (figure 36) is the function that writes out an image to a file or a device.

This function does not fit on your laptop screen. How long is it?

It is 60 lines long. But note that it is extensively commented. A reader should have no problems with it.

Let’s focus on its length for now and return to the comments later. How can you split up the function?

Notice how even the first question points to a problem and how the presenter reacts in a defensive manner. Like design inspections, code inspections are not about the creator. They are about improving code so that the maintainers “get it” quickly. And even the first impression suggests to the panelists that the function is too large to comprehend quickly.

But, note how the panelists also veer from the prescription for inspections. Instead of focusing on finding flaws, the second question implies suggestions of how to improve the presented code.

Is it acceptable to weave in suggestions?

Generally speaking, it is not the task of a code inspection to make suggestions on how to fix problems. The intent of the review is to understand how the function accomplishes its purpose and to uncover problems. Keep in mind that the developer of the code has studied the problem in gory detail and has figured out how to tackle it with code. Once a problem is pointed out, the developer may come up with several solutions and, if needed, can ask for advice if a trade-off is to be made.

Once exception to this rule concerns “developers in training.” In the specific situation when a mentor reviews code with someone who is considered “in training,” weaving in potential fixes is acceptable. But even then it is best to extract the fixes from the mentored person rather than stating them directly. This furthers the learning process.

Hence a restatement of the last question is in order:

...

Does the function perform distinct tasks, tasks that could be factored out? Can you just walk us through the various steps that this function takes to write the image?

The first expression writes the header of the file. It could be factored out as a function.

If you name it write-header, it is also clear what it computes.

Noted. And yes, a good function name might make the comment superfluous.

Also, the expression is deeply nested. Perhaps it’s best to split it into named components while you create the separate function.

Noted.

Does this mean the big code block that initializes last is writing out the body of the image?

Now the panelist’s naming suggestion continues the thinking of the presenter and drives the conversation forward. The follow-up question is natural in two ways: the variable initialization of last is indeed huge, and a header is usually followed by a body. Although this point isn’t mentioned in the section on basics, it is basic to keep initializations short.

Let’s continue with the inspection:

Yes, the expression is somewhat large and performs side effects.

Can you identify the tasks that this initialization expression performs?

The loop goes over the body of the image and writes it out in chunks.

But it also seems to return something.

Yes, it returns how large the last sequence of bytes is that must be written out.

Can’t this loop go into another function, one that writes the body?

No, it isn’t just the loop. As the comments above suggest, the pixel-index vector is also needed for writing out the body.

Oh, okay.

Stop! What just went wrong with this dialog?

The panelist once again made a suggestion—and the presenter pushed back. Here the interaction demonstrates that presenters must know when to accept criticism and when to tell the panelist(s) why a remark is wrong. An inspection must be a give-and-take.

The inspection could also have taken an alternative direction:

Yes, the expression is somewhat large.

Can you identify the tasks that this initialization expression performs?

The loop goes over the body of the image and writes it out in chunks.

What does it return?

It returns the number of bytes that must be still written out.

The comment about #:return says so. Could it be replaced with a better name for this variable?

True, something like length-of-last-run would make this comment completely superfluous.

Yes. And as a matter of fact, almost every line of code comes with a comment. Isn’t that too much?

The comments are left over from when we worked out the function.

Go over them and see how many of them are still necessary.

Will do. Any other criticism?

Just a question. Now we understand that the function writes a file header and a body. Does it also write a “footer” or whatever it might be called?

For now, three major criticism is enough for 60 lines of code. Take a close look. What else might a conversation critique?

21.2 A Code Inspection Memo🔗

The product of a code inspection is a memo that details the criticism that come up and aren’t immediately resolved. It is not the task of the presenter to take notes, unless given time to do so. Indeed, during a code inspection, the Taking Notes presenter should focus on just three points: explaining the code; understanding the questions and criticism; answering those properly or pushing back when panelists’ criticism are addressed in the code—just like during a design inspection. A formal code inspection allocates one person—the secretary—to this one task; the presenter’s partner, if present, may take additional notes.

  To: Guillaume S.

  From: Matthias F.

  Date: 31 Feb. 1981

  Subject: your img-write-qoi function

  

  An inspection of the QOI library revealed the following problems:

  

  1. The function is too long.

  

     SUGGESTION It might be possible to break it up into three

     functions: one for the file header, one for the body of

     the image, and one for the remaining bytes.

  

  2. The initialization expression for the variable last is too long.

  

     SUGGESTION Turn the loop into a function that returns how many

     bytes of remain to be written at the end.

  

  3. The code comes with too many comments, many of which add

     nothing to the code. The first one is in the loop header:

  

        (for/fold ([pixel-prev qoi-pixel-init]

                   [run-length 0]

                   ; The loop will return the length of the last run.

                   #:result run-length)

          ...)

  

     This comment merely repeats the #:result clause and adds nothing.

Figure 37: A code inspection memo

A formal memo may look come with the following information:
  1. the name(s) of the presenter(s),

  2. the names and roles of the panelists,

  3. date and time,

  4. a link (URI) to the presented code, and

  5. a list of the problems discovered during the inspection:

    • a description of a problem must enable the presenter(s) to reconstruct it;

    • the description must be in terms of the code, not in terms of what the presenters said;

    • if a suggestion comes up and the presenter accepts it, the description may include it but should clearly label it as such.

Figure 37 displays a sample memo based on the preceding snippets from a code inspection. If the setting is semi-formal, it usually suffices to send an email with just the list of problem descriptions (and suggested fixes), with a CC to the remaining people present.

  

  #; {Image [OutputPort] -> Void}

  ;; save `img` in the QOI format to the `out` port

  (define (image-write-qoi img [out (current-output-port)])

    (write-qoi-head img out)

    (define last-run-length (write-qoi-body img out))

    (write-qoi-op-runs last-run-length out)

    (write-bytes QOI-END-MARKER out))

  

  

  #; {Image OutputPort -> Void}

  (define (write-qoi-head img out)

    (define width  (integer->integer-bytes (image-width  img) 4 #f #t))

    (define height (integer->integer-bytes (image-height img) 4 #f #t))

    (define header (bytes (image-channels img) (image-colorspace img)))

    (write-bytes (bytes-append QOI-MAGIC width height header) out))

  

  

  #; {Image OutputPort -> Void}

  (define (write-qoi-body img out)

    (define pixels (image-pixels img))

    (define LENGTH (bytes-length pixels))

    (define INDEX  (make-vector 64 (make-bytes 4)))

    ;; Process pixels in raster-scan order:

    (for/fold ([prev qoi-pixel-init] [run-length 0] #:result run-length) ([n (in-range 0 LENGTH 4)])

      (define pixel (subbytes pixels n (+ 4 n)))

      (cond

        [(equal? pixel prev)

         (values pixel (add1 run-length))]

        [else

         (write-qoi-op-runs run-length out)

         (write-piece pixel INDEX prev run-length out)

         (values pixel 0)])))

  

  

  #; {Bytes [Vector Bytes] N OutputPort -> Void}

  (define (write-piece pixel INDEX prev out)

    [define pos (qoi-index-position pixel)]

    (cond

      [(equal? pixel (vector-ref INDEX pos))

       (write-qoi-op-index pos out)]

      [else

       (define-values (dr dg db da dr-dg db-dg) (pixel-diff pixel prev))

       (vector-set! INDEX pos pixel)

       (cond [(not (zero? da))

              (write-qoi-op-rgba pixel out)]

             [(and (<= -2 dr 1) (<= -2 dg 1) (<= -2 db 1))

              (write-qoi-op-diff dr dg db out)]

             [(and (<= -32 dg 31) (<= -8 dr-dg 7) (<= -8 db-dg 7))

              (write-qoi-op-luma dg dr-dg db-dg out)]

             [else (write-qoi-op-rgb pixel out)])]))

Figure 38: Reacting to a code inspection memo

21.3 Reacting to a Code Inspection🔗

Figure 38 shows how the presenter(s) could react to the criticisms from the code walk. The main function—image-write-qoiconsists of four lines of code: the first one writes the file header; the next two deal with the main body of the image file; and the last line writes an end-of-image marker.

The revised image-write-qoi function is an example of a composite function. It composes the functionality of three auxiliary functions:
  • write-qoi-head, which writes the file header;

  • write-qoi-body, which deals with most of the body; and

  • write-bytes, a generic function that marks the end of the file.

Each of these pieces represents a natural segment of the original version of the main function.

Consider write-qoi-head, the function that writes the file header. The corresponding expression in the original version of image-write-qoi is deeply nested. This new auxiliary function consists of three variable definitions and one expression: write-bytes. The names of the variables makes it abundantly clear which role each piece plays and which order the pieces are computed. No comments are needed to explain any of this computation.

Similarly, write-qoi-body consists of three variable definitions and the main shape of the large loop in the original image-qoi-write function. It contains one comment, concerning the order in which the loop traverses the image’s bytes. The author clearly considered this remark critical and its content difficult to discern at one glance from the loop’s body. Also note how the author split out the non-trivial, atomic step of writing pixels to the file. Doing so keeps both auxiliary functions to a reasonable size and creates comprehensible units of code.

21.4 Inspecting Code for Errors and Test Coverage Problems🔗

Spotting errors in code is challenging. To claim an error, a panelist must fully understand the specification—informal, rigorous, or formal—and the presented code. Understanding the specification means being able to imagine how the program should behave for a large part of its input spectrum or in a range of situations. Understanding the code might mean grokking one function or it might demand seeing how several methods jointly compute the desired result.

Balancing the two aspects is a challenge for every panelist. Some highly experienced developers may be able to accomplish it in some situations, perhaps because it resembles problems they have encountered before. But, for the sake of keeping the conversation productive and for the sake of egoless programming, it might be best for both to simply ask standard questions about test coverage. If the tests cover the imagined inputs or situation, there might not be a problem; otherwise, the presenter and the panelists may wish to work through such the imagined scenario together.

Let’s make this concrete. Figure 39 displays a class fragment for a game called “Three in a Row.” Two players place tokens on a square board of size n x n. The first player to connect three tokens along a row, a column, or a diagonal wins.

Figure 39: Inspecting for errors and test coverage problems

As the data-representation comment in BoardState says, the developer has chosen to represent the state of the game board with a String array. Each one-character String in this array represents an open slot or a slot taken by one of the players. To decide whether it is a winning board state, the developer has added methods for extracting rows, columns, and diagonals, each represented with a String array.

In this context, it is time to observe an inspection:

The most interesting methods are those that extract rows, columns, and diagonals.

Okay, why don’t we look at the extraction of diagonals to get started.

It’s best to highlight the method. (See boxed portion of figure 39.)

Well, the "d" and "r" part of the name are puzzling at first, but the purpose statement clarifies it.

In order to extract the diagonal, the method sets up a loop that proceeds from the given board position, one step at a time.

Is diagonal the loop accumulator?

As a matter of fact, yes. If the loop has taken yea-many steps, it contains that many Strings.

...

So far, so good. The presenter and the panelist discuss how the code computes the diagonal, avoiding all mention of “how you, the presenter” would do it. The first interaction also illustrates how a panelist—usually the head—directs the code inspection to an interesting point, here, the diagonal-extraction method.

An experienced developer may point out the problem directly:

...

That makes sense. But could you explain how the for loop iterates through the two-dimensional board

Sure. The loop takes three steps, with i set to 0, 1, or 2.

Doesn’t the indexing into board with 1 or 2 raise an exception if row and column represent the corner of the board?

Then again, if an experienced panelist is dealing with a new developer, requesting a unit test might be the preferred approach so that the presenter can discover the flaw instead of being told about it:

...

That makes sense. But could you explain how the for loop iterates through the two-dimensional board

Sure. The loop takes three steps, with i set to 0, 1, or 2.

Could you show us a test case where the drDiagonalFrom is called with a starting position that is only one step from the board’s border?

I don’t think we have such a test. As a matter of fact, I don’t think we have any tests where the loop starts just one step away the edge of the board.

So is this a problem?

Yes, it is. We simply didn’t think that a player would play it that close to the edge.

I don’t think we should make such an assumption.

Correct. We don’t want this code to raise an out-of-bounds exception.

...

Note how the presenter reasons out in real time that the code would raise an exception—a true learning experience.

21.5 Pushing Back During a Code Inspection🔗

The inspection of the code in figure 39 also makes a good example of when a presenter may wish to push back:

...

That makes sense. But could you explain how the for loop iterates through the two-dimensional board

Sure. The loop takes three steps, with i set to 0, 1, or 2.

Could you show us a test case where drDiagonalFrom is called with a starting position that is only one step from the board’s border?

We don’t need such a test. The winning method calls drDiagonalFrom only if the diagonal exists.

Oh, that’s surprising. You may wish to expand the purpose statement of the method to explain this pre-condition.

That’s a good idea. Someone might add a call to drDiagonalFrom in the future without realizing that it relies on this pre-conditon.

From this angle, it might even better to eliminate the code that establishes the pre-condition and to fold it into the method.

Yes!

Before we move on, let’s revisit this check in winning.

Several aspects of this exchange are worth nothing. First, a presenter should not just listen to criticism and accept it. It is perfectly reasonable to reject a criticism. Second, in this case, the calling context establishes a critical pre-condition, which implies the next two points. Third, the presenter should explain this class in a top-down fashion, starting from the public entry points, and ask the panelists to pay attention to the logical relationship with drDiagonalFrom when the definition of winning is visible. Fourth, the panelists may not rely on the word of the presenter alone here. They must re-direct the code inspection to the winning method so that they can convince themselves that the pre-condition is properly establised—and also governs calls to similar methods. Finally, the presenter understands that expanding the purpose statement with an appropriate remark about the pre-condition is an act of socially responsible software development.

  class Server:

      """

      a server for signing up remote players for a Train game

      """

  

      host:          str = "0.0.0.0"

      server_socket: socket.socket

      port:          int

      index:         int = 0

      proxies:       \underline{\texttt{Set[AbstractPlayer]}} = set()

  

  

      def __init__(self, port: int):

          self.port = port

  

      def sign_up_step():

          """

          EFFECT tracks signed-up player-clients and their age

          """

          ...

          self.index = self.index + 1

          self._handle_1_client(client_socket, self.index)

          ...

          ... Referee(\underline{self.sort_proxies()}).run() ...

          ...

  

      def _handle_1_client(self, client: socket.socket, i: int):

          """

          EFFECT adds a single player-client to `proxies`

          """

          ...

          with Timeout(CLIENT_NAME_WAIT, ...):

              player_name  = client.recv(RECV_SIZE).decode("utf-8")

              proxy_player = ProxyPlayer(player_name, client, i)

              \underline{self.proxies.add(proxy_player)}

         ...

Figure 40: Inspecting for design mistakes

21.6 Inspecting Code for Design Mistakes🔗

Generally speaking, design mistakes are the most difficult-to-spot, and yet, they may have the worst impact on all aspects of the overall product. Their source varies but here are some common places to check out: the choice of data representation; the division of functionality into separate tasks; the choice of intermediate data structures for composing helpers; a mismatch between the data representation and the processing functions; premature and probably useless abstraction or optimization. The effects, as mentioned at the beginning of this section, are equally varied: opaque code; complex logical errors; and more.

Figure 40 presents a short excerpt from a game server. The relevant piece of the manager’s specification reads as follows:

The (board game) server waits for sign-ups over TCP ... for a minimum number (here, 4) of remote clients to connect and send over a name. ... If a sufficient number of players sign up by the end of the waiting period, the server hands over the players to a referee, sorted in ascending order of their “sign-up age.” ...

The last clause means that the server hands the clients (as players) to the referee in the reverse order in which they connect—that is, the “youngest” player starts first, as in many board games.

Here is how the creator of this code and a panel might interact:

Here (figure 40) is the server class. Its state represents its external connectivity and the players that have signed up since the call to sign_up_step.

Most of the fields make sense. Though it’s not clear why there is an index field.

The index represents the reverse age of the connected players.

It might be best if we check how it is used.

Look at sign_up_step. It is the only public method.

Okay.

It waits for a connection, increments index, and calls _handle_1_client to deal with a single connection.

What happens if this call fails to complete the sign-up step? Isn’t index “wrong” in this case?

“Wrong” is the wrong word, because index does not have to be the precise rank in the sig-up order.

Is it enough if it increases over time?

Yes. Its value is stored with the proxy player so that sign_up_step can sort the proxies before it hands them to the referee.

This may make sense. Let’s check _handle_1_client next.

The code inspection starts well, with a quick look at the information the class represents and a directive from the panelists at what to look next. By the fourth interaction, the panelists seem confused about the role of index and its value. While the presenter can temporarily diffuse the challenge, the panelists keep questioning the code with a direction.

_handle_1_client waits for a string to appear on the new connection. Once it has the string it can create a proxy player.

What does the proxy player do? It seems to need index in addition to the connection.

A proxy player has the same interface as a local AI player. But, instead of making game decisions locally, it forwards the data via its connection.

That makes sense. Why does it need index? And why is it named i in this method?

As I said, the index is stored in the proxy so that sign_up_step can sort the set of players before it hands them over to the referee.

Is there any other use of proxies in the class?

No, there are just those two: one in _handle_1_client for adding a proxy.

And one in sign_up_step to sort?

Indeed.

This sounds like a set is the wrong choice of data representation.

The extensive integration tests pass.

No doubt, but a set is an unordered data structure, and the specification calls for an ordered one.

Yes, that’s true. Perhaps it would be better to keep the proxy players in a list and just add new players to the front.

Doing so would eliminate index and the sorting step, though a data-interpretation comment about the ordering would still be good for future readers.

The presenter and panelists jointly figure out a design problem. Even though the code is correct, the use of set signals to a reader that the proxies are collected in an unordered manner. A future reader of this code would have to reconstruct the relationship between index and sort_proxies to understand what is going on. By using an ordered data structure such as a list to represent the information about the connections the code signals up front that an ordering is needed. The use of a list also directly echoes the age-order part of the specification, which may also help future readers.

This simple design mistake causes only minor problems. It misdirects a reader, and it consumes a bit of extra time to perform the sorting step. It is easy to imagine though that such small mistakes pile up and cost maintainers extra time to wrap their head around the code. Similarly, a lot of small performance losses add up to a large one. It is therefore good to catch and eliminate even such small mistakes as early as possible.

Exercise 16. Write a code inspection memo like the one in figure 37 for this code inspection.

22 Systematic Design, the Basics🔗

Given that functions and methods are the fundamental building blocks, their development deserve special care. While “programming a function” might have served as a phrase in the early days of computing, here we use “designing a function” instead to bring across the importance of acting carefully and systematically at this level. The word “programming” encompasses just too many sloppy ways of creating code.

Systematically designing methods and functions starts with the proper and careful design of a data representation. After all, functions and methods consume and produce data, which is often illustrated with the most common diagram in computing:A piece of functionality may produce more than one output so this is just a rough sketch.

The diagram says that developers typically know what kind of data flows into and out of piece of functionality, but they do not know how to create it yet. Like any good engineer, a software developer must study the available information—the nature of inputs and outputs—-and then base the creation on this knowledge. Doing so guides the design process, and it helps future readers of the code to reconstruct the logic of the design.

The question is where does data come from and what this implies for its representation. And this question brings us to the most critical insight concerning systematic program design:

Data represents the information that enters or exits the software system. Data also represents intermediate results, which flow from one component in the system to others.

So let’s start with a close look at how information gets represented as data. Once we understand this first form of data representation, we look at how developers pick data represents for intermediate results.

22.1 Developing Data Representations for Information🔗

Software systems interact with their context by absorbing and injecting information. This context comes with many forms of information that the system must consume: names, distances, temperatures, pressure, touch, and so on. A device turns this information into plain bytes inside a computer, e.g., a keyboard, a mouse, a thermometer, a touch screen, and so on. These devices deliver sequences of bytes, and the software system reads these bytes. The act of reading turns information-as-bytes into data—specifically data in the chosen programming language. Conversely, a software system turns data into bytes, usually through some act of writing. The computer hardware delivers these bytes to devices, as pictures on screens, sounds on a speaker, directions to an A/C system, and so on.

Figure 41: Information and data

A concrete example should help. Take a look at figure 41. It depicts a software system—the bottom box—for predicting the weather. The system relies on weather stations to measure the current temperature and the air pressure at specific locations. These weather stations contain some simple software that capture the measurements and send them across the network to the prediction software as a sequence of bytes; these days, the bytes comprise a JSON JSON is the currently popular choice. Tastes for such notations changes over time. object with appropriate attributes. The modeling software reads this sequence of bytes—via some JSON library—and turns them into a data representation suitable for further processing.

In most cases, developer may ignore the byte-level layer and may instead focus on information and data. Turning one into the other is thought of as a direct, instantaneous step for the purpose of developing a data representation. In the context of the running example, the developer has chosen a class with three fields as a data representation (bottom) for the information (top right). What happens in between does not matter. Concretely, the fromJSON function in the Readings class translates this information into the internal data representation, and the implementation is usually just a use of some library plus an optional check that the resulting data meets some conditions. The important choice is the Reading class, which for this simple example, makes objects with three relevant attributes.

22.1.1 Information that is Considered Atomic🔗

Choosing a data representation for basic information is straightforward for contemporary programming languages:

information

     

data

single names

     

strings, such as "Bob"

distances

     

numbers, such as 1.5

keystrokes

     

chars, such as #\a

switch positions

     

Booleans, or

     

symbolic values, such as "on", "off"

For complex information—often called structured—developers have often made mistakes such as using strings for what looks like plain text. So let’s start with a negative rule.

22.1.2 Structured Information: Don’t Use Strings to Represent It🔗

Before programming languages came equipped with data constructors, developers also overused arrays to represent structured information. Although this choice is a slight improvement over strings, the rule essentially applies to arrays, too. Imagine a reader who encounters a program that represents a person’s first name, middle initials, last name, titles, and suffixes as a single string. Here are six examples that keep the first and last name the same and illustrate what other pieces of information may show up:

  "Shriram Fisler"

  "Shriram K. Fisler"

  "Shriram K. Fisler, Jr."

  "Shriram K. Fisler, Esq."

  "Dr. Shriram K. Fisler, Esq."

  "Prof. Dr. Shriram K. Fisler, Esq."

Unless the developer of this data representation takes great care of hiding this choice, reading the code and its string-extraction patterns imposes a high cognitive load on the human reader. Second, imagine the processing that has to take place to extract the various pieces—the last name, the titles, the suffixes, etc.—from a plain string. In short, both human readers and the language implementation have a hard time coping with strings as a representation of this highly structured information.

22.1.3 Structured Information: Use Structs, Records, Objects🔗

Structured information comes in many forms, but all of them share the principle of relating pieces of information to each other. Indeed, this is the definition of the word “structured” in the phrase “structured information.”

Take a second look at the examples of a person’s names with titles and suffixes. They illustrate how many pieces of basic information can show up and how they relate to the overall information: a medical doctor uses “Dr.” as a prefix title; a person typically has a first and a last name; a lawyer may use “Esq.” (short for “Esquire”) as a suffix title. For this form of information, a developer can send a clear picture of the pieces of information and their relationship via a record structure:

  (struct person

     [{titles : [List String]}

      {first  : String}

      {middle : Char}

      {last   : String}

      {suffix : [List String]}])

  

  (define example

    (person [list "Prof." "Dr."] "Shriram" #\textbackslash{K} "Fisler" '[]))

In this code snippet, a reader sees expressions such as (person-first example), and it is immediately clear what it produces. Similarly, the language implementation is going to evaluate this expression in constant time, with just a few machine instructions.

Here are some other examples of how complex information might be represented with data:

information

     

data representation

names of people,

     

a struct of strings

amounts of money

     

a struct, an exact rational

points and directions

     

vectors, complex numbers

a handful of game money tokens

     

a set of ...

sequence of temperatures

     

a list, for sequential access

sequence of temperatures

     

an array, for random access

sequences of names of people

     

a list of structs

hierarchically arranged decisions

     

a tree

the ancestors of a person

     

a tree

people, their social relationships

     

a graph

Exercise 17. Pick one case and figure out the rationale for the recommendation on the right-hand side of the table.

The above table mentions lists, trees, graphs, and so on. In many cases, these forms of data are ideas that developers inject into the conversation about information. The “graph of people” does not exist in the real world; individual pieces of information make up the collection of these links. In the world of data—in whatever chosen language people work—this collection is turned into a single piece of data so that developers can have an effective conversation about the code, and the execution of the code satisfies basic efficiency requirements.

22.1.4 From Information to Data Representations, And Back🔗

Stepping back, the structure of information tends to guide the design of data representations. Basic information maps to atomic data. For structured information, contemporary languages come with an extensive collection of data-structure libraries. These libraries many representation choices straightforward. If a developer must represent a sequence of information—say the names of athletes in the order in which they finished a competition—then it is natural to use a built-in list type. Even for structured information as complex as a graph, a language’s ecosystem may supply a suitable form of data structure—in this concrete case, a directed graph. Many problems still call for bespoke data representations, however.

In case the structure of the information does not easily map to a supported data collection, it remains imperative to proceed systematically with the development of a data representation. A systematic development tends to be reflected in the resulting choices and code elements. It is thus likely to convey the intention of the creator.

The Representation Statement Once a developer understands the pieces of information that need representation, there are two key preparation steps to take. First, the developer must identify the relationships among the pieces of information. Second, the developer must articulate representation statements—something akin to thesis statements for paragraphs in an essay—for the various kinds of data involved. Identifying the relationships among pieces of data helps with stating a purpose for each kind of data. The goal of the representation statement is to delineate what is—and what isn’t—represented. The importance of a clear thesis grows exponentially with the number of relationships among pieces of information.

Consider the following example. Suppose we wish to construct a software system that allows people to explore the biological ancestor relationships among people. Perhaps they want to make sure that a newborn gets the name of some ancestor—as is common in some Western traditions—or that the newborn’s name does not coincide with the name of any ancestor—as is common in some Eastern traditions. This short description of information clearly identifies person with name as one kind of information and ancestor-relationship as a second one.

Given this context, a developer could start the development of a data representation with a code snippet like the following:

  // ... represent naming information for a single person

  struct Person [first : String, last : String]

  

  const jon : Person = {first = "Jon", second = "Parson"}

  const ada : Person = {first = "Ada", second = "Parson"}

  const bob : Person = {first = "Bob", second = "Parson"}

The snippet states up front that it is a data representation for a person. A struct is appropriate because as the data about one person flows from one function to another, the names should stay together. Note how this data representation does not mention anything about ancestor relationships. By writing down the thesis, the developer can focus on the one task at hand: representing naming information of a person. Anything else is out of scope. Finally, the snippet illustrates the chosen data representation with concrete examples. Given the simplicity of information, it is obvious how the developer takes information about a parson and turns it into data.

Concisely put, a representation statement explains

how every instance of information is represented as data.

Using this statement as a guideline, a developer can implement the functionality that turns a sequence of bytes into a (useful) data representation.

The Interpretation Statement The developer—and every maintainer—must also understand

how pieces of data are interpreted in the realm of information.

Using an interpretation helps implement the functionality that turns a piece of data into a sequence of bytes that affect the actual world. Without these two statements, data representations can cause serious trouble. Remember the interpretation of distance from Comments are Needed in a Small Number of Places and how a lack of an interpretation can cause a huge amount of damage.

Note The word “every” is missing from the above guideline. Formulating a data representation determines a set of values in the chosen programming language. Due to the nature of contemporary programming languages, it is often extremely difficult or even impossible to describe this set in such a way that every piece of data has an interpretation.

Let’s look at some examples of interpretations:

data representation

     

possible interpretation(s)

  int distance

     

in m, cm, feet, yards, etc.

  List<Person>

     

an alphabetical list of members

  BinTree<Person>

     

a (binary) tree of ancestors

  struct Name {

   String first;

   String mi;

   String last; }

     

does this choice need one?

Here is one more:

  struct Coordinate {

   int x; // the distance from the left margin

   int y; // the distance from the top (in pixels)

  }

It describes the common screen coordinate system.

Exercise 18. For each example in the table, explain why an interpretation is needed and/or make up some example data to illustrate the interpretation. How does the data definition of Coordinate differ from the Cartesian coordinates you know from middle school? Does this data definition need the interpretations of the two fields?

In summary, developing a data representation calls for two comments: (1) one that explains how information is represented as data and (2) another one that explains how a piece of data is interpreted as information. A data representation must come with some examples to illustrate these comments.

Representing Relationships While a named person is a concrete, single piece of information, some data lacks such concrete counterpart in the world of information. Recall sample domain in this subsection calls for the representation of ancestor relationships. People tend to refer to the ancestor tree. But, obviously, there is no such tree in the domain of the scenario. It is an imagined, abstract piece of element of our analysis of the domain.

When pieces of information are related in this imaginary way, it is best to introduce a separate data representation. Here is a sketch of an ancestor-tree representation:

  // ... represent ancestor relationships among Persons

  type Tree = Known | Unknown

  

  struct Known [person : Person, mother : Tree, father : Tree]

  struct Unknown[]

  

  const u : Unknown = {}

  const f : Known = {person = jon, mother = u, father = u}

  const m : Known = {person = ada, mother = u, father = u}

  const c : Known = {person = bob, mother = m, father = f}

  

  // `c` is the ancestor tree of the child of `m` & `f`

  // neither `m` nor `f` have known ancestors

The representation statement delineates this data representation from the first one. It is not about the representation of a person’s names, but about the ancestor relation among persons. The examples of ancestor Trees are constructed from the examples of Persons. Although the interpretation of this particular data representation is apparent, the last two lines of the snippet show what a developer might want to write down just to be sure.

22.1.5 From Information to Classes via Doodling🔗

Since the proper development of data representations lays the foundation for the systematic design of software, let us take a close look. We do so in the context of class-based, objected-oriented programming, which still dominates many parts of the software development field. The following list of scenarios is not exhaustive but covers many cases. It simultaneously articulates some basic principles and suggests a sketching notation for “doodling” data representations (on a napkin or a black board). You may have encountered both in a prior course on programming , so consider this list a concise summary.

Combining Several Simple Pieces into One Bundling several pieces of information into one piece of data is the most basic choice of a data representation. Doing so signals to the reader of this code that these pieces belong together and that functions and methods should deal with them together.

The preceding subsection introduces such an example: the first and last name of a person always belong together. Other examples are the coordinates of a point in space; an amount of money and its denomination; or the measurements of a single piece of furniture.

Let’s make this concrete with a virtual example of information, a text-field editor. Assuming that editing takes place at the position of the cursor, the data representation of such an editor combines three distinct forms of information: the part of the text that is to the left of the cursor, and the part that is to the right. Representing text as a string is a first natural choice, and thus the appropriate graphical notation is this:

The notation says that the TextField representation is a class of objects, each of which comes with two strings.

Even though this particular class signals a straightforward interpretation, a diagram like the above should come with a brief comment for the implementors:

  TextField tf = new TextField("hello ", "world");

  // if the cursor is a vertical bar,

  // `tf` represents "hello |world"

This one-line statement can be understood as both a representation statement and an interpretation. It should definitely suffice for someone to continue the development from here.

Combining Simple and Complex Pieces into One As mentioned, the structure of information should drive the development of a data representation. if a piece of information refers to other forms of information, a class-based representation should consist of classes that refer to each other.

Take the example of a geometric line. You learned in your geometry class that two points determine one line. To determine a Cartesian point in the plane, it suffices to fix two integers (or doubles). Based on this description, a developer may contemplate two different data representations:

flattened

   

nested

   

   

   

legend:

   

   

the diamond-labeled field is represented

   

by an instance of the class at the arrow head

   

Stop! Which one would you choose and why?

The explanation in geometry books clearly identifies two distinct pieces of information: lines vs points. Given this context, it is clear that a nested representation mirrors these textbook explanations. By contrast, a flattened representation imposes a small, but non-trivial cognitive load on the reader, namely, to realize that (x0,y0) determines one point and (x1,y1) the other. Since the domain of geometry also comes with distinct operations on points and lines, which occasionally depend on each other, a separate representation has the advantage of offering natural places for these operations.

On occasion, a class may also describe objects that contain instances of library classes. This form of containment is especially useful when the field represents a collection of pieces of data, and a suitable form of collection container comes with the chosen programming language. For example, a developer may quickly realize that a data representation for a text-field editor should use lists of characters instead of plain strings:

   

   

   

legend:

   

each diamond-labeled fields is represented

by an instance of the container class at the arrow head

   

This alternative representation is inspired by the insight that editing operations address individual characters, meaning it is best to represent the two pieces via Chars not Strings.

From far enough away, the two nested diagram look the same. Each comes with one class whose two fields point to the same secondary class. This is a mere coincidence, however. To make this point explicit, here is a doodling diagram for representing the state of an automated game player for the Ticket to Ride game mentioned in the preceding chapters:

In this diagram the main class also has two fields, but each points to two distinct classes. The next section returns to this data representation and presents details.

Actions are Information, Too Functional programmers understand that actions are information and occasionally deserve a direct representation. Consider the somewhat abstract and mathematical example of infinite sets, which a logician may represent with a characteristic function from elements to Booleans; if the result is true, the element is in the set and otherwise not.

A concrete example is a game-playing strategy, which is an action in response to understanding the current game state (and some planning). With just a bit of squinting, a well-trained developer understands a strategy as a function and considers representing it as such. In a class-based language such as Java, this data representation is a class with one method, apply:

In a functional language, a strategy might just be a first-class function that is plugged into a player mechanism, which is a second-order function.

Viewing Distinct Pieces Uniformly When a form of information comes in distinct flavors, the rest of the program should use all pieces uniformly. In a class-based language that supports interfaces, we can think of this case as a typethe interface—with several variantsthe flavors. A developer who must use a language without interfaces, a class with dummy methods can serve the same purpose.

Sketching such an arrangement requires a new kind of convention:

A plain arrow—without diamond label—denotes one of two situations: (1) the Derived class implements the Super interface or (2) the class extends a Super class.

Let’s use this new convention to doodle a data representation for an automated player that can execute one of several strategies:

The player class realizes the mechanics of performing actions that are independent of a strategy. Hence a developer is likely to program its implementation to a uniform strategy interface. Then again, the implementors of such players may wish to try out different strategies or even compositions of strategies (say chains). The above diagram sketches how an interface can uniformly describe two game-playing strategies.

Representing information with variants tends to lead to a data representation with abstraction:

This diagram indicates that the strategies are used uniformly via an interface and that they share some common helper functions. Indeed, this arrangement may even set up a template-hook-pattern, which lays out the general play function in AStrategy with some holes so that the concrete implementations just fill in those holes. The key to either arrangement is that it is now straightforward to add more strategies so that the player’s developer can experiment some more.

Arbitrarily Large Pieces of Information The final form of information to be represented is the most interesting one: when there is no limit on the size of an individual piece. While this idea may surprise you at first, it is actually the case that all interesting computations involve such forms of unlimited information. Keep in mind that “no limit” does not mean “infinite.”

Simple examples from the real world are sequences of pieces of information: a grocery receipt; tables of stock prices; listings of home for sales; and so on. Most such listings are of variable length, and there is no predetermined limit on the length of such sequences. You never know how many pop tarts a father has to buy for a growing family. Companies come and go; few companies from 100 years ago are still listed on the stock market yet many more companies are public now. Similarly, homes are added to sales listings and removed all the time.

Ignore for a moment that most programming languages come with a built-in form of list data, which a developer could use to represent such sequences. If a list representation had to be developed, a developer might start with a diagram like the following: If you are thinking of using null to represent empty lists, research “null billion dollar mistake.”

The interface enables uniform access to all forms of lists. At the same time, the data representation must distinguish at least two list variants: a base class that represents an empty list or a list of one item perhaps versus a class that represents a lengthening of an existing list, one that combines an item with an existing list. This last insight leads to a self-referential or recursive doodle diagram, one where arrows form a cycle.

While diagrams with cycles look complex at first glance, a second take tends to The axioms of Pressburger arithmetic generate a decidable theory. Pressburger expressions are common in code and therefore play a role in compiler optimizations. produce insights into what the data representation encodes. Consider the problem of representing arithmetic expressions, specifically expressions in what is dubbed Pressburger arithmetic. Roughly speaking, expressions in Pressburger arithmetic consists of variables, constants, and additions; practical extensions allow multiplication by constants. (Why?)

Take a look at the following three pieces of texts that someone may write down in an attempt to make examples of our definition:

4 + 5 * x + -1 * y

     

valid Pressburger arithmetic expression

4 + y * x + -1 * y

     

valid general (Peano) arithmetic expression

4 + 5 * x -1 + * y

     

ill-formed

Stop! Explain the comments next to each expression.

Like all data, arithmetic expressions are likely to be sent as sequences of characters—essentially, strings. Since only some strings are valid expressions and of those only a (strict) subset are Pressburger expressions, a software system must traverse these strings and turn Turning such strings into the data representation presented here is dubbed parsing in the study of natural and artificial languages. them into a proper data representation. Well-designed programs turn such expressions into trees, an idea that research on programming languages has confirmed time and again over five decades. Even though there is a natural limit on the size of such expressions, it is much better to choose a tree representation that does not impose any limits upfront.

Here is a cyclic doodle diagram, a first step in the development of a data representation for Pressburger arithmetic:

Stop! How does the diagram mirror the above definition?

Exercise 19. Represent the valid Pressburger expression from above as a tree, using notation from a class-based language. Why is it impossible to represent the valid Peano expression? Sketch methods that (1) find the set of all Variables in such expressions; (2) replace variables with numbers; and (3) determine the value of such expressions, given a table that maps each Variable to a number.

22.1.6 Choosing from Several Alternatives🔗

When there is more than one choice, a developer has to think through the possible uses of the data to make a decision. Questions to consider are
  • how easy it is to create an instance and how much this costs;

  • which operations are frequently used and should be inexpensive; and

  • which operations are rarely used and are acceptably slow and costly.

Above all, though, a developer must keep in mind how central the data is to the overall software system. If this data is created and used once, it may not deserve much attention; then again, if it is used in the inner loop of a central algorithm, paying attention to these questions from the very beginning makes sense.

Performance, Profiling

In many cases, answers to the above questions are discovered once developers implement the functionality and measure it. The chosen representation may miss an access capability. For example, the implementation may clarify that the rest of the program needs random access to pieces of a sequence but the chosen form of data allows only sequential access. No matter what, it is then time for a developer to backtrack and revise the choice of data representation. Once again, well stated representation and interpretation statements help the older version of the developer understand the younger version’s choices and make changes to it. Without these comments, modification may all too easily mess up unstated assumptions.

Recall the two data representations for a test-field editor’s state: one just uses two strings and the other two lists of characters. A developer is likely to observe that the implementation of cursor-based editing operations demands repeated access to individual characters. Indeed, these methods always access the last character of the prefix and the first of the suffix. Hence it is also the case that the prefix is best represented as the reverse of the sequence of visible characters.

A forward-looking, socially responsible developer knows that this logical invariant deserves an interpretation comment: The state of a text editor demands a much more sophisticated data representation that two lists.

  // the prefix is the reverse of the visible chars

  List<Char> pre = ['o','w']

  List<Char> suf = ['r','l','d']

  TextField tf   = new TextField(pre,suf)

  // if the cursor is a vertical bar, `tf` represents "wo|rld"

Furthermore, the interpretation is illustrated with an example and a comment that specializes the abstract interpretation statement for this example.

Now compare this choice with the two alternative representations of geometric lines (see above). The “flattened” alternative represents lines with a single object that has four fields; the “nested” one follows geometry books and thus prescribes objects with two fields, each of which is another object. While the brief comparison of these two alternatives suggests the nested representation offers a good organization, coding to this data representation may reveal that accessing the x and y coordinates are the most frequently run operations. In this (extremely rare) case, a developer may wish to go with a flattened data representation of lines to avoid the indirect lookup. If so, this choice deserves a representation statement that includes a rationale for this choice.

22.2 Developing Data Representations for Composition🔗

An actual software system composes many functions and methods, because it is impossible for one function to do it all. Composition means that a function f must produce output data that plays the role of an input for g, a second piece of functionality. This observation suggests an alternative to the first diagram, a variant that is illustrative of the second half in the data-representation development process:

The ellipsis in the center of the diagram, labeled “to be designed,” points to a difficult aspect of the data development work: the choice of data representation for intermediate results.

While the structure of external information may dictate the representation for the input to some function that interacts with the software system’s context—such as fthe development of its output is dictated by concerns related to internal pieces of functionality—such as g in the above diagram. Hence developing a data representation for the result of f is about reconciling two sets of constraints: keeping f simple and making its output a good input for g. This need for reconciliation is what makes this part of developing data representations difficult—meaning it demands the full and special attention of developers.

If possible, developers should still pay attention to the structure of input information. In many cases, a data representation of information that must explicate implied information is a good first example of an intermediate result. Recall the trees of ancestors and graphs of friendships that the preceding section mentions. Such pieces of data are often the result of a function that connects the software system to its environment. The challenge is to think through possible future uses of these forms of data so that internal functions benefit from the work of the first function.

Let’s make this idea concrete. Imagine a subway information system. Every morning, the system is booted with information about the currently operating stations and connections. Customers walk up to public information kiosks—or open an app on a mobile device—and query the system how to get from one station to another. One possible response could be “board the red line, direction Alewife; switch at Park to the green D line, direction Reservoir; the Fenway stop is the sixth stop on this line.” Finding a route from one place to another is a graph-search problem but the input to the software system is unlikely to be a graph.

Assume that the input is something like descriptions of the operating subway stations and lines. For example, one such input might be a JSON array for one complete line:

  [..., Fenway, Kenmore, Hynes, Copley,

          Arlington, Berkeley, Park, ...]

Functionality f would turn this linear data into a graph representation, and g might be the function that queries the graph to respond to individual customer questions. Although it is clear at this point that a graph representation is needed, it is still open which one serves g best. If the developer expects that may customers need the same information, the data representation may consist of a graph and a cache. Furthermore, a subway line is not an arbitrary graph of information but comes with additional implied structure; a developer might anticipate that representing this structure could also speed up queries.

In this example, some form of graph representation is implied by the external information—and yet, even this much insight isn’t enough to determine the full data representation. In other cases, a developer may have to explore what the best intermediate data representation is. Exploring means decide, design functionality, observe, measure, and backtrack to try something else. No matter what, however, a developer cannot design a piece of functionality without knowing the shape of the input data (and sometimes the output data). Indeed, in most cases these shapes dictate the organization of the function and greatly facilitates backtracking. A proper documentation (representation and interpretation statements) of the data representation clearly facilitates this process.

22.3 Methods vs Functions🔗

Once a developer has developed data representations, it is time to systematically design functionality, that is, functions and/or methods. Thus far, we have acted as if the two concepts are the same. Although they are closely related, they differ and the difference deserves an explanation.

Almost all programming languages support the definition of functions and methods. In a class-based, object-oriented language such as Java, a function is a static method. In a functional language such as Racket, a method is either a function stored in a struct or a function in a class, which is really just a notation for structs with special kinds of functions.

It usually takes several methods to implement a complete function. This idea is best illustrated with an example. Consider the case of processing lists. Let’s start with the task of adding 1 to every item on a list of numbers.

This example is contrived to keep this section short. Experienced developers would use libraries.

As pointed out in the passage on doodling data representations, a list represents sequences of arbitrary size. In a diagram, this relationship needs a description that points back to itself and comes with (at least) two variants: empty and a combination of an existing list with another element. In Java, this arrangement is best represented with an interface and two implementing classes. See figure 42 for the “doodle” diagram and the methods.

To implement the functionality, a systematically proceeding designer would add a method to every “box” in this class diagram. The method in the interface Java currently undermines this form of systematic design to some extent, due to its failure to manage the stack properly. specifies the header of the functionality; that is, it represents the specification of the function. The other two methods are implementation pieces of the functionality. Meaning, the two methods implement the part of the functionality that is relevant to the data represented by the two classes, respectively: the empty list or the extended list.

  interface IListN {

    // add 1 to each int on `this`

    IListN add1()

  }

  

  class EmptyN

     implements IListN {

   IListN add1() {

    return this; }

  }

  

  class CombineN

     implements IListN {

   int first;

   IListN rest;

   IListN add1() {

    return new CombineN(first + 1, rest.add1())

   }

  }

  

Figure 42: Integer Lists in Java, implemented by hand

In a functional language, a developer might use an algebraic datatype or, in a language such as Racket, two structs to represent the two variants:

  (struct empty-list [])

  (struct combine   [first others])

  ;; A [List X] is: (1) [empty-list]; (2) [combine X [List X]]

To implement the “add 1 to all numbers” functionality, a developer creates a function that deals with both cases in one conditional, one clause per form of data:

  

  #; {[List Number] -> [List Number]}

  ;; add 1 to every element of `alist`

  (define (add1 alist)

   (match alist

    [(empty-list)          (empty-list)]

    [(combine first rest) (combine (+ first 1) (add1 rest))]))

This function definition uses algebraic pattern matching to distinguish the two cases and to deconstruct the pieces. Note how each clause in the conditional corresponds to one of the concrete methods in the Java design.

Generally speaking, the design of a function is orthogonal to the design of the same piece of functionality with methods. A functional design uses a conditional to separate the various forms of input data from each other and to deal with each separately. By comparison, an object-oriented design places a method with each relevant form of data; the dynamic dispatch behind method calls realizes the conditional of the function design.

Note An object-oriented developer should use conditionals only to distinguish distinct result situations; in a functional language, such conditionals are nested within the dominant conditional in a function.

Unsurprisingly, each approach has different advantages and disadvantages. Roughly speaking, object-oriented program design facilitates the addition of new variants of data. A developer just adds yet another class that implements the specification interface including its methods. By contrast, functional design makes it easier to add another piece of functionality; it suffices to design a function. Most modern programming languages lead developers to favor one design over another but still accommodate the other one. Developers must keep these different style and their implications in mind as they move from the data development stage to the functionality design stage.

Exercise 20. Use your favorite IDE to run the program from figure 42 in your preferred object-oriented programming language.

Design and add the product functionality, following the same object-oriented recipe used to produce the program in figure 42. The purpose of the functionality is to compute the product of all numbers.

Now add CombineN2, another implementation of IListN. It is variant class that adds two ints to an existing list.

If you know the basics of a functional programming language, use it to solve the same problems.

22.4 Designing Functions, Systematically🔗

The design of a piece of functionality starts with a problem statement. It should be save to assume that whoever will maintain the resulting code will also access to, and be able to read, the problem statement. The developer studies this problem statement and identifies distinct computational tasks, which yields a worklist. Each such task should eventually correspond to a function in the code base:

Simple tasks correspond to atomic functions. Complex tasks correspond to composite functions.

Designing any kind function demands discipline. Assuming a data representation has been chosen, a developer should—at a minimum—stick to the following steps. How to Design Programs

The Purpose Statement A developer must first articulate a clear and concise purpose of the functionality: what it computes. If it is possible to convey the purpose with a well-chosen function name, great; otherwise, a comment should supplement the definition and state the purpose explicitly.

It is obvious that a developer cannot properly design without articulating a purpose statement but thinking about it also helps up front with something else: distinguishing between atomic and composite functions.
  • The design should aim for an atomic function, if it is straightforward to articulate what the function computes from its inputs.

    In case a systematic development still yields an overly large function, it might still be necessary to turn it into a composite function.

  • Otherwise, the developer should consider designing a composite function. This is especially true if there is a temptation to enumerate several tasks or at least if formulating the purpose is a struggle.

The Type Signature Next a function needs a type signature. As the start of this section explains, it is the first code-like idea that a developer understands before the function exists; revisit the diagram on see first page of this section. Without understanding what kind of data flows into and out of the function, it is simply impossible to describe the desired computation.

Typed languages enforce this. If the chosen language does not come with type notation, the developer must still write down a type-like signature as a comment and program to this type-like signature, not the function’s internals. The key is that some other comments should define—again with a type-like notation—all the terms used in such an informal signature.

Working Through Behavioral Examples Using the purpose statement and the signature, the developer can work through behavioral examples. The data definition for the inputs should suggest representative inputs as well as corner cases. Working through the examples yields an understanding of how the function computes its results. Of course, working through examples also produces unit tests—see below.

If the goal is to design an atomic function, the developer should be able to imagine how every step of the work can be implemented with built-in language constructs, basic functions, or library functions. The function itself is also fair game, because recursive functions are atomic. By contrast, working through the examples for a composite function may appeal to existing functionality in the project, other functions on the worklist, or functions yet to be specified. Examples for composite methods yield basic integration tests.

The Outline & Programming For many cases of input and/or output types, the type itself suggests how to organize the function. The analogy to writing is laying out all the ideas and organizing them into an outline.

The outline for an atomic function differs a lot from the one for a composite function. For an atomic function, the outline often involves distinguishing distinct forms of inputs or choosing an iteration form to match the processed data. For a composite function, outlining means dividing the given data among the helper functions identified in the previous steps and figuring out how to compose these functions.

At this point, defining the function is essentially filling in gaps between the ideas. Each piece of the outline delivers some intermediate result. The final task is to combine these intermediate results into the result that the function is supposed to compute.

Unit Tests The creation of a function is not finished until it has been equipped with unit tests, and they all pass. This may happen during step 2 or at the end, by translating worked examples into tests. If the language permits placing some unit tests with a function definition, a developer should do so to illustrate uses.

22.5 Atomic vs Composite: A Tiny Case Study🔗

Let’s take another look at the already-mentioned game program in which a referee component interacts with player components. When it is a player component’s turn, the referee shares the current, publicly visible state of the game. In response, the player requests the execution of some action on this state on its behalf. Naturally, the referee must check the legality of this action according to the rules.

In a game such as Ticket to Ride, the game is played on a game map, which consists of connections between places. A player’s goal is to collect connections. To do so, the player trades assets in its possession for connections during a turn.

Here is the precise rule:

The referee hands players colored cards and rails, which they keep hidden from each other. With these assets, players may acquire and occupy available connections on a game map. If a connection of color c consists of n segments, a player must have n cards of color c and n rails to acquire and occupy the connection, respectively, during a single turn. Once a player has acquired and occupied a connection, other players cannot collect it.

From the perspective of software development, this rule description is the problem statement.

  

  #; {GameMap State Connection -> Boolean}

  ;; can the active player collect the given connection

  (define (legal-action? gmap gs conn)

    (cond

      [(not (set-member? (available-connections gmap gs) conn))

       #false]

      [else

       (define active (state-active-player gs))

       (define seg#   (connection-seg# conn))

       (and

         (<= seg# (pstate-cards active (connection-color conn)))

         (<= seg# (pstate-rails active)))]))

Figure 43: An atomic version of legal-action?

To start with, the rule description implies that a developer needs to choose data representations for game maps, connections, game states, and player states:

  

  #; {type GameMap = .. Connection ..}

  ;; the map on which the game is played

  

  

  #; {type Connection = ..}

  ;; ... and the connections it consists of

  

  

  #; {type State = [List PlayersState]}

  ;; the referee's knowledge about the state of the game

  ;; which is just the state of all players

  ;; and the order in which they take turns

  

  (struct pstate [.. cards .. rails ..])

  

  #; {type PlayersState = .. Cards .. Rails .. }

  ;; a referee's knowledge about a single player

As for the desired functionality, a developer can now say that the referee component must come with a function that determines whether a player may acquire and occupy a connection in the current state of the game, given a fixed game map. Since the problem description is relatively compact, a developer might wish to write an equally compact function definition. Figure 43 shows the result. The function computes the set of available connections; makes sure the requested connection is a member; and then evaluates the conditions about acquiring and occupying the connection.

Exercise 21. According to the preceding section, a reader must be able to connect each task in the problem statement with elements of the implemented functionality. In this spirit, circle in red the pieces of legal-action? in figure 43 that correspond to the act of acquiring the connection. Then circle in green the pieces relevant to occupying a connection.

An alternative is to develop a composite function. According to the problem statement, checking the legality of a player’s action consists of two tasks related to the player itself:
  • one that ensures the player can acquire the connection with n cards of color c; and

  • another one that checks that the player can occupy the connection with n rails.

In addition, the referee must see to the availability of the connection, because a player can acquire and occupy a connection only if it is available.

Looking at the problem statement this way, it specifies tasks as follows:
  • see to the availability of the desired connection;

  • check that a player can acquire and occupy the connection, which in turn implies the execution of the above two tasks.

That is, the desired functionality needs two functions that compose two auxiliary functions.

  

  #; {GameMap State Connection -> Boolean}

  ;; can the active player collect the given connection

  (define (legal-action? gmap gs conn)

    (and

      (set-member? (available-connections gmap gs) conn)

      (can-acquire-and-occupy? (state-active-player gs) conn)))

  

  

  #; {PlayersState Connection -> Boolean}

  (define (can-acquire-and-occupy? active conn)

    (and (can-acquire? active conn)

         (can-occupy? active conn)))

  

  

  #; {PlayersState Connection -> Boolean}

  (define (can-acquire? active conn)

    (define color     (connection-color conn))

    (define needed    (connection-seg# conn))

    (define available (pstate-cards active color))

    (<= needed available))

  

  

  #; {PlayersState Connection -> Boolean}

  (define (can-occupy? active conn)

    (define needed    (connection-seg# conn))

    (define available (pstate-rails active))

    (<= needed available))

Figure 44: A composite view of legal-action?

The code of figure 44 presents this alternative. Each identified task is its own function. As a result, the functions completely align with the sentences in the rule description. Better still, each function can be understood and unit-tested in isolation, because its code is formulated in precisely the terms of the rule description.

Exercise 22. Re-do exercise 21 for figure 44. That is, circle the pieces of functionality corresponding to acquisition and occupation, respectively, in two different colors. Are they as easy to find in figure 43 as in figure 44?

Let’s step back to look at the big picture. Working through this example drives home a key insight. Although it is possible to formulate a short, seemingly simple function in response to the problem statement, a bit of reflection reveals that this organization is not in one-to-one correspondence with the listed tasks. Even if splitting up the function means adding a few extra lines of code, it looks like a socially responsible action.

Exercise 23. Let’s change the rule. A player may occupy a connection of n segments only if it has 2 * n rails in its possession.—Even this tiny exercise indicates how much easier it is to find the relevant place if the creator of the code has organized the code so that it corresponds one-to-one to the tasks mentioned in the problem statement, especially if the functions come with suggestive names. Imagine how such small differences add up for a code base with tens of thousands of lines.

22.6 Designing Methods, Systematically🔗

The design process for a method differs from the one for functions just a bit. Since both realize a piece of functionality on data representations, a large How to Design Classes overlap should not surprise. Conversely, the wording “on data representations” should suggest where the differences come from.

As Methods vs Functions indicates, data representations in class-based, object-oriented languages consist of interconnected classes and interfaces. For each piece of functionality that involves this class, there is a method tailored to this class. What all this means is that interconnections among classes mean methods delegate computations exactly along these relationships. More specifically,
  • if some kind of information is represented with a single class, this statement is obvious.

  • if information requires a class C that contains objects of another class D, then a method for functionality f in C delegates to a method in D for those parts of f that involve D.

  • if some kind of information is a collection of disjoint sets of information, the interconnected classes represent the information as a union type. The interface is the union type itself, and all implementing classes are the variants. A piece of functionality for such information is realized with an abstract method in the interface and concrete, specialized methods in the variants.

  • for information whose size is unbounded, the data representation is a union type with self-references, such as those in figure 42.

If people doodle class diagrams on a napkin or a white board to develop object-oriented data representations, they have a good foundation for discussing the allocation of functionality. Arrows are used to indicate containment and inheritance (implementation) relationships. These diagrams thus suggest the straightforward rule that

the design of methods follows the arrows in a class diagram.

Given this setup, the design of methods should follow the same five essential steps spelled out in Designing Functions, Systematically: (1) understanding and formulating the purpose; (2) adding a type signature; (3) working through examples; (4) making an outline and filling its gaps; and (5) running the unit tests that come from the third step.

The fourth step differs a bit due to the mechanics of object-oriented languages. Recall from Methods vs Functions that these languages eliminate the need for conditionals that distinguish the variants of a union representation. The dynamic dispatch of method calls automatically directs a computation to the proper method. Or as stated in the referenced section, it takes several methods to realize a single function. Hence outlining a piece of functionality with methods means outlining several methods when a union type with an interface is involved.

22.6.1 Favor Functions, Favor Immutability🔗

The title of this section is a slogan, due to Josh Bloch, the designer of Java’s API. Many years of experience with this API led Bloch to critique his own design as too imperative (stateful) and to propose this slogan in his book on programming effectively in Java.

A slogan like this one does not surprise people who know the history of object-oriented languages. Alan Kay, the architect of Smalltalk—arguably the first object-oriented programming language—writes in his recollection of the design process that “assignment statements ... express very low-level goals, and more of them will be needed to get anything done. Generally, we don’t want the programmer to be messing around with state.”

In other words, the idea of reducing the use of assignment statements to fields and even local variables goes back to the very beginning of programming language design. To be clear, the slogan does not say “never use assignment statement.” It merely tells developers to think twice before using them.

While the originally reasoning behind this slogan may have been rather abstract, modern practice confirms it with rather concrete arguments. The following table summarizes these arguments concisely:

immutable objects and functional methods

positives

     

negatives

unit testing

     

algorithmic efficiency

direct semantics

     

programming patterns

The following paragraphs explain each point.

Unit testing has become the most important tool for developers to confirm that methods work properly. Over the past couple of decades, the mechanism has proven its value so clearly that some companies and some teams demand at least one unit test for each and every method in a code base. Setting up a unit test for a functional method of an immutable object is straightforward: apply the method to input values and compare the results to expected output values. By contrast, testing an imperative method properly takes three steps: (1) setting up a context; (2) apply the method, checking its outcome, and making sure it doesn’t accidentally mutate fields whose value should remain the same; and (3) tearing down the context. It is easy to see why developers should prefer functional methods.

While ordinary English usage of “semantics” has a negative connotation, (programming) language researchers use the word to talk about the precise meaning of phrases. In this sense, direct semantics means that developers can understand the method’s functionality in isolation and, essentially, as an application of middle-school pre-algebra. Practically put, a developer can comprehend some method M based on some basic knowledge about the fields, plus the methods that M calls.

Always programming with immutable objects may reduce the algorithmic efficient of a method. Concretely, a functional method may run logarithmicly slower than an equivalent imperative method (measured according to the size of the input). The good news is that logarithmic factors are usually discounted by algorithm researchers. But developers must nevertheless be aware of this potential problem.

Finally, using immutable objects and functional methods pervasively tends to force programmers to occasionally use some boilerplate patterns. Roughly speaking, code may exhibit patterns such as the repeated decomposition of structured data and re-composition with one new piece on place. Or, functional methods may have to return an object when equivalent imperative, stateful methods return a base value. When such patterns appear frequently, developers should reconsider the use of immutable objects.

Let us briefly return to the automated player components mentioned several times in this book. As mentioned, such a component typically employs strategies to make decisions when it is granted a turn. These strategies use the current public game state as the basis for these decisions. Every game player knows that “decision” really means weighing options, comparing (at least) two different game states after applying two distinct actions to the current one.

This description clearly tells us that a player component deals with several game states at once. Starting from the given one, it creates others:
  • if the game state is mutable, a turn action changes the state. Thus exploring several different alternative actions means (1) applying each action, (2) assessing the value of the state, (3) undoing the action. Once all actions have been explored, the best one is chosen.

    Undoing can happen in several different ways. The state representation may include a deep copy method. In this case, the strategy method makes a copy first and discards it later. Or, the state representation may include an undoLastAcion method, in which case the strategy component must use it before exploring the next action.

  • If game states are immutable, applying a turn action to this state produces a new object. A strategy can thus explore all actions by simply applying them, assessing the value of the resulting states, and making a decision. The garbage collector of the chosen language automatically discards the generated, low-value states.

Faced with such a design decision, developers should at first heed Bloch’s slogan. If the software system exhibits performance problems and if profiling confirms that the problems are due to the immutable nature of objects, then—and only then—they should rework the design with mutable objects.

Readings

Joshua Bloch. Effective Java. Pearson Education, Limited. 2001.—Bloch designed Java’s API. In the first edition of his book, he dedicates an entire chapter with this title to the idea of “functional” programming in an object-oriented language. At first glance, this advice may contradict many college-level and K-12 courses on object-oriented programming, but Bloch’s experience clearly informs him, and he tells us of the significant advantages of this approach.

Alan Kay. The Early History Of Smalltalk. Association for Computing Machinery. 1996.—Kay is the key architect of the first major object-oriented language, Smalltalk. In this article, he describes the design of Smalltalk, a project that started in 1972 and spanned the decade. Reading Kay’s design rationales and comparing them to contemporary teachings is also an exercise worth every student’s time.

Figure 45: Designing methods

22.7 A Second Look at the Tiny Case Study🔗

The guideline of following the arrows in a diagram has two consequences. First, the relationships of interfaces and classes essentially dictate how one method call others and, due to dynamic dispatch, this leads to small methods. Second, representing all forms of information with classes tends to make for easy decisions concerning atomic vs composite methods.

Let’s return to the example from Atomic vs Composite: A Tiny Case Study; seeing both designs should help you understand the fundamental differences easily. The design of the legality check of a player’s action starts again with the development of a data representation.

  // N is short for Natural (numbers)

  

  class State:

    List<PlayerState> allStates = ..

    Set<Connection> available = ..

    Boolean legalAction(Connection c):

      PlayerState active = this.allStates.getActivePlayer()

      c in available && active.legalAction(c)

  

    PlayerState getActivePlayer():

      ..

  

  class PlayerState:

    Rails rails = ..

    Cards cards = ..

    Boolean legalAction(Connection conn):

      Natural n = conn.segments()

      Color   c = conn.color()

      this.rails.has(n) && this.cards.has(n,c);

  

  class Rails:

    N count = ..

    Boolean has(N n):

      this.count >= n

  

  class Cards:

    HashMap[Color, N] count = ...

    Boolean has(N n, Color c)

     this.count[c] >= n

Figure 46: A composite view of legalAction as a method

Instead of diving right into the formal description of classes, we start with a class diagram sketch. Take a look at figure 45. The diagram’s entry point is the State class, which—for the purpose at hand—comes with a single field: state. The arrow from this field to the PlayerState with annotated with * tells us that a state consists of many instances of PlayerState, a class that represents the information about an individual player. Now, this second class has two fields: rails and cards. They hold on to single instances of Rails and Cards, respectively. Finally, these last two classes contain fields whose type comes with the imagined language: N for natural number, Map for an association of Color with N.

The idea is of course that the referee component keeps track of the game state. When a player requests to acquire and occupy a particular connection on the game map, the referee’s legality-checking method delegates this task to the state object. The two arrows from the State object to PlayerState and Connection, respectively, suggest two helper methods: one for checking whether the player has what it takes to acquire and occupy a connection, and another one for checking whether the connection is available. Pushing forward, this pattern is repeated with the two arrows from PlayerState to Rails and Cards. Again, the legality-checking method in PlayerState delegates to two auxiliary methods and combines their results. Figure 46 displays the resulting methods. Take a close look and reflect on how much it resembles the function design and where it differs.

22.8 Why Systematic Design is Socially Responsible🔗

Systematically designing helps both the creator of code and its future reader. It starts with focused theses statements for both the development of a data representation and pieces of functionality. If we accept that code is a message from one developer to a future developer—that also runs—then the clarity of the message demands focus, meaning a delineating thesis.

The last sentence intentionally substitutes “thesis” for “purpose statement.” You have likely written essays in response to some prompt. You know that paragraphs are the basic building blocks of essays, just like functions are the brick and mortar of code. Now imagine writing an essay or even a paragraph without first determining the focus of this unit of writing. The result will be convoluted prose. One sentence will present one idea, followed by a sentence that presented an unrelated idea. It will be prose that jumps from one thought to another, prose that makes readers lose their trains of thought. They will probably re-read pieces and re-read them again just to make some sense out of it. Eventually they may give up.

All of this is true for code. Thinking of, and even better writing down, a clear purpose statement delineates a data representation and pieces of functionality from each other. That is, coming up with this statement means answering the following questions:

Which part of the information should a data structure represent? What should it not represent? Which part of the overall task does a piece of functionality implement? Which part of the task is excluded? Delegated to some other functionality?

The thesis statement for data representations consists of two parts: a representation and an interpretation. The former conveys how a piece of information becomes a piece of data, the latter tells a developer what a piece of data means as information. Consider the following examples: int distance, int weight, and int temperature. All three entities are integers; for all three kinds of information, there many ways of measuring them and turning them into data; and given some integers in whatever programming language, interpreting them properly needs help.

Exercise 24. Imagine two different ways to measure a distance and turning it into an integer. Make up two different interpretations of an integer as a weight. Research how many different ways different people use to converse about temperature.

Let’s next look at each step for designing functionality and how each helps readers understand code.

The purpose statement for pieces of functionality concisely describe what is computed; occasionally it describes how the result is computed, namely when the underlying algorithm is complex; and it should explain to which task in the problem statement it is linked. Many times a well-chosen method or function name accomplishes all three goals. But, it is also true that too often developers conflate tasks, have overlapping computations, and complicate ways to express a computation—without explanation. A reader must then wonder whether the complexity is intrinsic—or bad coding. If a creator starts the development with a clear focus—a goal of addressing just one (major) task—a lot of these problems are avoided.

In many cases, a type signature tells readers almost everything they need to know about calling a method or function if it comes with a good name (and possibly a purpose statement). The combination saves time because a reader can skip the actual definition during a first pass over some component. This point holds especially in settings where programmers favor immutability; a basic method must compute its results from the values of its inputs and the object’s fields. In addition, a type signature is critical for someone who wishes to use the function for some other code; type signatures enable IDEs to make up essential hints, say, in what order arguments are passed. While adding a type signature is mandatory in statically typed programming languages, it is also important to write them down in untyped (or optionally typed) languages. They play the same role in these contexts, and some IDEs look for properly shaped comments that look like type signatures.

Yes, programming to a type signature in a language without type checking takes mental discipline. The word “untyped” implies that the language comes without a pre-defined notation for formulating types and, hence, it cannot check the consistency of type annotations with code. Then again, the lack of a pre-defined Maintaining mental discipline is hard, which is the primary reason why many developers prefer typed languages. Types aren’t necessarily checked properly, though. Worse, typed-checked programs may still go wrong in ways that are essentially type errors. type notation gives the developer the freedom to superimpose a rigorous notation that fits the problem. Writing down signatures in this notation; defining all their elements; and programming to all these is then the equivalent of mentally declaring and checking types—and readers of untyped code will be grateful if both are done properly. While it is not a perfect substitute for types, it will be helpful.

Working through examples either leaves behind a “paper trail” of thoughts behind a complicated design and, at a minimum, yields unit tests. Both are extremely useful. Leaving behind a paper trail is imperative when a method implements a complicated algorithm. It illustrates how the method works with a concrete example, which gets the reader started. When combined with a look at the data representation, such worked examples also illuminate how the function takes care of other cases or corner cases.

A unit test serves a future reader and maintainer both as an illustration of what the function computes and what its calling protocols are. For both aspects, it is ideal if a programming languages allows developers to place short unit tests next to a function in the code. Here is an example of how this works, specifically how it works for a Racket version of the onTarget method from Names Matter:

The example shows how quickly such a sample call illustrates the signature. In this case, it shows that Posn means “complex number” and there is no need to look up the data-representation comment.

Even in Java, where unit tests cannot be placed next to a signature in an interface, developers follow a particular system of creating parallel directories and placing unit tests there. Everybody in this ecosystem knows about this system and can find examples (if not as conveniently as in Racket). IDEs help with this task; their attached testing framework look for the tests in these places and runs them on demand. Again, it is all about systematic development, protocols that programmers follow so that their successors can benefit.

The outline of a definition seems to become invisible once a developer fills in the gap to complete the code.—Stop! Think back to reading essays. Does the outline truly disappear? Can you tell whether the author had an outline in mind or just started writing?—While an outline might be unimportant for some simple functions—atomic or composite—an experienced developer can actually see it as soon as it comes to data-structure traversals. Suppose that a function’s input is a list, that is, a linear data structure. Depending on the chosen language, a reader will expect to find a comprehension, a loop, or a structurally recursive function—three equivalent ways to traverse a list and process each item. Code that processes the list in a non-structural way causes cognitive dissonance in the reader; colloquially, the reader is likely to be stumped. In this case, the creator made one of two possible mistakes: the outline is done wrong or the definition is missing a concise comment that explains the workings of the algorithm with a how statement and worked examples.

Again, systematic development is about both creating and comprehending code. When developers follow such guidelines, they make themselves productive. A developer who knows about these guidelines can then easily comprehend the code weeks, months, or even years later,

23 Code Inspections, Systematically🔗

If developers design code systematically, they can also present it using the design system. Starting from the problem statement, they can show how their data representation mirrors the concepts of information found there. The problem statement also dictates the pieces of functionality needed, so it should justify the entry-point functions and methods. When it comes to composite pieces of functionality, the developer may need to explain the design of the data that flows from one piece to another. Finally, the design steps for methods provide the scaffolding for presenting those.

Conversely, when developers review the code of their peers, they can use the principles of systematic design to understand and critique the product. They can check whether the data representation matches the description of the information in the problem statement. They can request a purpose statement if a function looks incomprehensible and its name doesn’t help understanding it.

The ideas presented in the preceding section make up one particular system of design principles. This section demonstrates how these principles help with code inspections. To make this concrete, we first introduce a variant of the Ticket to Ride problem from A Sample Project: Analysis, Discovery, Planning: the Labyrinth game.

23.1 The Labyrinth Game: A Problem Statement🔗

A Sample Project: Analysis, Discovery, Planning presents the analysis of a games-for-hackers project. A company provides game servers to which hackers can connect automated player components. The company’s idea is to use variants of board games, initially Ticket to Ride. Another game under consideration is Labyrinth.

Here is an illustrative excerpt from the game description:

The Labyrinth game takes place in an ever-changing maze of twisty passages. Players navigate these passages to collect treasures from uniquely identified places.

The game comes with fifty tiles. Each tile displays one of eight connector shapes and a unique, unordered pair of gems.

The referee uses 7 x 7 tiles to set up the initial game board. The last tile—a spareis kept on the side. The connector shapes on the tiles set up paths. Every even-numbered row and column of the board can slide in either direction. To move a row or column, a player inserts the current spare tile and pushes the row or column from one direction until the tile at the opposite end comes out. This latter tile becomes the new spare. A player may not undo the sliding action of the preceding turn.

The game description implies the existence of at least two pieces of functionality that participating hackers must implement for a data representation of the board:

  • the slide action for rows and columns;

  • the determination of tiles reachable from a current position.

Let’s see how presenters and panelists may interact once the board and its first pieces of functionality have been implemented.

23.2 Presenting Systematically Designed Code🔗

If a milestone involves the development of a data representation, a presenter starts with an overview of the chosen data structures. For complex choices, it is best to include a justification and examples; for simple ones, a presenter may skip those.

  // board representation for a game of Labyrinth

  public class Board {

   private Tile[][] grid; // row major

  

   public Board(Tile[][] grid) {

    \underline{this.checkDimensions(grid);}

    \underline{this.checkGemUniqueness(grid);}

    this.grid = grid;

   }

  

   // raise an exn if the dimensions don't satisfy the spec

   private void checkDimensions(Tile[][] grid) {

    ...

   }

   ...

  }

  

  // tile representation: gems plus path segments

  public class Tile {

   private UnorderedPair<Gems> gems;

   private Char segments;

  }

Figure 47: A Java data representation for the game of Labyrinth

Good morning. The goal of the current milestone is to develop a data representation for the game board and some of the obviously needed methods.

Sounds good. Tell us all about it.

The first two steps of the development were pretty much straightforward. Since the game board consists of tiles, we created two classes (figure 47): Board and Tile.

That sounds right. What are Gems?

A Gem is essentially a String.

Okay.

We also have a data representation for directions.

Directions?

The directions for the sliding actions.

Really? Shouldn’t an enumeration of the four possibilities suffice?

That’s a choice we explored. It forced us to replicate code, so we came up with an alternative representation.

I guess we will explore it when we look at the functionality. Let’s focus on the board representation now.

There are three things to observe. First, the presenter enumerates all classes involved in the development of the board representation. It doesn’t consist of just one single class but several. Second, the panelist questions one aspect of the design choice, and the presenter explains that the design of the requested functionality calls for the extra form of data—without going into details. The presentation stays at the overview level. Finally, the (head) panelist controls the presentation and re-directs the presentation to what the panel expects to focus its energy on: the board representation.

The Board class is essentially just a wrapper around a 2-dimensional array of tiles.

Where does the array store rows and columns?

At first we struggled with this decision. In the end we decided it doesn’t matter.

Yes, from the perspective of the game it doesn’t matter, but we need to know.

That’s why we documented it on line 2 of the class definition.

Thanks for pointing this out. Why does the constructor start with two method calls?

The purpose of these calls is to ensure that every new Board object satisfies the game specifications.

Shouldn’t this invariant be established by the code that calls the constructor?

We don’t think so but if we decided to re-arrange things like this, this would be easy.

True. We may make these checking methods public. But why do you disagree?

Our course on designing classes taught us that a constructor should enforce such an invariant.

Yeah, you never know whether the calling code checks properly and, if the constructor does it, it’s guaranteed.

Yes!

Do the remaining methods preserve the game specification for boards?

The presenter and the panel again discuss a design choice. This time the presenter pushes back against the panelist’s implied suggestion, because at this moment, it is just that.

  ... in the Board class ...

  

  // raise an exn if any two tile display the same pair of gems

  private void checkGemUniqueness(Tile[][] grid) {

   Set<UnorderedPair<Gem, Gem>> seenGems = new HashSet<>();

   for (Tile[] tileRow : grid) {

    for (Tile cell : tileRow) {

      if (seenGems.contains(cell.getGems()))

         throw new IllegalArgumentException("duplicate gems");

      seenGems.add(cell.getGems());

    }

   }

  }

Figure 48: A Java constructor “contract” for the game of Labyrinth

Once the data representation is understood, the presenter can move on to pieces of functionality.

Perhaps we should look at the constructor’s contracts first.

As you wish. But I bet checking dimensions is easy.

I was thinking of the uniqueness check for gems (figure 48).

Go ahead.

The purpose of this method is to raise an exception if it finds a problem.

Okay.

It is an atomic method that traverses the given array with two nested for loops.

Doesn’t it have to track the gems it has seen?

Yes, it uses an accumulator, a set of gems.

Why does it track them in a set?

It is the fastest way to check whether one tile comes with the same gems as another.

Oh yes, that’s correct. Why does your code use “cell” as the name for each tile?

We are thinking of the elements of an array as mutable cells.

Fine, let’s move on to actual functionality.

The presenter proceeds step by step, starting with a purpose statement—to raise an exception. Next the presentation provides an overview of a basic atomic method. And the rest dives into some basic details concerning the loop. In a setting with experienced developers, the head panelist might re-direct the conversation after the overview has been delivered.

Figure 49: Doodling for the development of the board representation

It might be best if I start with the doodle diagram (figure 49) that my partner and I drew as we revised the first implementation of insertAndSlide.

Sure. What does the dashed line mean? I have never seen this before.

That’s the key to this diagram. It says the method needs a data representation for directions.

Okay. But why?

As we designed the functionality, we realized that the code for all four variants looked nearly the same.

Do you mean the code for sliding a row left or right and a column up or down?

Precisely. And the differences were isolated in how the algorithm traverses just one sequence of tiles.

Interesting.

With IDirection, there is just one major method for all four.

This sounds really cool. Let’s see

Before I dive in, I just want to point out the three critical methods that IDirection specifies: start, hasNext, and next.

These make it look like an iterator.

Almost. But an iterator isn’t quite right.

Got it. Let’s take a close look.

The presenter could have shown this diagram at the very beginning of the code inspection but injecting it into the inspection now is also a good choice. For the first part of the inspection, Board and Tile are the most relevant classes; the diagram for their relationship is trivial. By delaying the diagram to this point, it provides a good overview of the data needed to design the requested functionality.

  class Board {

   ...

   // effect: slide column `n` of this board if `d` is vertical;

   // otherwise slide row `n` in direction `d`---by inserting `in`

   // return the tile that's pushed out

   public Tile insertAndSlide(Tile in, int n, Direction d) {

    \underline{this.checkConsistency(n,d);}

    Tile newSpare;

    if (d.isVertical()) {

     Tile[] seq = this.extractColumn(n);

     newSpare = slide(seq,in,d);

     replaceColumn(n,seq);

    }

    else {

     Tile[] seq = this.extractRow(n);

     newSpare = slide(seq,in,d);

     replaceRow(n,seq);

    }

    return newSpare;

   }

  

   // raise an exn if `n` and `d` are incompatible

   private void checkConsistency(int n, Direction d) {

    ... this.grid ...

   }

   ...

  }

Figure 50: Java method for sliding a row or column

Here is the implementation of the slide action (figure 50).

Okay.

Our insertAndSlide method changes the board’s tiles array and returns the tile that is pushed out.

Why does it return this tile?

The Board class is a data representation of the game board. The spare tile is separate from the game board; it is not a part of the board.

True enough. Even the game description keeps the spare tile separate from the board. It is a part of the game state and when we design its data representation, we need to include the spare tile there.

That was our thinking.

Make sure to double check the component descriptions.

Just stating the purpose statement of the method reminds of the principle of top-down planning, bottom-up programming. The second is going to rely on the component design of the first. Or, it is going to add to it. In this spirit, this last remark of the reviewer is directed at the secretary to make sure the data representation for the spare tile isn’t forgotten.

The method is given the tile to insert, a row or column index, and a direction.

That sounds reasonable.

But not all arguments make sense, and not all combinations of arguments make sense. That’s why the method runs a contract check.

Is this like the contract check in the constructor?

Yes, it is.

Too bad Java doesn’t have a proper contract system.

Indeed.—The method composes a workhorse, slide, with two pairs of helper methods.

Okay.

Depending on whether the goal is to slide a row or a column, the method uses a helper method to extract the appropriate slice from the tiles array. When slide has done its work, the method replaces the original row or column in the tiles array with the new sequence of tiles.

This looks quite concise and elegant.

When a presentation covers a composite method, it is important to give the panel an idea how many methods are involved. Doing so prepares the panelists for how much code is going to be covered to explain a single piece of functionality. Unprepared panelists asks questions at bad moments.

  class Board {

   ...

   // slide `seq` of this board in direction `d` by inserting `in`

   // return the tile that's pushed `out`

   private Tile slide(ArrayList<Tile> seq, Tile in, Direction d) {

    Tile out = null;

    for(int i = d.start(); d.hasNext(i); i = d.next(i)) {

     out = seq.get(i);

     rowN.set(i,in);

     in = out;

    }

   return out;

  }

  

   private ArrayList<Tile> extractRow(int n) {

    ... this.grid ...

   }

  

   private void replaceRow(int n, Tile[] seq) {

    ... this.grid ...

   }

  

   private ArrayList<Tile> extractColumn(int n) {

    ... this.grid ...

   }

  

   private void replaceColumn(int n, Tile[] seq) {

    ... this.grid ...

   }

  }

Figure 51: Auxiliary Java methods for sliding a row or column

The slide method is atomic. Its purpose is to insert a tile into a one-dimensional array of tiles, shift the tiles in a given direction, and return the tile that is pushed out.

This sounds almost like the purpose of slideAndInsert.

It differs in that this method is given a one-dimensional array. It does not work on grid.

Indeed.

The method iterates over the array in the order specified via direction. At each step, it swaps out the tile in the current cell for the current value of in. When the loop is done out contains the tile that is pushed out through the sliding action.

Now the idea of a separate IDirection type makes complete sense. Each direction represents whether to start at one end or the other, how to proceed, and when to end. But for that, it must know the dimensions of the grid.

True! We assume that some global Java interface will supply system-wide constants such as the grid size.

We need to double-check or update the component design to make sure this is true.

We already added such an interface to the code base. We needed it for our unit tests.

Good but this should be reflected in the design README. One last question. Why does the method initialize newSpare to null?

The for loop is going to replace it with the first tile in the array.

What if the array is empty?

In that case, the method returns null unfortunately.

It could return in, which would avoid the billion-dollar mistake.

The code presentation proceeds systematically, covering each design step, and ends in the discovery of a minor flaw. At this point a panelist might ask to see the unit tests because the presenters mentioned them. Or the head panelist may direct the presenter to cover the auxiliary methods.

23.3 Inspecting Systematically Designed Code🔗

On occasion, a developer gets lost in details while presenting code, reading every line to the panel. In that case, the head panelist must step in and re-direct the code walk. This situation can come about for many different reasons:
  • a panelist’s question causes the presenter to veer from a basic plan;

  • the developer is stressed;

  • the design does not follow the basic principles; and

  • the presenter does not follow a top-down plan, possibly because the chosen programming language suggests a bottom-up organization.

All are equally common, but the first two differ in nature from the last two. While the first two often just reflect a lack of practice and routine—and inspections improve over time—the last two are logical problems and subject to guidance using the principles presented here.

  class Board:

      """ Represents the game Board, a matrix of Tiles

  

      Args:

          rows (int): its height

          cols (int): its width

      """

  

      def __init__(self, rows: int, cols: int, spare: Tile):

          self.rows = rows

          self.cols = cols

          \underline{self-spare: Tile = spare}

          self.board: List[List[Tile]] = self.__randomized_board()

          \underline{self.last_slide_move: Optional[Slide] = None}

Figure 52: A Python data representation for the game of Labyrinth

So let’s take a look at how these problems may show up in a code walk and how a (head) panelist can use the design principles to re-direct the presenter and make the inspection a success. To keep things familiar and yet inject some variety, the inspection covers the board design combined with the reachability functionality for the Labyrinth game; ssee the first page The Labyrinth Game: A Problem Statement.

Good morning. Here (figure 52) is the class we developed for milestone 2.

Why don’t you remind us of the goals?

The first goal of the milestone is to create a data representation for the board.

And the second one?

The second one is to develop some methods that might be useful to plan a trip through the maze.

Let’s focus on the data representation, since it is up on the screen already.

Our board is just a matrix. The constructor takes in the number of rows and columns and generates the board.

Slow down. There seems to be a conflict between the method header and the class’s purpose statement.

Oh yes. The constructor also takes in the spare tile.

So is the spare tile a part of the board class?

We figured it might as well be.

But the purpose statement mentions only a matrix. The spare doesn’t show up. It is actually questionable whether the spare tile is a part of the board.

Even if we assume that everyone is familiar with what the code walk is about, it is always a good idea to remind an audience of the purpose of a meeting, here, the review of a specific data representation and functionality. Next, notice how the panel immediately observes a gap between the class’s purpose statement—its thesis—and the implementation. The game description cleanly distinguishes the board from the spare tile, yet these developers combined the two without stating so up front—which spells trouble for a clean design. Also, the presenter’s response to the panelist’s question does not provide a design rationale. While a panel may wish to question this decision, we skip this part of the conversation.

When panelists ask “why” questions, the goal should be to to expose gaps between the plan and the code—and only then:

The fourth line of the constructor is the key. It makes a random board of the given size.

Why is the board always random?

We figured games are played on new board configurations.

Unit tests will need fixed inputs so that the output is predictable. Can you show us the unit tests for your reachability method?

Ah, we don’t have any. We had not considered unit tests.

Oh.

In this case, the developers overlooked the obvious use case for a data representation: unit testing.

The last exchange exposes yet another contradiction, once again contrasting the purpose statement with the actual constructor:

We will address this gap soon.

We’re not done yet with the constructor. Please explain the last line of the constructor.

Our reading of the rules suggests that the board must represent the last sliding action.

Even if, this line introduces a new form of data.

How so?

What is Slide?

Oh yes, it is our data representation of a sliding action.

So it is another form of data. But what might be worse is that the purpose statement doesn’t mention this aspect.

We will reword the purpose statement.

I think you might be better off reconsidering your data representation. The focused representation statement might be a good starting point. In the meantime, let’s move on to the requested functionality.

In sum, this conversation shows how a panelist can use the planning and the representation statement to inspect the actual code. The planning process separates the representation of the game state from the representation of a game board; the latter should really just mirror the physical reality of the board. While the representation statement seems to suggest that the code implements this mirroring, going through the five lines of the constructor reveals that this is not the case. Finally, by keeping the remaining design principles in mind—especially unit testing for the requested functionality—the panelists exposed another weakness of the code base.

  import collections

  

  class Board:

      ...

  

      def __horizontal(self, side, row, column, accessable_tiles): ...

  

      def __vertical(self, side, row, column, accessable_tiles): ...

  

      def __adjacent(self,row,column):

          tile = self.grid[row][column]

          accessible_tiles = []

          for s in tile.accessible_sides:

              if s == "left" or s == "right":

                  self.__horizontal(s, row, column, accessible_tiles)

              else:

                  self.__vertical(s, row, column, accessible_tiles)

          return accessible_tiles

  

      def reachable_tiles(self, row, column):

          """

          returns list of all accessible locations

          """

          queue = collections.deque()

          queue.append((row,column))

          accessible_tiles = set()

          path = []

          while queue:

              row, column = queue.popleft()

              if (row, column) in accessible_tiles

                     or self.grid[row][column] == 0:

                  continue

  

              accessible_tiles.add((row, column))

              path.append((row, column))

  

              for neighbor in self.__adjacent(row, column):

                  queue.append(neighbor)

          path.remove((row,column))

          return path

Figure 53: Python board functionality for the game of Labyrinth

Once the review panel understands the data representation, it can turn its attention to the reachability functionality:

Here is the first piece of the requested functionality (figure 53).

Okay.

The first two methods are __horizontal and __vertical.

Stop! These are private methods. They can’t possibly be the starting point.

No, they are not.

Which of the two pieces are you presenting?

I am trying to present the functionality that computes the tiles that an avatar can reach from its current spot

Good. Why don’t we start with this method then. We will look at the private methods if and when needed.

Our code follows the on-line suggestion of putting magic methods first and public methods last. In this case, we want to focus on reachable_tiles.

Go for it. The purpose statement is clear. Consider adding an informal type signature, too.

Noted. So the first four lines initialize the necessary data structures.

Stop! Please provide an overview. We already know that this method is a composite. So explain the general idea first.

The directions from the panelist directly follow the design principles concerning composite pieces of functionality. An understanding of a composite methods demands guidance, meaning, an explanation of its overall purpose, the number of methods involved, and perhaps even the depth of the dependency chain.

Okay. The reachable_tile method consists of a loop that needs three helper methods.

What is the basic purpose of these helpers?

The important one is __adjacent. Given a grid position, it returns the list of immediately reachable neighbors.

That makes sense. As a matter of fact, that would be truly descriptive name for this method. It then wouldn’t matter that it comes without purpose statement.

The panelist’s remark points to the first principle of function design: articulate a clear purpose statement or give the method an equally clear name.

Noted. This method uses conditional to add vertical or horizontal neighbors to the list it is constructing.

Now the names of these two private methods make some sense. Let’s go back to reachable_tiles.

As I said before, the method itself starts with four lines that set up basic data structures.

It might be best to describe the organization of the method body first.

The method uses a worklist algorithm. Each element of the list is a grid position. The loop removes one of these positions, determines its immediately reachable neighbors, and sticks those back into the worklist.

This description makes sense. It is a common approach to implement recursive task descriptions. How does it avoid following cycles in this labyrinth?

A conditional checks whether the newly removed position is already on the list of reachable tiles.

Good. Let’s take a look at the details now.

The key to note here is how the panelist requests an overview of how the method computes the result. Even a quick glance at the code suggests that the composition is non-trivial, so an explanation of the “how” is appropriate and it may even deserve a refinement of the purpose statement.

The first two lines initialize the worklist.

Why is it called queue?

Yeah, worklist might be a better name. The third line creates the set of positions that the method has already visited.

Is it used for cycle detection?

Indeed. And the last line sets up the list that the method eventually returns.

Why is this variable called path? In what sense is it a path?

Come to think of it, the content does not at all describe a path.

What is the content?

It’s the list of reachable coordinates.

This almost sounds like the last two variables represent the same information, one as a set and the other as a list.

That’s quite possible.

Why does the method return a list? Could it return a set instead?

We thought that the ordering might matter.

How?

At this point, it is clear that the panel has discovered two naming problems and at least one design issue. If the two pieces of data represent the same information, only one is needed. And if the clients of the Board class don’t need to rely on an ordering of the reachable positions, returning the set works just as well as returning the list.

Since we don’t have any clients yet, we should be able to change the signature of the method so it returns the set.

Let’s keep this in mind. Explain the loop.

The loop runs as long as there is a position in the deque. The first line of the loop body removes the leftmost position. And the conditional checks whether this position has been visited before.

Why is there a zero condition?

I forgot. We might have used 0 to mark empty grid positions.

Are there any?

Not really. We should remove this part of the condition. Anyways if the condition succeeds, the rest of the loop body is skipped.

Okay.

The next two lines update the set of visited positions and the path of accessible tiles.

The two lines add the exact same data to the containers. Since they contain the same data beforehand, they really are in sync all the time.

Yes, we will reconsider just using one of them.

What does this nested for loop do?

The call to __adjacent returns a list of grid positions that are immediate and reachable neighbors of the currently processed position. And the for loop adds them to the worklist.

Isn’t queue a container?

It is.

So reachable_tiles could hand it to __adjacent and get it to update the structure?

Yes. As a matter of fact __adjacent asks its helpers to do just that.

This might be worth considering then.

This last part of the inspection is a deep-dive into the details of the method, ending in a lightweight suggestion. Such line-by-line inspections are useful when the code looks more complex than the purpose statement—or a comment concerning the algorithm—suggest.

  class Board:

      ...

      def reachable_tiles(self, row, column) -> Set[Tuple[int,int]]:

          """

          returns the set of of locations reachable from (row,column)

          uses a worklist algorithm of positions-still-to-visit

          """

          reachables = set()

          worklist   = collections.deque()

          worklist.append((row,column))

          while worklist:

              row, column = worklist.popleft()

              if (row, column) in reachables

                  continue

              reachables.add((row, column))

              self.__add_adjacents(row, column, worklist)

          return reachables

Figure 54: Reacting to the code walk

23.4 A General Guide to Code Inspections🔗

As the preceding subsections show, presenting and inspecting code make up two sides of the same coin. The same principles apply to both: the presenter uses them to guide a reader through a code (re)design; the panelists uses them to question the design of the code (base). While the preceding subsections illustrate this idea with examples, this section presents a general guide to both sides of code inspections. The key recommendation is to create README files that serve both as outlines for a code inspection and as guides for future readers of the code base.

23.4.1 README Files🔗

Documents: Start the README, Before the Coding Begins suggests that software developers should place the design ideas into a README file at the top-level of a code repository. Once developers turn to the creation of code, they should update this README file with comments about the code. That way it can serve as a living explanation of the design rationale and a navigation document for future maintainers. As the code base grows, the README file may grow too large for a structured document. The simplest way to break it up into manageable chunks is to equip each folder with a README file and to link these files with each other like the chapters of a book.

A top-level README file may contain a brief overview of the specification, with links to other documents and readings; download and installation instructions; links to a user guide if needed; instructions for running the program(s), the integration tests, or the complete set of unit tests; a table of contents pointing to the READMEs in various folder; and diagrams that explain the most important static and dynamic relationships among the components of the code base.

The README files in subdirectories have a local focus: the code in the files and its purpose. Assuming each subdirectory corresponds to a distinct component of the project, its README starts with a purpose statement for the component and comes with a table of contents that lists all the files in the folder and their purposes. If the code uses a class-based language, the files may contain classes, and the explanations in the README file clarify how they relate to each other statically and dynamically.

Let’s look at some of these elements in some detail, using the example of a game-referee component that comes with a game observer:
  • Given the context of the top-level README, a purpose statement for the referee component may consist of a single line:

      this component implements the Ticket referee

    If the top-level file does not supply enough context, a developer may wish to add a short paragraph:

      The referee is organized as a mechanism that sets up an

      initial state; consults external players for requests to

      transition from one state to the next; applies a rule

      checker to each request; transitions to the next state

      until a final state is reached.

  • The table of contents lists four files, each containing one data representation or one piece of functionality:

      referee::  mediates between players and the referee state

      state:     a representation of the referee's game knowledge

      obs-ref:   an interface for a game observer

      observer:  a primitive observer;

                 allows developers to view games

    To ensure that developers write down focused purpose statements per class, it is best to generate this table from the files instead of maintaining it by hand. Doing so guarantees that (1) the purpose statements exist in the various files and (2) the README file’s table of content isn’t out of sync with the actual files. After all, a README file is a form of comment, and as observed before, comments are easily out of sync with the code.

  • When a component exists of several classes (modules, files, data representations, pieces of functionality), navigation is greatly facilitated with a doodle diagram of the static relationships, meaning whether one piece of the component refers to another:

      +---------+                      +---------+     +----------+

      | Referee | -------------------> | Obs-Ref |     | Observer |

      +---------+      +----------+    +---------+     +----------+

      | State s | ---->|  State   | ---|| (knows)

      | Rules r |      +----------+

      +---------+      |../Common |

                       +----------+

    This ASCII diagram brings across a number of basic facts of how the four pieces in the folder relate to each other. The referee contains a State object; its content consists of pieces from the Common ontology (for players and the referee) and other pieces of data. It relates to the observer interface but not the observer itself. The former knows about the referee’s state, presumably to render it.

  • The diagram of static relationships leaves open how the Referee gets to know about an actual Observer object. A diagram of the dynamic relationships settles this point. See figure 55 for the dynamic interactions among these two pieces.

    Again, this ASCII diagram quickly brings across that the context creates an observer before it instantiates the referee, handing it a reference to the observer. During the game, the referee object calls the observer once before each turn, handing over the current state; it informs the observer that the game is over with the final state.

      Context

        |

        | new

    o = | ---> Observer

        |          |

        |          |

        |          |

        |          |

        |          |

        |          |  new(O)

        |          | ----------------------> Referee

        |          |                            |

        |          |                            |

        |          |   update(State s)          |

        |          | <------------------------- |

        .          .                            .

        .          .                            .

  %% before each legal turn                     .

        .          .                            .

        .          .                            .

        .          .                            .

        |          |                            |

        |          |   update(State s)          |

        |          | <------------------------- |

        |          |                            |

  %% until the game ends:                       |

        |          |                            |

        |          |   game_over(State S)       | (last State)

        |          | <------------------------- |

        |          |                            |

        |         ---- the observer shuts down  |

        |                                       |

Figure 55: An example of a README diagram of dynamic relationships

While tools may exist to generate such diagrams from code, the results tend to be uninformative, containing too much or too little detail. Developers are better off drawing such diagrams on boards and placing scanned copies into the folders. With mark-down formats, it is easy to include such diagrams in README files.

23.4.2 Presenting🔗

If the README files are kept up-to-date every time, a developer can use them to get a code presentation started. As a matter of fact, with such READMEs the presentation almost runs itself. To start with, the developer should point to the problem statement (link) in the README and remind the panelists of the role of the component in this context. Even if a developer presents just a change to a component, it is good practice to begin the code walk with such a reminder.

Before going into any details, the developer next provides an overview. If the READMEs include diagrams for the static and dynamic relationships, the developer should use them, focusing on those parts that are relevant to the goal of the code walk.

As a reminder, the general goals of a code presentation are to discover design flaws, errors, and comprehensibility problems. Given these goals and the general constraints on time for inspections, the developer must carefully choose what to present. That is, the presenter should explicitly state the goal(s) of the code walk, meaning those pieces of code that will be the focus of the presentation—and how to get there from the global overview:

  • If the code walk involves an entire new component, the presenting developer is keenly aware of which pieces of code deserve attention and which ones don’t.

  • If it is about a large change, act as if it is the review of new component.

  • If the presentation concerns a small change, explain the path to the modified pieces of code.

  • If help is needed with a known bug, explain the failing test case and, if useful, run it to show the symptoms of the failure.

Comprehensibility issues will come up as a result of presenting the path

An explanation of a path has two components: the static program text and the dynamic call chain. The first is needed because a future reader must start with text; the dynamic call chain(s) is needed to understand whether the calls are legitimate and the function works for all calls. For a concrete example of where both are needed, see Pushing Back During a Code Inspection, especially the sample dialog on see the second page.

To repeat this point with a simple example, consider the goal of a developer to present the complex code of function h and its only call site in the one of these two composite methods:

  Integer f(String s):

    String t  = lowercase_of(s);

    Integer i = h(t);

    return i;

               

  Integer f(String s):

    lower_the_case_of(s);

    Integer i = h(s);

    return i;

Even if there is no manifest flow of values from call to another, a developer must understand and expose imperative data flow, too. Both establish a similar calling context for h, but in two different ways. The one on the left uses a primitive on string values that produces a version of the string with all lowercase letters. In contrast, the one on the right modifies the given string, lowering the case of all letters. A presenter must explain this much of the context before diving into the definition of h.

As a developer presents a path from the entry point(s) of a component—for example, the public methods of a class; the calls from an integration test script—each unit of code deserves a basic high-level explanation. Once again, if the table of content of a corresponding README file can be generated, the developer knows that each module or class-level unit comes with a purpose statement and can leverage it for the presentation.

For each function or method, a developer first states their purpose. If the names are self-explanatory, the developer should be able to formulate a coherent sentence using this name; otherwise, the developer can read out the purpose statement. Explaining the pieces of functionality then depends on their nature:
  • For a composite method, a developer begins with an enumeration of the tasks it composes, how it composes them, and the flow of intermediate data among the pieces. See f above on this page for a composite method that uses plain sequential composition; the data flow for the left variant is explicit and the one for the right is implicit.

    The key is to explain the entire function before diving into any of the tasks. Doing so should establish what kind of data flows into the function to be inspected and what happens to the data that results from the call. Once the panelists understand this context, it is time to dive into the function or method that needs close inspection—h in the above example.

  • For an atomic method, a developer first explains the what and how of the computation holistically. After that, it is appropriate to explain the functionality on a line-by-line basis. If methods and functions are kept to a reasonable size, the functionality may explain itself mostly but in any case, a developer will have an easy time explaining how the lines accomplish the desired goal.

In sum, a presentation proceeds in a present top-down manner. The “top” is the big picture; the “down” is the path from the entry point(s) of a component to those pieces of code that deserve special attention. The “bottom” are those pieces of functionality with which the developer(s) had a hard time and that just need more pairs of eyes on them.

As for the interactions with the panelists, a presenter is usually confronted with three kinds of questions. The first and most basic kind is about the comprehensibility of what is written down. Since panelists are potential future readers, a presenter’s task is to recognize when such an issue requires a change and when not; when in doubt, take a note and improve. Perhaps a simple rule of thumb is that once a second presentation attempt has failed, it is time to think of a different way of writing this code.

The second kind of question is about bugs. In this case, the conversation proceeds until the developer has understood the mismatch between the specification and the code or has justified why there is no mismatch. For this latter case, the panel may wish to analyze the problem as a potential comprehensibility problem.

Finally, the third kind of conversation is about raise design concerns. In all likelihood, a developer has already considered design alternatives. Hence, presenting the chosen design as the best understood trade-off may pacify the panel; if not, the focus must be on understanding why the panel considers the design flawed so that the developer can go back and come up with an alternative or an improvement.

23.4.3 Inspecting🔗

From the perspective of the code reader, inspecting code is the mirror image of presenting code. To start with, a panelist must insist on seeing the relevant README files and the expected pieces in those files:
  • If the README files don’t exist, have the presenter state so and have the secretary take a note.

  • If the README files miss a statement of purpose for the documented component, request it.

  • If the README files lacks an overview of the component’s pieces and their static/dynamic relationships, ask whether those aren’t needed.

Once the overview is completed, the panelists must keep the content of the READMEs in mind; it is now their task to check whether the code is consistent with the overview based on the README files. Let’s state this as a distinct guideline:

panelists check whether the code reflects the README file’s information.

Before a developer moves from the overview to the presentation of the code itself, it is important to ensure that all participants understand which pieces of the component deserve special attention. The head reader must make sure that the presenter stays on the path from the overview to these attention-worthy pieces of code. In other words, if the presenting developer spends time on obvious or irrelevant code, the head can re-direct.

Once the presentation arrives at an interesting piece of code—a function, a method, or a collection of those—the panel needs to understand for each piece whether it is to be atomic or composite:
  • For a composite method, the panelists must understand how data flows from each separate task to the next. It is particularly important to understand the logical state in which potentially problematic helper methods are called.

    If the developer does not present the entire method before jumping to the implementation of a sub-task, the head reader must prune this dynamic jump. It should suffice to use the purpose statement of each task to explain the flow of data.

    A panel may wish to see unit tests that check whether the integration of all these tasks satisfies expected properties.

  • For an atomic method, the panel must insist on a precise explanation of the purpose, which is either explicitly written down or apparent from a well-chosen name. If only a name exists and the name isn’t clear enough, the panel should say so. This shortcoming may impose a cost on future readers.

    A panel may request to see unit tests to clarify the purpose of a method. This may happen at any time during the discussion of an atomic method. It is most often useful before the presenter dives into a line-by-line explanation or after this explanation is completed.

    Once the purpose of the method is understood, the panel should read the lines of code. The head panelist may even read those aloud. At the end of the presentation of an atomic method, the panel should be convinced that it achieves the stated purpose.

While the preceding items explain the mechanics of how to follow a presentation of a software component, they leave open when to raise design questions, how to expose bugs (mismatches with the specification), or where to point out readability questions. Let’s tackle these questions, one at a time.

Developers design at several levels. They design components and interactions between components, after getting at least a preliminary understanding of a software system’s requirements. Project focuses on this level. This high-level design spells out coarse specifications for components. For example, it may come with a request for a referee component with a game observer. The details of this component—how to represent the referee’s game knowledge, how to interact with the observer—are left to the designer(s) of the component, which brings us to the second level of design, the one likely to be presented at the beginning of a code inspection. Finally, as the introduction to this part spells out, developers should also bring a “design attitude” to the creation of data representations and functionality.

Based on this summary of design levels, the answer to the first question is straightforward. Assuming that the specifications have been inspected (see Interface Inspections, Systematically), panelists encounter the refinement of a component specification into a design near the beginning of a well-organized presentation. If they see something unusual or spot a gap in the explanation of purpose statements for pieces (classes, modules) and static or dynamic relationships, they must question the presenter there and then while the relevant information is on the screen. In contrast, questions concerning the systematic design of data representations and pieces of functionality are much more naturally discussed while the actual code is on the screen.

When some piece of code is visible, it is also the correct time to pose correctness questions. Following the idea of systematic design, this kind of conversation can always start with a request to see unit tests, specifically a unit test that covers a particular case. If the developer presents such a test, the panelists have two tasks:
  • One of the panelists’ task is to check the expected output. When the expected output is challenged, the team could re-read the specification together. The goal is to decide whether the expected output lives up to the specification. If not, the issue gets noted; otherwise, the panel proceeds to the second task.

  • The other task is to make sure the inputs represent the case under consideration and drive the execution to the questionable code. When in doubt, the panelists may request a step-by-step justification. As they follow the presentation, the panelist should question what is visible, not some code off-screen.

Once the panelists and the developer agree that a bug exists, they move on; it is the task of the developer, not the panel, to fix mistakes.

Lastly, panelists represent future readers of code, meaning they should be able to comprehend code in a reasonable amount of time. If any piece of code is difficult to read, the panelists stop the code walk, state the code’s purpose, and read it aloud. Doing so, slows down the presentation and may eliminate the comprehension obstacle. If it doesn’t, the presenter can request a description of what the obstacle is so that any corrective action addresses it directly.

In all cases, the conversation should not continue until the presenter and the panelists have agreed on an issue, be that a design issue, a correctness problem, or a comprehensibility obstacle. It is a good practice for the head panelists to have the presenter re-state what the issue is as the last step before the presentation continues. Also, in no case should a panelist push a re-solution of any issue—unless the entire team agrees that the severity of the issue demands it. Inspection time is too restricted and too valuable to try to resolve problems on the spot.