CS 51 Week 7, L1:

Those Doggone Objects!


Today's topics:
Week 7 Pithy Design Quotes

Keep Secrets!

++ An interface *guides the user's thinking*. Interface design is about deciding what the user does and doesn't need to know.

-- Wherever there is modularity there is the potential for misunderstanding: Hiding information implies a need to check communication.



Object-Oriented Programming (OOP)

Object-oriented programming (OOP) is a programming paradigm that uses "objects" to design computer programs. In object-oriented design, a program is viewed as a collection of cooperating objects, as opposed to a more traditional procedueral view in which a program is seen as a list of instructions to the computer. In OOP, each object is capable of receiving messages, processing data, and sending messages to other objects. Each object can be viewed as an independent little machine with a distinct role or responsibility.

CLASS:: A class is a data abstraction that encompasses both the state of an object (its attributes) and the operations that it can do (its operations or "methods"). In the case of the Dog, its name and owner are examples of attributes, while changeOwner() or sit() or bark() would be examples of things a given Dog could do.

OBJECT:: An instance of the class (something made by using the constructor) is an object. Thus Dog is a class while Lassie (created by the constructor of Dog) is a object.

METHODS:: The set of operations defined by a class are called the methods. This is the functional interface for our data object. Within a program, using a method should only affect one particular object; all Dogs can bark, but you need one particular dog to do the barking.

Message passing:: The process by which an object sends data to another object or asks the other object to invoke a method.

Many common applications fit this way of thinking quite well: simulators, graphical user interfaces (GUIs), computer games, documents and organisational data.

Classes provide several benefits:

Brief History of Object-Oriented Languages and C++

Object-oriented programming

The Language C++

Hints for C++ code writing in this class

In this class we will use C++ as an object-oriented language, and try to maintain a purist (consistent/logical) style to code writing. The goal is to get practice with the conceptual "logic" behind object-oriented programming, and bring in special cases only when really called for.

Revisiting our DOG in C++

Dog is a Class that defines what things dogs can do. We can create many instances of Dogs and then have them do things. The best way to look at this is by considering three separate parts: Interface, Implementation, and Usage.

Interface (Class declaration written in dogclass.h)

class Dog {

 public:
  // Here we define explicitly how the user interacts with Dogs

  // CONSTRUCTORS
  Dog(string name, string breed);    // We are defining a specific constructor
  				     // This disallows the default constructor
  // DESTRUCTORS
  ~Dog();                             // Default destructor

  // SELECTORS
  // Since we made all data private, we must declare explicit selectors
  string getName();
  string getBreed();
  string getOwner();
  ...

  // MUTATORS
  // Can not change name, breed
  void setOwner(string ownername);

 private:
  // We have hidden all the data behind the abstraction barrier
  // This makes it easier to change the implementation if needed

  string name;
  string breed;
  string owner;

};

Implementation (Class definitions written in dogclass.cxx)

// Constructors
//---------------
Dog::Dog(string aname, string abreed){

  name = aname;			// name, breed are required fields
  breed = abreed;               // A "safe" constructor makes sure
  owner = "Not Assigned";	// there is a reasonable default 
  .....
}

// Example Methods
//-----------------
string Dog::getOwner()                  {return owner;}
void Dog::setOwner(string ownername)    {owner = ownername;}


// General pattern
//----------------
// for each method in the interface, create the actual function
// returntype Dog::method(arguments) { do stuff }

Simple usage pattern (written in main.cxx)

// How to compile and run
// g++ -o test dogclass.cxx main.cxx
// ./test

using namespace std;
#include "dogclass.h"

int
main(int argc, char *argv[]) {

  Dog foo("Harvey", "Retriever");
  Dog bar("Rufus", "Beagle");

  foo.setOwner("Radhika Nagpal");
  foo.getOwner();
  bar.getName();
  // foo.name will generate an error. why?

  return 0;
}


How did we create Abstraction Barriers?

The compiler will enforce the interface we have specified. Note that we do not need to explicitly say "Dog" when we call methods on an instance of Dog. In fact, later on we may define a class Cat, and cat may have its own getName method, but this will not case any name collision.

The rules of public and private are important. The private data and functions can only be used within the implementation of that class. The public functions can be used by the creator of an object from that class. Each object gets its own copy of the internal mutable data - AND it can only affect its own internal data. It can not access internal private data of other objects, even if they belong to the same class.

Each of these provides an important way for us to keep and enforce secrets. Good style means using these conventions properly.

Adding Functionality: Pretty Print

Lets do some simple things to increase the functionality of our Dog Class. First we can add a method that allows us to pretty print information about the Dog. This is extremely useful for debugging!

