Lecture
Overview
The micro process of object-oriented development is driven by the stream of scenarios and architectural products that are generated and successively refined in the macro process. The micro process is, for the most part, the daily labor of an individual developer or a small team of developers.
The micro process applies equally to the programmer and to the architect of a software system. From the programmer's point of view, the micro process offers guidance in making the countless daily tactical decisions that are part of the process of creating and tuning the system's architecture. From the architect's point of view, the micro process is the basis for evolving the architecture and trying out alternatives.
In the micro process the traditional phases of analysis and design are deliberately intermingled, and management is carried out "as far as possible." As Stroustrup notes, "there are no recipes that could replace intelligence, experience, and good taste in design and programming... The various phases of a software project, such as design, programming, and testing, are inseparable from one another" [11].
As shown in Figure 6-1, the micro process usually consists of the following activities:
Now let us examine each of these activities in detail.
Identifying classes and objects
Purpose. The purpose of identifying classes and objects is to find the boundaries of the problem domain. In addition, this activity is the first step in thinking through the object-oriented decomposition of the system being developed.
We apply this step in analysis, when we discover the abstractions that make up the vocabulary of the problem domain and bound our problem by deciding what is important and what is not. Such actions are necessary in design, when we invent new abstractions that are constituent parts of the solution. Moving on to implementation, we apply the identification procedure to invent simple abstractions from which more complex ones are built, and to discover commonalities among existing abstractions in order to simplify the system's architecture.
Products. The main product of this step is a data dictionary that is updated as the project evolves. At first it is enough to compile a list of the cast of characters, consisting of all the noteworthy classes and objects, named with names that reflect their meaning [12]. As the dictionary grows, one can build a simple database, or a more specialized design tool that directly supports the chosen development method [Formally, the data dictionary of an object-oriented development should contain the specifications of every element of the architecture]. In its more formal varieties, the data dictionary serves as a subject index to all the other components of the project, including the diagrams and specifications of the object-oriented design notation.

