Design

From Dr. Joey Paquet Web Site
Jump to: navigation, search

Suppose that we have a requirements document and that both client and supplier are satisfied with. What happens next? The next important task is to design the software. Before discussing design, we briefly review the process as a whole. According to the Waterfall Model, we should complete the design, then the implementation, and finally, the testing. However, following the Waterfall Model literally is rarely a good idea. In practice, the following pseudocode suggests a better approach:

Design the core architecture of the system 
 while (system incomplete) 
 { 
  Design one part completely 
  Code and test that part 
 } 
  • We should make good use of available resources. If we have many programming teams, we can develop several parts concurrently.
  • The principal risk is a weak architecture or core design ⇒ unimplementable system ⇒ failure.
  • Components do not run by themselves ⇒ extra code, a.k.a. “scaffolding”, to test parts.
  • It may be a good idea to start with the main program and user interface and then to add functionality gradually.

The Conceptual Design tells the client what the system will do. The Technical Design tells implementors how to build the system. We discuss conceptual design first.

Contents

Conceptual Design

The conceptual design answers questions such as these:

  • Where will the data come from?
  • What will happen to the data in the system?
  • What will the system look like to users?
  • What choices will be offered to users?
  • What is the timing of events?
  • What will the reports and screens look like?

A good conceptual design should have the following characteristics:

  • It is written in the client’s language.
  • It contains no technical jargon.
  • It describes the functions of the system.
  • It is independent of implementation.
  • It is linked with the requirements documents.
Problem
there doesn’t seem to be much difference between the conceptual design and the requirements! The difference is this:
  • The requirements describe what is required.
  • The conceptual design describes what will be provided. Here is a simple example:
Requirement
the user must be able to open a file.
Conceptual design
the user opens a file by performing the following actions.
  • Select File from the main menu. System displays a list of files.
  • Select a file.
  • Select OK or CANCEL.

Both the requirements and the conceptual design can be used as contracts between supplier and client. Modern practice is to keep the client involved during the design process to ensure that the design meets the requirements in a suitable way.

Technical Design

The technical design includes:

  • a description of the hardware components and their functions;
  • a description of the software components and their functions;
  • data structures and data flow.

For object oriented design, replace data structures and data flow by class descriptions of various kinds. The technical design usually shows how the conceptual design can be implemented by a collection of components. Components may be either hardware or software. We will be concerned mostly with software components in this discussion. Each component has an interface. Components interact through their interfaces.

Architectural Design

If we think in terms of “building software”, the idea that a software product has an “architecture” is natural. “Architecture” is just another name for the overall organization of the software. Nevertheless, the importance of software architecture, and the number of varieties of software architecture, have been realized and exploited only recently. We describe a few of the more common architectural styles below.

Hierarchical

A hierarchical architecture has the form of a tree in which the root is the “main program” and the leaves are primitive functions (that is, functions that do not call other functions). Intermediate nodes of the tree are functions that call and are called by other functions. Hierarchical organization is usually the result of top-down design: a problem is recursively decomposed into smaller and smaller parts. Hierarchies have several disadvantages:

  • they are designed to perform a single function (the “main program”);
  • they are organized around control flow (functions calling functions) — data tends to be neglected;
  • the “leaf ” functions are very specialized and usually cannot be used in any other applications.

Layered

Components in a layered architecture are arranged in layers (as you might expect). The lowest layers perform the simplest, most concrete tasks, and higher layers perform progressively more complex and abstract tasks, using the lower level(s) functionalities. The top layer performs the tasks that the user understands and is effectively the user interface of the system.

The layered architecture is a generalization of the hierarchy. In fact, there are few “true” hierarchies: most actual hierarchies are actually layered because leaf nodes are shared by higher-level nodes. The rule for constructing layered systems is that a higher layer may use the services of its own layer and lower layers but a lower layer may not use the services of a higher layer. If we number the layers 1 (top layer), 2, 3, . . . , N (bottom layer), then layer X may call layer Y only if X ≤ Y . In some systems, there is a stronger constraint: a layer can use services only of the layer below it: Y − 1 ≤ X ≤ Y .

Operating systems are often constructed in layers. The bottom layer consists of device and interrupt handlers that are close to the hardware. The next layer implements processes, queues, and other facilities that depend only on the bottom layer. The next layer implements services that use processes and queues, such as memory management and print spoolers. The layer above that managers user activities. The top layer implements the parts of the operating system that the user actually sees, usually in the form of small programs that execute commands.