Interface (dogclass.h)

#include iostream
#include string
using namespace std;

class Dog {
 public:
   void Dog::printInfo() {
};

Implementation (dogclass.cxx)

void 
Dog::printInfo() {
  cout << "DOG NAME: " << getName() << "," << getID() 
       << " and Breed: " << getBreed() << endl;
  cout << "Dog belongs to " << getOwner() 
       << " at address " << getAddress() << endl;
}

Simple usage

Dog foo("Harvey", "Retriever");
foo.printInfo();

produces

DOG NAME: Harvey,1 and Breed: Retriever
Dog belongs to Not Assigned at address Not Assigned 


Comments:

Adding Functionality: Categories

In some cases we may want to create variables that can take on only a limited number of values (like booleans). Furthermore it would nice to have the compiler help detect mistakes. We can do this using enum or enumerated types.

Interface (dogclass.h)

enum DogCategory { SMALL, MEDIUM, LARGE };   // enumerated type

class Dog {
 public:
  DogCategory Dog::getCategory();
  void Dog::setCategory(DogCategory cat);

 private:
  DogCategory category;
};

Implementation (dogclass.cxx)

  Within the Constructor 
       category=SMALL; //default

  DogCategory Dog::getCategory()               {return category;}
  void Dog::setCategory(DogCategory cat)       {category = cat;}

Simple usage

Enum provides limited help with error detection, for example
 foo.setCategory(LARGE);   works
 foo.setCategory(2);       compiler error

Even though Enum is implemented such that each value is an integer
 cout << foo.getCategory();

Adding Functionality: Address Object

Objects can include other objects. For example we could define an address object with its own interface (or maybe there is already such a class defined by someone else). Then we can use it to provide an address for the Dog.

QUES: Why use a separate object for Address rather than put the fields directly into Dog?

// Interface
class Dog {
 public:
    void setAddress(string street, string city, int zip);
 private:
    Address addr;
}

// Implementation
void Dog::setAddress(string street, string city, int zip) {
   addr.street = name;
   addr.city = city;
   addr.zip = zip;
}


We could even be more fancy: setAddress would check in some databse whether or not the new address is verifiable and only then allow one to set the address. If it fails, we can return an error - in object oriented languages we return an "exception" and we will learn more about that soon.

Adding Functionality to the Class

Until now we have seen how to create data and methods that are connected to objects within the class. But its also possible to create functionality that is tied to the class and not any particular object. To do this, we will use the the keyword static.

Consider the following two examples

Now lets see how we can implement them

Adding Functionality: Unique IDs

Interface (dogclass.h)

class Dog {
 public:
  int getID();

 private:
  // each instance of a Dog will have an ID
  int ID;   

