Kanchan is a software developer for Vedika Software in Calcutta, India. He can be contacted at kanchan@vedika.ernet.in.
One goal of object-oriented languages and methods is to enable problems and solutions to see eye-to-eye more naturally than procedural languages and conventional design methods. Most object-oriented design methodologies, however, are largely theoretical, and those that are practical often aren't comprehensive enough to help a designer/programmer from start to finish. As a result, much is left to the imagination of the programmers actually implementing the project.
In this article, I'll present a methodology that is practical, easy to understand, and can serve as a reference in solving problems. This methodology consists of two phases: The first presents an abstract model of the problem, and the second prepares an implementation model.
While the abstract model is language independent, the implementation model is based on C++. Essentially, the abstract model is a collation of practical ideas from different methodologies. The implementation model is a rule-based approach to class design for the objects specified in the abstract model.
Before the design process begins, it is important to clearly define your goals. This is done during the analysis phase and presented in a document called the "statement of problem." The statement should be complete enough that it doesn't leave room for further assumptions about system functionality during design.
The statement of problem starts with an introduction to the problem, its context, and the skills of the eventual users of the system being designed. This includes:
The most important part of the abstract model is identification of objects. The abstract model assumes that the problem was defined well during analysis, but it can also point out flaws in the statement of problem, and thereby improve it. (The abstract model draws heavily on the first chapter of Robert Murray's C++ Strategies and Tactics, "Abstraction.")
The goals of the abstract model are to:
Step 1. Identify the single, top-most problem object in a system. In most cases, this is easy: The top-most object is found in the first line of the statement of problem. Likely candidates for such an object include the system/application itself.
Step 2. Determine the functions performed by this object. This statement of functionality is the "executive summary." Minute details aren't necessary, just a broad description of the functionality. The basic goal of this step is to establish the behavior of the object.
In a point of sale (POS) system, for instance, the functionality might be to let customers enter purchase instructions for a product. The executive summary could address such issues as obtaining credit-card details and invoice printing, as well as anything the statement of problem may have overlooked; inventory maintenance or credit-card validation, for instance.
Executive summaries must be crisp and clear, and thus require revision and refinement. Anything vague is best left out. The summary is a description: You need to think about how the object fits in and interacts with its context. Since what you write for the first time may not be the most suitable executive summary, be prepared to revise it.
If you are not able to find a role for an object, categorize it as a placeholder (Product quantity, Currency, and the like) and call it a "primitive object."
Step 3. Find the component objects and determine which combined objects they include. Refer to the statement of problem and the executive summary of a given object for the terms used.
If the executive summary is well written, finding objects isn't difficult. For instance, if the executive summary of a POS system focuses on customers, products, invoices, credit cards, and the like, then these elements are likely object candidates.
The process of identifying objects is called "discovering." At times, however, discovery is not sufficient and objects must be invented. Such inventions are based solely on your judgment and appreciation of the problem.
For example, for a POS system, you might invent product-table objects. If the problem needs to track customer history, then you will also need a customer-table object. However, you should avoid inventing a process as an object. In such cases, you should find the object on (or by) which this function is performed and mark it as another component object.
The components in this step, along with the behavior in Step 2, comprise the "attributes" of the object.
Step 4. Establish the relationship between the problem object and the component objects (not vice versa). Evaluate the relations according to the following criteria:
Step 6. Finalize the interface of each object. Questions will arise, such as "How can this object be used?", "What is required to create the object?", or "What is the functionality expected of this object?" Answering them gives you a list of parameters without which the object is not useful. The answers must be derived from the context of the statement of problem.
Putting yourself in the shoes of someone using your objects and asking, "How do I use this?" will make it easier to establish the interface. This holds true, regardless of whether the user is a fellow team member, someone who buys your library, or you yourself.
Go through the abstract model repeatedly until you are confident that it represents the problem correctly and completely. It's important to document each step. Make sure that you list all the "discovered" objects and then those that were "invented." Since the invention of objects is based on certain inferences, these inferences should also be prominently noted. This will help if anyone other than you goes through the design later. You can represent the model in terms familiar to you and your team. You may want to use a CASE tool to represent it visually. In any case, it should be easy to refer to.
The implementation model takes the abstract model and turns it into a C++ class declaration. Many methodologies leave this to programmers who, if inexperienced, can compromise an elegant design. With this in mind, I'll now describe a set of rules which takes the guesswork out of class design. These rules may not always produce the best class designs, but they can save you from costly mistakes.
For the implementation model, the relationship among the objects is critical. The rules will help you in converting the relations table into a class hierarchy (assuming that ObjectA is the problem object, and ObjectB, the component object).
Rule 1. Is-A. ObjectA is derived from ObjectB.
Has-A. ObjectA keeps the complete object of ObjectB. It could keep it as a pointer or nonpointer but not as a reference.
Uses-A. ObjectA keeps the pointer or reference of ObjectB but not a complete data member. Choice between a pointer and reference is governed by Rule 3.
Rule 2. One:One. ObjectA keeps only one instance of ObjectB. Whether the data member is pointer, reference, or complete object is determined by Rule 1.
One:Many. ObjectA keeps a list of instances of ObjectB. Rule 1 determines whether an individual instance of the list is a pointer, reference, or complete object.
Rule 3. CreationTime. ObjectA should instantiate ObjectB at construction time.
In the case of ObjectA Uses-A ObjectB (Rule 1), ObjectA should take the reference/pointer to a valid ObjectB as the constructor param and maintain an instance of ObjectB as a reference, rather than a pointer.
In the case of ObjectA 'Has-A' ObjectB (Rule 1), the instance of ObjectB is maintained as a complete object rather than a data member.
AnyTime. ObjectA must have a member function which can be invoked as a command to instantiate ObjectB. If ObjectA Uses-A ObjectB (Rule 1), then ObjectB can only be kept as a pointer. In such cases, the function should accept a valid pointer to ObjectB as param. Furthermore, ObjectA can maintain the instance of ObjectB only as a pointer, not as a reference.
If ObjectA Has-A ObjectB (Rule 1), then ObjectA should maintain the instance of ObjectB as a pointer that should be newed during this function call. If ObjectB is kept as a complete object in ObjectA, then ObjectB should have methods (function, insertion operator, assignment operators, and the like) to initialize itself.
Rule 4. One Way. Nothing special.
Two Way. ObjectB must have the reference/pointer to ObjectA as a data member. It can be a reference only if the relation is established during creation time (Rule 3) of ObjectA; otherwise it should be a pointer. In the previous case, the constructor should take the reference/pointer to a valid ObjectA as a constructor param. If ObjectB is being Used-by ObjectA (Rule 1) and many such objects are using it simultaneously, then it would need to maintain a list of all such instances (a situation referred to as Many:One or Many:Many relationship).
Rule 5. Mandatory. If a relation is established between ObjectA and ObjectB during creation of ObjectA (Rule 3), then ObjectA must throw an exception (or invalidate itself) if the relation could not be established. If the relation is not established at creation time (Rule 3), there may be a flaw in the design. If it is found acceptable/valid, there must be a clause attached which says the functionality of ObjectA is affected/unavailable until the relationship is established.
Optional. Provisions to initialize the data member of ObjectB must not be in the constructor--these should be through a member function. A clause must be attached which determines that all functionality is affected/unavailable until the relation is established.
If ObjectA Uses-A ObjectB (Rule 1), then some other object in the system must Has-A ObjectB. Assuming that ObjectC is such an object, there should be only one such object (which Has-A ObjectB) in the system at any point in time. It is the responsibility of that object (ObjectC) and no other to create and delete ObjectB. The instance of ObjectB must be created before it is "used" by ObjectA. This leads to the following subrules:
In any nontrivial project, it's impossible to design the complete system in one sitting. Consequently, you will take the top-most object and break it down to the first level. You should then assign functionality to each identified object and establish the relationship among them. This will yield the framework of the system you are developing. Each object you identified in this stage will typically turn out to be a complete module in itself, which you can treat as a separate system for design purposes.
Considering the complexity of today's software projects, it is unrealistic to expect this methodology to solve all problems. However, this approach will clear some of the major stumbling blocks from the design phase. Keep in mind that the methodology is still evolving and will become more concrete in the coming years.
Booch, Grady. Object-Oriented Analysis and Design with Applications, Second Edition. Menlo Park, CA: Benjamin/Cummings, 1994.
Charney, Reginald B. "Data Attribute Notation." Dr. Dobb's Journal (August 1994).
Murray, Robert B. C++ Strategies and Tactics. Reading, MA: Addison-Wesley, 1993.
Copyright © 1995, Dr. Dobb's Journal