Layered architectures are sometimes described as rings. The system is viewed as a set of concentric circles; a ring is the space between two circles. The innermost circle is the system kernel . A component in an outer ring can call a component in an inner ring, but not vice versa. The best-known system with a ring architecture is multics. (unix was designed because Bell Labs staff felt that multics was unnecessarily complicated and, in any case, would not run on a PDP/11 with 32Kb of main memory.)

Pipes and Filters

A pipe is a channel that connects two software components; one component produces data and the other consumes the data. A filter is a component that inputs data from one or more places, processes the data in some way, and outputs it to other places. Systems are built by using pipes as connectors between components. The unix operating system uses pipes and filters internally and also makes them accessible to users. The unix command

$ who | pr -3 | lpr 

uses three filters: who takes data from the operating system and outputs a list of users; pr -3 receives a list and outputs the same list formatted as three columns; and lpr takes any data and sends it to the printer (actually the print spool process). These filters are connected by pipes, indicated by “|” in the command.

The important thing about a pipe is that it is an abstraction: we think of data “flowing” through it. In fact, data sometimes flows and sometimes doesn’t. In the example above, we can imagine the name of the first user flowing through each stage. But suppose that we inserted a sort filter somewhere in the sequence: the sorting process cannot output anything until it has seen all the data (the last user in the input might be aardvark, which would probably be the first to be output). Although sort is not “really” a filter, it can be used exactly like other filters: it is the responsibility of the operating system to decide how to organize any necessary buffering in the pipes between filters.

Event-Oriented Systems

Traditional architectures assume that the flow of control is determined by the programmer. From time to time, the program stops and requests input from the user. Flexibility is provided by providing the user with various options, but these are often limited.

Event-oriented architectures (ideally) assume that any event can occur at any time. They are often separated into a layer that is sensitive to events and a layer that processes the events. The processing layer must provide the event-handling layer with pointers to a set of functions called callback functions. When an even occurs, the event handling layer detects it, selects the appropriate callback function, and invokes it.

In a window system, for example, the events include mouse movements and button clicks, and keyboard events. The event handling layer receives all of these. There is usually an active window that is marked so that the user can recognize it. All events are passed to callback functions for that window.

Repository

A repository is a system organized around a collection of data that is shared by many processes. Some software development environments (SDEs) are organized around a repository that holds source code, compiled code, executables, symbol tables, documentation, and other artefacts related to software development. The SDE provides a set of tools — editor, compiler, debugger, revision control, documentation manager, etc. — that examine and update the data in the repository.

Client-Server

A client system issues a request that is handled by a server system. The server might provide a simple service, such as storing files for a community of users, or a more complex service, such as booking airlines seats for customers.

Object-Oriented Systems

The object oriented paradigm is sometimes referred to as an “architecture” (e.g., Pfleeger, page 200; Shaw and Garlan, page 39). However, the paradigm is really more general than that, because most architectures (including those listed here) can be implemented in an object oriented way. Architectures can be combined. For example, a server might be implemented as a layered collection of object-oriented classes.

Design Documentation

A typical set of design documents includes:

The Conceptual Design
a description, in non-technical language, of the ways in which the system will meet the client’s requirements.
The Technical Design
a document in two parts.
  • System Architecture: Also known as the High Level Design, the system architectural design is a description of each component of the system together with a diagram (or diagrams, if necessary) showing how the components are related, often including Module Interface Specifications specifying exactly the software interfaces between high level modules.
  • Detailed Design: a description of each component. The description should include the data structures managed by the component and the functions that it provides. There should be enough detail to enable a programmer to write code for the component without having to consult a designer.

The documents should include any information that will be useful to maintainers in the future. In particular, there should be a rationale for any decision that is not obvious. This avoids the following scenario:

  • The designers propose solution X. After a while, they realize that solution Y , which at first sight looks worse than X, is in fact better than X. Solution Y becomes part of the design.
  • A couple of years later, when most of the original designers have left in search of higher salaries, a maintenance team is fixing a bug in the code. Someone in the team points out that Y is brain-damaged and that there is an obvious improvement, namely X. The other members of the team agree and Y is replaced by X.
  • A month later, the software fails. The maintenance team examine the core dumps and realize that the fault was in X and (if they are smart) also realize that Y behaved correctly in this situation. They restore the code Y in place of X.
  • This is actually a rather optimistic scenario. More often, the maintenance team has forgotten about Y and introduce a third solution, Z, which is unlikely to be as good as Y and may introduce new faults into the software.

