The following is an exercise in software design intended for teaching purposes. We will consider a class representing a duration of time, expressed in hours, minutes, and seconds. Durations may be positive, negative, or zero, and we must be able to add and subtract durations, compare two durations, and increment or decrement a duration by one second. Durations will be displayed in the familiar [-]H:MM:SS format, where the number of hours is not restricted to two digits.
Although this is a relatively simple class, it provides an opportunity to explore an important principle of object-oriented programming: the distinction between what a class does and how it is implemented. In particular, we will see how two different internal representations of the same information can lead to substantially different implementations, even though both satisfy exactly the same requirements.
We begin with a requirements specification. This document describes the public interface and the expected behaviour of the class without specifying how the information must be stored internally. In other words, it describes what the class must do, not how it must do it:
We will then examine two possible designs, both satisfying the same requirements.
The first design, Time_hms, uses a representation that closely resembles how humans normally describe durations. It stores the hours, minutes, and seconds in three separate fields, together with a Boolean variable indicating whether the duration is negative. This makes constructing and displaying durations straightforward, but arithmetic requires us to deal with carrying, borrowing, and the signs of the operands:
Design 1: hours, minutes, and seconds
The second design, Time_s, stores the entire duration as a single signed 64-bit integer representing the total number of seconds. The sign, hours, minutes, and seconds must be calculated whenever they are requested or displayed, but arithmetic and comparisons become ordinary integer operations:
You can examine the C++ implementations of both classes, together with a program demonstrating their use, in the source directory. Notice that the two classes provide the same public operations, even though their implementations are quite different. A program using either class should observe the same behaviour for all operations covered by the specification.
At first glance, because both the input and output are expressed in hours, minutes, and seconds, it may seem natural to use the same representation internally. However, a representation that is convenient for input and output is not necessarily the most convenient for computation. Consider the advantages and disadvantages of the two designs:
Storing the total number of seconds: If we store the duration as a signed 64-bit integer, we can represent durations with magnitudes of approximately 2.56 quadrillion hours, far exceeding the range required by the specification. Adding or subtracting two durations becomes ordinary integer addition or subtraction. Comparing two durations is equivalent to comparing their integer values, and incrementing or decrementing a duration requires only incrementing or decrementing an integer.
However, more work is required in the constructor, where hours, minutes, and seconds must be converted into a total number of seconds. Similarly, the component accessors and formatted output must convert that total back into hours, minutes, and seconds. Arithmetic must also avoid signed integer overflow.
Storing the sign, hours, minutes, and seconds: If we store the components separately, construction and access to the individual components are straightforward. Formatted output is also relatively simple.
Not exceeding the 64 bits used by the previous implementation, the largest duration in magnitude we can store is only 4.29 billion hours.
However, addition and subtraction become more complicated. We must account for carrying and borrowing, handle the signs of the operands, and ensure that we never produce a negative zero such as -0:00:00. Similarly, comparing two durations requires us to consider their signs and then, as necessary, their hours, minutes, and seconds.
Even incrementing or decrementing a duration requires special handling at boundaries. For example, incrementing 5:59:59 must produce 6:00:00, while decrementing 6:00:00 must produce 5:59:59. Additional care is required when crossing zero.
Neither design is inherently incorrect. Both can satisfy the same requirements, but the choice of representation determines which operations are straightforward and which require additional work. In this example, the representation that simplifies arithmetic and comparisons also requires more work during construction, component access, and output.
This document compares these two designs, considering the complexity of construction, arithmetic, comparisons, accessors, output, and storage.
The purpose of this exercise is not to develop a production-quality time library. Rather, it is to illustrate how the choice of an internal representation affects the complexity of an implementation. In particular, a representation that is natural and convenient for humans is not necessarily the representation that is most convenient for computation.
By keeping the public interface independent of the internal representation, we can change the implementation without requiring changes to the programs that use the class. This separation between interface and implementation is one of the important benefits of object-oriented programming.
This exercise and its underlying ideas are those of Douglas Wilhelm Harder. The requirements specification, design documents, and C++ implementations were developed with the assistance of ChatGPT.