I was just reading a modern “what is clean code” book and once again found the same example being used to motivate the discussion of “data abstraction” as in many previous “clean code”/”O-O design”/”coding standard” books: The classic Point class.
Yes, the Point is currently represented in Cartesian coordinates but someday you might want to switch it to polar coordinates, and thus you should use data abstraction to expose getters and setters instead of fields.
The problem is that there is no circumstance whatsoever under which you would ever want to change a Point representation from Cartesian to Polar, or vice versa, where you would want that change to happen in isolation without changing the rest of the code.
I’ll explain why—but first I’ll just say that I understand that Point is used as an example because it is simple to understand and doesn’t take a lot of space on a page in a book. But there are other examples one could use that also satisfy those needs that, in addition, make sense as a motivation for using data abstraction. For example, a URL class, where you might initially have a representation that separated the components of a URL into protocol (enum)/site (string)/port (int)/path (string)/query (string) parameters and then switch to represent it as simple string, or vice versa. Easily understood as an example, a simple class to put in your book, and you might actually want to make that switch someday.
But back to the Point. The reasons you would never want to switch a Point representation from Cartesian to Polar, or vice versa, in your application without changing a line of your code is because the two representations have different semantics and performance. They are just not interchangable.
Let’s start with the most basic problem: Although the transform from Cartesian coordinates to Polar coordinates, or the reverse, is a simple mathematical equation, it is only true over the field R of real numbers. In the world of real computers, we compute using finite-precision floating point numbers. If you are working with Cartesian coordinates but holding them in instances of a class which is representing them in Polar form, then you won’t necessarily get back your original values of X and Y when you ask for them. That’s because the transform to Polar and back involves transcendental functions and the IEEE standard does not require transcendental functions to be exactly rounded – and that is without even considering the code in system libraries that may run before and after the machine instruction that causes the IEEE floating-point function evaluation: that system library code is completely unstandardized and usually uncharacterized as well.
So you can’t round-trip your coordinates, and if your Point class represents points in Polar coordinates than even a simple expression like 1 == Point(1, 5).getX() might well return false.
But the semantics problem is deeper than just arithmetic precision. Simply put: The applications which you would use Cartesian coordinates for are completely different than the applications which you would use Polar coordinates for and it is not just that the operations on points are different (with different frequencies of use) but that the usage of points is completely different with respect to fundamental ideas such as: where is the origin? and, is the origin fixed in space or is it relative to a particular (possibly moving) object?
Applications where you would use Polar coordinates include terrestrial navigation with sonar-like devices, and those in engineering/physics involving energy radiation patterns. Polar coordinates would be so natural in these domains that you wouldn’t even think of using Cartesian coordinates. Applications where you would use Cartesian coordinates would include any navigation on a grid, and of course, anything involving raster graphics. You would never consider storing Points in Polar form if you were manipulating raster graphic images. If you were working on a program that composited various GIFs and PNGs in various ways and someone suggested representing the points in Polar form you’d think he was nuts. And if he said “we need to use data abstraction on the components of a Point because we might someday want to represent these points in Polar form” you would not be convinced by his argument.
So we don’t even need to get into issues such as the difference in performance of different operations, e.g., computing the distance between two points.
Note that the data abstraction argument doesn’t work even if you are definite that you are using Cartesian coordinates but would like to leave open the possibility of switching from an integer representation to a floating-point representation, or vice versa. First, you have the precision problem again. But worse: even the designer’s conception of basic operations may change!
Consider the operation of defining a rectangular area, and then asking whether a point is within that area or not. For example, you want a rectangle which has its lower-left corner at (0, 0) and is 3 units wide and 5 units high. And then you would ask “is the point (3,5) inside the rectangle”?
If you’re working with the kinds of applications where you naturally represent points using floating-point (e.g., architectural or mechanical CAD) then the answer is yes. But, if you’re working with the kind of applications where you naturally represent points using integers (e.g., raster graphics), the answer, typically, is no. It isn’t that the definition of the operation changed when you moved from floating-point to integers, it’s that the designer’s conception of how to use Points and what kinds of operations you do on them is different. And so there is a semantic difference in the operations in the two domains, and that means you aren’t going to switch representations without considering which parts of your application need to be rewritten.
So please: changing a Point’s representation from Cartesian coordinates to Polar coordinates, or vice versa, is not a good motivating example for data abstraction—use a different one.
P.S. Two further points on Point, not as important as the argument above:
First, you may still want to introduce getters and setters even if you never intend to change the underlying representation. For example, if you are using Polar coordinates, you may want setters so you can enforce a canonical representation of points (e.g., ρ≥0, 0≤θ≤2π, ρ=0 → θ=0). That’s data abstraction, but it is not what the authors of these books are trying to emphasize.
Second, in most programming languages, and for many applications, the Point class you define won’t even hold the points you have the most of/are using most often. In any graphics application you may use a Point class to hold a few standalone points, but you would probably not create a 1-dimensional array of them and call it a “polyline” and you would never create a 2-dimensional array of them and call it an “image plane”. You couldn’t afford to do that for storage/efficiency reasons. Except in C++ (if your Point class doesn’t have virtual functions) and in C# (where you’d declare it as struct, not a class), your points, as instances of a class, are going to be stored on the heap and be larger than just the two fields you need – they’re going to also hold a class header and be rounded up in size to the minimum heap allocation unit. In many cases this will double the size of the Point! Also, every Point will need to be accessed via a pointer and the runtime (or VM) will not put logically “adjacent” points physically next to each other in memory, thus you’ll have no locality of reference and your performance will be terrible. Furthermore, when you define it as a class you can no longer allocate an array or other structure of points without having to initialize each one individually rather than with a bulk set-bytes-to-zero. When you write that kind of application, most of your operations on groups of points will be done with hardcoded loops and operations on raw fields, not with points that are instances of your class Point. So that’s another reason for not using Point as an example for data abstraction.