As a software program grows in complexity, it can easily contain hundreds of classes. If you’re a programmer working with such a class library, how do you make sense of it? One way to impose structure is by organizing your classes into logically related groups. Classes concerned with an application’s user interface can belong to one group, and utility classes can belong to another.
In UML, groups of classes are modeled with packages . Most object-oriented languages have an analog of UML packages to organize and avoid name collision among classes. For example, Java has packages, C# has namespaces (although Java packages, and C# namespaces differ significantly in other details). You can use UML packages to model these structures.
Package diagrams are often used to view dependencies among packages. Since a package can break if another package on which it depends changes, understanding dependencies between packages is vital to the stability of your software.
Packages can organize almost any UML element—not just classes. For example, packages are also commonly used to group use cases. Package diagrams form part of the development view, which is concerned with how your system’s parts are organized into modules and packages, as shown in Figure 13-1.
Suppose that during the design of a CMS, you decide to keep classes related to security (for example, performing user authentication) grouped together. Figure 13-2 shows the security
package and a few other packages from the CMS in UML. The symbol for a package is a folder with a tab. The name of the package is written inside the folder.
Packages organize UML elements, such as classes, and the contents of a package can be drawn inside the package or outside the package attached by a line, as shown in Figure 13-3. If you draw the elements inside the package, write the name of the package in the folder tab.
The notation shown in Figure 13-3 is used to model Java classes belonging to a Java package. In Java, the package
keyword at the beginning of a class specifies that a class is in a package. Example 13-1 shows a Java code sample corresponding to the Credentials
class in Figure 13-3.
package security;
public class Credentials {
...
}
Packages can also contain other packages, as shown in Figure 13-4.
It’s common to see deeply nested
packages in enterprise applications. Java applications typically use the URL-in-reverse package naming convention (omitting the www part of the URL). For example, the ACME company with the URL http://www.acme.com would put all its packages under the acme
package, which is under com
, as shown in Figure 13-5.
Even at this point, these packages consume a lot of space, and if you want to show classes inside the indexing package, each package containing it would have to expand in size accordingly. Luckily, there’s an alternate notation that can be easier to work with. You can “flatten” nested packages to write them as
, and so on. This converts Figure 13-5 into the less cluttered Figure 13-6.packageA::packageB::packageC
Currently, a small amount of UML tools don’t support the notations shown in Figure 13-3. However, almost all tools can show that a class belongs to a package using one of the notations shown in Figure 13-7. The notation to the far right is the standard UML namespace notation, discussed next in “Namespaces and Classes Referring to Each Other.”
To specify the package that a class belongs to, most UML tools allow you to enter the package name in a class specification dialog or manually drag the class into the package it belongs to in a tree display of the model elements.
Breaking up your classes into packages introduces some bookkeeping. If you’re a Java programmer, you may have encountered a related issue before. To use an ArrayList
in a Java program, you have to specify that ArrayList
is located in the java.util
package. That is because Java packages define their own namespaces, or naming contexts. If an item is not in the current namespace, you have to specify where it is located.
Similarly, a UML package establishes a namespace. So, if an element in a package wants to use an element in a different package, it has to specify where the desired element is. To specify the context of a UML element, you provide the fully-scoped name, which includes the package name and the element name separated by double colons, as in packageName
::
. The fully-scoped name for the class className
Credentials
belonging to the package security
is security::Credentials
. If you have two classes with the same name in different packages, using the fully-scoped name allows you to distinguish between them.
Elements in a namespace must have unique names. This means the security
package cannot have two classes named Credentials
, but there can be two classes called Credentials
belonging to separate packages, for example security
and utils
. As discussed previously in “UML Tool Variation,” your UML tool may display the classes in Figure 13-8 differently.
Why does this matter? To specify that a class has a relationship with another class, you may have to specify a namespace.
Classes in the same package are part of the same namespace, so they can refer to each other without using fully-scoped names. Since they are in the same package, IdentityVerifier
can have an attribute of type Credentials
and not have to specify the package (see Figure 13-9).
On the other hand, a class outside the security
package, such as User
, would have to provide a scope when accessing Credentials
, which it can do by using the fully-scoped name—security::Credentials
. Later, in "Importing and Accessing Packages,” you’ll see that there are other ways to provide scope when accessing a class in a different package.
In Java, a fully-scoped name corresponds to specifying the Java package, e.g., security.Credentials
instead of just Credentials
.
In UML, elements in a nested
package can refer to elements in the containing package without scoping the name, which in Figure 13-10 means that an element in indexing
could refer to an element in search
without using the fully-scoped name.
The implication that elements in nested packages have automatic access to elements in containing packages doesn’t match with some implementation languages. For example, in Java, if a class in the indexing
package uses a class in the search
package, it has to provide a scope either by using its fully-qualified name or by importing the search
package. Despite the fact that UML semantics of nested packages differ from Java packages, you could still use Figure 13-10 to model a package search.indexing
in a Java system.
Elements in a package may have public or private visibility . Elements with public visibility are accessible outside the package. Elements with private visibility are available only to other elements inside the package. You can model public or private visibility in UML by writing a plus or minus symbol in front of the element’s name, as shown in Figure 13-11.
In Java, public and private visibility corresponds to a class being public or private to a Java package. A Java class is marked as public to a package by the public
access modifier, as in:
public
class Credentials {}
If the public keyword is absent, then the class is private to the package. Many UML tools don’t offer the plus and minus symbols to show element visibility, so don’t be surprised if yours doesn’t.
The previous sections showed that sometimes a class in one package needs to use a class in another package. This causes a dependency between packages
: if an element in package A
uses an element in package B
, then package A
depends on package B
, as shown in Figure 13-12.
Understanding the dependencies among your packages is useful for analyzing the stability of your software, as discussed in “Managing Package Dependencies.” In fact, the most common use of UML package diagrams is to give an overview of the core packages in your software and the dependencies among them, as shown in Figure 13-13.
"Managing Package Dependencies,” later in this chapter, revisits package dependency diagrams, showing you how to use them to understand and improve the stability of your software.
When a package imports another package, elements in the importing package can use elements in the imported package without having to use their fully scoped names. This feature is similar to a Java import, in which a class can import a package and use its contents without having to provide their package names.
In an import relationship, the imported package is referred to as the target package
. To show the import relation, draw a dependency arrow from the importing package to the target package with the stereotype import
(see Figure 13-14).
A package can also import a specific element in another package instead of the whole package, as shown in Figure 13-15.
When importing a package, only public elements of the target package are available in the importing namespace. For example, in Figure 13-16, elements in users
can see Credentials
and IdentityVerifier
but not MD5Crypt
.
Not only do elements have visibility—the import relation itself has visibility. An import can be a public import
or private import
with public as the default. A public import means imported elements have public visibility inside the importing namespace; a private import means imported elements have private visibility inside the importing namespace. You show a private import with the stereotype access
instead of import
.
The difference between import
and access
arises when a package imports a package that imports or accesses others. Imported elements have public visibility in the importing package, so they get passed on with further imports, whereas accessed elements do not.
In Figure 13-17, package B
imports C
and accesses D
, so B
can see public elements in C
and D
. A
imports B
, so A
can see public elements in B
. A
can also see public elements in C
because C
is publicly imported into B
, but A
cannot see anything in D
because D
is privately imported into B
.
Import and access relationships can be used to model the programming world concepts of importing of classes into another namespace so that elements in the importing namespace may refer to elements in the target namespace without scoping the name. For example, the package relationships in Figure 13-14 could be used to model the Java code example in Example 13-2.
package users;
// importing all public elements in the security package
import security.*;
class User {
Credentials
credentials;
...
}
The element import in Figure 13-15 corresponds to the Java implementation shown in Example 13-3.
package users;
// importing only the Credentials class
import security.Credentials;
class User {
Credentials
credentials;
...
}
Many modelers don’t bother with specifying the import and access relationships, and instead show generic package dependencies, discussed earlier in "Package Dependency.”
Having complicated dependencies among packages can lead to brittle software since changes in one package can cause its dependent packages to break. Figure 13-18 shows a dependency disaster: a change in any one package could ultimately affect every other package.
Robert C. Martin’s Agile Software Development (Prentice Hall) establishes principles and metrics regarding dependencies between packages and deployment modules. A couple of these, such as avoiding cyclical package dependencies and depending in the “direction of stability,” can be investigated by looking at package diagrams.
If you have cycles in your dependencies, you can break the cycles in different ways. You could factor out a new package that both packages can depend on or you could decide that all the classes really belong together anyway, as shown in Figure 13-19.
Depending in the order of stability means that a package should depend only on packages more stable than itself. An unstable package depends on many other packages; a stable package depends on few packages. Studying package diagrams can help you spot potentially vulnerable designs resulting from the core packages of your system (such as those containing interfaces) depending on unstable packages.
Just as packages group classes of similar functionality, packages also group other UML elements such as use cases. Figure 13-20 shows some use case packages from a CMS.
Rolling up use cases into higher levels of your system can help organize your model, allowing you to see which actors interact with which portions of the system, as shown in Figure 13-21.
Packages are used to group UML elements such as classes and use cases. You may want to review those chapters for more detail about showing the contents of a package. Class diagrams are covered in Chapter 4; use case diagrams are covered in Chapter 2.
One of the most important applications of package diagrams is to view dependencies in your system. Other important high-level system diagrams include component diagrams, which show the key software pieces, and deployment diagrams, which show how the pieces get deployed to hardware. Component diagrams are described in Chapter 12; deployment diagrams are covered in Chapter 15.