Class and Method Specifications

Table Of Contents

Introduction

This handout describes how to document the specifications of classes and methods. This document focuses on practical issues.

This document uses a MutableLine class as an example. We do not provide properties or method bodies in our example. This document covers specifying the behavior of classes and methods (what they should do), not their implementation (what they actually do and how they do it).

Abstraction Functions and Representation Invariants covers how to document a class's implementation.

/**
 This class represents the mathematical concept of a line segment.
 
 **Specification Properties**:
    - start-point : point - The starting point of the line
    - end-point   : point - The ending point of the line
 
 **Derived Specification Properties**:
    - length : real - length of the line
 
      length = sqrt((start-point.x - end-point.x)^2 + (start-point.y - end-point.y)^2)
 
 **Abstract Invariant**: A line's start-point must be different from its end-point.
 
 */
public class MutableLine {
  
  // ... Properties not shown.
  
  /**

   **Requires**: p != start-point

   **Modifies**: self

   **Effects**: Sets end-point to p

   */
  public func set(endPoint p: Point) {
    // ...
  }
  
  // ...
  
}

Because several concepts discussed here are interrelated, let’s starts with a short list of definitions before diving into the details.

Abstract Value: What an instance of a class is supposed to represent. For example, each instance of MutableLine represents some line segment.

Abstract State: The information that defines the abstract value. For example, each abstract line has a start point and an end point.

Specification Properties: Describes components of the abstract state of a class. For example, the abstract state of a MutableLine is made up by the specification properties start-point and end-point.

Derived Specification Properties: Information that can be derived from specification properties but is useful to give a name to. For example, MutableLine has the derived property length, which describes the length of the line segment.

Abstract Invariant: A condition that must stay true over the abstract state of all instances of a class. For example, MutableLine requires that no instance has the same start and end point. Abstract invariants are expressed in terms of the abstract state. Note that this is not the same as the Representation Invariant (RI) that describes properties of the concrete representation. An abstract invariant, if one is present, specifies constraints on abstract values only.

Method Specifications: Describe a method's behaviors in terms of abstract state. For example, MutableLine's set(endPoint:) method updates the end-point specification property.

The above concepts are included in a class’s external specification. They help document for clients how to use the class. They are written in a styleized markdown form that XCode uses when presenting Quick Help for names and types. When you command-click on MutableLine in the editor, you would thus see something like:

The rest of this document is organized as follows. First, it explains how to document what a class abstractly represents using abstract state, specification properties and derived properties. Then, it explains how to specify method behavior, in terms of abstract state.

Abstract Values and Abstract State

The abstract value of an object is the information that clients use when reasoning about an object. For example, a line segment is defined in terms of a start point and an end point. This does not necessarily imply that the concrete representation of the object has two point properties. That is one representation, but there are others, such as a start point, an angle, and a length. Abstract values are typically at a less detailed level than an implementation because this helps clients reason about only what matters to them. For example, clients of some representation of strings just need to know that a string is a sequence of characters, not whether that sequence is implemented with an array, a linked list, a combination of the two, or some completely different way. The notion of sequence is more abstract than particular ways to reprsent sequences.

Mathematical Abstract Values

For some ADTs, the abstract values are well-described by concepts and notation that are common in mathematics and well-understood by software developers. Examples include:

  • a set of integers
  • a sequence of characters (i.e., a string)
  • a pair of real numbers (or a triple, or in general a tuple)

