We must be careful to distinguish between extensional or real-world objects, the objects that appear as components in object-oriented models, and the objects that appear as components in object-oriented programs. Often model objects represent real-world objects, but model objects can also represent real-world abstractions, such as events, roles, and concepts. Program objects don't merely represent real-world objects and abstractions, they also perform a function within the program. In fact, most program objects don't represent anything in the real world. They simply perform some well defined function. (Think of a queue or a stack, for example.)
All further unqualified uses of the term "object" will refer to either program or model objects. Where it makes a difference, the context should make it clear which is meant.
An object is a component of an object-oriented model (or program) that encapsulates internal properties and responds to messages.
When an object receives a message, a message handler function is invoked. (Different types of messages may invoke different message handlers, or if no appropriate message handler can be found, some sort of error results.) The message handler responds to the message by some combination of:
1. Modifying its internal properties
2. Sending messages to other objects
3. Computing a value
4. Returning information to the sender
The properties of an object are also called its attributes or its state.
Program objects are actually composite variables. Often (but not always), attributes correspond to the values stored in the object's component variables. These component variables are also called fields (Java) or member variables (C++).
Encapsulation means contains, but also implies access to attributes is strictly controlled by the object.
Message handlers are also called methods (java) or member functions (C++).
The behavior of an object refers to the way it responds to all possible messages. In other words, the object's methods.
Here is a notation commonly used to refer to a particular property of an object:
objectName.propertyName
Of course this notation only produces the value of this property if the object grants the appropriate access rights.
Here is a notation commonly used to send a message to an object::
objectName.messageName(argument1, argument2, ...);
Here are some other commonly used notations:
objectName->messageName(argument1, argument2, ...);
send(objectName, messageName, argument1, argument2, ...);
Assume checking and savings are names of objects representing bank accounts. The principle attribute of a bank account is its balance:
checking.balance = balance of checking account
savings.balance = balance of savings account
Bank accounts can receive deposit and withdraw messages. Of course these messages include the amount of money to be deposited or withdrawn. When a bank account object receives a withdraw messages, it invokes a method that deducts the specified amount from its balance. When a deposit message is received, a method is invoked that increments the balance by the specified amount:
savings.withdraw(500); // savings.balance = savings.balance - 500
checking.deposit(500); // checking.balance = checking.balance + 500
In Java, objects are implicitly references[1]. This results in reference or sharing semantics for assignments. For example, the assignment statement:
savings2 = savings;
results in savings and savings2 being names for the same bank account. Reference semantics is much simpler than copy semantics, but can lead to confusing aliasing problems that programmers must always guard against:
savings2.withdraw(500); // savings.balance -= 500!
A class is a template (i.e. a pattern or schema) for creating objects. The act of creating an object from a class is called instantiation. For this reason, objects are sometimes called instances. A class declares the attributes its instances posses, the types of messages they may receive, and the associated methods that will be invoked.
Example (Java):
class Account {
// fields:
double balance = 0;
// methods:
void withdraw(double amt) {
if (amt <= this.balance) {
this.balance -= amt;
}
}
void deposit(double amt) {
this.balance += amt;
}
}
1. Note comments and style conventions (indentation, alignment, etc.).
2. Note initialization of field.
3. The signature of the withdraw method is:
void withdraw(double amt)
The method body is the block:
{
if (amt <= this.balance) {
this.balance -= amt;
}
}
4. Note the use of the implicit parameter, this, which refers to the object currently executing the method. It is unnecessary to qualify fields and method calls inside a method body unless the method has parameters or local variables that shadow the fields. For example:
void deposit(double balance) {
this.balance += balance;
}
Implicit parameters can be useful. Assume the following class is supplied:
class IRS {
public static final double SUSPICIOUS =
10000;
public static void report(Account acct)
{
//audit acct holder!
}
}
Here is how an account might report a suspiciously large deposit:
void deposit(double amt) {
balance += amt;
if (IRS.SUSPICIOUS <= amt) {
IRS.report(this);
}
}
Objects are created from classes using the new operator:
Account checking = new Account();
Account savings = new Account();
The argument to the new operator is a call to the default (parameterless) constructor. Constructors initialize the fields of the newly created object. This eliminates the dangerous gap between variable creation and variable initialization.
Of course programmers can define their own constructors. A word of warning: this causes the default constructor to disappear. This can cause problems, because the default constructor is assumed to exist in many situations.
Example:
class Account {
private double balance;
public Account(double balance) {
if (0 <= balance)
this.balance = balance;
else
this.balance = 0;
}
// replace default constructor:
public Account() { balance = 0; }
// etc.
}
UML is a collection of eleven types of diagrams that are commonly used to model different views of object-oriented systems. (These may be software systems, hardware systems, or enterprise systems.)
A UML class diagram is used to model the architecture of an object-oriented program. It shows the important classes and their relationships. A class diagram is analogous to a blueprint created by an architect and consulted by builders.
A class icon is simply a box labeled by the name of the class
If desired, we can also show the attributes account objects posses:
We can also show the types of messages account objects can receive:
Computer Aided Software Engineering (CASE) tools such as Rational Rose are often used to create class diagrams and generate the corresponding class definitions:
UML object diagrams show objects and their relationships. An object icon is a box containing the name and class of the object. These are separated by a colon and underlined:
We can also show the attributes of an object and their current values:
A package is a named set of classes, functions, interfaces, and sub-packages. Packages are useful for partitioning large programs into libraries and subsystems. A package diagram shows an application's important packages and their dependencies. Packages appear in these diagrams as labeled folders called package icons. A dependency is a dashed arrow pointing from an importer package to an exporter package, and indicates that changes to the exporter package may force changes to the importer package.
For example, the following package diagram indicates that components in the Business Logic package import (use) components defined in the Database package, while components in the User Interface package import components defined in the Business Logic package. Of course some of these components may have been imported by the Business Logic package, so the dependency relationship is transitive.
Packages can be implemented in C++ using names spaces:
namespace Database
{
class Query { ... };
class Table { ... };
// etc.
}
namespace BusinessLogic
{
using namespace Database;
class Transaction { ... };
class Customer { ... };
// etc.
}
namespace UserInterface
{
using namespace BusinessLogic;
class DialogBox { ... };
class Menu { ... };
// etc.
}
A flight simulation program will probably want to represent airplanes as instances of an airplane class. Suppose we learn that from the flight simulator's point of view, the important attributes of an airplane are its altitude and air speed, and the important operations are takeoff(), fly(), and land(). Here's the corresponding class icon:
Notice that we have suppressed information about the types, visibility, and initial values of the attributes, as well as the parameters, visibility, and return values of the operations. This is often done when such information is unavailable, unnecessary, or premature.
Of course UML allows us to add this information. For example, it probably makes sense for air speed and altitude to be doubles initialized to zero. (Airplanes are created on the ground and standing still.) Suppose we decide that Airplane may eventually serve as a base class for classes representing special types of airplanes such as military planes and passenger planes. In this case we may want to make altitude and air speed protected instead of private.
Assume we also learn that the takeoff(), fly(), and land() operations are indeed parameterless and have void return values. Of course these operations should be public; however, we find out that they all need to call a supporting function called flaps(), which raises and lowers the wing flaps by a specified integer angle with a default argument of 30 degrees. Because flaps() is only a supporting function, we decide to make it private.
Finally, suppose our unit testing regimen demands that every class provide a public, static operation called test() that creates a few objects, calls a few member functions, then returns true if no errors occurred and false otherwise. Here is the class icon showing all of this added information:
Note that UML indicates visibility using "+" for public, "#" for protected, and "-" for private. Static attributes and operations are underlined. (We use C++ types such as double, int, bool, and void for pre-defined primitive types.)
Some CASE tools (Computer Aided Software Engineering) can generate class declarations from a class diagram containing a sufficient amount of detail. Let's take a moment to consider how an idealized CASE tool might generate a C++ class declaration from our Airplane class icon.
The Airplane class should be declared inside of a header file called airplane.h, which may include some standard header files. The if-not-defined macro, #ifndef/#endif, is used to prevent airplane.h from being included multiple times within the same implementation file, which would result in a compiler error:
/*
* File: airplane.h
* Programmer: Pearce
* Copyright (c): 2000, all rights reserved.
*/
#ifndef AIRPLANE_H
#define AIRPLANE_H
#include <string>
#include <iostream>
using namespace std;
class Airplane
{
// see below
};
#endif
Naturally, C++ will interpret attributes as member variables and operations as member functions. If we zoom in on the class declaration we notice that the public member functions are divided into four groups: constructors and destructor, getters and setters, operations, and the test driver:
class Airplane
{
public:
// constructors & destructor:
Airplane();
virtual ~Airplane();
// getters & setters:
double getAltitude() const { return
altitude; }
void setAltitude(double a) { altitude =
a; }
double getSpeed() const { return speed;
}
void setSpeed(double s) { speed = s; }
// operations:
void takeoff();
void fly();
void land();
// test driver:
static bool test();
protected:
double altitude;
double speed;
private:
void flaps(int d = 30);
};
The last two groups are self explanatory. A default constructor is needed to initialize the member variables. An empty virtual destructor is a common feature of a base class. For example, assume MilitaryPlane is derived from Airplane:
class MilitaryPlane: public Airplane { ... };
Deleting an airplane pointer that points to a military plane will automatically call the destructors for both classes:
Airplane* p = new MilitaryPlane();
// do stuff with p ...
delete p; // calls ~Airplane() and ~MilitaryPlane()
By default, our idealized CASE tool automatically generates member functions that allow us to read (getters) and modify (setters) each member variable. In the case of airplanes, we may want to comment out the setters so that clients can only modify air speed and altitude using the takeoff(), fly(), and land() operations.
Of course our CASE tool also generates an implementation file called airplane.cpp. No class icon contains enough information to specify how its operations should be implemented, so these are left as stubs. Notice, however, that the constructor is implemented; it simply initializes the member variables to their specified initial values.
/*
* File: airplane.cpp
* Programmer: Pearce
* Copyright (c): 2000, all rights reserved.
*/
#include "airplane.h"
Airplane::Airplane()
{
speed = 0.0;
altitude = 0.0;
}
Airplane::~Airplane() {}
void Airplane::takeoff() { /* stub */ }
void Airplane::fly() { /* stub */ }
void Airplane::land() { /* stub */ }
void Airplane::flaps(int d /* = 30 */) { /* stub */ }
bool Airplane::test() { return true; }
Since we are using an idealized CASE tool, it has thoughtfully provided a main() function in a file called main.cpp:
/*
* File: main.cpp
* Programmer: Pearce
* Copyright (c): 2000, all rights reserved.
*/
#include "airplane.h"
int main(int argc, char* argv[])
{
cout << "Airplane::test() =
" << Airplane::test() << endl;
return 0;
}
Creating files and declarations such as these is often done during early iterations through the implementation phase. After files fill up with implementation details, it can be hard to sort out complex dependencies between files, and simple features such as the default constructor, the virtual destructors, and the getter and setter functions can be forgotten. Also, not all code generators are of the same quality as the one used by our idealized CASE tool, so programmers need to know how to translate class diagrams into declarations by hand.
Here is the format of the Airplane.java file:
/*
* Airplane.java
* Copyright (c): 2000, all rights reserved.
*/
// imports:
import java.util.*;
/**
* Instances of this class represent
virtual airplanes.
* @author Pearce
* @version 1.0
*/
public class Airplane {
//
see below
}
The Airplane class declaration is as follows:
public class Airplane {
/** Altitude of this airplane in feet.
*/
protected double altitude;
/** Speed of this airplane in knots. */
protected double speed;
// construction and finalization:
public Airplane() {
altitude = 0.0;
speed = 0.0;
}
public void finalize() throws Throwable
{ super.finalize(); }
// getters &
setters:
public double getAltitude() { return
altitude; }
public void setAltitude(double a) {
altitude = a; }
public double getSpeed() { return
speed; }
public void setSpeed(double s) { speed
= s; }
// operations:
public void takeoff() { /* stub */ }
public void fly() { /* stub */ }
public void land() { /* stub */ }
/**
* Raises or lowers wing flaps.
* @param d = degrees flaps are to be raised or lowered.
*/
private void flaps(int d) { /* stub */
}
/**
* Raises wing flaps by 30 degrees.
*/
private void flaps() { flaps(30); }
// test driver:
/**
* Constructs and tests airplanes.
* @return <code>true</code> if successful,
* <code>false</code> otherwise.
*/
public static boolean test() { return
true; }
public static void main(String[] args)
{
System.out.println("Airplane.test()
= " + test());
}
}
[1] A reference is an entity that encapsulates the address of an object and its fields. In certain contexts a reference automatically dereferences itself, yielding the contents of its object's fields. This frees programmers from the details of pointer management.