Chap.2: BASIC CONCEPTSLet's start with an example, where every object of the class Department contains several objects of the class Employee, and each Employee knows its Department. This relation is often called an AGGREGATE, or 1-to-many relation, and it is a frequently occuring data structure in most applications. If you code this aggregate from scratch, very likely you would code it with pointers embedded in classes Department and Employee: Class Diagram:
Implementation:
There are several ways of implementing the list. Instead of the usual NULL-ending list, we used a circular list (ring). As explained in Chap.3, a ring allows fast run-time checking of data integrity. This library uses rings for all its classes. The diagram above does not show the assignment of the functions that will manipulate the aggregate. Most programmers would assign these functions arbitrarily to classes Department or Employee, depending on where the function could be most naturally implemented. For example: Employee* Employee::nextEmployee(){ return next;}
Department* Employee::myDepartment(){ return parent;}
void Department::addEmployee(Employee *e){ ... };
Employee* Department::firstEmployee(){
Employee* e=NULL; if(tail)e=tail->next; return e;}
As explained in [1] and [2], if classes Department and Employee are not part of one aggregate, but participate in several data structures (a quite common situation), the result is spaghetti++ code which is difficult to debug and maintain. Also, classes Department and Employee (and possibly many other classes) could not be compiled independently. Any time you change one of the headers, the entire project must be recompiled. The cure for this problem is to implement a special manager class for each data structure. This class typically contains no or little data, but it has all the functions that manipulate the data structure. To provide fast access to all the pointers, this class must be a friend of the classes that participate in the data structure. For example, in our example, we can introduce class DEaggregate: class DEaggregate {
public:
Employee *nextEmployee(Employee *e){return e->next;}
Department *myDepartment(Employee *e){return e->parent;}
void addEmployee(Employee *e){ ... }
Employee *firstEmployee(Department *d){
Employee* e=d->tail; if(e)e=e->next; return e;}
...
};
class Department {
friend class DEaggregate;
Employee *tail;
...
public:
...
};
class Employee {
friend class DEaggregate;
Department *parent;
Employee *next;
Employee *prev;
...
public:
...
};
We use manager classes to represent not only simple data structures such as aggregates, but also structural design patterns. Instead of coding the aggregate again for every new application, it makes more sense to design a generic aggregate with C++ templates, and store it in a class library. The pointers that we need in the application classes such as Employee, can be injected through inheritance. In the following code, class P is the application class which will be the parent of the aggregate, and class C is the class which will be the child of the aggregate. The integer parameter i, with default 0, prevents conflict when using the same organization between the same classes more than once (e.g. for multiple lists): The following classes will come from a library: template<class P, class C, int i=0> class Aggregate {
public:
C *next(C *c){return (AggregateChild<P,C,i>::c)->next;}
P *parent(C *c){return (AggregateChild<P,C,i>::c)->parent;}
void addTail(P *p,C *c); // add c as a tail under p
void remove(C *c); // remove child c from its parent
C *head(P *p){ C* c=(AggregateParent<P,C,i>::p)->tail;
if(c)c=c->next; return c;}
...
};
template<class P, class C, int i=0> class AggregateParent {
friend class Aggregate<P,C,i>;
C *tail;
public:
AggregateParent(){tail=NULL;}
};
template<class P, class C, int i=0> class AggregateChild {
friend class Aggregate<P,C,i>;
P *parent;
C *next;
C *prev;
public:
AggregateChild(){parent=NULL; next=prev=NULL;}
};
The application uses the library classes. Since we have only one aggregate between Department and Employee, we can use the default value for paremeter i. In essence, this is the way the Pattern Template Library is coded and used: class Department : public AggregateParent<Department,Employee> {
... // no members or functions related to the aggregate
};
class Employee : public AggregateChild<Department,Employee> {
... // no members or functions related to the aggregate
};
|