Design Principles and Issues

How do we tell whether a design is good or bad? How do we create a good design? Complete answers to these questions are hard to obtain. Here we present a few guidelines.

Modularity & cohesion

A good design usually consists of a collection of well-defined, discrete components or modules. If the programming language is object oriented, the modules often correspond to classes (or possibly to groups of classes). There are various ways of organizing the modules, as explained in the previous section. They can This module sounds cohesive:

Module Spooler receives printable files from various processes, stores the files on disk if necessary, and sends them to the printer. 

This module does not sound cohesive:

Module Manager maintains a list of client names and is responsible for issuing invoices to clients. It sends email messages to clients who do not pay their bills and handles all other email messages for the system. On the second and fourth Thursday of each month (but not during the summer), it deletes accounting data that is more than one month old and defragments the master disk. 

There are several degrees of cohesion, which can be categorized as:

coincidental 
A component whose parts are unrelated to one another. In this case, unrelated functions or data elements are found in the same component for reasons of convenience, or as a result if quick changes ir fixes. For example, a component that checks a user’s security classification and also prints this week’s payroll is coincidentally cohesive.
logical 
Several logically related functions or data elements are placed in the same component. For example, one component may read all kinds of inputs, regardless of where the data is coming from (files, network, etc.) or how it wil be used; ”input” is the glue that holds the component together. Since the input can have different purposes, we are grouping many unrelated functions in one place.
temporal 
Sometimes, a component is used to initialize a system or set of variables. Such a component performs several functions in sequence, but the functions are related only by the timing involved, hence the name ”temporal” cohesion. Both temporally and logically cohesive components are difficult to change. Suppose you must modify the design of system function X. Because logically or temporally cohesive components perform several different functions, to change the affected functionality X, you must search through all components for the parts related to X.
procedural 
Often, functions must be performed in a certain order to achieve a result. For example, data must be entered before they can be checked and then manipulated: three functions in a specific sequence must be designed. When functions are grouped together in a component just to ensure this order, the component is procedurally cohesive.
communicational 
Alternatively, we can associate certain functions because they operate on or produce the same data set. For instance, sometimes unrelated data are fetched together because the fetch can be done with only one disk access. Components constructed in this way are communicationally cohesive. However, communicational cohesion often destroys the modularity ond functional independence of the design. sequential : If the output from one part of a component is input to the next part, the component has sequential cohesion. Because the component is still not constructed based on functional relationships, it is possible that the component will not constrain all of the processing related to a specific functionality.
functional 
Our ideal is functional cohesion, where every processing element is essential to the performance of a single functionality, and all essential elements are contained in one component. A functionally cohesive component not only performs the functionality for which it is designed, but also performs only that functionality and nothing else. It is thus more likely that changing this particular functionality will affect only one component. The notion of cohesion can be extended to object-oriented design by remembering the overall goal: to put objects and actions together only when they have one common and sensible purpose. For example, we say that an object-oriented design component is cohesive if every attribute, method, or action is essential to the object. Object-oriented systems often have highly cohesive designs, because the compositional process forces the actions to be placed with the objects they affect.

Coupling

Two modules are coupled if they depend on each other in any way. As with cohesion, there are various degrees of coupling. Unlike cohesion, less coupling is better. Modules A and B are tightly coupled if A can modify B, for example, by changing B’s data. They are loosely coupled if A passes data to B. Obviously, there must be some coupling between modules, or the system could not work as a system. The goal is to keep the coupling as loose as possible, because tight coupling makes the system hard to modify. As for cohesion, there are several degrees of coupling, which can be categorized as:

content 
This least desirable kind of coupling occurs when one component actually modifies another. Then the modified component is completely dependent on the modifying one. We call this content coupling. Content coupling might occur when one component modifies an internal data item in another component, or when one component branches into the middle of another component.
common 
We can reduce the amount of coupling somewhat by organizing our design so that. However, dependence still exists, since making a change to the common data means tracking back to all components that access that data to evaluate the effect of that change. This is called common coupling. With common coupling, it can be difficult to determine which component is responsible for having set a variable to a particular value.
control 
When one component passes parameters to control the execution of another component, we say there is control coupling between the two. It is still impossible for the controlled component to function without direction from the controlling one. Design with control coupling is sometimes acceptable, but must be restricted as to minimize the amount of controlling information that must be passed from one component to another and to localize control to a fixed and recognizable set of parameters forming a well-defined interface.
stamp 
When a data structure is used to pass information from one component to another, and the whole data structure is passed, there is stamp coupling between the two. The main problem with stamp coupling is that sometimes only a part of the information stored in the passed data structure is actually used by the called component. The information that is not used actually induces unnecessary coupling.
data coupling 
This is a restriction of stamp coupling, where data is passed to the called component, but where only the necessary information is passed; and ideally all informations are passed as separate parameters, even though they come from the same data structure. uncoupled : Uncoupled components have no interconnections at all; they are completely independent. It is obviously impossible to have all uncoupled components in a system, as each component would in fact be separate systems.

Putting these two items (cohesion and coupling) together gives the popular slogan: High cohesion and low coupling. Easy enough to say, but how do we achieve it in practice? For example, the Facade Pattern shows how we can reduce coupling in a particular situation. If there is a tightly-coupled group of classes that forms a subsystem, it may help to introduce a new class that mediates all access to classes in the subsystem. Although this may not reduce the actual number of dependencies in the system as a whole, information about the subsystem is now hidden behind the interface of the new class.

Fault Tolerance

A good design should be tolerant of both external and internal errors. A system is fault tolerant if all (or at least a majority) of external and internal errors have been anticipated by the designers and are handled in a suitable way by the system. An external error is an error over which the system has no control: for example, a user might enter an invalid command. An internal error is an error within the system itself or, in common language, a ”bug”. It is important but straightforward to protect the system against most external errors. The key step is to validate all inputs to the system carefully, so that the system rsffejects inappropriate data and processes only “good” data. Even after validation, internal errors may still cause the system to fail. Dividing by zero or computing the square root of a negative number causes most processes to raise a signal. If the signal is not handled and processed correctly, the system may crash (e.g., Ariane).

Simplicity

Given a choice between a complex design and a simple design, choose the simple design. Of course, we assume here that both designs meet the requirements. If the requirements are very complex, it may be impossible to achieve a simple design. Nevertheless, the designer should try to remove all unnecessary complexity from the system. Simplifying is hard and requires experience. Simple techniques often help. For example, a design diagram is hard to read if it contains many crossing lines. If you redraw it to reduce the number of intersections, you will understand it better and may be able to simplify it. A large number of links to a class may indicate low cohesion or high coupling (see above). Increasing cohesion and reducing coupling will tend to simplify a design. Don’t worry about efficiency during design. It is difficult to predict which parts of the system will limit performance at the design stage. Designing for optimal performance is usually unnecessary (although not always!) and will lead to a design that is more complex than it needs to be. (See below for further discussion.)

===Dividing Responsibility=== A useful principle in object oriented design is that responsibilities should be divided evenly between classes. A system in which one class does all the work and the other classes merely store data is probably badly designed. CRC cards is a simple and effective design technique that follows this principle. Dividing responsibilities is a way to make a smooth transition between requirements and design, by thinking about system functionalities, and refine them in terms of subsystem and eventually individual class responsibilities and implementation functions.

Design Rationale

During the design process, the designers discuss many alternatives and reject all but one. It is important to include the reasoning in the design so that maintainers do not waste time implementing the rejected alternatives in the hope of improving the system. Of course, it is easy to think of situations where the maintainers should adopt a rejected alternative. For example, the designers might decide to store data on disk rather than in RAM because memory is insufficient. A couple of years later, when more memory is available, this decision could be reversed. But, even in this example, the design rationale is useful because it tells the maintenance designers that the only objection to using RAM was that there was not enough of it.

Levels of Abstraction

Modularity is sometimes not enough when tackling problems of huge proportions. Sometimes, even though everything is separated in modules, there are still too many modules and it becomes very easy to get lost in this ”forest” of modules. In this case, modules can be themselves categorized into bigger modules, thus introducing the notion of l evels of abstraction. In this view, the components at one level refine those in the level above. As we move to lower levels, we find more details about each component. The levels of abstraction help us to understand the problem addressed by the system and the solution proposed by the design. By examining the levels from the top and working down, the more abstract problems can be handled first, and their solution carried through as the detailed description is generated. In a sense, the more abstract top levels hide the details of the functional or data components from us.

Efficiency in Design

