As we've mentioned before, a UML class diagram is a good way to think about a program's class structure. Now that we have some familiarity with the classes of our Pop Framework, we can use the relations between these classes as the basis for a more detailed discussion of UML class diagrams.
Keep in mind that UML diagrams are meant as visual tools to be used to clarify the structure of your program. They are not formal, precise objects like pieces of code. The vagueness ? or even, horrors!, the downright sloppiness ? of a UML diagram is a reality that you simply have to get used to. It's a bit inimical to a programmer's usual way of thinking. This is because UML is meant to be a communication channel that non-programmers (like customers and managers and computer-science theorists) can use as well as programmers. Always keep in mind that the point of drawing one of these diagrams is to clear things up. The point is not to show every possible detail. And remember that, unlike code, there is not, and never will be, any objective standard for being a truly correct UML diagram. Code either compiles and runs or it doesn't ? but a UML diagram is simply a springboard for thought and discussion.
There are, by the way, a number of programs which will automatically generate UML diagrams from a directory containing your C++(or, for that matter, Java) code. But often a hand-drawn and custom-designed UML diagram is more informative.
The basic principles of drawing a UML class diagram are pretty simple. First you write down the names of the most important classes in your program, drawing rectangles around them. One way to find out the names of all the classes in an existing Visual Studio project is to take a look at the Class View, using View/Class View (Version 7) [or View/Workspace/Class View (Version 6.0)]. And then you draw lines among your classes expressing their relationships. Of course if you haven't written the program yet, then you need to first give some thought to what classes you might need to use ? we'll say more about this process of 'object-oriented analysis' in Chapter 4: Object-Oriented Software Engineering.
There are three main kinds of relationships that classes can have with each other: inheritance, composition, and association.
Say ClassA and ClassB are classes. If I say ClassB inherits from ClassA, this means that ClassB has the same members and methods as ClassA plus some possible new members and methods. It's also possible that ClassB overrides some of the ClassA methods to implement them differently. When ClassB inherits from ClassA, we also say that ClassB is derived from ClassA, or that ClassB is a child class of ClassA. Most concisely, if ClassB inherits from ClassA, we say that 'ClassB is a ClassA.'
In a UML class diagram, we use a single line with a big hollow triangle-arrow at one end to express the relationship of inheritance. If ClassB is a child of ClassA, we draw a line with an arrow pointing from ClassB to ClassA. In other words the arrow points at the parent; this is a kind of 'ancestor worship' situation in which the parent is pointed out rather than the child! In the case where we have a number of child classes beneath a single parent, we use a horizontal bar to combine the three inheritance arrows into one, thus cleaning up the picture a little bit.
Figure 3.5 is a picture of some of the classes that are used by the cGameStub class.
One thing you'll notice is that we are allowed to 'fork' an inheritance line. That is, in order to reduce clutter, if ClassB and ClassC both inherit from ClassA, we can draw a single hollow-triangle-headed arrow to ClassA and have the arrow's shaft fork in two to have two tails, one ending at ClassB, one ending at ClassC.
Just to make sense out of what these classes refer to, you might want to run the Pop program and choose the Game | 2DStub option to see these classes game in action. The cGameStub itself inherits from the base class cGame. If you look at the game onscreen, you'll see a variety of moving critter objects. The triangular critter that you move with the arrow keys is a cCritterStubPlayer object, and it shoots cCritterPlayerBullet objects. The critters that look like bitmaps are the cCritterStubRival objects, that is, your enemies. They are shooting cCritterStubRivalBullet objects at you. The polygonal critters are cCritterStubProp objects, and they are not shooting anything, since they inherit from cCritter and not from cCritterArmed.
We use the word composition to refer to the situation where ClassA has a ClassB object as one of its members. The operative phrase here is 'ClassA has a ClassB.' In this situation we often say that a ClassA object owns a ClassB object. And if you have a ClassA objectA with a ClassB objectB member, the objectB can say that objectA is its owner.
Regarding composition, note that there are two different ways in which a ClassA can have a ClassB member: either ClassA has a ClassB object, or ClassA has a pointer to a ClassB object. That is, either ClassA has a member field ClassB _bmember or it has a ClassB* _pbmember. (In C++ we very commonly start our member field names with an underscore _.) The former kind of ClassB member is called an embedded member or an instance member of ClassA, while the second kind of ClassB member is called a pointer member or a reference member. If the ClassB *_pbmember is truly related to ClassA by composition, we expect that (a) the ClassB constructor will initialize _pbmember with a new call and (b) the ClassB destructor will destroy _pbmember with a delete call. Condition (b) is sometimes expressed by saying the ClassB reference member of ClassA satisfies the 'cascading delete' condition.
The word aggregation is used for a weaker version of composition where ClassA may have a class ClassB reference member without this member satisfying the cascading delete condition. That is, if a reference member object is not deleted when its owner object is deleted, then we have an aggregation relationship rather than a composition relationship. Making such fine distinctions when discussing class relationships can sometimes be counter-productive, and we are not going to say much more about the difference between composition and aggregation.
We draw a composition line with a diamond at one end ? which we might as well call the tail. This is used to mean that the class object at the diamond end owns or has as members the class objects at the other end. As mentioned before, you can think of the diamond as a 'socket' where we 'plug in' one or more instances of the class at the other end of the composition line.
Another enhancement to the composition line is to write a little numerical symbol like 1, 2, or * at the head (the non-diamond end) of a composition line to indicate either how many different ClassB objects might belong to a given ClassA object. The '*' symbol stands for any number from one on up. A cGame can own any number of cCritter objects, so we put a * by cCritter (see Figure 3.6).
If we don't put multiplicities on a composition line, we will usually mean that there's meant to be only a single member object at the head, although it's also permissible in UML to take a lack of numbers to mean that you simply don't feel like mentioning (or haven't thought about) the number of members.
Some UML experts like to graphically distinguish between the composition relationship and the weaker aggregation relationship by filling in the diamond with solid black for composition and leaving it hollow for aggregation. But we won't do this here, we'll use the hollow diamond to stand for (usually) composition or (rarely) aggregation. In a nutshell, the diamond-headed line means 'has a' or, if there is a star at the end, 'has several.'
A final thing to mention about composition lines is it is not considered acceptable to 'fork' a composition line in analogy to the way we can fork an inheritance line.
The notion of being related by association generalizes the notion of composition. If two classes are related by composition, we can also say they're related by association, but we can use the association relationship more broadly than that. We might say ClassA and ClassB are associated in any of the following cases. ClassA and ClassB are associated if (a) each ClassA object has a ClassB object as an explicit member (the same as composition); or if (b) ClassA has a method that returns a ClassB object. Working the other way around, we also say ClassA and ClassB are associated if (c) each ClassB object has a ClassA object as an explicit member (the same as composition), or if (d) ClassB has a method that returns a ClassA object.
In speaking of association, we don't distinguish between actual objects and pointers to objects; that is, we think of case (a), for instance, as true regardless of whether the ClassB member is an instance member or a reference member.
We use a plain line to indicate the association relationship (Figure 3.7). It's pretty clear that a cGame object is associated with cCritter objects.
Occasionally people will even speak of ClassA and ClassB as being associated if one of the ClassA methods takes a ClassB as an argument, or the other way around.
Given how easy it is for two classes to be thought of as associated, you might fear that UML diagrams would turn into spider-web diagrams very much like what's known in graph theory as a 'complete graph', in which every node is connected to every other node. But in practice we don't draw every conceivable association line.
Part of the job in drawing class diagrams is knowing what to leave out. It's usually better to have three or four small, simple class diagrams instead of one large, complicated one.
As well as the hollow-triangle-headed inheritance lines, the diamond-tailed composition lines and the plain association lines, UML class diagrams also have navigation lines. A navigation line is an association line that has been decorated with barbed arrow heads at one or both ends. If a barbed arrow points from ClassA to ClassB, this means that ClassA has a way of 'navigating' to some specific ClassB objects. This would be the situation in cases (a) and (b) mentioned above: ClassA has a ClassB member or has access to a method that returns a ClassB object. We'd put an arrow pointing from ClassB to ClassA in the cases (c) and (d) mentioned above.
To 'navigate' to an object might mean being able to get a copy of the object or get a pointer to it. Or, in a broader sense, to 'navigate' to an object might just mean being able to do something to it, perhaps by calling some kind of mutator method.
In the Pop Framework, a cGame owns an array that lists all of its member cCritter objects, and each cCritter actually has an accessor that returns a pointer to the cGame that owns the cCritter. So we can navigate in both directions (see Figure 3.8).
As with the composition line, we can put multiplicities on association or navigation lines. Here we can put multiplicities at either end to indicate either how many different ClassA objects might associate with the same ClassB object or how many ClassB objects might associate with a given ClassA object (see Figure 3.9).
In true composition cases with cascading delete, it only make sense for a ClassB object to belong to one single ClassA object, so we assume by default that the multiplicity at the diamond tail of a composition line is 1. So we will often see lines in which there is a diamond at one end and a star at the other, as Figure 3.10, indicating that a given class is composed with multiple instances of another class.
If we don't put multiplicities on an association line, we will usually mean that it's a 1 to 1 association, although it's also permissible in UML to take a lack of numbers to mean that you simply don't feel like mentioning (or haven't thought about) the multiplicities. Always keep in mind the UML is meant to be a fairly loose way of expressing things, and not a precise language like computer code.
It's not considered good form to draw an arrow on a line with a diamond at one end, so if we want to show the composition relationship along with the navigation from cCritter to cGame we draw a diamond line for the composition and an arrow line for the navigation as in Figure 3.10.
Now let's draw a big UML diagram showing the relationships among our custom Pop Framework classes and the MFC-generated classes CPopDoc and CPopView. This is given as Figure 3.11.
Regarding Figure 3.11, note that the author had to redraw it a number of times to try and make it as useful as possible. If your UML diagram makes things seem more confusing, then you need to keep working on it. It usually takes a few tries to get a UML class diagram into its most useful form. A typical thing that happens, for instance, is that you have lines crossing each other, and then you will, if possible, want to rearrange the locations of the classes so that the lines don't cross. Or you might leave out some of the less important associations. Or you might split the diagram into several pieces. In this case, we split off the standard MFC part of the diagram from the computer game-oriented Pop Framework part of the diagram that is shown here. The MFC part is shown in Figure 5.14.
With an eye to the diagram, let's say a bit more about how the Pop Framework works. Once again, the moving objects one sees in the game are cCritter objects. Each CPopDoc document holds a single cGame* _pgame pointer. A cGame holds an array of pointers to all the active cCritter objects. The actual appearance of a cCritter is separated off into a separate object called a cSprite ; each cCritter holds a cSprite* _psprite.
The motions of the critters are affected by user input, which is often fed in from a cListener, and also by various simulated physics forces. Each critter has an array of cForce objects. The sprites, listeners, and forces don't need to maintain a pointer to their owner critter. (We do in fact pass a pointer to the owner as a function argument when we call the listen and force functions of the cListener and the cForce, so a case could be made for having navigation arrows go from cListener and cForce back to cCritter.)
The display of the game objects is the responsibility of the CPopView. A cGraphics object is used to convert the critters' real-valued positions into pixel-valued positions within the visible window of the CPopView. We have two kinds of cGraphics implementations, the cGraphicsMFC and the cGraphicsOpenGL.