UML Class Diagrams

The Unified Modeling Language (UML) is a useful tool for object-oriented analysis and design. One part of the UML, the class diagram, is especially good at clearly and succinctly describing the static relationships between objects. Figure 1 shows the class diagram used to describe the relationships between some of the objects used in one of the projects from the book Programming Game AI by Example.

Figure 1. Example UML class diagram.

If this is the first time you’ve seen a UML class diagram you’ll probably find the figure perplexing, but by the time you’ve finished reading this appendix it will make perfect sense. (Touch wood J)

Class Names, Attributes and Operations

First of all let’s start off with the name, attributes and operations of a class. Classes are represented by a rectangle divided into three compartments. The name of the class is given in bold at the top of the rectangle, attributes are written beneath, and operations in the bottom compartment. See Figure 2.

Figure 2. The class/object rectangle.

For example, if one of the objects in a game is a racing car it can be specified as shown in Figure 3.

Figure 3. Examples of attributes and operations

Of course, a racing car object is likely to be much more complex than this but we only need list the attributes and operations of immediate interest. The class can easily be fleshed out more fully at a later stage if necessary. (Quite often, I don’t show any attributes or operations at all and use class diagrams simply to show the relationships between objects, as demonstrated by Figure 1). Note that the operations of a class define its interface.

The type of an attribute can be shown listed after its name separated by a colon. The return value of an operation may also be shown in the same way, as can the type of a parameter. See Figure 4.

Figure 4. Specifying the type.

In my text I rarely use the ‘name : type’ format for parameters as it often makes the diagrams too large to fit on the page comfortably. Instead, I just list the type, or sometimes a descriptive name if the type can be inferred from it.

Visibility of Attributes and Operations

Each class attribute and operation has a visibility associated with it. For instance an attribute may either be public, private or protected. This property is shown using the symbols + for public, - for private and # for protected. Figure 5 shows the racing car object with the visibility of its attributes and operations listed.

Figure 5.

As with the types, it’s not imperative that you list the visibilities when drawing class diagrams, they only need to be shown if they are immediately important to the part of the design you are modeling (or, as in my case, describing).

When all the attributes, operations, types and visibilities etc are specified it’s very easy to convert the class into code. For example, C++ code for the RacingCar object looks like this:

class RacingCar

{

private:

 

  vector m_vPosition;

 

  vector m_vVelocity;

 

public:

 

  void Steer(float amount){...}

 

  void Accelerate(float amount){...}

 

  vector GetPosition()const{return m_vPosition;}

};

Relationships

Classes are not much use on their own. In object-oriented design each object usually has a relationship with one or more other objects, such as the child – parent type relationship of inheritance, or the relationship between a class method and its parameters. The following pages describe the notation the UML specifies to denote each particular type of relationship.

Association

An association between two classes represents a connection or link between instances of those classes, and is denoted using a solid line. Unfortunately, at the time of writing, UML practitioners seem unable to agree upon what the italicized text in the previous sentence actually means so I use the assumption that an association is said to exist between two classes if one of them contains a persistent reference to the other.

Figure 6 shows the association between a RacingCar object and a Driver object.

Figure 6. An association.

This class diagram tells us that a racing car is driven by a driver and that a driver drives a racing car. It also tells us that a RacingCar instance maintains a persistent reference to a Driver instance (via a pointer, instance or reference) and vice-versa.  In this example both ends have been explicitly named with a descriptive label called a role name, although much of the time this is not necessary as the role is usually implicit given the names of the classes and the type of association linking them. I prefer to only name the roles when I believe it is absolutely necessary as I feel it makes a complex class diagram simpler to comprehend.

Multiplicity

The end of an association may also have multiplicity, which is an indication of the number of instances participating in the relationship. For instance, a racing car can only have one or zero drivers, and a driver is either driving a car or not. This can be shown as in Figure 7 using 0..1 to specify the range.

Figure 7. An association showing multiplicity.

Figure 8 demonstrates how a RacingCar object can be shown to be associated with any number of Sponsor objects (using an asterisk to indicate infinity as the upper bound of the range), and how a Sponsor can only be associated with one RacingCar at any time.

Figure 8.

Figure 8 shows the long-hand way of specifying an unlimited range and a range of 1 but often (and certainly within any text I write) you will see these relationships expressed in short-hand as shown in Figure 9. The single asterisk denotes an unbounded range between 0 and infinity and the absence of any numbers or an asterisk at the end of an association infers a singular relationship.

Figure 9.

It is also possible for a multiplicity to represent a combination of discrete values. For instance, a car may have two or four doors -- 2, 4.