  // The class has as a whole has a nextID, and a function assignID
  // these are private (what does that mean?)
  static int nextID;
  static int assignID();
}

Implementation (dogclass.cxx)

// In the Constructor
Dog::Dog(string aname, string abreed){
  name = aname;
  breed = abreed;
  ID = Dog::assignID();
  ...
}

// Implementation
int Dog::nextID=0;

int
Dog::assignID(){
  // increment previous ID and return that
  // note that this funcion can only use "static" variables (why?)
  Dog::nextID++;
  return Dog::nextID;
}

Adding Functionality: Test suite

In CS51, we would like you to create a public static function called "testsuite" as part of each class that you create. The purpose of testsuite is to capture a sequence of tests that cover the functionality of your class. By attaching it to the class, we are simply saying that it logically belongs as part of the class creation. It also provides a good setting for debugging - how do I test a single class in isolation?

Interface (dogclass.h)

class Dog {
 public:
  static void testsuite();
};



Implementation (dogclass.cxx) :

Here is an example of a testuite for the Dog class. Its not a comprehensive test but it gives us some feel for how the pieces work

void
Dog::testsuite() {

  // HEADER
  //--------
  cout << "\n Testing the Dog Class written in C++ \n"
       << "========================================\n";

  // CONSTRUCTORS (Two methods)
  //-------------------------------
  // 1. declare using constructor (allocated using local memory (stack))
  // 2. declare by explicitly allocating global memory (heap)
  //    note: in C++ use NEW and DELETE (not malloc/free)

  Dog foo("Harvey", "Retriever");
  Dog *fooref = new Dog("Rufus", "Saint Bernard");

  // Now we can look at what is inside these structures
  // We have successfully forced the data structure to have default values

  cout << "\n TESTING PRINTINFO AND SELECTORS \n \n";
  foo.printInfo();
  fooref->printInfo();   
  // note: currently equivalent to (*fooref).printInfo() but later
  // we will see some subtle differences. DO NOT USE THIS SYNTAX

  // MUTATORS
  //-----------
  // We can test our ability to change values in the dog structures
  // try mutating things that don't have set defined. what happens?
  // try accessing private data directly. what happens?

  cout << "\n TESTING MUTATORS \n";
  cout << "Original Owner is " << foo.getOwner() << endl;
  foo.setOwner("Radhika Nagpal");
  cout << "New Owner is: " << foo.getOwner() << endl;
  fooref->setOwner("Thomas Carriero");

  // ENUMERATED TYPES
  //------------------
  // We can test how categories work
  // Note that this exposes the bad part about this way of creating types
  // since SMALL, MEDIUM, LARGE are really numbers and print as such

  cout << "\n TESTING CATEGORY and ENUM \n";
  cout << "Category old " << foo.getCategory() << endl;
  foo.setCategory(LARGE);
  cout << "Category new " << foo.getCategory() << endl;
  // note that foo.setCategory(2) will fail as invalid conversion

  // DESTRUCTOR
  //-----------

  delete fooref;
}

Simple usage

int
main(int argc, char *argv[]) {
  Dog::testsuite();
  return 0;
}



The World of Mutable Objects

As mentioned before, when we work with mutable data we have to start dealing with some thorny issues. The most thorny is the question of Reference vs Value or identity vs value.

When we pass an object to some function, we need to consider whether that function needs to be able to make a permanent change to the object. If it does then we must give it the "identity" of the object - in other words pass by reference.

Passing By Reference Vs Value (in C++)

We define a class related helper function (i.e. private static)

void 
Dog::testmutation(Dog &dogbyref, Dog dogbyvalue){
  // notice the syntax (we treat both argument names similarly)
  dogbyref.setOwner("mutated owner for dogref");
  dogbyvalue.setOwner("mutated owner for dogvalue");
}


Now we call this from within testsuite

void
Dog::testsuite() {
  cout << "\n TESTING REFERENCE vs VALUE \n";

  Dog newdog("Juno", "Samoyed");
  Dog foo("Harvey", "Retriever");

  newdog.setOwner("Matt Welsh");
  foo.setOwner("Radhika Nagpal");

  cout << "Juno owner: " << newdog.getOwner() << endl;
  cout << "Harvey owner: " << foo.getOwner() << endl;
  Dog::testmutation(newdog,foo);
  cout << "Juno owner: " << newdog.getOwner() << endl;
  cout << "Harvey owner: " << foo.getOwner() << endl;
}

What will happen?

The above code behaves similar to expectations. Both foo and newdog are created within the scope of testsuite. But when testsuite calls testmutation, it gives the identity of newdog and the value of foo. What this means is that foo is "copied" into the scope of testmutation. Within the scope of testmutation, both input arguments are modified. However the modification for the second argument was on a copy of foo. Thus when testmutation completes, that modified copy is lost.

This gives us one way to indicate whether we intend mutability (reference) or not (value). However this copying can be very costly... especially as out objects get more complex and large. It can also get complicated because as we create more complex objects we will have to create copy constructors to explain to the compiler exactly what it means to copy something.

Instead though, if we pass by reference we don't have to copy but we expose ourselves to unexpected change. It would still be nice if we could indicate that an object is meant to be treated as immutable. To do this we can use the keyword const. One way of thinking about const is that you are telling the compiler that your intention is for the data not to be modified and the compiler can check whether this is true.

Moral of the Story: In C++ we encourage that you to always pass objects by reference.

There are many subtle effects that can break by incorrect copying. Instead, you can use const to protect important data - however you pay a different cost because you have to disallow any functions that may mutate their input. We will see more of const as we go along.

Scope and Memory Management

Another thing to keep in mind is the "scope" of the names and identities of objects you create -- where and when are they meaningful? Here are two things to keep in mind

Dog::testsuite(){

  Dog foo("Harvey", "Retriever");
  Dog *fooref = new Dog("Rufus", "Saint Bernard");

  foo.printInfo();
  fooref->printInfo();   

  delete fooref;
}


Within testsuite, I can call other functions and pass foo by reference (e.g. testmutation)
At the end of testsuite, foo is automatically deleted.

Within testsuite, I can use fooref very similarly. 
But At the end, I must delete fooref otherwise I have a memory leak
Or I could have testsuite return fooref 
and someone else can remember and be responsible for deleting it

Its important to use the second method sparingly, because it will require you to do alot more management of memory yourself. However as we will see the second method is very important in constructors that need to dynamically allocate memoery that must persist after the constructr is finished.In that case it is important to create an appropriate destructor that deletes the memory allocated by construct.




CS51, Spring 2008, Radhika Nagpal