Before continuing with more examples of compound data and data abstraction, let us consider some of the issues raised by the rational number example. We defined operations in terms of a constructor rational and selectors numer and denom. In general, the underlying idea of data abstraction is to identify a basic set of operations in terms of which all manipulations of values of some kind will be expressed, and then to use only those operations in manipulating the data. By restricting the use of operations in this way, it is much easier to change the representation of abstract data without changing the behavior of a program.
For rational numbers, different parts of the program manipulate rational numbers using different operations, as described in this table.
Parts of the program that... | Treat rationals as... | Using only... |
---|---|---|
Use rational numbers to perform computation | whole data values | add_rational, mul_rational, rationals_are_equal, print_rational |
Create rationals or implement rational operations | numerators and denominators | rational, numer, denom |
Implement selectors and constructor for rationals | two-element lists | list literals and element selection |
In each layer above, the functions in the final column enforce an abstraction barrier. These functions are called by a higher level and implemented using a lower level of abstraction.
An abstraction barrier violation occurs whenever a part of the program that can use a higher level function instead uses a function in a lower level. For example, a function that computes the square of a rational number is best implemented in terms of mul_rational, which does not assume anything about the implementation of a rational number.
>>> def square_rational(x):
return mul_rational(x, x)
Referring directly to numerators and denominators would violate one abstraction barrier.
>>> def square_rational_violating_once(x):
return rational(numer(x) * numer(x), denom(x) * denom(x))
Assuming that rationals are represented as two-element lists would violate two abstraction barriers.
>>> def square_rational_violating_twice(x):
return [x[0] * x[0], x[1] * x[1]]
Abstraction barriers make programs easier to maintain and to modify. The fewer functions that depend on a particular representation, the fewer changes are required when one wants to change that representation. All of these implementations of square_rational have the correct behavior, but only the first is robust to future changes. The square_rational function would not require updating even if we altered the representation of rational numbers. By contrast, square_rational_violating_once would need to be changed whenever the selector or constructor signatures changed, and square_rational_violating_twice would require updating whenever the implementation of rational numbers changed.