Comp 15

Classes, Data Abstraction, and Representation Invariants

Classes for Modularity and Encapsulation

Classes give us a way to package data and the functions that operate on that data together: a class is a struct that has data elements, plus:

Data Abstraction with Classes

Data abstraction is the process of ecapsulating all in the information about a user-defined data type in one place. Good data abstraction involves having a strong abstraction barrier that prevents clients from interfering with the implementation.

Let's consider a class whose instances represent points in the X-Y plane:

Point1.cpp:

Notice how we put functions and data together into the class. We made the functions public, which means anyone with a Point object can use them. The data is there, too, but we made that private, which means only the functions within the class can access the data members. Abstraction barrier!

Look in main(). We declare variables of type Point: a class makes a new type. A value whose type is a class is called an instance of that class. It's also called an object.

It's normal to want to initialize a value when you create it, so C++ has something called a constructor, which is a function that is called automatically when someone makes a new instance. In this case, the constructor takes two parameters: the x- and y-coordinates of the Point to be created. (Constructor is really a misnomer: it doesn't construct anything. It's really an initializer.)

To invoke member functions, also known as methods, you put an instance on the left and select the member function you want and pass the arguments. The member functions live in the object, just as data members live in a struct!

You don't have to pass the object the function is to operate on (it's moved to the left of the dot). The function will work on the object is was taken out of.

The :: is called the scope resolution operator. It's there to tell C++ that the thing it's attached to really belongs in the class named on the left. Without it, C++ would assume the function was an outsider and thus would not coordinate its use with any objects or give it access to data members.

While looking at the member functions, like get_x(), note that member functions can just use the member variables. C++ will assume that such references mean the data members in the object the function was called from.

Terminology:

Exercise: Add a distance() member function to the Point class and add code in main() to test it.

Real Modularity: Reusing Points

Our code above is nice, but no one else can use the point implementation. We can't even use it in another application with out cutting and pasting (boo, hiss). To solve this problem, separate out the following three things:

The Point interface goes in a .h (header) file: Point.h. The Point implementation goes in a separate .cpp file: Point.cpp. The client code, which uses the Point code, is written in yet a separate file - PointClient.cpp. This code includes the Point.h header file, and uses whatever is declared as public there

What's going on here? Why do we hide all the data structure and function information somewhere else? This way,

Moreover, the implementation can now be easily reused, indepedently of the application.

If we discover a better way to implement the same interface, we can change the implementation, and the client doesn't have to change at all.

And, if the client finds a better implementation of the same interface, they can use that one without changing their code.

Rectangles

Let's build a rectangle abstraction on top of points, but using all the new tools. This would be great to attempt yourself, and then double-check your code against what is here.

Getters, Setters, Data Abstraction, and Representation Invariants

How can I be sure,
In a world that's constantly changing?
How can I be sure,
where I stand with you?

–The Rascals

Control of visibility is very important!

It is very common to provide getters and setters for elements of an object's state: a getter returns the current value of a state element, and a setter updates the value of a state element.

Getters and setters should only be provided for elements of logical or abstract state. The concrete state should not be revealed (unless it maps directly to the logical state, but the client should not worry about that!).

Setters should only be provided for elements of the logical state that can change (i. e., that are mutable) and that it makes sense for the client to update individually.

For example, the rectangles have lower left and upper right points. It would be very common to have member functions (methods):

Why? Why not make the fields public and then clients clients can just get and update the fields themselves?

Data abstraction: The client should not know or care how we represent our data.

That permits client and implementation to be more decoupled, more independent, more modular.

Consider our rectangles. We're storing only two points of the rectangle, but why should a client work only for rectangles with lower left and upper right points? This is an arbitrary decision that has nothing to do with their application.

We could extend our rectangles:

class Rectangle
{
public:
        Rectangle(Point low_left, Point up_right);

        Point get_lower_left ();
        Point get_upper_left ();
        Point get_upper_right();
        Point get_lower_right();


        void  set_lower_left (Point new_lower_left);
        void  set_upper_left (Point new_upper_left);
        void  set_upper_right(Point new_upper_right);
        void  set_lower_right(Point new_lower_right);

        int   get_width ();
        int   get_height();

        void print();

private:
        Point lower_left, upper_right;
};

      

Notice that the implementation is still storing 2 points, but it's allowing clients to behave as if it has all four corners available as well as the width and the height. It could store all those things, but it doesn't have to.

The client doesn't care about the actual state of an object, only its logical state, i.e., the state of the thing the object represents. In a way, the implementer can lie about what's in an object. As long as they can produce a value or record an appropriate state change, the client is happy.

Exercise:
Implement all the getters in the Rectangle class above.

Representation Invariants: How you can be sure in a world that's constantly changing.

You may have started to think about the setters in the above example. First, you must consider whether you want to have setters at all. Data values that cannot change are said to be immutable. Mutable values can change. Our points above are immutable.

If you do want to support mutations, what do they mean? For example, if the client is thinking that a Rectangle instance represents a rectangle (with sides parallel to the edges of the screen), what does it mean to change the lower left corner?

Point p1(0, 0), p2(10, 20), p3(5, 6);
Rectangle r(p1, p2);

r.set_lower_left(p3);
        
Can we really change one corner out of four? If so, then the figure isn't a rectangle any more! So, consider set_upper_left(). How would you implement that?

Stranger: what if I set the upper left corner to be to the right of the upper right corner?

A representation invariant is a property of an object that is true whenever anyone outside the abstraction looks at it. It's a consistency property.

Therefore, setters should either update all the necessary members of an object in a consistent way or fail. (They can silently fail to do anything, effectively ignoring the request, return a failure indication, or throw something called an exception.)

Mark A. Sheldon (msheldon@cs.tufts.edu)
Last Modified 2021-Jan-14