发表于2003/1/2 9:15:00 535人阅读
Object-Oriented really is better than Structured
Everyone talks about it but no one defines it
'Object-oriented' has clearly become the buzzword of choice in the industry. Almost everyone talks about it; Almost everyone claims to be doing it (or using it); and almost everyone says its better than sliced bread. But very few people seem to spend much time justifying it. This unexamined exploitation of the term does a great disservice to the Object-oriented method. Objects may be the best way devised so far to cut through the miasma of complexity found in large, real-world software systems. But as long as vendors and marketers are allowed to misuse and mangle the concept beyond recognition (and claim benefits that it will never be able to realize), Object-Orientation will be unable to fulfill its true promise. In order to use the tool wisely, we need to examine closely what Object-Orientation is about and discover whether or not it can live up to the claims of its proponents. Does Object-Orientation really produce better software than more traditional structured techniques?
I'd like to take a step back from the day in and day out tasks of software construction and think about what it is we are doing. What, in its barest form, is software construction? What are we doing when we sit down to design a new system or program a new feature into an existing one? Only after we have placed our activity in a framework of this sort can we evaluate the tools and methods that we use and decide how we can do better. If our goal is to produce better systems in a shorter time, then finding tools that support this goal ought to be one of our chief concerns. It's possible (and I believe it to be the case) that Object-Oriented methods and techniques are, for the moment, the tool for which we have been looking.
What is Software?
What is computer programming? Niklas Wirth sums it up eloquently in the title of his book: Algorithms + Data = Programs. To put it another way, "A Software system is a set of mechanisms for performing certain actions on certain data."
This means that there are two orthogonal (yet complementary) ways to view software construction: we can focus primarily on the functions or primarily on the data. The heart of the distinction between traditional structured design methodologies and newer object-oriented methodologies lies in their primary focus: Structured Design techniques center on the functions of the system: "What is it doing"; Object-oriented techniques center on the data (objects) of the system: "What is being done to". As we will see, this seemingly simple shift in focus radically changes the process of software design, analysis and construction. Object-Orientation really can provide tools to produce software of higher quality.
What is Quality software?
Software engineering is primarily about finding ways to produce quality software. What do we mean when we speak of "quality" in software? Quality in software can be measured in external characteristics (e.g. easy to use, runs fast) or internal characteristics (e.g. modular design, readable code). The external metrics are the only ones that really matter in the end. No one really cares if you used a good modular design to construct your software, they just care that it runs well. However, since the internal (hidden) metrics are key to producing the external ones, both must be taken into account during the course of software design and construction. Table 1 is a list of the more common external factors found in quality software. More details on some of these will be provided below.
|Correct||Does the right thing on normal data|
|Extendible||Can adapt easily to changing requirements|
|Reusable||Can be used in systems other than the one for which was created|
|Compatible||Can be used easily with other software|
|Efficient||In time, computer memory, disk storage, etc…|
|Portable||Can be transferred easily to other hardware and software environments|
|Verifiable||Easy to test, easy to design test cases for, Easy to detect when (and where) the software failed, etc…|
|Maintains Integrity||Protects itself from abuse and misuse|
|Easy to use||For the user and for the future programmers|
Table 1: Characteristics of Quality Software
Correctness and Robustness
It is easy to confuse Correct software and Robust software. Correct software works properly when fed normal inputs. It meets all of the requirements in its specification and does not fail within the range for which it was designed. Software robustness implies correctness and more. Robust software is able to handle circumstances outside of its design. These circumstances include inappropriate user input, hardware failure and run-time errors. Robust systems fail gracefully, without the lost of critical data. It is easy to see that both characteristics are necessary for any system to be judged high-quality software. If a system is not correct, then it is not useful. If a system is not robust, then it will be fragile and unable to cope with real-world situations.
Requirements change. This is one of the indisputable facts of software engineering. High quality software is able to deal with changes relatively painlessly. This sort of adaptability is not an issue for small projects but becomes crucial when programming-in-the-large. The two main principles of creating extendible software are:
- Design Simplicity: A simpler design and architecture lends itself to change much more readily than a complex one.
- Decentralization: Breaking up complex problems into small, manageable and independent chunks means that the chunks can be dealt with by themselves. This means that changes can be implemented carefully without disrupting the rest of the system.
These principles allow us to understand the problem in parts without becoming bogged down in the myriad of incomprehensible details.
Reusability and Compatibility
Reusability can be viewed at several levels: analysis, design, and implementation. Reusability promotes quality in several ways
- If designs and code can be reused, then we can start with already tested, tried and true components whose quality is already high.
- The time and energy saved via reuse can be applied to improve other areas of software quality (e.g. correctness or robustness).
Software compatibility is a measure of how easy it is to combine different software products together into new arrangements. Compatibility stems from common design decisions. For example, the UNIX file system is designed to allow small tools to work together well in solving complex problems. On the other extreme is the typical mess of competing Word Processing or Graphics file formats. In this case, much effort must be made to create translators that allow one program to work with another.
Compatibility and reuse go hand-in-hand because truly reusable software (e.g. plug & play components) must be compatible with its new environments. Compatible software promotes quality by leveraging past efforts when building new systems. Software with a low compatibility factor requires a tremendous amount of effort to go into building customized translation systems. This effort could be better spent designing better systems or improving code efficiency.
These first five attributes of quality software (correctness, robustness, extendibility, reusability and compatibility) are the ones that benefit most when switching from structured to Object-oriented. We will be concentrating on them for the rest of this article but this does not mean that Efficiency, Portability, Verifiability, Integrity and Ease-of-use are of lesser value. All of the listed attributes are vital to producing truly top-quality software.
Where traditional, structured top-down design fails
Now that we know what we're looking for when we create software, we can evaluate how well the techniques we have work.
A brief overview of top-down structured design
Top-down design (also known as top-down decomposition or stepwise refinement) is characterized by moving from a general statement about the process involved in solving a problem down towards more and more detailed statements about each specific task in the process. Figure 1 is a sample top-down decomposition of a Data Import Routine. Notice how the breakdown is strictly along functional lines; we care only about what each module does.
Top-down design works well because it lets us focus on fewer details at once. It is a logical technique that encourages orderly system development and reduces the level of complexity at each stage of the design. For obvious reasons, top-down design works best when applied to problems that have a clearly hierarchical nature. Unfortunately, many real-world problems are not hierarchical. Top-down function-based design also has other limitations that become apparent when developing and maintaining large systems.
- The functional viewpoint is difficult to evolve
- Real systems are hard to characterize functionally
- The functional focus loses sight of the data
- Functional orientation produces less reusable code
Problems with the technique
The functional viewpoint is difficult to evolve
Every real-world system undergoes change and evolution. The top-down approach creates a good software model for the initial requirements of a system. But as that system changes and new requirements are added, the functional architecture becomes more and more unwieldy. Because the software is designed around a relatively fixed tree structure (see Figure 1), changes usually require extensive pruning and grafting. The clean and well laid out design quickly becomes a horror story of new and dangling connections. Maintenance becomes more and more difficult as the original architecture slowly devolves with each new or changed requirement.
Real systems are hard to characterize functionally
Most large systems do not have a top. For example, a database system involves tools for querying data, changing data, keeping data consistent, etc… There is no one function central to these diverse concerns. Defining these systems in terms of a single top-level function is artificial and yields overly complex and non-adaptive architectures.
"One of the most important decisions you make about a system is how to do the first level of decomposition, and in top-down design you're asked to make that decision at the beginning of the design process, when you have the least information... Perhaps the most serious weakness is that top-down, functional design requires a system to be characterized by a single function at the top, a dubious requirement for many modern event-driven systems."
The functional focus loses sight of the data
As you can see from the example above (Figure 1), the top-down design does not capture anything about the data involved in the program. Functions always do something to data. Usually, the same data is shared among a number of functions (for example, updating, deleting, inserting and querying a database table). Since the decomposition only highlights the functional aspects of the problem, the influence of the data structures on the problem is lost.
Functional orientation produces less reusable code
Top-down design works by continually refining a problem into simpler and simpler chunks. Each chuck is analyzed and specified by itself, without much regard (if any) for the rest of the system--This is, after all, one of the reasons that top-down design is so effective at analyzing a problem. This method works well for the initial design of a system and helps ensure that the specifications for the problem are met and solved. However, each program element is designed with only a limited set of requirements in mind. Since it is unlikely that this exact set of requirements will return in the next problem, the program's design and code is not general and reusable.
Top-down design does not preclude the creation of general routines that are shared among many programs; but it does not encourage it. Indeed, the idea of combining reusable programs into a system is a bottom-up approach quite the opposite of the top-down style.
But what about sub-routine libraries?
Of course, there are time when the functional approach works well. These tend to be cases where:
- A large group of similar problems can be identified and grouped together.
- Each of these problems is relatively small and can be characterized by a small number of parameters.
- Each problem is distinct from the others (otherwise, the commonality will necessitate re-using the design and code in each problem).
- No complex data structures are involved (otherwise, the conceptual autonomy of each problem module would be lost).
Areas that match these criteria include mathematical sub-routine libraries for solving linear algebra problems and differential equations.
The difficulties with this approach when dealing with more complex problems are that you are limited to either having individual routines with many parameters or to having many small routines with few parameters. In the first case, the number of parameters will make the routine difficult to use and difficult to maintain. The code will most likely include numerous nested if...endif or case...endcase statements; a nightmare to update as new cases are added to the problem space. In the second case, the large number of routines will make it difficult to remember exactly which one to use. Furthermore, there will likely be a great deal of common code scattered amidst these many routines (since many of them will be very similar). This common code will be difficult to update and maintain. Sub-routine libraries are useful in certain domains, but they do not solve the general problems inherent in structured, top-down design.
Since he says it particularly well, I will let Bertrand Meyer summarize:
"Top-down functional design [is] poorly adapted to the development of significant software systems. Top-down design remains a useful paradigm for small programs and individual algorithms... But it does not scale up to sizable practical systems. The point is not that you cannot develop a system top-down: you can. But in doing so you trade short-term convenience for long-term inflexibility, you unduly privilege one function over the others and (often) the interface over the deeper features of the system, you lose sight of the data aspect, and you sacrifice reusability."
How and Why Object-Oriented Methods Succeed
Now that we have examined Top-down design and found it to be wanting in certain areas, we can turn our attention to Object-Oriented design and see whether or not it addresses any of the problems that we have discovered.
Overview of Object-Oriented Analysis & Design
Object-Oriented analysis begins with an examination of the real-world 'things' that are part of the problem to be solved. These things (which we will call objects) are characterized individually in terms of their attributes (transient state information) and behavior (functional process information). In Object-Oriented terms, we discover and describe the classes involved in the problem domain. In parallel to these individual characterizations, we also model the links or collaborations between the problem domain's objects (and therefore our solution's classes). These links can take the general forms of aggregation (this is part of that), delegation (this uses that) or inheritance (this is a that).
Object-Oriented Design then turns from modeling the problem domain towards modeling the implementation domain. Our class structure now begins to include descriptions of computer specific entities, for example: User Interface classes (windows, menus, etc…), Task Management classes (Processes, Semaphores, etc…), and Data Management classes (lists, stacks, queues, etc…). Because object-oriented analysis and design use the same language (and can use the same notations), it is generally easier (and more profitable) to have the two processes running in parallel and iteratively. As Grady Booch points out:
"The boundaries between analysis and design are fuzzy, although the focus of each is quite distinct. In analysis, we seek to model the world by discovering the classes and objects that form the vocabulary of the problem domain, and in design, we invent the abstractions and mechanisms that provide the behavior that this model requires."
It is extremely important to note that the goal during the analysis and design process is not focused solely on developing a solution for the current understanding of the problem, but rather to design and build general classes with complete and useful structures. The class models are fleshed out and completed above and beyond the particular needs of today. To use Meyer's colloquial expression, we want our classes to have a complete shopping list of attributes and behavior.
- Example, if the solution requires a stack of integers, the object-oriented approach would realize that stacks are interesting and useful entities in and of themselves and therefore, quite worth modeling independently of the particular needs of integers.
- Example, if a solution required keeping track of a small list of phone numbers, we would not model a phone-number-list class. Instead, we would model a general list class and then model the phone number list using it (either by delegation or inheritance). This list class would have all of the functionality that lists require (add elements, delete elements, search for an element, sort the list, etc…). Notice that we can design our class without worrying about the rest of the problem domain. This ability to view classes in the abstract and in (partial) isolation is one of the strengths of the object-oriented methodology. We can keep our attention focused in the small even while solving large and complex problems.
The advantage to this more general approach to class design is that our classes will tend to be reusable in new situations and more generally extensible and compatible with one another. They will be reusable because they are designed not for a single problem, but to model the complete characteristics of a real-world (or a least computer implementation world) thing. Since it is likely that this same 'thing' will reappear in other problems, our complete model will be useable there even if the thing is being used in a very different manner. They will be more extensible because they are designed generically, without being tied to the particulars of one problem. More functionality can be added, or the implementation may be changed, without harming the fundamental abstractions involved. The classes will be more compatible because they are designed to be complete in themselves with clearly defined (and protected) interfaces; this means that they will be able to exist in new situations without change.
Given this, it is then likely that our models will not break down and fail as the requirements for the current problem change. As an added benefit, they will be useful for the solutions of many other future problems. As we continue to use object-oriented methods, we will find that we have a treasure chest of reusable component classes generally applicable to the problems we need to solve.
Object-Oriented Design, A Summary
Meyer defines Object-Oriented design formally as "the construction of software systems as structured collections of abstract data type implementations (see sidebar)". More informally, he defines it as "the method which leads to software architectures based on the objects every system or subsystem manipulates (rather than "the" function it is meant to ensure)". An object can be viewed as the realization that certain knowledge and certain operations are conceptually related to each other, so that it makes sense to bundle them together.
To build a system with an object oriented approach means to analyze the problem and find the objects involved in the system. The general characteristics, traits and behaviors of these objects are then modeled and implemented as classes in an Object-Oriented programming language. Once the objects of the problem domain have been modeled and created as classes (or obtained from previous projects or 3rd-party class libraries), these classes are then assembled together to model the system within the computer framework. This bottom-up data-based approach leverages past efforts and enables the creation of custom systems built from pre-fabricated parts.
Why Object-Orientation works
Object-oriented analysis, design and programming methodologies work together synergistically to produce computer solutions that better model their problem-domains than similar systems produced by purely structured techniques. The systems are easier to adapt to changing requirements, easier to maintain, more robust and promote greater design and code re-use. The reasons for these improvements include:
- Object-Orientation works at a higher level of abstraction
- The Object-oriented software life cycle requires no "vaulting" between phases
- The data on which a system is based tends to be more stabile than the functionality it supports
- Object-orientated programming encourages and supports good programming techniques
- Object-orientated design and programming promote code re-use.
Object-Orientation works at a higher level of abstraction
Because we humans are extremely limited, we have devised techniques to become more effective. One of our most powerful techniques is the form of selective amnesia called 'Abstraction'. Abstraction allows us to ignore the details of a problem and concentrate on the whole picture. Top-down design supports abstraction at the function-level. Object-Oriented design supports it at the object-level. Since objects encapsulate both data (attributes) and function (behavior), they work at a higher level of abstraction. The development can proceed at the object level and ignore the rest of the system for as long as necessary.
The Object-oriented software life cycle requires no "vaulting"
Traditional techniques of managing the software life-cycle require the use of very different languages, styles and methodologies for each step of the process (see Figure 2). Moving from one phase to another requires an often complex translation of perspective between models that can be in almost different worlds. The translation not only slows the development process but also increases the size of the project and the chance for errors to be introduced in moving from one language to another. The object-oriented approach, on the other hand, uses essentially the same language to talk about analysis, design, programming and (if using an Object-oriented DBMS), database design. This streamlines the entire software development process, reduces the level of complexity and redundancy, and makes for a cleaner system architecture and design.
Functions are not the most stable part of a system, the data is. Over time, the requirements of a system undergo radical change: New uses and needs for the software are discovered; new features are added and old features are removed; New reports are devised and new ways of entering and modifying the data are evolved; new information is collected and existing information is using in innovative ways. During the course of all this change, the underlying heart of the system remains comparatively constant. This heart is the data. For example, reports and tax forms may change over time, but the fact that tax preparation software must deal with Income, Expenses, Deductions, Tax Rate tables, etc… does not. A relational database management system (RDBMS) may use more efficient indexing schemes or have new command languages created for data access, but the fact that an RDBMS must deal with tables, indexes, schemas and relations does not. This means that a system built out of "structured collections of abstract data type implementations" (see above) will be able to continue to use these same data-types (classes) throughout the software life-cycle. The functional implementations of the classes will change, but "Encapsulation and information-hiding work together to isolate one part of the system from other parts, allowing code to be modified and extended and bugs to be fixed, without the risk of introducing unnecessary and unintended side-effects."
Of course, the data and classes of a system change over time. Fortunately, object-oriented methods make data evolution easier to manage, less painful to implement and more robust than similar changes would be in the functional model. In a well designed system, the application's data is carefully hidden behind the class interface. Changes in the data effect only one class (or related sub-system of classes) at a time and can be carefully managed and tested. Furthermore, the power of inheritance means that we can add new features by sub-classing existing classes. This means both that we do not need to re-invent the wheel for each change and that the new code is much less likely to break already existing code.
Encourages and enforces good programming techniques
For the last several generations, designers and programmers have been reared on the mother's milk of information hiding, correct variable scoping, and modularity. We have been taught to create routines with strong cohesion, loose coupling and clear designs. All of this remains completely valid in the object-oriented world. In fact, object-oriented design and programming add support for these goals into the language, making their achievement even easier. A class in an object oriented design carefully delineates between it's interface (specifications of what it can do) and the implementation of that interface (how it does what it does). The routines and attributes within a class are held together cohesively by the object which they are modeling. In a properly designed model, the classes will by grouped neatly into sub-systems that are loosely coupled and the linkages between classes in different sub-systems will be minimized.
This is not to say that object-oriented programming is a panacea; there is nothing magical here that will promote perfect design or perfect code. But by raising the level of abstraction from the function-level to the object-level and by focusing on the real-world aspects of the system, the object-oriented methodology tends to promote cleaner designs that are easier to implement and provide for better overall communication. Using an object-oriented language is not strictly necessary to achieve this benefit. But an object-oriented language (e.g. C++, Smalltalk or Eiffel) adds support for object-oriented design and makes it easier to produce more modular and reusable code via the concepts of class and inheritance.
Data-based design promotes code re-use
The code and designs in object-oriented software development are reusable because they are modeled directly out of the real-world problem-domain. Each class stands by itself or within a small circle of peers (for a sub-system). Within this framework, the class does not concern itself with the rest of the system or how it is going to be used within a particular system. This means that classes are designed generically, with reuse as a constant background goal. Furthermore, object-oriented programming languages add inheritance and genericity to the programmer's toolbox. These powerful techniques allow new classes to be built from old. In this case, only the differences and enhancements between the classes need to be designed and coded. All of the previous functionality remains and can be reused without change.
- Software engineering is primarily about finding ways to produce quality software. Some of the characteristics of quality software are: Correct, Robust, Extendible, Reusable, Compatible, Efficient, Portable, Verifiable, Integrity, and Easy to use.
- Top-down design (also known as top-down decomposition or stepwise refinement) is characterized by moving from a general statement of what a program does down towards more and more detailed statements about each specific task.
- Top down design is inappropriate for the development of significant software systems because it trades short-term convenience for long-term inflexibility, it unduly privileges one function over the others, it loses sight of the data behind the problem, and it sacrifices reusability.
- Object-Oriented design is the construction of software systems as structured collections of abstract data type implementations.
- Object-Oriented Design and Object-Oriented Programming improve upon Top-down design by focusing on the data of the system rather than what the system does. This approach produces systems that are easier to evolve, more flexible, more robust and more reusable than top-down structured ones.
- Object-Oriented methods are better because:
- They work at a higher level of abstraction.
- No "vaulting" is needed between phases.
- Data tends to be more stabile than the functionality it supports.
- They encourage and support the classical virtues of good programming and design.
- They promote code re-use and provide tools that make it possible.
As anyone who has read him can tell, I am extremely indebted to the lucid presentation of Betrand Meyer in his Object-Oriented Software Construction. Other good references include Steve McConnell's Code Complete, Grady Booch's Object-Oriented Analysis and Design, Object-Oriented Modeling and Design by James Rumbaugh et. al. and Designing Object-Oriented Software by Rebecca Wirfs-Brock et. al.
Last updated August 05, 1998 - Copywrite 1998 by Gary Warren King