If you are specifying such a class, then you're in luck. You can use conventional notation for specifyng the class's abstract values and methods. Such notation includes:

  • set comprehension: { x | P(x) } denotes the set of all elements x that satisfy the property P. More generally, { f(x) | P(x) } denotes the set of values of the expression f(x) for all x that satisfy the property P. For example, { x * x | x > 10 } represents the set of all numbers whose square root is greater than 10.
  • set union: x ∪ y denotes the union of two sets x and y. (This can also be written x + y when there's no danger of confusion with addition.)
  • set membership: a ∈ x or a in x tests whether a is an element of the set x.
  • sequence construction: [a, b, c] denotes a sequence of three elements.
  • sequence concatenation: x : y denotes the concatenation of two sequences x and y. (This can also be written x + y when there's no danger of confusion with addition or union.)
  • sequence indexing: x[i] denotes the ith element of a sequence x.
  • set or sequence size: |x| denotes the number of elements in a set or sequence x.
  • tuple construction: <a, b, c> is a tuple of three elements. This is also written (a, b, c). Unlike sequences, tuples are fixed-length, so we don't normally think about concatenating them.

You aren't obliged to use this syntax. Some of it is more standard than the rest: set-comprehension syntax is standard in just about all of mathematics, but sequence concatenation isn't particularly standardized. You may find it clearer to write sequence concatenation as a function like concat(x, y). What really matters is clarity and lack of ambiguity, so if you have any doubt whether your reader will understand you, just define it: “...where concat(x,y) is the concatenation of two sequences x and y.”

Specification Properties

Usually, abstract values are not only simple mathematical objects like numbers or sets. In these cases, it's more useful to think about the abstract value as if it were an object with properties. For example, a line has a start and an end; a mailing address has a number, street, city, and zipcode; and a URL has a protocol, host name, and resource name.

Mathematically, this is the same as a tuple; it's just a tuple whose parts have useful names for humans, rather than simply parts in some order without names. So even though we could use tuples, it's convenient, and more readable, to break the abstraction state into named parts, where each part is a specification property. (Specification properties are more commonly called abstract properties, because they're properties of the abstract value, as opposed to rep properties which are properties of the representation value. Unfortunately, abstract has another meaning in many programming languages, so we will avoid that potentially confusing terminology.)

Specification properties often (but not always) correspond to public properties or observers on the abstract data type. Because the structure of abstract values is a matter of interest to the clients of your class, the specification properties should be listed in the class overview. In CS326, we follow the following convention for enumerating them.

/**
 Represents an appointment for a meeting.

 **Specification Properties**:
      - date : Date         - The time
      - room : Int          - The room number of the meeting's location
      - with : Set\<Person>  - Whom the appointment is with

 */
class Meeting {

By convention, in specification properties, lowercase types like sequence or set refer to mathematical entities. Capitalized types refer to other ADTs (classes or interfaces). Where you have a choice, prefer a mathematical entity as the type of a spec property; it is better to use sequence than List, for example. It's more elegant, and reduces the coupling between your specification and particular Swift types.

The presence of a specification property does not imply anything about the interface or implementation of the class. Although spec properties often correspond to public properties or observer methods, that's not always true. (An observer method is one that computes a value without performing any side effects.) However, the interface might not provide any observers that query the spec property's state, so clients of the class might not have access to the information stored in the spec property. (An example is that a stack implementation might have a spec property for the elements of the stack, but a client might only be able to push and pop rather than being able to obtain the full state of the stack.) Likewise, the implementation might not actually have a concrete property of the spec property's type: that information may be computed from multiple concrete properties, or it might not be available at all. The point is that specification properties are useful for providing method specifications in terms of the abstraction being provided.

Derived Properties

Derived properties are information that can be derived from the specification properties that it is useful to give a name to. For example, consider this class:

/**
  Represents a square.

 **Specification Properties**:
      - length : int    - The length of the square's sides
 
 **Derived Specification Properties**:
      - area : int      - The area of the square
 
        area = length^2.
 
 **Abstract Invariant**: length > 0
 */
class Square { 
    ...
}

The derived property area can be derived by squaring the length specification property. A derived property’s documentation should state how it is derived from the specification properties.

A derived property’s purpose is to help with writing method specifications, abstraction functions, and representation invariants: It is easier to write and understand area than length^2. They are a shorthand that can make class and method specifications easier to understand. Because a derived property is defined entirely in terms of specification properties (or other derived properties), method specifications do not need to state a method's effects on a derived properties. For example:

/**
 Represents a square.

 **Specification Properties**:
      - length : int    - The length of the square's sides
 
 **Derived Specification Properties**:
      - area : int      - The area of the square
 
        area = length^2.
 
 **Abstract Invariant**: length > 0
 
 */
class Square {
  
  /** The length of the square's sides. */
  private var size : Int
  
  // Abstraction Function:
  //  AF(r) = a square, s, with s.length = r.size.
  //
  // Representation Invariant:
  //  size > 0
  
  /**
   
   Creates a new Square with length = len.
   
   **Requires**: len > 0
   
   **Effects**: a new Square s with s.length = len
   
   - Parameter len: the length of a side

   */
  public init(len : Int) {
    assert(len > 0, "Len must be positive")
    self.size = len;
  }
  
  /**
   Returns the difference in area between this and s.
   
   - Parameter s: the other square
   - Returns: self.area - s.area
   */
  public func differenceInArea(s : Square) -> Int {
    return (self.size*self.size) - (s.size*s.size);
  }
  
  /**
   Creates a new Square with length = len.
   
   **Requires**: len > 0
   
   **Effects**: sets self.length to len
   
   - Parameter len: the new length
   
   */
  public func setLength(len : Int) {
    assert(len > 0, "Len must be positive")
    self.size = len;
  }
}

Notice how the method specification of differenceInArea uses the derived property area to make it easier to explain what it returns.

It is never necessary for a method specification to indicate its effect on a derived specification property because the class documentation has defined the derived specification property in terms of specification properties. Since area is a derived property, the constructor does not need to say what the newly constructed Square's area is. Similarly, the method specification for setLength does not need to document its effect on area.

When we relate concrete implementations to abstract values using Abstraction Functions, we will similarly only need to describe the abstraction in terms of specification properties and then the derived properties will follow from the specification properties.

Note that one could have made area a specification property instead of a derived property. This would relieve the programmer from the responsibility of documenting how area can be derived from the specification properties. However, in this case, the constructor and setLength would be required to specify their effects on area.

Suppose you have a derived specification property f. It is permissible for there to be a concrete property in the implementation that stores the value of f, or for there to be a method that computes the value of f, or for there to be no such property or method. That is an implementation detail that is of no interest to clients of the specification.

Method Specifications

Method specifications describe the behavior of a method in terms of its preconditions and postconditions. Note that method specifications may only refer to specification properties, method arguments, and global variables (also known as public static properties), never to the concrete properties of the implementation.

Preconditions

Preconditions are properties that must be true when the method is called. It is the responsibility of the caller to guarantee that these properties hold. If the preconditions do not hold, the method is allowed to behave in absolutely any fashion, including crashing the program, continuing with incorrect results, informing the user of the problem, or gracefully recovering from the problem. Callers should always assume that preconditions are not checked by a method. However, it is good practice — though not required — for an implementation to check its preconditions (if the check can be performed efficiently) and throw an exception if they are not satisfied.

Preconditions are indicated by the “Requires” clause in a method specification. If a “Requires” clause is omitted from the method specification, it is assumed that the method does not have any preconditions.

Requires (default: no constraints): The preconditions that must be met by the method's caller

Postconditions

Postconditions are properties that a method guarantees will hold when the method exits. However, if the precondition did not hold when the method was called, then nothing else is relevant, and the method may behave in any fashion whatsoever. In particular, if the precondition does not hold upon method entry, then the postcondition need not hold on method exit.

A postcondition can be written as a single complex logical formula, but it is convenient to separate it into logically distinct parts. We’ll use "Returns", "Effects", and "Modifies". (In the descriptions below, "default" indicates what is assumed if that clause is omitted from the method specification.)

Returns (default: no constraint on what is returned): The value returned by the method, if any

Modifies (default: nothing, which means that there are no side effects): Variables whose value may be modified by the procedure: They are not guaranteed to be modified, unless otherwise indicated by the effects clause. If object x has specification properties f, g, and h, then "Modifies x" means that any combination of x.f, x.g, and x.h might be modified. "Modifies x.g, x.h" would be more restrictive. Often, programmers are more interested in quantities that are not listed in the modifies clause, since those are guaranteed not to be changed by the method.

Effects (default: true, which means "can have any effect" on the references listed under modifies): The side effects of the method, such as changes to the state of the current object, to parameters, or to objects held in global variables: If a specification property is listed in the modifies clause but not in the effects clause, then it may take on any value allowed by the abstract invariants of this class of objects. The difference between the modifies and effects causes is that modifies lists everything that may change and effects indicates what changes occur.

(Note: specifications often also include information about exceptions that may be thrown. We will not be using exceptions very often, but if you do write code that could generate an exception, include a “Throws” annotation to indicate the type of exception and when it occurs.)

Using Spec Properties for Specifications

Specification properties are useful for writing specifications of the ADT's operations. Here's a specification for a method on the MutableLine class:

  /**

   **Requires**: l.start-point equals self.end-point
   
   - Parameter l: The line segment to add
   
   - Returns: a line segment that is equal to self + l;
                that is, l appended to self
   */
  public func add(l : MutableLine) -> MutableLine {
    ...
  }

Note that we use different syntax for Parameter/Returns, which are directly supported inside XCode’s documentation renderer. Requires, Modifies, and Effects are non-standard tags (though they should be!).

Specifications may refer to specification properties (such as start-point and end-point), but never to representation properties. Rep properties depend on a particular implementation, so we don't want to expose them in a specification, which can be implemented in many different ways.

Using Derived Spec Properties for Specifications

When you're writing specifications for operations, you may find it useful to use derived spec properties. A derived spec property is just a spec property that can be written in terms of other spec properties. In other words, it's a shorthand. You can freely use derived spec properties in a method specification (and easing such specifications is the point of derived spec properties):

  /**
   - Parameter l: The other line segment
   
   - Returns: true is this.length > l.length
   */
  public func isLonger(l : MutableLine) -> Bool {
    ...
  }

Subclasses and overridden methods

A subclass often has a different (stronger) specification than its superclass, and often has a larger abstract state than its superclass. When the specification and abstract state are identical to those of the parent (for instance, an implementation of an abstract class), then there is no need to repeat them in the subclass. However, it is helpful to include a brief note indicating that the superclass documentation should be used instead. That note helps readers to distinguish whether the specification is the same, or the author simply didn't document the class.

When the specifications differ, then you have two options. The first option is to repeat, in the subclass, the full superclass documentation. The advantage is that everything is in one place, which may improve understanding. The second option is to augment the existing specification -- for example, to add a few new specification properties and constraints on them. Whichever you do, make sure that you clearly indicate your approach.

Similar rules hold for a method that overrides another method. It is acceptable to leave the documentation blank if the specification is identical, but be sure to indicate that this is the case.