Roles and Activities > Developer Role Set > Software Architect > Identify Design Elements
The Activity: Use Case Analysis results in analysis classes, which represent conceptual things which can perform behavior. In design, analysis classes evolve into a number of different kinds of design elements:
In addition, in design we shall also identify:
These finer distinctions enable us to examine different aspects of the design:
By separating concerns and handling each issue represented by these concepts separately, we simplify the design process and clarify our solution.
Events are external and internal occurrences which cause some action within the system; they may occur at random and potentially independently of one another. The need to respond to events in a timely manner is a primary driver for concurrency requirements in the system. The determination of the events to which the system must respond and the characterization of these events will drive the identification of essential parts of the architecture of the system.
To identify events to which the system must respond, look to the use-cases and their associated realizations. For each stimulus sent from an actor to the system, consider how the system becomes aware of the stimulus: is the system polling the user, waiting for a response, or is the receipt triggered by a device or sensor which detects the event?
System interrupts are a typical mechanism by which the system becomes aware, asynchronously, that something interesting has occurred. Interrupts are most useful when they are used to communicate information about aperiodic events (things that do not occur on regular intervals) that require immediate response. Interrupts may lead to the occurrence of a call event or signal event, or, if the interrupt came from a clock or timer, to a time event.
Polling is more appropriate for cases where the events occur on a natural, periodic cycle or occur at random but more or less continuously, and where the system does not have to respond absolutely immediately. For these kinds of events, the system can buffer the events and respond to them on a periodic interval; use of interrupts would be costly and wasteful. Polling may also be used to detect a the satisfaction of some condition, which then leads to the occurrence of a change event.
An important aspect of the architecture is to determine which events must be responded to immediately, and how the system will communicate the arrival of those events, i.e. to characterize the event as a call event (where the caller waits for the event to be handled), a change event (signifying a state change in some Boolean condition), a signal event (signifying the receipt of a signal entity for asynchronous communication, where the sender does not lose control when the signal is sent), or a time event (signifying that some absolute time has been reached or that some time period has elapsed). The choice of event kind will strongly influence the shape of the architecture.
For events which do not require immediate response, the software architect must determine which can be responded to periodically and on what intervals the system should service the events (different events may require different service intervals). This may be represented by properties on the event itself representing the priority, or latency that can be tolerated, and expected mean frequency and frequency distribution of the event.
A signal is an explicit named entity intended for explicit, asynchronous communication between objects - and often (but not exclusively) the receiving object, and possibly the sending object, will be modeled as active objects. A signal has an explicit list of parameters, the values for which are specified by the sender at the time the instance of the signal is sent. A signal event is the receipt by an object of a signal sent to it, and a signal event instance is associated with just one signal instance.
Signals are used to communicate between internal parts of the system, and may be used to communicate faults or exceptions, although they can be used in any circumstance where asynchronous communication is required. Signals are also typically implemented using interrupts, though polling techniques may be used as well. The importance of identifying signals in the architecture is to establish common rules for how and when signals will be used, and to identify which signals will use which signaling mechanisms (e.g. which specific interrupts or polling messages). Failure to agree on these can be quite disastrous and may lead to highly unpredictable system behavior.
Note that a signal event is handled by a state machine, and the receipt of a signal may or may not trigger a transition in the machine (depending on the state at the time the signal is received), and cause an action to be executed.
In real-time systems, capsules are restricted to using signals for inter-capsule communication, and synchronous communication can be achieved by the use of pairs of signals, one in each direction.
Identify Classes. When the analysis class is simple and already represent a single logical abstraction, it can be directly mapped, 1:1, to a design class. Typically, entity classes survive relatively intact into Design. Since entity classes are typically also persistent, determine whether the design class should be persistent and note it accordingly in the class description.
When identifying classes, they should be grouped into Artifact: Design Packages, for organizational and configuration management purposes. See Guidelines: Design Package for more information on how to make packaging decisions.
Identify Active Classes. Consider the concurrency requirements of the system in the context of the analysis objects identified: is there a need for the system to respond to externally generated events, and if so, which analysis classes are 'active' when the events occur? External events in the Use-Case Model are represented by stimuli coming from actors, interacting with a use case. Look at the corresponding Use-Case Realizations to see which objects interact when an event occurs. Start by grouping the objects together into autonomous sets of collaborating objects - these groupings represent an initial cut at a group that may form a composite active class.
In real-time systems, these identified sets of objects should be grouped into capsules, which have strong encapsulation semantics.
The instances of active classes represent independent 'logical' threads of execution. These 'logical' threads of execution are not to be confused with or mapped literally to threads of execution in the operating system (though at some point we will map them to operating system threads of execution). Instead, they represent independent conceptual threads of execution in the solution space. Our goal in identifying them at this point in design is to be able to partition the solution into independent units based on natural 'concurrency seams' in the system. Dividing the work in this way makes the problems of dealing with concurrency conceptually simpler, since independent threads of execution can be dealt with separately except to the extent that they share underlying passive classes.
In general, an active class should be considered whenever there exist concurrency and concurrency conflicts in the problem domain. An active class should be used to represent some external concurrent object or concurrent activity within the computer. This gives us the ability to monitor and control concurrent activities.
Another natural choice is to use active classes as internal representatives of external physical devices that are connected to a computer since those physical entities are inherently concurrent. These "device driver" classes serve not only to monitor and control the corresponding physical devices but they also isolate the rest of the system from the specifics of the devices. This means that the rest of the system may not be affected even if the technology behind the devices evolves.
Another common place for using active classes is to represent logical concurrent activities. A logical activity represents a conceptual concurrent "object", such as, for example, a financial transaction or a telephone call. Despite the fact that these are not directly manifested as physical entities (although they take place in the physical world), there are often reasons to treat them as such. For instance, we may need to temporarily hold back a particular financial transaction to avoid a concurrency conflict or we may need to abort it due to failures within the system. Since these conceptual objects need to be manipulated as a unit, it is convenient to represent them as objects with interfaces of their own that provide the appropriate functional capabilities.
A particular example of this type of conceptual object is an active object controller. Its purpose is to continuously manage one or more other active objects. This normally involves bringing each object into the desired operational state, maintaining it in that state in the face of various disruptions such as partial failures, and synchronizing its operation with the operation of other objects. These active object controllers often evolve from Control objects identified during Activity: Use-Case Analysis.
Because of their capacity to simply and elegantly resolve concurrency conflicts, active classes are also useful as guardians of shared resources. In this case, one or more resources that are required by multiple concurrent activities are encapsulated within an active class. By virtue of their built-in mutual exclusion semantics, such guardians automatically protect these resources against concurrency conflicts.
For real-time systems, capsules should be used in place of active classes: wherever you identified the need for an active class according to the heuristics described above, a capsule should be substituted.
Identify Subsystems. When the analysis class is complex, such that it appears to embody behaviors that cannot be the responsibility of a single class acting alone, the analysis class should be mapped to a design subsystem. The design subsystem is used to encapsulate these collaborations in such a way that clients of the subsystem can be completely unaware of the internal design of the subsystem, even as they use the services provided by the subsystem. A design subsystem is logically equivalent to the Artifact: Component in the Artifact: Implementation Model.
A subsystem is, effectively, a special kind of package which has only interfaces as public elements. The interfaces provide a layer of encapsulation, allowing the internal design of the subsystem to remain hidden from other model elements. The concept subsystem is used to distinguish it from "ordinary" packages, which are semantic-free containers of model elements; the subsystem represents a particular usage of packages with class-like (behavioral) properties.
The decision to create a subsystem from a set of collaborating analysis classes is based largely on whether the collaboration can be or will be developed independently by a separate design team. If the collaborations can be completely contained within a package along with the collaborating classes, a subsystem can provide a stronger form of encapsulation than that provided by a simple package. The contents and collaborations within a subsystem are completely isolated behind one or more interfaces, so that the client of the subsystem is only dependent upon the interface. The designer of the subsystem is then completely isolated from external dependencies; the designer (or design team) is required to specify how the interface is realized, but they are completely free to change the internal subsystem design without affecting external dependencies. In large systems with largely independent teams, this degree of de-coupling combined with the architectural enforcement provided by formal interfaces is a strong argument for the choice of subsystems over simple packages. See Guidelines: Design Subsystem for more information about the factors which affect the choice to use subsystems as design elements.
Interfaces define a set of operations which are realized by some classifier. In the Design Model, interfaces are principally used to define the interfaces for subsystems. This is not to say that they cannot be used for classes as well, but for a single class it is usually sufficient to define public operations on the class which, in effect, define its 'interface'. Interfaces are important for subsystems because they allow the separation of the declaration of behavior (the interface) from the realization of behavior (the specific classes within the subsystem which realize the interface). This de-coupling provides us with a way to increase the independence of development teams working on different parts of the system, while retaining precise definitions of the 'contracts' between these different parts.
For each subsystem, identify a set of candidate interfaces. Using the grouped collaborations identified in the previous step, identify the responsibility which is 'activated' when the collaboration is initiated. This responsibility is then refined by determining what information must be provided by the 'client' and what information is returned when the collaboration is complete; these sets of information become the prototype input and output parameters and return value for an operation which the subsystem will realize. Define a name for this operation, using the naming conventions defined in the Artifact: Design Guidelines. Repeat this until all operations which will be realized by the subsystem have been defined.
Next, group operations together according to their related responsibilities. Smaller groups are preferable to larger groups, since it is more likely that a cohesive set of common responsibilities will exist if there are fewer operations in the group. Keep an eye toward reuse as well - look for similarities that may make it easier to identify related reusable functionality. At the same time, though, don't spend a great deal of time trying to find the ideal grouping of responsibilities; remember, this is just a first-cut grouping and refinement will proceed iteratively throughout the elaboration phase.
Look for similarities between interfaces. From the candidate set of interfaces, look for similar names, similar responsibilities, and similar operations. Where the same operations exist in several interfaces, re-factor the interfaces, extracting the common operations into a new interface. Be sure to look at existing interfaces as well, re-using them where possible. The goal is to maintain the cohesiveness of the interfaces while removing redundant operations between interfaces. This will make the interfaces easier to understand and evolve over time.
Define interface dependencies. The parameters and return value of each interface operation each have a particular type: they must realize a particular interface, or they must be instances of a simple data type. In cases where the parameters are objects that realize a particular interface, define dependency relationships between the interface and the interfaces on which it depends. Defining the dependencies between interfaces provides useful coupling information to the software architect, since interface dependencies define the primary dependencies between elements in the design model.
Map the interfaces to subsystems. Once interfaces have been identified, create realization associations between the subsystem and the interfaces it realizes. A realization from the subsystem to an interface indicates that there are one or more elements within the subsystem that realize the operations of the interface. Later, when the subsystem is designed, these subsystem-interface realizations will be refined, with the subsystem designer specifying which specific elements within the subsystem realize the operations of the interface. These refined realizations are visible only to the subsystem designer; from the perspective of the subsystem client, only the subsystem-interface realization is visible.
Define the behavior specified by the interfaces. Interfaces often define an implicit state machine for the elements that realize the interface. If the operations on the interface must be invoked in a particular order (e.g. the database connection must be opened before it can be used), a state machine that illustrates the publicly visible (or inferred) states that any design element that realizes the interface must support should be defined. This state machine will aid the user of the interface to better understand the interface, and will aid the designer of elements which realize the interface to provide the correct behavior for their element.
Package the interfaces. Interfaces are owned by the software architect; changes to interfaces are always architecturally significant. To manage this, the interfaces should be grouped into one or more packages owned by the software architect. If each interface is realized by a single subsystem, the interfaces can be placed within the facade of the subsystem. If the interfaces are realized by more than one subsystem, they should be placed within a separate package owned by the software architect. This allows the interfaces to be managed and controlled independently of the subsystems themselves.
Protocols are similar to interfaces in event-driven systems: they identify the 'contract' between capsules by defining a matched set of signals which are used to communicate between independent threads of control. While interfaces are primarily used to define synchronous messaging using a function call model of invocation, protocols are primarily used to define asynchronous communication using signal-based messaging. Protocols allow the separation of the declaration of behavior (the set of signals) from the realization of behavior (the elements within the subsystem which realize the interface). This de-coupling provides us with a way to increase the independence of development teams working on different parts of the system, while retaining precise definitions of the 'contracts' between these different parts.
For each capsule, identify a set of in and out signals. Using the grouped collaborations identified in earlier steps, identify the responsibility which is 'activated' when the collaboration is initiated. This responsibility is then refined by determining what information must be provided by the 'client' and what information is returned when the collaboration is complete; these sets of information become the prototype input parameters for a signal which the capsule will realize through one of its ports. Define a name for this signal, using the naming conventions defined in the Artifact: Design Guidelines. Repeat this until all signals which will be realized by the capsule have been defined.
Next, group signals together according to their related responsibilities. Smaller groups are preferable to larger groups, since it is more likely that a cohesive set of common responsibilities will exist if there are fewer signals in the group. Keep an eye toward reuse as well - look for similarities that may make it easier to identify related reusable functionality. At the same time, though, don't spend a great deal of time trying to find the ideal grouping of responsibilities; remember, this is just a first-cut grouping and refinement will proceed iteratively throughout the elaboration phase. Give the protocol a meaningful name, one that describes the role the protocol plays in capsule collaborations.
Look for similarities between protocols. From the candidate set of protocols, look for similar names, similar responsibilities, and similar signals. Where the same signals exist in several protocols, re-factor the protocols, extracting the common signals into a new interface. Be sure to look at existing protocols as well, re-using them where possible. The goal is to maintain the cohesiveness of the protocols while removing redundant signals between protocols. This will make the protocols easier to understand and evolve over time.
Map the protocols to capsules. Once protocols have been identified, create ports on the capsules which realize the protocols. The ports of the capsule define its 'interfaces', the behavior that can be requested from the capsule. Later, when the capsule is designed, the behavior specified by the ports will be described by the state machine for the capsule.
Define the behavior specified by the protocols. Protocols often define an implicit state machine for the elements that realize the interface. If the input signals on the interface must be received in a particular order (e.g. a 'system-ready' signal must be received before a particular error signal can be received), a state machine that illustrates the publicly visible (or inferred) states that any design element that realizes the protocol must support should be defined. This state machine will aid the user of the capsules which realize the protocol to better understand their behavior, and will aid the designer of capsules to provide the correct behavior for their element.
Package the protocols. Protocols are owned by the software
changes to protocols are always architecturally significant. To manage this, the
protocols should be grouped into one or more packages owned by the software architect.
This allows the protocols to be managed and controlled independently of the
capsules which realize the protocols.
Rational Unified Process