This is the second in the series on Data Attribute Notation (DAN). DAN is an object-oriented coding style that emphasizes data abstraction. DAN binds the abstract concepts defined in a project's analysis and design stages with the actual implementation stage.
This article covers how DAN can represent relationships that occur in most problems. Also discussed are functions as attributes and how DAN can represent iterator classes.
Static and Dynamic Relationships
Relationships can be static or dynamic. A static relationship is always true, even if there are no instances of classes representing those relationships. 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.
The married 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 example, each person needs a Spouse attribute. Then 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 like spouse to evaluate a dynamic relationship like marriage.
Representing Relationships
In this article, we use declarative code to represent relationships. Declarative code is easy to write and easy to check for correctness. It can be non-procedural , since most declarations may be re-ordered.
The rest of this article uses people and car owners as examples.
Relationships As Functions
The listing below shows the function owner() that returns a non-zero value if the given person owns the given car model.
class Person { /*...*/ }; enum Model { Ford, Chevy }; Person ted(Ford); int owner(Person& p, Model m); // ... if (owner(ted,Chevy)) // ...
The value returned by the owner() is determined at run-time.
Non-Member Vs Member Functions
Non-member functions have these benefits:
-
They handle derived arguments fairly well.
-
They can be extended to n-ary relationships.
-
They express dynamic relationships well.
-
They handle non-commutative relationships well (i.e. they can distinguish between a @ b and b @ a, where @ is some relationship between objects a and b).
-
They can be overloaded to handle similar functions for different types, e.g. home owners or car owners
A non-member function can be overloaded, but can not serve as base for another function in the same sense that one class can serve as a base for another class. A member function of a base class, on the other hand, can be overridden by a derived class member function. Further, if the member function is virtual, invoking the correct function depends on the type of the instance for which the function is invoked. Thus, relationships represented by functions can be extended when using virtual member functions. Another benefit of using member functions is that the implied first argument (i.e. the this pointer) is not implicitly converted. This eases the problem of a function accepting either base or derived class instances as arguments.
Relationships As Classes
Classes can represent static relationships. For example, an Owner relationship between a Person and a Car is shown below.
class Person { }; class Car { }; class Owner { Person p; // part 1 Car c; // part 2 };
A FordOwner relationship can be defined as a class using composition and inheritance:
class Ford : public Car { }; class FordOwner : public Person { Ford f; };
The listing below goes all the way and defines a ChevOwner only in terms of inheritance:
class Person { }; class Chev { }; class ChevOwner : public Person, public Chev {};
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, a useful derived class composed only of inherited base classes is a FilledCircle . It inherits all attributes from its two base classes: FillPattern and Circle .
Functions are Attributes
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 function member isRenewed can be invoked.
int isRenewed() { return Renewed(*this)==1; } // . . . Owner o; // . . . if (o.isRenewed()) // . . .
The function isRenewed could also be defined as an attribute class, 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; }
This shows that dynamic relationships can be converted into a static relationship of some kind as represented by an attribute class.
Multiple Relationships
Relationships can be one-to-one, one-to-many, many-to-one and many-to-many. The previous relationships were all one-to-one. I have shown that a one-to-one relationship can be represented by a function or an attribute class. I would like to 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 "many" is the argument list. In the case of a non-member function, the first argument is the "one" and the "many" is the rest of the argument list. (Neither of these two implementations implies 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 any form mentioned earlier in the car and owner example. That is, it could a complete composite of all the "many", it could be 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". Non-member 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. Given this fact, complete relationships can be represented by one class per relationship. This code section shows classes representing each of these relationships.
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 };
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 persons 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 .
Iterators
The relationship classes we have 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 this.
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; }
A more DAN-like approach for the same example would look thus.
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; }
Declarative Code
In the listing above, 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 is: 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, we can declare any fragment to be a class of the simplified form. The following example illustrates this technique.
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); };
This contains no definition for the insert operator << function. This was intentional. 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, the listing below is true.
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;
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.
Let us return to the example of the iterator. For simplicity sake, let strings of tokens serve as arguments to the Init, Body and Term constructors.
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);
This is pure declarative in nature. The only sequencing rule used is that things must be defined before use.
Conclusions
In this part of the series, we have discussed relationships. We have shown that a relationship can be static or dynamic as defined by the problem domain. Representing a relationship can be done in a number of ways. We mainly discussed declarative code using classes. In using the classes we also discussed the effect of using composition, partial inheritance and total inheritance. We introduced classes as forms of relationships. We also discussed member and friend functions as forms of dynamic relationships. In defining many-to-one and one-to-many relationships, we spoke about the concept of combining the "many" into classes expressing the commonalty of the "many". We used iterators as an example of coded relationships and showed how they could be represented using DAN. From this step, we became more abstract and illustrated how any program can be represented using DAN-type declarative statements.