Figure 6-1. The micro process.
Thus, the data dictionary is the central repository of the abstractions relating to the system. At first it is acceptable to keep the data dictionary open to change: some characters may turn out to be classes, some objects, others attributes, and still others simply synonyms of other abstractions. Gradually the contents of the dictionary are refined by introducing new abstractions, eliminating superfluous ones, and merging similar ones.
Creating a data dictionary at this step yields three essential benefits. First, working with it helps develop a common and exhaustive terminology that can be used throughout the project. Second, the dictionary is a natural table of contents for all the project's materials and a system of entry points for accessing the project in arbitrary order. This is especially useful when a new developer joins the team and must quickly come up to speed. Third, the data dictionary allows the architect to take in the whole project at a single glance, which may lead to the discovery of new commonalities that might otherwise be missed.
Activities. As we described in Chapter 4, identifying classes and objects involves two kinds of creative acts: discovery and invention.
Not every member of the team must be equally skilled at everything. Analysts, especially those working with domain experts, must be good at discovering abstractions, that is, at finding meaningful classes and objects in the problem domain. Meanwhile, architects and senior developers invent classes and objects that solve purely programming problems. We will discuss the nature of these creative acts in the next chapter.
In any case, the basis for identifying classes and objects is the classification methods described in Chapter 4. The usual order of activities is as follows:
In each of these approaches, CRC cards are an effective catalyst for "brainstorming" and help to knit the team more closely together by pushing its members toward communication [This is terribly banal, but some software designers really are not very sociable].
Some classes and objects will be defined incorrectly early in the project's life cycle, but this is not always bad. Many of the tangible things and roles that we enumerate in the life cycle will make it all the way through to implementation - so fundamental are they to our conceptual model. As we come to understand the problem, we will probably change the boundaries of some abstractions, redistributing responsibilities, merging similar ones or (most often) breaking large abstractions into groups of collaborating ones, thereby forming certain mechanisms of the solution.
Milestones and measures. We will successfully complete this phase when we have a reasonably stable data dictionary. Since the micro process evolves iteratively, one should expect that the dictionary will be finished and closed only at a very late stage of the project. For now we are satisfied with a rich, even redundant set of abstractions with meaningful names and a sensible distribution of responsibilities.
A sign of quality, therefore, will be that the dictionary does not undergo serious changes each time we pass through a new iteration of the micro process. Instability of the dictionary shows that the developers have not yet achieved what is wanted, or that something is wrong in the architecture. As development proceeds, we can monitor the stability of the lower levels of the architecture by tracking the results of local changes to collaborating abstractions.
Identifying the semantics of classes and objects
Purpose. The purpose of identifying the semantics of classes and objects is to determine the behavior and attributes of each abstraction identified in the previous step. In doing so, we refine the abstractions we have outlined, distributing responsibilities among them thoughtfully and measurably.
In the analysis stage we apply this step to distribute responsibilities across the various behaviors of the system. In the design stage we apply the procedure of identifying semantics in order to distribute responsibilities clearly among the parts of the implementation. In implementation we move from free-form descriptions of roles and responsibilities to specifications of concrete protocols for each abstraction and, ultimately, to the exact signatures of each operation.
Products. This step yields several products. The first is a refinement of the data dictionary by means of which we initially assigned responsibilities to the abstractions. In the course of design we can produce specifications for each abstraction (as described in Chapter 5), listing the names of the operations in the protocol of each class. Then, as soon as possible, we express the interfaces of these classes in the implementation language. For C++ this means creating .h files, in Ada package specifications, in CLOS generic functions for each class, and in Smalltalk it is the declaration, but not the implementation, of the methods of each class. If the project involves a database, especially an object-oriented one, at this step we obtain the general framework of our data schema.
In addition to these essentially tactical decisions, we compose object diagrams and interaction diagrams conveying the semantics of the scenarios created in the course of the macro process. These diagrams formally capture the storyboard of each scenario and thus describe the explicit distribution of responsibilities among the collaborating objects. At this step finite state machines first appear to represent some of the abstractions.
So that the development team can evolve a consistent notational language and to keep track of the responsibilities of each abstraction, we can, as in the previous step, use a specialized database or other, more specific design tools. Once we have written the formal class interfaces in the chosen language, we can use our design tools to check and enforce the decisions we have made.
The main benefit of greater formality of products at this step is that it helps the developer see the purpose of all the protocols of an abstraction. An inability to define the meaning clearly is a sign that the abstractions themselves are shaky.
Activities. Three activities are associated with this step: storyboarding, isolated class design, and pattern scavenging.
The main subjects of storyboarding are the primary and secondary scenarios obtained in the macro process. In the course of this activity, the semantics are identified in a top-down manner. Where the system's function points are concerned, strategic decisions are made. A typical course of action may be as follows:
Informally, we can use CRC cards for storyboarding. For greater formality the development team should compose object and interaction diagrams. At the analysis stage, storyboarding is usually performed by a team including, at a minimum, an analyst, a domain expert, an architect, and a quality assurance person. At the design stage and later, during implementation, storyboarding is performed by the architect and senior developers to refine strategic decisions, and by individual developers to refine tactical decisions. Bringing additional team members into storyboarding is a highly effective way of training beginning developers and passing on to them the established vision of the architecture.
Early in a project's development we can specify the semantics of classes and objects in free form, simply describing the responsibilities of each abstraction. Usually a phrase or a sentence is enough; if that is not enough, we have a sure sign that the responsibility in question is excessively complex and should be split into smaller ones. At later stages of development, when we come to refine the protocols of individual abstractions, we can give the names of specific operations without defining their full signatures, which we will work out later. In this way we obtain a correspondence: each responsibility is carried out by a set of operations, and each operation somehow participates in carrying out the responsibilities of the corresponding abstraction. After that, in order to capture the dynamic semantics of class protocols [As we described in Chapter 3, a protocol specifies that certain operations must be invoked in a certain order. In all but the most trivial cases, operations are rarely encountered in isolation; each one's execution has its own preconditions, checking which often requires invoking other operations] that have event-driven or state-dependent behavior, we can construct finite state machines for them.
At this step it is important to concentrate more on behavior than on structure. Attributes represent structural elements, and so there is a danger, especially at the early stages of analysis, of constraining implementation decisions by specifying certain attributes prematurely. Attributes should be identified at this stage only insofar as they are necessary in building the conceptual model of the scenario.
Isolated class design is a bottom-up identification of semantics. Here we concentrate our attention on individual abstractions and, applying the heuristics for class design described in Chapter 3, consider their operations. This activity is by its nature more tactical, because here we are dealing with the design of classes rather than of the architecture. Its order of execution may be as follows:
It is important to avoid defining inheritance relationships prematurely - this often leads to a loss of type integrity.
At the early stages of development, individual classes can be designed in isolation. However, as soon as we define inheritance structures, this step will include placing operations in the class hierarchy. Considering the operations associated with a certain level of abstraction, we must decide at which level of abstraction to place them. Operations that may be used by several classes at the same level should be placed in their common superclass, which may have to be created. Actions that are shared by classes that are in no way related should be encapsulated in a mixin class.
The third activity - pattern scavenging - is concerned with the sharing of abstractions. In identifying the semantics of classes and objects, we must note patterns of behavior that may prove useful somewhere else. This process may proceed in the following order:
Identifying and describing semantics applies to class categories just as it does to individual classes. The semantics of classes and of their categories define roles, responsibilities, and operations. For an individual class, the operations may in time be expressed as its member functions; in the case of a class category, these operations represent the services exported from the category, and are ultimately implemented by a set of collaborating classes or by a single class. Thus the activities described above apply both to class design and to architectural design.
Milestones and measures. We will successfully complete this step when we have a more or less sufficient, primitive, and complete set of responsibilities and/or operations for each abstraction. Early in development it is enough to have an informal list of responsibilities, and thereafter we gradually refine the semantics.
The quality measures include all the class heuristics described in Chapter 3. Complex and murky responsibilities and operations indicate that the abstractions are still insufficiently defined. An inability to write a concrete header file or otherwise formalize the interface of the classes also indicates that the abstractions are poorly formulated, or that the wrong people defined the basic concepts [Beware of analysts and architects if they will not or cannot express the semantics of their abstractions concretely; this is a sign of arrogance or helplessness].
When walking through scenarios, expect heated debate. This helps developers share their architectural visions and develop the art of defining abstractions. Abstractions that have not been tested are not worth trying to code.
Identifying the relationships among classes and objects
Purpose. The purpose of identifying the relationships among classes and objects is to refine the boundaries of each abstraction discovered earlier in the micro process and to recognize all the entities with which it interacts. This activity formalizes the conceptual and physical separation among abstractions that was begun at the previous step.
We apply this step in analysis to specify the relationships among classes and objects (including certain important inheritance and aggregation relationships).
The existence of an association implies some semantic dependency between two abstractions and the possibility of navigating from one entity to the other. This design stage is needed in order to specify the interactions that form the mechanisms of our architecture and the grouping of classes into categories and of modules into subsystems. In the course of implementation we bring the associations into a more concrete form: instantiation, using, and so on.
Products. The main products of this step are class, object, and module diagrams. Although in the end we must express the decisions we made during analysis and design in a programming language, the diagrams give a broader overview of the architecture and, in addition, allow us to reveal relationships that are hard to formulate in the implementation language being used.
In analysis we compose class diagrams on which the associations among abstractions are indicated, and add to them the details obtained at the previous step (the operations and attributes of some abstractions) that are needed to convey the essence of our decisions. In design we refine these diagrams to reflect the tactical decisions made about inheritance, aggregation, instantiation, and using.
It is neither possible nor necessary to create an exhaustive set of diagrams that would define every possible kind of relationship among our abstractions. One must concentrate on the "interesting relationships," with the understanding that the "interesting" ones include those relationships among abstractions that reflect fundamental architectural decisions or express details necessary for implementation.
The product of analysis at this stage is class diagrams that contain class categories identifying clusters of abstractions grouped into layers and partitions. These products are also useful for documentation.
In analysis we also build object diagrams, thereby completing the walkthrough of scenarios begun at the previous step. The difference is that we can now examine the interactions among classes and objects and discover previously hidden common mechanisms of interaction that ought to be exploited. This usually leads to local restructurings of the inheritance hierarchy. In design we use object diagrams together with a more detailed description of states, in order to show the operation of our mechanisms dynamically. The explicit product of this step is a set of diagrams that identify the mechanisms of interaction.
In implementation we must make decisions about the physical partitioning of our system into modules and about the allocation of processes to processors. We can express these decisions on module and process diagrams.
At this same step the data dictionary is also updated. It reflects the allocation of classes and objects to categories and of modules to subsystems.
The main benefit of the products obtained is that they help to display and understand the relationships that connect conceptually and physically distant entities.
Activities. Three activities are associated with this step: specifying associations, identifying the various interactions, and refining associations. Specifying associations is one of the principal activities in analysis and in the early stage of design. As was explained in Chapter 3, associations are semantically weak: they denote only some semantic dependency, the role of each participant in the relationship, the cardinality of the relationship, and possibly the direction of permissible navigation. However, for analysis and the early stage of design this is often enough, since all the important details of the relationships between two abstractions are conveyed, while protecting us from hasty implementation decisions. The typical order of carrying out this stage is as follows:
Class diagrams are the principal models produced at this stage. Identifying interactions takes place mainly during design and, as described in Chapter 4, is a problem of classification. And so it too requires creativity and intuition. Depending on the current state of the macro process, we must consider several different types of interaction:
The third kind of activity in this phase of the micro process — refining associations — pertains to both analysis and design. In analysis, we may replace certain associations with other, semantically more precise relationships, so as to reflect our advances in understanding the application domain. Thus, by transforming associations and adding new concrete relationships, we prepare a sketch of the implementation.
The relationships of inheritance, aggregation, instantiation, and using are the most important kinds of associations of interest to us, together with such properties as labels, roles, cardinality, and so on. The typical order for refining associations is as follows:
Milestones and measures. We successfully complete this phase when we have defined the semantics and relationships of the abstractions of interest fully enough to begin implementation.
The measures of quality are cohesion, coupling, and completeness. In reviewing the relationships we have discovered or invented during this phase, we want to end up with abstractions that are cohesive and loosely coupled to one another. At the same time, we must identify all the important relationships at this level of abstraction, so that implementation does not require introducing significant new relationships or making unnatural use of those we have already defined. If at the next step we find that our abstractions are awkward to implement, this is a sign that we have not yet settled on a suitable set of relationships among them.
Implementing classes and objects
Purpose. In the analysis phase, implementing classes and objects is needed to carry the existing abstractions to a level sufficient to discover new classes and objects at the next level of abstraction; these will themselves subsequently be fed into a new iteration of the micro process. In design, the purpose of implementation becomes creating a tangible representation of our abstractions by producing successive executable versions of the system (the macro process).
This step is deliberately performed last, because the micro process concentrates attention on behavior and defers decisions about representation as long as possible. Such a strategy protects the developer from premature decisions that may leave no chance of easing and simplifying the architecture, and it preserves freedom in the choice of implementation (for reasons of efficiency, for example), while guaranteeing that the existing architecture is preserved.
Products. At this step we make decisions about the representation of each abstraction and about the mapping of these abstractions onto the physical model. Early in the development process we formulate these tactical decisions about representation in the form of refined class specifications. Decisions of general interest, or ones suitable for reuse, we also document in class diagrams (showing their static semantics), state transition diagrams, and interaction diagrams (showing their dynamic semantics). Once it becomes clear which language the design will be implemented in, one can begin programming in pseudocode or in executable code.
To expose the connections between the logical and the physical in our implementation of the system, we introduce module diagrams, which can then be used to show visually how our architecture maps onto its software implementation. Beyond that, one can apply specific tools that either generate code from the diagrams or recover the diagrams from the implementation.
This step also includes updating the data dictionary, adding the new classes and objects that were discovered or invented while implementing the existing abstractions. These new abstractions are part of the input for the next cycle of the micro process.
Activities. There is one principal activity associated with implementation: selecting the structures and algorithms that represent the semantics of the abstractions defined earlier in the micro process. Unlike the first three stages of the micro process, which focus on the external representations of abstractions, this stage places the emphasis on their internal representation.
In the analysis phase, the products of this activity are relatively abstract: we are not so much concerned with implementation proper as we are interested in finding new abstractions to which responsibilities can be delegated. In the design phase, and especially in the later stages of class design, we do move on to practical decisions.
The typical order of activities is as follows:
Milestones and measures. In the analysis phase we consider that we have successfully completed the implementation phase when we have identified all the important abstractions among those needed to carry out the responsibilities of the abstractions identified in this cycle of the micro process. In the design phase, implementation is considered successfully completed when we have obtained an executable, or nearly executable, software model of our abstractions.
The main indicator of success in this phase is simplicity. Complex, awkward, or inefficient implementations point to deficiencies in the abstraction itself or to a poor representation of it.
Comments