Lecture
Overview
The macro process serves as a controlling framework for the micro process. The macro process prescribes a set of measurable deliverables and activities that allow the development team to assess risk, make timely adjustments to the micro process, and focus on collective analysis and design. The macro process is the activity of the entire team on a scale of weeks to months.
Many elements of the macro process belong to the practice of software project management itself, and are therefore carried out the same way for object-oriented systems as for any others. Among them are configuration management, quality assurance, walkthroughs, and documentation. In the next chapter we shall examine these practical matters in the context of object-oriented design. This chapter concentrates on describing what is specific to the object-oriented approach — or, in Parnas's phrase, on how we fake a rational design process in order to produce an object-oriented system.
The macro process is primarily the concern of the development team's technical manager, whose goals differ somewhat from those of the individual developer. Both are interested in the quality of the final software product and in meeting the customer's requirements [Well, of course, not all of them, but most. Unfortunately, some managers are more interested in building their empire than in building the software product. Add to this the earlier remark about analysts and designers. I think Dante could find a suitable place for them]. However, the end user cares little whether parameterized classes or polymorphism were used correctly in the design; the customer is far more concerned with schedule, quality, completeness, and correct operation of the program. That is why the macro process is centered on managing risk and identifying the overall architecture — two controllable components that are decisive for the schedule, completeness, and quality of the project.
The macro process largely preserves the traditional phases of analysis and design, and the process is reasonably ordered. As shown in Figure 6-2, the macro process typically comprises the following activities:

Figure 6-2. The macro process.
For every nontrivial software development effort, the macro process continues even after the system has been built and deployed. This is especially evident in organizations that specialize in creating families of programs, into which substantial capital investment is often made.
The underlying philosophy of the macro process is incremental development. As Wonk defines it, "in incremental development, the system is built step by step, with each new version containing the functionality of the previous one plus new functions" [14]. This approach fits the object-oriented paradigm exceptionally well and offers many opportunities for managing risk. As Gilb puts it: "Incremental delivery of programs to the customer was invented to warn us in advance of trouble ahead" [15].
Let us now examine each activity in the macro process in detail. Naturally, one of the marks of a mature development organization is knowing when these rules should be bypassed, which we shall note separately in our review.
Conceptualization
Purpose. Conceptualization must establish the core requirements for the system. For every fundamentally new piece of software, or even for a new application of an existing system, there comes a moment when the idea of a new application takes hold in the mind of a developer, architect, analyst, or end user.
This might be a new business venture, an additional item in a product line, or, say, a new function in an existing software system. The goal of conceptualization is not to define the idea completely, but to form a view of it and test it mentally.
Products. The primary products of conceptualization are prototypes of the system. Certainly, every substantially new software system needs some rough prototype, even one thrown together "quick and dirty." Such prototypes are incomplete by their very nature and are worked out only in outline. Nevertheless, interesting prototypes should be kept (even ones that were rejected), because in this way the organization maintains a corporate memory of the original conception and preserves the link to the initial assumptions. During design, this archive provides invaluable material for experimentation, to which analysts and architects can return when they want to try out new ideas.
Obviously, for grand applications (of national or international significance), building prototypes alone may turn out to be a major undertaking. After all, it is far better to run into difficulties during implementation, discovering that some assumptions about the system's functionality, efficiency, size, or complexity are wrong, than to dismiss a forward-looking solution. Such neglect may threaten financial or social catastrophe.
Let us emphasize: prototypes are good, but they should be thrown away. They must not be allowed to evolve directly into the finished system unless there are sufficiently serious grounds for it. A tight schedule is not a valid reason: optimizing short-term development while ignoring the software owner's subsequent costs is a classic example of false economy.
Activities. Conceptualization is by its very nature a creative activity, and consequently it should not be constrained by rigid development rules. Perhaps the most important thing for an organization is to create a structure that provides sufficient resources for new ideas to arise and be explored [If the organization does not do this itself, then individual developers will do it anyway, without asking the company they work for. That is how new software firms come into being. Their appearance is good for the industry as a whole, but not for the orphaned company itself]. New ideas can come from the most varied sources: end users, user groups, developers, analysts, designers, distributors, and so on. It is important for management to keep a record of these ideas, ranking them by priority and allocating limited resources so as to explore the most promising ones. Once a specific direction has been chosen for exploration, the typical order of subsequent activities is as follows:
There is nothing specifically object-oriented about conceptualization. Every programming paradigm must make provision for proof of concept. However, as is often the case, prototype development usually proceeds faster where a mature object-oriented environment is at hand.
Quite often concepts are tried out in one language (Smalltalk, for example), while development of the final product is carried out in another (say, C++).
Milestones and measures. It is important that clear criteria be established for evaluating the prototype. Work on a prototype is more often scheduled by time (meaning that the prototype must be finished by a certain date) than by requirements. This is not always a bad thing, since it artificially bounds the effort spent on the prototype and forestalls attempts to ship a conceptually premature product.
Senior managers can gauge the health of an organization by its attitude toward new ideas. Any organization that does not generate new ideas of its own is either already dead or close to it. The most prudent course of action in such a situation is to spin off independent units, or else to leave the business altogether. On the other hand, any organization awash in new ideas but unable to assign them sensible priorities is out of control. Such companies often waste substantial resources by jumping into product development too early, without exploring the risks. The most prudent thing here would be to formalize the production process and establish a proper transition from concept to product.
Analysis
Purpose. As Mellor puts it, "the purpose of analysis is to provide a description of the problem. The description must be complete, consistent, readable and reviewable by all interested parties, and testable against reality" [16]. In our terms, the purpose of analysis is to provide a model of the system's behavior.
It must be emphasized that analysis focuses not on form but on behavior. In this phase it is inappropriate to occupy oneself with class design, representation, or other tactical decisions. Analysis must explain what the system does, not how it does it. Any statement about the "how" made during analysis (contrary to this rule) may be regarded as useful only for demonstrating the system's behavior, not as a testable requirement on its design.
In this respect the goals of analysis and design are quite different. In analysis we seek a model of the world, identifying the classes and objects (their roles, responsibilities, and interactions) that form the vocabulary of the problem domain. In design we invent artificial actors that implement the behavior required by the analysis. In this sense, analysis is an activity that brings together the users and the developers of the system, uniting them through the writing of a common vocabulary of the problem domain.
Focusing on behavior, we set about determining the function points of the system. Function points, first described by Allan Albrecht, denote externally visible and verifiable elements of the system's behavior [17]. From the end user's point of view, a function point represents some elementary action of the system in response to some event [As Dreger notes, in information systems management theory a function point represents a distinct end-user business function [18]]. Function points often (but not always) denote a mapping of inputs onto outputs, and thus represent the transformations performed by the system. From the analyst's point of view, function points represent quanta of behavior. Indeed, function points are a measure of a system's complexity: the more of them there are, the more complex it is. In the analysis phase we convey the semantics of function points by means of scenarios.
Analysis never takes place in isolation. We do not aim for an exhaustive understanding of the system's behavior, and we would even assert that carrying out a complete analysis before beginning design is not only impossible but undesirable. The process of building a system raises questions about its behavior that in practice cannot be answered with any guarantee by analysis alone. It is enough to analyze all the primary elements of the system's behavior plus some number of secondary ones, added to ensure that no essential behavioral patterns have been missed.
A reasonably complete and formal analysis is needed first and foremost so that the project's progress can be traced. Traceability is needed to make the project measurable, so as to guarantee that not a single function point has been overlooked. Traceability is also the basis of risk management. In developing any nontrivial system, managers will face the need to make hard choices, either in allocating resources or in resolving some tactical problem. With the ability to trace the process from function points through to implementation, it is far easier to assess the impact of such problems on the architecture.
Products. DeChampeaux holds that the product of analysis should be a description of the system's purpose, accompanied by performance characteristics and a statement of the resources required [19]. In object-oriented design we obtain such descriptions by means of scenarios. Each scenario represents one function point. We use primary scenarios to illustrate key behavior, and secondary ones to describe behavior in exceptional situations.
As discussed in earlier chapters, we use the CRC card technique to storyboard scenarios, and then apply object diagrams to illustrate more precisely the semantics of each scenario. Such diagrams should show the interaction of objects that provides the system's functions, and the ordered process of that interaction, consisting of objects sending messages to one another. Besides object diagrams, one may bring in class diagrams (to show the existing associations among the classes of objects) and state transition diagrams (to show the life cycle of the most important objects).
Often these products of analysis are combined into a single formal document, which states the analysis requirements on the system's behavior, illustrating them with diagrams, and sets out such nonbehavioral aspects of the system as efficiency, reliability, security, and portability [20].
A by-product of analysis will be a risk assessment: the identification of danger areas that may affect the design process. Discovering the risks that exist at the beginning of the design process will ease possible architectural trade-offs in the later stages of development.
Activities. There are two principal activities associated with analysis: domain analysis and scenario planning.
As we described in Chapter 4, domain analysis must identify the classes and objects that inhabit the given problem domain. Before setting about the development of a new system, one usually studies systems that already exist. In this case we can profit from the experience of other projects in which similar decisions were made. The best outcome of domain analysis may be the conclusion that we need not design a new product at all, but should instead reuse or adapt an existing program.
Scenario planning is the central activity of analysis. Interestingly, on this point there seems to be a convergence of opinion among the other methodologists, especially Rubin and Goldberg, Adams, Wirfs-Brock, Coad, and Jacobson. The typical order in which it is carried out is as follows:
As described in the next chapter, scenario planning is carried out by analysts in collaboration with domain experts and architects. A quality assurance person should additionally take part in scenario planning, since scenarios represent testable behavior. Involving quality assurance staff at the very beginning of the process helps to establish high quality standards from the outset. It is also effective to involve other members of the team, so as to give them a chance to take part in the design process and to speed up their understanding of the system's structure.
Milestones and measures. We successfully complete this phase when we have refined and signed-off scenarios for all the fundamental kinds of system behavior. By signed-off we mean that the products of analysis have been reviewed by experts, end users, analysts, and architects; by fundamental we mean the kinds of behavior that are central to the given application. Again, we do not expect a complete analysis — it is enough to consider only the principal behaviors and a few secondary ones.
The degree of excellence of an analysis will be measured, in particular, by its completeness and simplicity. A good analysis identifies all the primary scenarios and, as a rule, the most important secondary ones. A sound analysis also includes a review of all the strategically important scenarios, since this helps to instill a shared vision of the system throughout the development team. Finally, one should look for behavioral patterns that would yield the simplest possible class structure and would account for everything the various scenarios have in common.
Another important component of analysis is risk assessment, which will facilitate future strategic and tactical trade-offs.
Design
Purpose. The purpose of design is to create an architecture for the evolving implementation and to establish the common tactical policies that the various elements of the system must use. We begin the design process as soon as some acceptable model of the system's behavior appears. It is important not to begin design before analysis is complete. It is equally important to avoid dragging design out in an attempt to obtain an ideal — and hence unattainable — analysis model [Such a situation is usually classified as analysis paralysis].
Products. There are two principal products of design: a description of the architecture and the establishment of common tactical policies.
We can describe the architecture by constructing diagrams or by creating successive architectural releases of the system. As described in earlier chapters, the architecture of an object-oriented system expresses the structure of the classes and objects within it, and so class diagrams and object diagrams can be used to show its strategic organization. In describing the architecture it is important to demonstrate visually the grouping of classes into class categories (for the logical architecture) and the grouping of modules into subsystems (for the physical architecture). Such diagrams may be circulated as part of a formal document describing the architecture, which should be available to all members of the team for review and for the introduction of corrections as the architecture evolves.
We use architectural releases of the system as a tangible demonstration of the architecture's structure. An architectural release is, as it were, a vertical slice through the architecture, conveying the most important (but not the complete) semantics of the essential categories and subsystems. An architectural release of the system must be a working program, which makes it possible to measure, study, and evaluate the architecture. As we shall see in the next section, architectural releases are the basis of the system's evolution.
Common tactical policies are the localized mechanisms that appear throughout the system. They include such design aspects as the principles of error detection and error handling, memory management, data storage and representation, and approaches to control. It is important to describe these policies explicitly, so as not to force developers to devise individual solutions to common problems and thereby wreck our strategic architecture.
We describe common policies in scenarios and in working releases of each mechanism.
Activities. There are three activities associated with design: architectural planning, tactical design, and release planning.
In architectural planning we occupy ourselves with the vertical and horizontal partitioning of the system. It encompasses logical decomposition, which consists in grouping classes, and physical decomposition, which consists in dividing the system into modules and assigning tasks to processors. The typical order of activities is as follows:
Architectural planning is focused on creating a framework for the system at the very start of the life cycle, and then developing it incrementally.
Tactical design consists in making decisions about a set of common policies. As described earlier in this chapter, poor tactical design can destroy even a very well thought out architecture. We can reduce this risk by explicitly singling out the tactical policies and resolving to adhere to them firmly. The typical order of activities is as follows:
Software releases lay the foundations of the system's architectural evolution. Based on the function points and risk assessments obtained in the analysis phase, releases are issued with ever broader functional capabilities and, in the end, reach the requirements imposed on the final system. The typical order of activities is as follows:
A natural by-product of release planning is a plan that defines the work schedule, the team's tasks, and the risk assessment.
Milestones and measures. We successfully complete this phase when we have an architecture that has been validated and approved, having passed through prototyping and formal reviews. In addition, all the important tactical policies and the plan of successive releases must be approved.
The principal mark of excellence is simplicity. A good architecture has the characteristics of an organized complex system (see Chapter 1).
The main benefits of this activity are the early detection of architectural mistakes and the adoption of common policies that make it possible to obtain a simpler architecture.
Evolution
Purpose. The purpose of evolution is to grow and change the implementation, refining it successively, so as ultimately to produce a finished system.
The evolution of an architecture consists to a large degree in the attempt to satisfy several mutually exclusive requirements on time, memory, and so on — one always constrains another. For example, if the computer's weight is critical (as in the design of space systems), then the weight of an individual memory chip must be taken into account. In turn, the amount of memory permitted by the weight limit constrains the size of the program that can be loaded. Relax any constraint, and an alternative solution becomes possible; tighten the constraint, and some solutions fall away. Evolution in the implementation of a software project helps determine which constraints are essential and which may be disregarded better than any monolithic set of practices can. For this reason, evolutionary development is focused first of all on functionality and only then on local efficiency. Usually, at the start of design we know far too little to foresee where the system's performance bottleneck will be. By analyzing the behavior of each new release, using histograms and similar techniques, the development team will in time be better able to understand how to tune the system.
Thus, evolution is precisely what the process of developing a program is. As Andert writes, design "is a time of innovation, refinement, and unrestricted freedom to change the code in order to achieve one's goals. Production is a controlled, methodical process of raising the quality of the product to the appropriate level" [24].
Page-Jones lists a number of advantages of such incremental development:
Products. The principal product of evolution is a series of executable releases representing iterative refinements of the initial architectural model. As a secondary product one should recognize the identification of behavior that is used to explore alternative approaches and to further analyze the dark corners of the system.
Working releases are issued according to the schedule laid out at the beginning of planning. For a modest-sized project requiring 12-18 months of development from start to finish, this might mean a release every two or three months. For more complex projects requiring greater development effort, a release might be issued every six months or less often. A less frequent schedule is suspect, since it does not force developers to complete their micro processes properly and may conceal areas of danger.
Who is a working release of the program made for? Early in the development process, the main working releases are handed by the developers to the quality assurance staff, who test them against the scenarios drawn up during analysis and accumulate information about the completeness, correctness, and robustness of the release's operation. This early accumulation of data helps in identifying quality problems that will be addressed in subsequent releases. Later, working releases are handed to end (alpha and beta) users in some controlled manner. "Controlled" means that the developers carefully verify the requirements for each release and determine the aspects that they wish to test and evaluate.
The specifics of the micro process imply that, out of numerous internal releases, developers ship only a few executable versions outside. Internal releases represent a kind of process of continuous integration of the system and conclude each cycle of the micro process.
It is implicitly understood that the system's documentation evolves along with the architectural releases. So as not to treat documentation as a primary occupation, it is best to obtain it as a natural, semi-automatically generated by-product of the evolutionary process.
Activities. Evolution is associated with two activities: the micro process and change management.
The work carried out between releases represents the development process in condensed form: it is precisely one cycle of the micro process. We begin with an analysis of the requirements for the next release, move on to designing the architecture, and explore the classes and objects needed to implement that design. The typical order of activities is as follows:
After each release, the dates and requirements in the master release plan should be reviewed. As a rule, these are minor adjustments of dates or the shifting of functionality from one release to another.
Change management is necessary precisely because of the strategy of iterative development. It is always tempting to make disorderly changes to the class hierarchy, to class protocols, or to mechanisms, but this undermines the strategic architecture and leads to developers becoming confused in their own code.
In practice, the following kinds of changes are to be expected as a system evolves:
Each kind of change has its own cause and cost.
A designer introduces new classes when new abstractions have been discovered or new mechanisms have become necessary. The cost of carrying out such changes is usually insignificant for development management. If a new class is added, one must consider where it will fall within the existing class structure. When a new class interaction is introduced, a minimal amount of domain analysis must be performed to make sure that it really does satisfy one of the interaction patterns.
Changing an implementation is also inexpensive. Usually, in object-oriented development the interface of a class is created first, and then its implementation is written (that is, the code of the member functions). Provided only that the interface is reasonably stable, one can choose any internal representation for the class and carry out the implementation of its methods. The implementation of an individual method may be changed later (usually to fix an error or improve efficiency). One can adjust a method's implementation so as to take advantage of new methods defined in an existing or newly introduced superclass. In any case, changing a method's implementation is comparatively inexpensive, especially if it was properly encapsulated.
In similar fashion one might change the representation of a class (in C++, the protected and private members of a class). This is usually done to obtain more efficient (in terms of memory or speed) instances of the class. If the class's representation is encapsulated, which is possible in Smalltalk, C++, CLOS, and Ada, then a change in the representation will not break the logic by which client objects interact with instances of the class (provided, of course, that the new representation delivers the class's expected behavior). On the other hand, if the class's representation is not encapsulated, which is also possible in any language, then a change in the class's representation is extremely dangerous, since clients may depend on it. This is especially true in the case of subclasses: changing the representation of a superclass will cause changes to the representation of all its subclasses. In any event, changing the representation of a class has its cost: the class's interface and implementation must be recompiled, and the same must be done for all of its clients, for the clients of those clients, and so on.
Reorganizing a system's class structure occurs fairly often, though less often than the other kinds of change mentioned. As Stefik and Bobrow note, "Programmers often create new classes and reorganize existing ones when they see a convenient opportunity to factor their programs" [26]. Changes to the class structure usually take the form of changing inheritance relationships, adding new abstract classes, and moving responsibilities and the implementation of common methods up to higher-level classes in the class hierarchy. In practice, a system's class structure is reorganized especially often at the beginning, and then stabilizes once the developers better understand the interaction of the key abstractions. Reorganizing the class structure is encouraged in the early stages of design, because it may result in a more concise program. However, reorganizing the class structure does not come for free. Usually, changing the position of a class high in the hierarchy renders the definitions of all the classes beneath it obsolete and requires their recompilation (and hence the recompilation of all the classes that depend on them, and so on).
One further important kind of change that must be resorted to as a system evolves is changing the interface of a class. A developer usually changes a class's interface in order to add some new aspect, to satisfy the semantics of some new role for the class's objects, or to add a new operation that was always part of the abstraction but had not previously been exported, and is now needed by some client object. In practice, using the heuristics for building classes that we discussed in Chapter 3 (especially the requirement of a primitive, sufficient, and complete interface) reduces the likelihood of such changes. However, our experience is never final. We shall never define a nontrivial class in such a way that its interface turns out right the first time.
Rarely, but it does happen, an existing method is removed; this is usually done only in order to improve the encapsulation of an abstraction. More often we add a new method or override a method already declared in some superclass. In all three cases this change is expensive, because logically it affects all clients, requiring at a minimum that they be recompiled. Fortunately, these last kinds of change, the addition and overriding of methods, are upwardly compatible. In fact, you will find that most of the interface changes made to given classes as a system evolves are upwardly compatible. This makes it possible to apply such sophisticated technologies as incremental compilation to reduce the impact of these changes. Incremental compilation allows us to recompile only individual declarations and statements rather than whole modules; that is, the recompilation of most clients can be avoided.
Why is recompilation such a nuisance? For small systems there is no problem here: recompiling the whole system takes a few minutes. For large systems, however, it is quite another matter. Recompiling a program of hundreds of thousands of lines may take up to half a day of machine time. Imagine that you needed to make a change to the software of a ship's computer system. How will you tell the captain that he cannot put to sea because you are still compiling? In some cases the cost of recompilation is so high that developers have to forgo making certain changes that represent perfectly reasonable improvements. Recompilation poses a particular problem for object-oriented languages, since inheritance introduces additional compilation dependencies [27]. For strongly typed object-oriented programming languages the cost of recompilation may be even higher; in these languages compilation time has been sacrificed for safety.
All the changes discussed so far are comparatively easy: the greatest risk is carried by substantial changes to the architecture, which can wreck an entire project. Often such changes are made by all-too-brilliant engineers who have too many good ideas [28].
Milestones and measures. We successfully complete the implementation phase when the releases grow into a finished product. The first measure of quality, then, will be the extent to which we have managed to implement the function points allocated to the intermediate releases, and how closely the schedule drawn up when planning them is being kept.
The two other principal measures of quality are the rate of defect discovery and the measure of volatility of the key architectural interfaces and tactical policies.
Roughly speaking, the rate of defect discovery is a measure of how quickly new defects are found [29]. By investing in quality assurance at the beginning of development, we can obtain quantitative quality measures for each release, which the team's managers will be able to use to determine areas of risk and to refresh the development team. After each release there should be a spike in defect discovery. A flat value for this measure usually indicates that defects are not being found, while an excessively high value indicates that the architecture has not yet stabilized, or that the new elements have been designed or implemented incorrectly. These measures are used in refining the goals of the next release.
The measure of volatility of an architectural interface or of tactical policies is a principal indicator of the architecture's stability [30]. Local changes are likely throughout the whole process of evolution, but if inheritance structures or the boundaries between class categories or subsystems are constantly being rebuilt, this is a sign of unresolved problems in the architecture, which must be taken into account as an area of risk when planning the next release.
Maintenance
Purpose. Maintenance is the activity of managing the evolution of a product during its use. It largely continues the preceding phases, except that it introduces fewer architectural innovations. Instead, more localized changes are made, arising as new requirements are taken into account and old defects are corrected.
Lehman and Belady made several indisputable observations while examining the process by which an already deployed software system "matures":
We distinguish the notion of preservation of a software system from its maintenance. In maintenance, developers make continual improvements to an existing system; maintenance is usually carried out by a group of people different from the development group. Preservation, by contrast, is based on committing additional resources to keeping an obsolete system going (one that often has a poorly designed architecture and is therefore hard to understand and modify). And so a business decision must be made: if the cost of owning the software product is higher than the cost of developing a new system, then the most humane course of action is to leave the old system alone or to put an end to it.
Products. Since maintenance is in a certain sense a continuation of the system's evolution, its products are similar to those we sought in the preceding phases. In addition to them, maintenance also involves managing a list of new tasks. Besides those requirements that for one reason or another were not addressed, it is likely that soon after a working system is released, developers and end users will exchange a multitude of wishes and suggestions that they would like to see embodied in subsequent versions of the system. Note that once more users have worked with the system, new defects and unexpected ways of using it will come to light that the quality assurance staff could not have foreseen [Users display prodigies of ingenuity in using a system in the most unusual ways]. The discovered defects and the new requirements are entered on the list and will be taken into account when planning new releases, according to their priority.
Activities. Maintenance differs somewhat from the evolution of a system. If the original architecture was successful, adding new functions and changing existing behavior happen naturally.
Besides the usual activities of evolution, maintenance requires determining the priorities of the tasks gathered in the list of comments and suggestions. The typical order of activities is as follows:
Milestones and measures. The milestones of maintenance are the continued production of evolving releases and the elimination of defects.
We consider that we are engaged in maintaining a system if the architecture withstands change; we shall know that we have entered the stage of preservation when the amount of resources required to achieve a desired improvement begins to grow sharply.
Summary
Comments