In this chapter
Classifiers, special properties of attributes and operations, and different kinds of classes
Modeling the semantics of a class
Choosing the right kind of classifier
Classes are indeed the most important building block of any object-oriented system. However, classes are just one kind of an even more general building block in the UML—classifiers. A classifier is a mechanism that describes structural and behavioral features. Classifiers include classes, interfaces, datatypes, signals, components, nodes, use cases, and subsystems.
Classifiers (and especially classes) have a number of advanced features beyond the simpler properties of attributes and operations described in the previous part: You can model multiplicity, visibility, signatures, polymorphism, and other characteristics. In the UML, you can model the semantics of a class so that you can state its meaning to whatever degree of formality you like.
In the UML, there are several kinds of classifiers and classes; it's important that you choose the one that best models your abstraction of the real world.
When you build a house, at some point in the project you'll make an architectural decision about your building materials. Early on, it's sufficient to simply state wood, stone, or steel. That's a level of detail sufficient for you to move forward. The material you choose will be affected by the requirements of your project—steel and concrete would be a good choice if you are building in an area susceptible to hurricanes, for example. As you move forward, the material you choose will affect your design decisions that follow—choosing wood versus steel will affect the mass that can be supported, for example.
As your project continues, you'll have to refine these basic design decisions and add more detail sufficient for a structural engineer to validate the safety of the design and for a builder to proceed with construction. For example, you might have to specify not just wood, but wood of a certain grade that's been treated for resistance to insects.
It's the same when you build software. Early in a project, it's sufficient to say that you'll include a Customer
class that carries out certain responsibilities. As you refine your architecture and move to construction, you'll have to decide on a structure for the class (its attributes) and a behavior (its operations) that are sufficient and necessary to carry out those responsibilities. Finally, as you evolve to the executable system, you'll need to model details, such as the visibility of individual attributes and operations, the concurrency semantics of the class as a whole and its individual operations, and the interfaces the class realizes.
The UML provides a representation for a number of advanced properties, as Figure 9-1 shows. This notation permits you to visualize, specify, construct, and document a class to any level of detail you wish, even sufficient to support forward and reverse engineering of models and code.
A classifier is a mechanism that describes structural and behavioral features. Classifiers include classes, associations, interfaces, datatypes, signals, components, nodes, use cases, and subsystems.
When you model, you'll discover abstractions that represent things in the real world and things in your solution. For example, if you are building a Web-based ordering system, the vocabulary of your project will likely include a Customer
class (representing people who order products) and a Transaction
class (an implementation artifact, representing an atomic action). In the deployed system, you might have a Pricing
component, with instances living on every client node. Each of these abstractions will have instances; separating the essence and the instance of the things in your world is an important part of modeling.
Some things in the UML don't have instances—for example, packages and generalization relationships. In general, those modeling elements that can have instances are called classifiers. Even more important, a classifier has structural features (in the form of attributes) as well as behavioral features (in the form of operations). Every instance of a given classifier shares the same feature definitions, but each instance has its own value for each attribute.
The most important kind of classifier in the UML is the class. A class is a description of a set of objects that share the same attributes, operations, relationships, and semantics. Classes are not the only kind of classifier, however. The UML provides a number of other kinds of classifiers to help you model.
▪ Interface | A collection of operations that are used to specify a service of a class or a component |
▪ Datatype | A type whose values are immutable, including primitive built-in types (such as numbers and strings) as well as enumeration types (such as Boolean) |
▪ Association | A description of a set of links, each of which relates two or more objects. |
▪ Signal | The specification of an asynchronous message communicated between instances |
▪ Component | A modular part of a system that hides its implementation behind a set of external interfaces |
▪ Node | A physical element that exists at run time and that represents a computational resource, generally having at least some memory and often processing capability |
▪ Use case | A description of a set of a sequence of actions, including variants, that a system performs that yields an observable result of value to a particular actor |
▪ Subsystem | A component that represents a major part of a system |
For the most part, every kind of classifier may have both structural and behavioral features. Furthermore, when you model with any of these classifiers, you may use all the advanced features described in this chapter to provide the level of detail you need to capture the meaning of the abstraction.
Graphically, the UML distinguishes among these different classifiers, as Figure 9-2 shows.
A minimalist approach would have used one icon for all classifiers; however, a distinctive visual cue was deemed important. Similarly, a maximal approach would have used different icons for each kind of classifier. That doesn't make sense either because, for example, classes and datatypes aren't that different. The design of the UML strikes a balance—some classifiers have their own icon, and others use special keywords (such as type
, signal
, and subsystem
).
One of the design details you can specify for an attribute or operation is visibility. The visibility of a feature specifies whether it can be used by other classifiers. In the UML, you can specify any of four levels of visibility.
Figure 9-3 shows a mix of public, protected, and private figures for the class Toolbar
.
When you specify the visibility of a classifier's features, you generally want to hide all its implementation details and expose only those features that are necessary to carry out the responsibilities of the abstraction. That's the very basis of information hiding, which is essential to building solid, resilient systems. If you don't explicitly adorn a feature with a visibility symbol, you can usually assume that it is public.
Another important detail you can specify for a classifier's attributes and operations is scope. The scope of a feature specifies whether each instance of the classifier has its own distinct value of the feature or whether there is just a single value of the feature shared by all instances of the classifier. In the UML, you can specify two kinds of owner scope.
Each instance of the classifier holds its own value for the feature. This is the default and requires no additional notation. | |
There is just one value of the feature for all instances of the classifier. This has also been called class scope. This is notated by underlining the feature string. |
As Figure 9-4 (a simplification of the first figure) shows, a feature that is static scoped is rendered by underlining the feature's name. No adornment means that the feature is instance scoped.
In general, most features of the classifiers you model will be instance scoped. The most common use of static scoped features is for private attributes that must be shared among all instances of a class, such as for generating unique IDs for new instances of a class.
Static scope works somewhat differently for operations. An instance operation has an implicit parameter corresponding to the object being manipulated. A static operation has no such parameter; it behaves like a traditional global procedure that has no target object. Static operations are used for operations that create instances or operations that manipulate static attributes.
You use generalization relationships to model a lattice of classes, with more-generalized abstractions at the top of the hierarchy and more-specific ones at the bottom. Within these hierarchies, it's common to specify that certain classes are abstract—meaning that they may not have any direct instances. In the UML, you specify that a class is abstract by writing its name in italics. For example, as Figure 9-5 shows, Icon
, RectangularIcon
, and ArbitraryIcon
are all abstract classes. By contrast, a concrete class (such as Button
and OKButton
) may have direct instances.
Whenever you use a class, you'll probably want to inherit features from other, more-general classes, and have other, more-specific classes inherit features from it. These are the normal semantics you get from classes in the UML. However, you can also specify that a class may have no children. Such an element is called a leaf class and is specified in the UML by writing the property leaf
below the class's name. For example, in the figure, OKButton
is a leaf class, so it may have no children.
Operations have similar properties. Typically, an operation is polymorphic, which means that, in a hierarchy of classes, you can specify operations with the same signature at different points in the hierarchy. An operation in a child class overrides the behavior of a similar operation in the parent class. When a message is dispatched at run time, the operation in the hierarchy that is invoked is chosen polymorphically-that is, a match is determined at run time according to the type of the object. For example, display
and isInside
are both polymorphic operations. Furthermore, the operation Icon::display()
is abstract, meaning that it is incomplete and requires a child to supply an implementation of the operation. In the UML, you specify an abstract operation by writing its name in italics, just as you do for a class. By contrast, Icon::getID()
is a leaf operation, so designated by the property leaf
. This means that the operation is not polymorphic and may not be overridden. (This is similar to a Java final
operation.)
Whenever you use a class, it's reasonable to assume that there may be any number of instances of that class (unless, of course, it is an abstract class and may not have any direct instances, although there may be any number of instances of its concrete children). Sometimes, though, you'll want to restrict the number of instances a class may have. Most often, you'll want to specify zero instances (in which case, the class is a utility class that exposes only static-scoped attributes and operations), one instance (a singleton class), a specific number of instances, or many instances (the default case).
The number of instances a class may have is called its multiplicity. Multiplicity is a specification of the range of allowable cardinalities an entity may assume. In the UML, you can specify the multiplicity of a class by writing a multiplicity expression in the upper-right corner of the class icon. For example, in Figure 9-6, NetworkController
is a singleton class. Similarly, there are exactly three instances of the class ControlRod
in the system. Multiplicity applies to attributes, as well. You can specify the multiplicity of an attribute by writing a suitable expression in brackets just after the attribute name. For example, in the figure, there are two or more consolePort
instances in the instance of NetworkController
.
At the most abstract level, when you model a class's structural features (that is, its attributes), you simply write each attribute's name. That's usually enough information for the average reader to understand the intent of your model. As the previous parts have described, however, you can also specify the visibility, scope, and multiplicity of each attribute. There's still more. You can also specify the type, initial value, and changeability of each attribute.
In its full form, the syntax of an attribute in the UML is
[visibility] name [':' type] ['[' multiplicity] ']'] ['=' initial-value] [property-string {',' property-string}]
For example, the following are all legal attribute declarations:
▪ | Name only |
▪ | Visibility and name |
▪ | Name and type |
▪ | Name, type, and multiplicity |
▪ | Name, type, and initial value |
▪ | Name and property |
Unless otherwise specified, attributes are always changeable. You can use the readonly
property to indicate that the attribute's value may not be changed after the object is initialized.
You'll mainly want to use readonly
when modeling constants or attributes that are initialized at the creation of an instance and not changed thereafter.
At the most abstract level, when you model a class's behavioral features (that is, its operations and its signals), you will simply write each operation's name. That's usually enough information for the average reader to understand the intent of your model. As the previous parts have described, however, you can also specify the visibility and scope of each operation. There's still more: You can also specify the parameters, return type, concurrency semantics, and other properties of each operation. Collectively, the name of an operation plus its parameters (including its return type, if any) is called the operation's signature.
The UML distinguishes between operation and method. An operation specifies a service that can be requested from any object of the class to affect behavior; a method is an implementation of an operation. Every nonabstract operation of a class must have a method, which supplies an executable algorithm as a body (generally designated in some programming language or structured text). In an inheritance lattice, there may be many methods for the same operation, and polymorphism selects which method in the hierarchy is dispatched during run time.
In its full form, the syntax of an operation in the UML is
[visibility] name ['(' parameter-list ')'] [':' return-type] [property-string {',' property-string}]
For example, the following are all legal operation declarations:
▪ | Name only |
▪ | Visibility and name |
▪ | Name and parameters |
▪ | Name and return type |
▪ | Name and property |
In an operation's signature, you may provide zero or more parameters, each of which follows the syntax
[direction] name : type [= default-value]
Direction may be any of the following values:
An input parameter; may not be modified | |
An output parameter; may be modified to communicate information to the caller | |
An input parameter; may be modified to communicate information to the caller |
An out or inout parameter is equivalent to a return parameter and an in
parameter. Out
and inout
are provided for compatibility with older programming languages. Use explicit return parameters instead.
In addition to the leaf
and abstract
properties described earlier, there are defined properties that you can use with operations.
Execution of the operation leaves the state of the system unchanged. In other words, the operation is a pure function that has no side effects. | |
Callers must coordinate outside the object so that only one flow is in the object at a time. In the presence of multiple flows of control, the semantics and integrity of the object cannot be guaranteed. | |
The semantics and integrity of the object is guaranteed in the presence of multiple flows of control by sequentializing all calls to all of the object's guarded operations. In effect, exactly one operation at a time can be invoked on the object, reducing this to sequential semantics. | |
The semantics and integrity of the object is guaranteed in the presence of multiple flows of control by treating the operation as atomic. Multiple calls from concurrent flows of control may occur simultaneously to one object on any concurrent operation, and all may proceed concurrently with correct semantics; concurrent operations must be designed so that they perform correctly in case of a concurrent sequential or guarded operation on the same object. | |
The operation does not have an implicit parameter for the target object; it behaves like a traditional global procedure. |
The concurrency properties (sequential
, guarded
, concurrent
) address the concurrency semantics of an operation, properties that are relevant only in the presence of active objects, processes, or threads.
A template is a parameterized element. In such languages as C++ and Ada, you can write template classes, each of which defines a family of classes. (You can also write template functions, each of which defines a family of functions.) A template may include slots for classes, objects, and values, and these slots serve as the template's parameters. You can't use a template directly; you have to instantiate it first. Instantiation involves binding these formal template parameters to actual ones. For a template class, the result is a concrete class that can be used just like any ordinary class.
The most common use of template classes is to specify containers that can be instantiated for specific elements, making them type-safe. For example, the following C++ code fragment declares a parameterized Map
class.
template<class Item, class VType, int Buckets> class Map { public: virtual map(const Item&, const VType&); virtual Boolean isMappen(const Item&) const; ... };
You might then instantiate this template to map Customer
objects to Order
objects.
m : Map<Customer, Order, 3>;
You can model template classes in the UML as well. As Figure 9-7 shows, you render a template class just as you do an ordinary class, but with an additional dashed box in the upper-right corner of the class icon, which lists the template parameters.
As the figure goes on to show, you can model the instantiation of a template class in two ways. First, you can do so implicitly, by declaring a class whose name provides the binding. Second, you can do so explicitly, by using a dependency stereotyped as bind
, which specifies that the source instantiates the target template using the actual parameters.
All of the UML's extensibility mechanisms apply to classes. Most often, you'll use tagged values to extend class properties (such as specifying the version of a class) and stereotypes to specify new kinds of components (such as model- specific components).
The UML defines four standard stereotypes that apply to classes.
Specifies a classifier whose objects are all classes | |
Specifies a classifier whose objects are classes that are the children of a given parent class | |
Specifies that the classifier is a stereotype that may be applied to other elements | |
Specifies a class whose attributes and operations are all static scoped |
The most common purpose for which you'll use classes is to model abstractions that are drawn from the problem you are trying to solve or from the technology you are using to implement a solution to that problem. Once you've identified those abstractions, the next thing you'll need to do is specify their semantics.
In the UML, you have a wide spectrum of modeling possibilities at your disposal, ranging from the very informal (responsibilities) to the very formal (OCL—Object Constraint Language). Given these choices, you must decide the level of detail that is appropriate to communicate the intent of your model. If the purpose of your model is to communicate with end users and domain experts, you'll tend to lean toward the less formal. If the purpose of your model is to support round-trip engineering, which flows between models and code, you'll tend to lean toward the more formal. If the purpose of your model is to rigorously and mathematically reason about your models and prove their correctness, you'll lean toward the very formal.
Less formal does not mean less accurate. It means less complete and less detailed. Pragmatically, you'll want to strike a balance between informal and very formal. This means providing enough detail to support the creation of executable artifacts, but still hiding those details so that you do not overwhelm the reader of your models.
To model the semantics of a class, choose among the following possibilities, arranged from informal to formal.
Specify the responsibilities of the class. A responsibility is a contract or obligation of a type or class and is rendered in a note attached to the class, or in an extra compartment in the class icon.
Specify the semantics of the class as a whole using structured text, rendered in a note (stereotyped as semantics
) attached to the class.
Specify the body of each method using structured text or a programming language, rendered in a note attached to the operation by a dependency relationship.
Specify the pre- and postconditions of each operation, plus the invariants of the class as a whole, using structured text. These elements are rendered in notes (stereotyped as precondition
, postcondition
, and invariant
) attached to the operation or class by a dependency relationship.
Specify a state machine for the class. A state machine is a behavior that specifies the sequences of states an object goes through during its lifetime in response to events, together with its responses to those events.
Specify internal structure of the class.
Specify a collaboration that represents the class. A collaboration is a society of roles and other elements that work together to provide some cooperative behavior that's bigger than the sum of all the elements. A collaboration has a structural part as well as a dynamic part, so you can use collaborations to specify all dimensions of a class's semantics.
Specify the pre- and postconditions of each operation, plus the invariants of the class as a whole, using a formal language such as OCL.
Pragmatically, you'll end up doing some combination of these approaches for the different abstractions in your system.
When you specify the semantics of a class, keep in mind whether your intent is to specify what the class does or how it does it. Specifying the semantics of what a class does represents its public, outside view; specifying the semantics of how a class does it represents its private, inside view. You'll want to use a mixture of these two views, emphasizing the outside view for clients of the class and emphasizing the inside view for those who implement the class.
When you model classifiers in the UML, remember that there is a wide range of building blocks at your disposal, from interfaces to classes to components, and so on. You must choose the one that best fits your abstraction. A well-structured classifier
Has both structural and behavioral aspects.
Is tightly cohesive and loosely coupled.
Exposes only those features necessary for clients to use the class and hides all others.
Is unambiguous in its intent and semantics.
Is not so overly specified that it eliminates all degrees of freedom for its implementers.
Is not so underspecified that it renders the meaning of the classifier as ambiguous.
When you draw a classifier in the UML,
Show only those properties of the classifier that are important to understand the abstraction in its context.
Chose a stereotyped version that provides the best visual cue to the intent of the classifier.