Design requires compromising. There are many trade-offs to be made during design, and a good designer is a person who has a good feeling for what is important and what is not. A common mistake is to place too much emphasis on efficiency. In fact, Hoare has said that the most common mistake in software development is over-concern with efficiency. Obviously, efficiency cannot be ignored. If a system has to perform a task 30 times per day and the task requires an hour to execute, the system is doomed. But this sort of extreme situation is relatively easy to recognize and avoid. In most situations, we can ask:

  • what are the time-critical aspects of the system?
  • How can we ensure that the critical time constraints are satisfied?

When we have answered these questions, we can forget about efficiency and concentrate on other aspects of good design. Most of the techniques outlined above have a negative effect on performance. For example, it may be “efficient” for module X to communicate directly with module Z but, to achieve low-coupling, we prefer that X communicates with Y only, and Y communicates with Z when necessary. In such cases, it is almost always better to minimize coupling, even though extra function calls are needed. If, later on, it turns out that the extra function call has a critical effect on performance, it can be changed.

A useful rule of thumb is that 90% of the time is spent executing 10% of the code. (In some programs, the ratio can be even higher than 90/10.) There is no point in putting effort into optimizing code that is rarely executed; all optimization effort should be put into the critical 10%. The problem is that we may not know which 10% to optimize until the system is running. The moral is: check the obvious factors, and then don’t worry about efficiency until you have to.

Prototyping

If you are not sure whether a design will actually work, it may be a good idea to prototype it. This means implementing the design in a “quick and dirty” way. Don’t invest a lot of time in programming, but get something running that is just enough to validate the design. There are two kinds of prototypes. A throwaway prototype is written, used to validate the design, and then discarded. A rapid prototype is written quickly but its code becomes part of the final system. You must be clear about which kind of prototype you are writing before you start; otherwise you run the risk of having “quick and dirty” code in the final system.

Steps to Good Design

The following hints assume that the design is object oriented:

  • Increments. Start with any design, bad or good, and improve it. Base subsequent changes on clear deficiencies of the current design, do not apply changes for cosmetic reasons during the design.
  • Refactoring. After the design has been proven correct through verification and validation through implementation, make the design more adaptable to the changes that are foreseeable in the near future and/or generally more simple and understandable, which amounts to the same.
  • Information Hiding. For each class, ask: what information does it hide? Should it hide more or less information?
  • Coupling. Which classes are highly-coupled (i.e., have many dependencies with other classes)? Is the coupling necessary? How can it be reduced?
  • Cohesion. Is it possible to describe the class in a couple of sentences? Does each class have a cohesive set of functions? Is every function needed? Are more functions needed?
  • Trade-offs. Balance efficiency against abstraction, simplicity, and coupling — assuming that in most situations, efficiency is less important than the other factors.

Design Reviews

Reviewing is an important part of software engineering (and engineering in general). Any product of development can be reviewed: requirements, specification, design, implementation, test results, etc. Reviewing is generally performed by a review team that includes both the people responsible for the product and others who can study the product with fresh eyes.

During the review, the reviewing team sit around a table and read the design documents slowly and carefully. The designer may lead the other members of the team through the design, but this is not necessary. It may even be undesirable because, by leading the discussion, the designer will impose the same view of the design upon everyone, and this may prevent errors from being noticed.

Anyone who has doubts about the design should mention them. It is important that reviewing is an “egoless” activity: the designer should not get angry or upset and start defending the design. The appropriate response is to judge the criticism carefully and decide whether it is justified.

A reviewing team does not usually correct the errors that its members find. Instead, the views of the team are recorded accurately; this enables the designers to work out corrections carefully and without being rushed. However, if the errors are trivial, the reviewing team may correct them during the review. Pfleeger suggests three design reviews:

  • The preliminary design review is a meeting of clients and suppliers during which the conceptual design is reviewed and (hopefully) approved by the clients.
  • The critical design review is a meeting of designers. The team responsible for the requirements (sometimes called “analysts”) may also be present. The purpose of the review is to ensure that the conceptual and technical designs are free of defects and meet the requirements.
  • The program design review is a meeting of designers and developers. The purpose of this review is to ensure that the detailed design is feasible and that the implementation team will be able to understand it.

All of these reviews are important. In a small project, however, the first two would be fairly brief and informal. The program design review is the most important and should be carried out carefully, even for a small project. Reviews are useful: consider including reviews in your project.