Reg is president of Charney & Day Inc. and is a voting member of ANSI's X3J16 Committee on the C++ Language. He can be reached on CompuServe at 70272, 3427 or on the Internet at rbcharney@delphi.com.
Data Attribute Notation (DAN) is an object-oriented coding style that emphasizes data abstraction. DAN, which I described in the article "Data Attribute Notation and C++" (DDJ, August 1994) binds the abstract concepts defined in a project's analysis and design stages with the actual implementation stage.
In this article, I'll cover how DAN can represent relationships that occur in most problems. I'll also discuss functions as attributes and how DAN can represent iterator classes.
Relationships can be static or dynamic. A static relationship is always true, even if there are no instances of the class to represent it. For example, class definitions declare a static relationship between the components of the class, even if the class is never instantiated. This is the "definition relationship." The truth value of dynamic relationships is determined during execution. For example, Ted is married to Alice as long as they are not divorced. This example shows that analysis and design determines whether a relationship is static or dynamic. If the system design does not support divorce, then Ted being married to Alice is a static relationship.
In a system that allows for divorce, marriage is a dynamic relationship. That is, the relationship must be checked at run time to see if Ted is married to Alice. Note that even here, a static relationship is needed to relate Ted to Alice. For instance, if each person has a Spouse attribute, you can ask if Ted's Spouse is Alice and Alice's Spouse is Ted. If they are, then Ted is married to Alice. Thus, there needs to be a static relationship such as spouse to evaluate a dynamic relationship such as marital status.
For the purposes of this discussion, I'll use declarative code to represent relationships. Declarative code is easy to write and easy to check for correctness, and it can be nonprocedural since most declarations may be reordered. The rest of this article uses people and car owners as examples.
Listing One shows the function owner() that returns a nonzero value if the given person owns the given car model. The value returned by the owner() is determined at run time.
Nonmember functions have the following benefits:
Classes can represent static relationships. For example, an Owner relationship between a Person and a Car is shown in Listing Two . Listing Three , however, shows that a FordOwner relationship can be defined as a class using composition and inheritance. Listing Four goes all the way and defines a ChevOwner only in terms of inheritance. In Listing Four, if a Person can be taxed and a Chev can get fixed, then you can get a ChevOwner fixed and taxed all at the same time. This shows that a class shares all the attributes of any one of its inherited parts. If the class represents a relationship and it inherits some of its parts, then what is true for any one of its inherited parts applies to the relationship as a whole. When a relationship like Owner inherits some of its attributes, those parts should make sense in toto. Thus, the previous example shows poor design. In contrast, FilledCircle is a useful derived class composed only of inherited base classes. It inherits all attributes from its two base classes: FillPattern and Circle.
DAN states that a class is defined by its attributes. Consistent with this is the fact that member and friend functions of a class are also attributes of the class.
For example, if an Owner has to renew his car license every year, an attribute Renewed can be defined. To check if this attribute is set, a member function isRenewed can be invoked, as in Listing Five . The function isRenewed could also be defined as an attribute class, IsRenewed. Listing Six shows that a dynamic relationship can be converted into a static relationship of some kind as represented by an attribute class.
Relationships can be one-to-one, one-to-many, many-to-one, and many-to-many. Having illustrated in the previous relationships that a one-to-one relationship can be represented by a function or an attribute class, I'll extend this to the other relationships. It seems fairly obvious that a one-to-many relationship can be represented by a function member, where the class instance is the one and the argument list is the many. In the case of a nonmember function, the first argument is the one and the rest of the argument list is the many. (Neither of these two implementations imply that the order of evaluation of the arguments is the same as the order of the arguments.) In the case of the many-to-one and many-to-many relationships, things get more interesting.
The many could be represented by a single class containing the many. This many class can have any form mentioned earlier in the car-and-owner example. That is, it could be either a complete composite of all the many or a mixture of inherited parts and parts composing the many class. It could also be a completely inherited class where all the parts of the many are inherited. The form of the class is problem dependent.
In a many-to-one relationship, a member function can be used if the class instances represent the many and the one represents the single function argument.
In a many-to-many relationship, a function member can have its class instance represent the first many, and the single arguments represent the second many. Nonmember functions have two arguments, each representing a many.
In implementing one-to-many, many-to-one, and many-to-many relationships, the most important concept is that the many can be represented by a single class, so complete relationships can be represented by one class per relationship. Listing Seven shows classes representing each relationship. Pure composition was used in all these classes. A mixture of composition and inheritance might be more appropriate, depending on the problem.
It is important to note that in any relationship, you must be able to encapsulate each side of the relationship into a class. For example, if a number of people own a number of cars, you have a group called People and a group called Fleet. FleetOwners is the resulting relationship. In the case of the one-to-one relationship called CarOwner, one side was encapsulated into one Person and the other side was encapsulated into a Car.
The relationship classes I've just discussed often contain collections. Iterator classes are used to iterate over collections of objects. The rest of this article uses iterator classes to show how code normally thought to be procedural in nature can be written in a declarative fashion using DAN.
Normally, an iterator class has next(), prev(), and reset() function members or their equivalent. For example, consider iterating over the collection FleetOwner so that a report is produced showing the list of cars owned in the collection Fleet. A classic C++ program using iterators would look something like Listing Eight ; Listing Nine describes a more DAN-like approach for the same example.
In Listing Nine, I have written executable code when my intention was to illustrate writing declarative statements. To express this fragment of code in declarative format, we need to agree that all fragments have a basic form. The form consists of an initialization stage (Init), a main stage (Body), and a termination stage (Term)--any or all of which can be empty. Further, when two fragments are placed together, the result is also a fragment. As such, you can declare any fragment to be a class of the simplified form. Listing Ten , which illustrates this technique, intentionally contains no definition for the insert operator << function. Combining code may be problem and language dependent. In a sequential machine, the termination stage of one fragment can be concatenated with the initialization stage of the next fragment. In a parallel machine, all initialization stages may be executed in parallel. In a C++ program, there is a difference between static and dynamic data. Thus, initialization code would need to be subdivided in those two types of data before initialization could be performed. Regardless of these variations, Listing Eleven is true. Both f1 and p1 are statically defined fragments. This is consistent with languages like C++ that are static in nature. Languages like Lisp exhibit behavior like f2 and p2 that can only be evaluated at run time.
Listing Twelve is a program in which strings of tokens serve as arguments to the Init, Body, and Term constructors. Listing Twelve is purely declarative in nature. The only sequencing rule is that everything must be defined before use.
Person ted(Ford); int owner(Person& p, Model m); // ... if (owner(ted,Chevy)) // ...
class Person { };
class Car { };
class Owner {
Person p; // part 1
Car c; // part 2
};
class Ford : public Car { };
class FordOwner : public Person
{
Ford f;
};
class Person { };
class Chev { };
class ChevOwner :
public Person,
public Chev
{
};
int isRenewed()
{ return Renewed(*this)==1; }
// . . .
Owner o;
// . . .
if (o.isRenewed()) // . . .
#include <iostream.h>
#include <string.h>
class Renewed {
int r;
public:
Renewed(const int rr=0)
{ r = rr; }
operator int() const
{ return r; }
};
class Car { };
class Person {
char *n;
public:
Person(const char *nn="")
{ strcpy(n,nn); }
friend ostream& operator <<
(ostream& os, Person& p)
{ os << p.n; }
};
class Owner : public Person
{
Car c;
Renewed r;
public:
Owner(const char *n) :
Person(n) { }
operator Renewed() const
{ return r; }
Owner& operator <<
(const Renewed& rr)
{ r = rr; return *this; }
};
class IsRenewed {
Renewed r;
public:
IsRenewed(const Owner& o)
{ r = Renewed(o); }
operator int() const
{ return r; }
};
int main()
{
Owner owner("Ted");
owner << Renewed(1);
if (IsRenewed(owner))
cout << Person(owner)
<< " has renewed\n";
return 0;
}
class Person { };
class Car { };
class People // any # of people
{
Person **pp;
};
class Fleet // any # of cars
{
Car **cp;
};
// 1:1 relationship of CarOwner
class CarOwner
{
Person p;
Car c;
};
// 1:m relationship of one person
// owning any number of cars
class FleetOwner
{
Person p; // one person
Fleet f; // many cars
};
// m:1 relationship of a group of people
// owning one Car
class TaxiCoop
{
People p; // many people
Car cp; // one car
};
// m:m relationship of many cars
// owned by many people
class FleetOwners
{
People p; // many people
Fleet f; // many cars
};
class FleetOwner
{
friend class FOIter;
Person p;
Fleet f;
};
class FOIter
{
int status; // =0 if empty
CurrElem c; // save cur elem
public:
FOIter(FleetOwner& fo);
int next(); // =0 at end
int prev(); // =0 at start
void reset();
friend ostream& operator <<
(ostream& os, FOIter& foi)
{ return os << foi.c; }
};
int main()
{
FleetOwner fo;
FOIter foI(fo);
foI.reset();
while(foI.next())
cout << foI << endl;
return 0;
}
class FleetOwner
{
friend class FOIter;
Person p;
Fleet f;
};
class Next { };
class Prev { };
class Reset { };
class CurrElem
{
public:
friend ostream& operator <<
(ostream& os, CurrElem& c);
};
class FOIter{
int status; // =0 if empty
CurrElem c; // save cur elem
public:
FOIter(FleetOwner& fo)
{ status = 0; }
operator Next();
operator Prev();
operator Reset();
operator int()
{ return status; }
friend ostream& operator <<
(ostream& os, FOIter& foi)
{ return os << foi.c; }
};
int main()
{
FleetOwner fo;
FOIter foI(fo);
Reset(foI);
while(Next(foI))
cout << foI << endl;
return 0;
}
class Code { };
class Init : public Code { };
class Body : public Code { };
class Term : public Code { };
class Fragment // order of parts
{ // in this class
Init ii; // determines the
Body bb; // order of
Term tt; // initialization
public:
Fragment(Init i,Body b,Term t)
: ii(i), bb(b), tt(t) { }
Fragment() { }
Fragment& operator <<
(Fragment& f);
};
class Code { };
class Init : public Code { };
class Body : public Code { };
class Term : public Code { };
class Fragment // order of parts
{ // in this class
Init ii; // determines the
Body bb; // order of
Term tt; // initialization
public:
Fragment(Init i,Body b,Term t)
: ii(i), bb(b), tt(t) { }
Fragment() { }
Fragment& operator <<
(Fragment& f);
};
Init i1, i2; Body b1, b2; Term t1, t2; Fragment f1(i1,b1,t1); Fragment f2; f2 << f1; Fragment p1(i2,b2,t2); Fragment p2; p2 << f1 << f2;
Init i1("FleetOwner fo;
FOIter foI(fo);
");
Init i2("foI << r;");
Body b2("while(Next(foI))
cout << foI << endl;
");
Term t1("return 0;");
Fragment f1(i2,b2,Term(""));
Fragment program(i1,f1,t1);
Copyright © 1995, Dr. Dobb's Journal