Imagine we are writing a program to manage a veterinarian's office. This veterinarian has a limited practice, only treating two kinds of pets: dogs and fish. Although this problem is unrealistically simplified and constrained, it demonstrates a program with more classes and class relationships than previous examples. A brief analysis identifies six classes connected with five relationships, as illustrated in the following UML class diagram.
Vet 1 class diagram. The example illustrates four UML class relationships: inheritance, composition, aggregation, and association. The inheritance relationships share a single arrowhead, making Dog and Fish different kinds of Pets. The composition relationship between an Owner and their Address makes the connection unrealistically permanent, but it allows the example to demonstrate the differences between composition and aggregation, the relationship between Dog and Date.
The Vet example demonstrates translating a UML class diagram into C++ classes and the code implementing the relationships. The constructors and setter functions initialize the relationships and member variables. The display function demonstrates the general syntax for using the various relationships. One aspect of the association relationship is odd: The relationship is between the Owner and Pet classes, but the following program instantiates a Dog object and binds it an Owner, demonstrating the substitution of a subclass object in place of a superclass. Chapter 12 explains how substitutability benefits programs and how programmers use it to create elegant and sophisticated solutions.
#include "Owner.h"
#include "Dog.h"
using namespace std;
int main()
{
Dog myPet("Dogbert", 300);
Owner theOwner("Dilbert", "115 Elm St.", "Ogden");
myPet.setShots(2000, 9, 1); // establishes an aggregation between a Dog and a Date
myPet.setOwner(&theOwner); // establishes the Pet end of association
theOwner.setPet(&myPet); // establishes the Owner end of association
theOwner.display(); // starts a sequence of function calls displaying all program data
return 0;
}
vet.cpp. The vet driver has a simple main function. Class diagrams specify the general relationships between classes, but programs must establish an actual binding between objects. The vet example instantiates Owner and Pet objects and builds the association relationship, one end at a time, with the setOwner and setPet functions. C++ implements association with pointers, so the program finds the addresses of the objects with the address-of operator, &. Alternatively, the setShots function builds a new Date object, establishing the aggregation relationship using the function arguments.
Owner.h
Pet.h
#pragma once;
#include "Address"
#include "Pet.h"
#include <iostream>
#include <string>
using namespace std;
class Pet; // (a)
class Owner
{
private:
string name;
Pet* myPet = nullptr; // (b)
Address home; // (c)
public:
Owner(string n, string s, string c)
: name(n), home(s, c) {}
void setPet(Pet* p) { myPet = p; } // (d)
void display() // (e)
{
cout << "Owner: " << name << endl;
home.display();
if (myPet != nullptr)
myPet->display();
}
};
The vet association classes. Association is a bidirectional relationship, so each class "knows about" the other. C++ programs implement association with pointers, one in each class. Neither class defines a destructor because the program instantiates the associated objects as local variables in main and deallocates them when it terminates. Additionally, the program builds a composition relationship between a pair of Owner and Address objects. The example highlights association-related code in blue and composition-related code in coral.
A forward declaration. Strictly speaking, only one class needs a forward declaration, but crafting the correct sequence is often difficult and leads to "fragile" (easily broken and difficult to reuse) code. The best practice is to include a forward declaration in both classes.
The variables implementing association.
The variable implementing composition between an Owner and an Address (although an unrealistic example - it suggests the owner can never move - it demonstrates the correct relationship syntax).
A setter function that builds one end of an association relationship by binding the parameter to this object. Programmers can't define functions operating on instances of forwardly declared classes in class specifications, but the restriction doesn't apply to pointers.
Association's bidirectionality demands caution when programmers chain function calls, such as the display function, to avoid runaway recursion.
Dog.h
Fish.h
#pragma once;
#include "Pet.h" // (a)
#include "Date.h"
#include <string>
#include <iostream>
using namespace std;
class Dog : public Pet // (b)
{
private:
int akcNum;
Date* shots = nullptr; // (i)
public:
Dog(string name, int akc)
: Pet(name), akcNum(akc) {} // (c)
~Dog() { delete shots; } // (ii)
void setShots(int y, int m, int d) // (iii)
{
if (shots != nullptr)
delete shots;
shots = new Date(y, m, d);
}
void display()
{
Pet::display(); // (d)
cout << "AKC#: " << akcNum << endl;
if (shots != nullptr)
shots->display();
}
};
#pragma once;
#include "Pet.h" // (a)
#include <string>
#include <iostream>
using namespace std;
class Fish : public Pet // (b)
{
private:
int color;
public:
Fish(string name, int c)
: Pet(name), color(c) {} // (c)
void display()
{
Pet::display(); // (d)
cout << "Fish color: " << color << endl;
}
};
The vet subclasses. Subclasses "know about" their superclass; the highlighted code reflects that "knowledge."
Incorporates the superclass's specification
Implements inheritance
Calls the superclass constructor (highlighted)
Calls the superclass display function
Additionally, the Dog and Date classes form an aggregation relationship, with the former acting as the whole and the latter as the part.
Aggregation requires the destructor and the setter function, setShots.
Initializing shots supports the if-statements in setShots and display
The destructor destroys the part object when the program destroys the whole, avoiding a memory leak
The setter function tests for and destroys an existing part before creating a new one from the parameter data
Address.h
Date.h
#pragma once;
#include <string>
#include <iostream>
using namespace std;
class Address
{
private:
string street;
string city;
public:
Address(string s, string c)
: street(s), city(c) {}
void display()
{
cout << "Street: " << street <<
" City: " << city << endl;
}
};
#pragma once;
#include <string>
#include <iostream>
using namespace std;
class Date
{
private:
int year;
int month;
int day;
public:
Address(int y, int m, int d)
: year(m), month(m), day(d) {}
void display()
{
cout << year << "/" << month <<
"/" << day << endl;
}
};
The vet part classes. Neither part class "knows about" the other program classes. Consequently, their header files don't include other program headers, nor do their constructors or display functions chain to or call functions in the other classes.