Given only the associations shown in Figure 9 we can infer how an interface for a RacingCar class might look:

class RacingCar

{

public:

 

  Driver* GetDriver()const;

  void    SetDriver(Driver* pNewDriver);

  bool    isBeingDriven()const;

 

  void    AddSponsor(Sponsor* pSponsor);

  void    RemoveSponsor(Sponsor* pSponsor);

  int     GetNumSponsors()const;

 

  ...

};

Navigability

So far the associations you’ve seen have been bi-directional: a RacingCar knows about a Driver instance, and that Driver instance knows about the RacingCar instance. A RacingCar knows about each Sponsor instance and each Sponsor knows about the RacingCar. Often however, you will need to express a uni-directional association. For example, it’s unlikely that a racing car need be aware of the spectators watching it, but it is important that a spectator is aware of the car it is watching. This is a one-way relationship and is expressed by adding an arrow to the appropriate end of the association. See Figure 10.

Figure 10. A Spectator has a uni-directional association with a RacingCar.

Notice also how the figure clearly shows how a spectator may be watching any number of racing cars.

Shared and Composite Aggregation

Aggregation is a special case of association and denotes the part of relationship. For instance an arm is a part of a body. There are two types of aggregation: shared and composite. Shared aggregation is when the parts can be shared between wholes and composite aggregation is when the parts are owned by the whole.

For example, the mesh (3D polygon model) that describes the shape of a racing car and is textured and rendered to a display can be shared by many racing cars. As a result this can be represented as shared aggregation, which is denoted by a hollow diamond. See Figure 11.

Figure 11. The relationship between a Mesh and a RacingCar is shown as shared aggregation.

Note that shared aggregation implies that when a RacingCar is destroyed its Mesh is not destroyed. (Also note how the diagram shows that a Mesh object knows nothing about a RacingCar object).

Composite aggregation is a much stronger relationship and implies the parts live and die with the whole. Sticking with our racing car example we could say that a chassis has this type of relationship with a car. A chassis is wholly owned by a racing car and is destroyed when the car is destroyed. This kind of relationship is denoted using a filled diamond as shown in Figure 12.

Figure 12. The relationship between a Chassis and a RacingCar is composite aggregation.

There is a very subtle difference between shared aggregation and association. For example in the design discussed thus far the relationship between a Spectator and a RacingCar has been shown as an association but as many different Spectators can watch the same car you might think it’s okay to show the relationship as shared aggregation. However a spectator is not part of the whole of a racing car and the relationship therefore is association not aggregation.

Generalization

Generalization is a way of describing the relationship between classes that have common properties. With regards to C++ generalization describes the is a relationship of inheritance. For example, a design might require that different types of racing car are sub-classed from RacingCar to provide vehicles for specific kinds of races, such as rally, formula 1 or touring. This type of relationship is shown using a hollow triangle on the base class end of the association as shown in Figure 13.

Figure 13. Expressing the base – derived class relationship.

Often in object-oriented design we use the concept of an abstract class to define an interface to be implemented by any subclasses. This is described explicitly by the UML using italicized text for the class name and for any of its abstract operations. Therefore, if RacingCar is to be implemented as an abstract class with one pure virtual method Update, the relationship between it and other racing cars is shown as in Figure 14.

Figure 14. Describing an abstract base class.

Note, some people prefer to make the relationship more explicit by adding “{abstract}” beneath the class name or after any abstract operation’s name.  This is shown in Figure 15.

Figure 15. Describing an abstract base class more explicitly.

 

Dependencies

Often you will find that a class depends on another for some reason, yet the relationship between them is not an association (as defined by the UML). This occurs for all sorts of reasons. For instance, if a method of class A takes a reference to class B as a parameter then A has a dependency on B. Another good example of a dependency is when A sends a message to B via a third party, which would be typical of a design incorporating event handling.

A dependency is shown using a dashed line with an arrow at one end, and can be optionally qualified by a label. Figure 16 shows how a RacingCar has a dependency to a RaceTrack.

Figure 16. The dependency relationship.

Notes

Notes are an additional feature you can use to zoom in on specific features that need further explanation in some way. For instance, I use notes to add pseudo code where necessary. A note is depicted as a rectangle with a ‘folded’ corner and has a dashed line connecting it to the area of interest. Figure 17 shows how a note is used to show how the UpdatePhysics method of a RacingCar iterates through its four wheels, calling each one’s Update method.

Figure 17. A note can be used to provide further detail.

Summing Up

Did you follow all that okay? To test yourself flick back to Figure 1 and see how much sense it makes. If you still find it confusing, read through this appendix again. If you understood it, well done! Now you can get back to the AI!