CPSC 333 --- Lecture 19 --- Wednesday, February 21, 1996 Heuristics to Improve Designs: - Increasing the Level of Cohesion of a Module: For a "utility module" --- one that's low down in the structure chart and actually does some work, rather than acting as a controller for other modules --- consider *factoring* the module --- splitting it into several modules that each performs a more "cohesive" subset of the original module's tasks. This might also work for "controller modules." However, it's also possible that you can also make a controller module more cohesive by *increasing* its responsibilities. Note, for example (in reverse), that the main module for the entire system is probably "highly cohesive," since by saying that it "controls the entire system," you can describe its responsibilities using a simple phrase. If you reduced its responsibilities --- perhaps, by leaving it in charge of input, and complex computation, but handing the "output control" off to another module --- you'd end up making the module a bit *less* cohesive than it was before. Turn this around, and you'll obtain an example of a module whose cohesion improves when it's given more to do. Note that it's possible to get "carried away," in solving cohesion problems by creating numerous modules that each has a trivial job. If you do this, then you risk ending up with a system that's impossible to implement quickly or to maintain, because of the numerous modules and interfaces to support --- each new module *does* have an overhead. - Reducing the Level of Coupling of a Pair (or Set) of Modules: If you actually *found* an instance of content coupling, then you should throw (at least part of) the design away, and start over. You haven't followed the rules that have been given here for producing an architectural design for a system. After you've made sure that all the modules (with common coupling) really *need* access to the data structure they share, and after you've ensured that modules (with external coupling) really need access to the same I/O device, you should create "informational clusters" for the data areas and I/O devices, providing controllable "interfaces" for these, as described above. For "control coupling:" check for "tramp data," and consider reorganizing the structure chart, so that if module A *does* affect the behaviour of module B, then one calls the other directly (or as close to that as possible --- certainly, with one of the modules being placed inside the "scope of control" of the other). Stamp coupling and lower levels of coupling are completely acceptable. In general, try to make interfaces as simple as possible. An "elementary" parameter is probably preferable to a "composite one;" *ten* elementary parameters probably aren't. Avoid using control flags or other mechanisms that will be difficult for later maintainers of the system to discover and to understand. Finally, avoid passing information unless that information is actually *needed* by the module receiving it. - Reducing the Size or Complexity of a Module: "Factoring" an overly complex module --- replacing it by several modules that are each shorter and simpler than the original --- is a way of solving this problem, just as it's a way to improve cohesion of utility modules. It's also possible that a module is complex because you've chosen a complex algorithm, when there's a simpler algorithm for the same problem that will be "efficient enough" for the system currently being developed. For example: If you're implementing a "sorting" module, *and* you have a guarantee that you'll never be required to sort more than some small number (say, twenty or thirty) of values, *and* performance requirements aren't excessive, then you might reduce the complexity of a sorting module that implements "quicksort" by implementing an "insertion sort" instead. (Of course, if the number of elements to be sorted really *is* as small as I've said, then it's plausible that the insertion sort will also be at least as *fast* as quicksort when applied to solve the small problems the system is handling, simply because you need to deal with larger problems before the improvements associated with "quicksort" become apparent!) - Elimination of Tramp Data, and Modules Whose Effect on Others are Widespread and Unpredictable See the above information about "reducing the level of coupling." - Elimination of Redundant Modules If you find two modules doing exactly the same thing, replace them by a single module that's called by more than one other. If you find two modules that are doing *almost* the same thing, it may be worthwhile to replace these by a single module that is *slightly* more general than either of the original. Again, don't get carried away: If you go too far, you may end up with a single module that's too complex to maintain. - Elimination of Modules Whose Behaviour is Unpredictable If a module is "unpredictable" in the sense given above --- you can't guarantee that it will do the same thing twice if it's called twice with the same inputs (and with the same values returned each time, by any modules that it calls) --- then make sure that this really is necessary. That is, make sure it really is one of the "exception to the rules" given above when "Design Principle #5" was discussed. If it isn't, then you should look for an algorithm for the problem that doesn't require "state memory" and that's deterministic. "Step B" --- Producing a Usable Structure Chart --- is achieved by repeatedly looking for one of the design problems given above and then fixing it using one of the above methods, until no serious problems are evident. As noted above, you should produce a module specification for each of the modules in your system. You'll probably be frustrated if you spend much time on this early on --- if possible, it's better to wait until your design is stable enough for you to be confident that you aren't specifying a module that you're about to eliminate. The same thing can be said for the job of finishing the data dictionary. On the other hand, *some* of these details will be useful so that you can assess cohesion, coupling, and complexity. Therefore, you should delay decisions as long as you can, but also recognize that you may need to do *some* of this work on module specifications and data dictionary definitions early on. Following architectural design, you should perform "detailed design." This will include selection of the data structures that will be used to implement data areas, and algorithms that will be used to implement modules. At this point (assuming that you're using a "procedural" programming language for implementation), all "nonprocedural" module specifications should be revised, by adding pseudocode for whatever algorithm will be used for implementation. Once you've done all this, "coding" should be a straightforward (possibly almost "mechanical") process. Wrapup: Pros and Cons of "Structured Design" This all made some sense in the nineteen-seventies! At that point, "Pascal" (or a language like it) could be considered to be "advanced." It was to be *expected* that any program you'd implement would have a hierarchical control structure, that interfaces were limited, that I/O was nontrivial (and could be "walled off" from the rest of the system by using something like "transform analysis"), and that most coding would be done "from scratch." In the nineteen-nineties, interfaces are much more interactive and complex. You have access to programming languages with additional features (including *much* better support for information hiding, as well as use of software components). Some things that were "nontrivial" programming problems in the past can be handled using one-line system calls, or using an interface that's become standardized (so that it may be less prone to change). Therefore, you will find that *some* of the details will change! However, many modern systems include "subsystems" that are at some distance from the interface, and don't have substantial performance requirements --- and it's possible to apply quite a bit of what's given above in order to design these. As well, some of the general ideas given above --- including replacement of hard decisions by lots of simpler ones, and the use of a "two stage" process in which you produce something that's not very good using an easy process, and then refine it by looking for and correcting design problems, have been carried over into more modern (but also less mature) design methods. Finally, many of the design "goals and problems" described in this file haven't really changed very much at all. ----- Now we'll jump to a later stage in development --- following coding of each of the modules in the system ... Integration and Testing References: Pressman's "Beginner's Guide:" Chapter 6 Pressman's "Practitioner's Guide:" Chapters 18--19 Testing can be defined as the process of exercising or evaluating a system by manual or automatic means, to verify that it satisfies specified requirements --- or to identify differences between expected and actual results. Software testing frequently takes up more than 40% of *development* time (not including maintenance). It can take up more time than that, for critical applications. Testing "Steps" and "Deliverables" All testing involves the following six steps: 1. Select *what* is to be measured by the test. 2. Decide *how* "whatever is to be tested" should be tested. 3. Develop the test cases. 4. Determine the *expected* or *correct* result of the test. 5. Execute the test cases. 6. Compare the results of the test to the expected results. Documents that are helpful for deciding what is to be measured: - requirements specification --- as well as user manual or man page, if one has been developed - design specification - source code While *execution* of most tests waits until after coding is ("almost") complete, *development* of some tests can begin much earlier --- as early as requirements analysis ... and requirements and design specifications generally have *test plans* as sections. These test plans document the method(s) of testing, test cases, and their expected results. When tests are performed, their results are compared with the "expected results" that were determined ahead of time. - Detected "errors" (mismatches between actual and expected results) are used to begin "debugging" of the source code - "Error rate data" can be used to form a "reliability model" --- in order to determine a "predicted reliability" of the system.