Comp 11

Object-Oriented Programming (OOP)

and Separate Compilation

Object-oriented programming (OOP) is a popular approach to problem solving that C++ was designed to support. We haven't been using these features so far, but now that you've seen come real problems, we're better able to understand what OOP is all about.

There is a lot of vocabulary: class, instance, instantiate, object, constructor, etc.

LEARN IT, SPEAK IT, MAKE IT A PART OF YOU.

Today:

Classes for Modularity and Encapsulation

Wouldn't it be nice to be able to create libraries for that we can reuse?

We had a library for you to use on the first project, and the ability to share code like that is extremely useful.

But consider some other issues in the project. Because all the functions related to the data needed to refer to the same array/gameboard/snake, we had to have the client (main()) declare the data and pass it to our functions. All the functions had to pass the data structures around. This is painful, and it violates abstraction: why should the client care how we represent our data, let alone have to set aside space for it? (Do you buy and maintain the pressing equipment for your dry cleaners?)

When passing around common data to a large number of functions, programmers begin to yearn for global variables.

There are three problems with the global variable idea:

Take another look:

It's as if you had a theatre, and whenever you wanted to have the lighting crew or the ticket office do something, you had to remind them what theatre they worked for! Or imagine a rock star with an entourage. When you want an appointment with the star, you don't want to have to tell the assistant whom they work for.

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

So now, the star actually travels with the entrouage. The lighting crew and ticket office people work in the theatre.

Data Abstraction with Classes

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

Last time you saw some examples involving points in the X, Y plane and rectangles using structs. Let's just look at points:

Point1.cpp:

Notice how we put the functions and the data together into the class, which is really just a struct. We made the functions public, which means anyone with a Point can use them. The data is there, too, but we made that private, which means only the functions in the class can use 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.

Did you call a function to initialize your seating chart? 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).

Strategy: Separate

What's going on here? We hide all the data structure and function information somewhere else.

Rectangles

We can build a rectangle abstraction on top of points as you did before, but using all the new tools.

Another problem: Kangaroos

Write a class and simple application for kangaroos that have a pouch (whose contents is represented by a string) and an age. Make a print() member function that prints out the kangaroo's age and pouch contents. Put everything in one file, first.

After you've given it a go, you can refer to Kangaroo1.cpp:

Now separate the pieces. Make the data members private while you're at it. Define put_in_pouch(string s), get_age(), and celebrate_birthday() member functions. put_in_pouch() should return a string that is the previous contents of the puch.

Kangaroo.h:

KangarooCourt.cpp:

Kangaroo.cpp:

Sample run:
bash-3.2$ ./KangarooCourt
Kangaroo 1:  [0 year old Kangaroo with "" in its pouch]


Kangaroo 2:  [0 year old Kangaroo with "peanut butter sandwich" in its pouch]


Happy Birthday to me
Happy Birthday to me
Happy Birthday dear me
Happy Birthday to me
Happy Birthday to me
Happy Birthday to me
Happy Birthday dear me
Happy Birthday to me
Happy Birthday to me
Happy Birthday to me
Happy Birthday dear me
Happy Birthday to me
Happy Birthday to me
Happy Birthday to me
Happy Birthday dear me
Happy Birthday to me
Happy Birthday to me
Happy Birthday to me
Happy Birthday dear me
Happy Birthday to me
Happy Birthday to me
Happy Birthday to me
Happy Birthday dear me
Happy Birthday to me
Putting jeep in Kangaroo 1's pouch (was "")
Putting porocupine in Kanaroo 2's pouch (was "peanut butter sandwich")

Kangaroo 1:  [1 year old Kangaroo with "jeep" in its pouch]


Kangaroo 2:  [5 year old Kangaroo with "porcupine" in its pouch]

$ 
	

Remember the library idea?

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.

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

Kanagaroos could have getters and setters for their pouch contents, age, and any other attributes we decide they should have.

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 2017-Sep-15