Object Oriented Design Tips
Here is an assortment of tips to keep in mind when using object oriented design. We cover the following tips in this article:
- Stay close to problem domain
- Object discovery vs. object invention
- Pick nouns or noun phrases as classes
- Method names should contain a verb
- Prefix adjectives when naming inheriting classes
- Do not add suffixes to class names
- Avoid one-to-one mapping from structured design
- Replace multiple get-set methods with operations
- Model classes that handle messages as state machines
- Use const whenever possible
- Restrict header file level dependency
- Don't reinvent the wheel; use STL
- Class with just get-set methods points to missed delegation
- Replace an array of structures with an array of objects
- Delegate work to helper class
- Multi-dimensional arrays point to incomplete class identification
- Multiple nested loops point to incomplete delegation
- Class with very large numbers of methods points to incomplete class identification
- Don't go overboard with inheritance
- Prefer delegation to inheritance
- Don't scatter the abstraction
- Consider group of objects to split work amongst team members
- Use nested classes for lightweight helper classes
- Use templates to improve type safety and performance
- Divide your code into framework and application parts
Stay close to problem domain
Design is a process of modeling the problem domain into programming constructs. Object oriented design simplifies the design process by maintaining a one-to-one mapping between problem domain objects and software objects. To succeed in object oriented design, keep your design as close as possible to problem domain objects. The interactions between your objects should mirror interactions between corresponding problem domain objects.
Problem domain objects is basically an object that can be found in the problem itself. For example, when developing a text editor real-world objects would be, Paragraph, Sentence, Word, ScrollBar, TextSelection etc. While developing a call processing module, the objects might be Call, Ringer, ToneDetector, Subscriber etc.
Object discovery vs. object invention
The first step in object oriented analysis is to discover the objects that can be directly identified from the problem itself. In many cases objects can be identified from the requirements. Objects discovered from the problem statement are extremely important. These objects will be the core objects in the design.
The next stage in object design is to "invent" objects. These objects are needed to "glue" together objects that have been identified during object discovery. Invented objects generally do not correspond to anything tangible in the problem domain. They are inventions of programmers to simplify design.
Consider the following statement from the requirements:
The circuit controller shall support digital and analog circuits. The circuit controller shall contain 32 DSPs. When the circuit controller receives a request to setup a circuit, it shall allocate a DSP to the circuit.
We discover the following objects from the requirement:
We invent the following objects based on our knowledge of the manager design pattern:
- DSPManager: Manages the 32 DSPs on the circuit controller
- CircuitManager: Manages the digital and analog circuits
We invent a Circuit base class for DigitalCircuit and AnalogCircuit by filtering properties that are common to DigitalCircuit and AnalogCircuit objects.
The relationship between the classes also follows from the requirement. CircuitController class contains DSPManager and CircuitManager classes. The CircuitManager contains an array of Circuit class pointers. The DSPManager contains an array of DSP objects.
Pick nouns or noun phrases as classes
Identifying objects is easy, they should always be nouns. As we have seen in the Circuit Controller example, we picked up nouns from the requirements as classes in our design. Even when you invent classes, keep in mind that they should be nouns. Abstract concepts don't qualify as object names.
Naming the objects is extremely important in object oriented design. Chances are that if you name your object correctly, the designers and maintainers will assign it functionality that fits its name. Also note that, if you have trouble naming an object, you probably have the wrong object. At this point go back and look at the problem again and see if you can pick an alternative object.
Method names should contain verbs
In any language, actions performed by nouns are specified using verbs. Why should object oriented programming be any different? Thus make sure all the operation methods should contain verbs.
Thus the Circuit class we discussed earlier would have methods like:
Notice that the methods do not include Circuit in the name (ActivateCircuit, BlockCircuit etc.) as being methods of Circuit its clear that they refer to operations on Circuit.
Prefix adjectives when naming inheriting classes
This one is fairly obvious. When a class inherits from a base class, the name for the new class can be determined just by prefixing it with the appropriate adjective. For example, classes inheriting from Circuit are called AnalogCircuit and DigitalCircuit. Following this convention leads to class names that convey information about the classes inheritance.
Do not add suffixes to class names
Do not add suffixes like Descriptor, ControlBlock, Agent to the class names. For example, DigitalCircuit should not be called DigitalCircuitDescriptor or DigitalCircuitControlBlock. Such names are longer and do not convey the exact role of the class.
Avoid one-to-one mapping from structured design
Many developers moving from structured design just continue with structured design in C++. The classes developed correspond more to similar structured constructs they have used in the past. Similarity between C and C++ confuses developers. Make no mistake, object oriented programming is a completely different technique. The emphasis here is to keep the design process simple by minimizing the difference between the problem domain and software domain.
Replace multiple get-set methods with operations
Developers complain that after moving to object oriented programming, they spend considerable time writing mindless get and set methods. Here is a simple tip on reducing the get and set methods. Consider the code below:
The above code can be replaced by moving the field filling in the message to the Circuit class. This way you do not need to define a large number of get operations. Also, any changes in the CircuitInfo field would result only in changes to the Circuit class. CircuitManager would be transparent as it does not look into CircuitInfo.
Model classes that handle messages as state machines
Whenever you encounter a class that has to perform some level of message handling, its always better to model it as a state machine. We have discussed this in the article on hierarchical state machines.
Use const whenever possible
C++ provides powerful support for const methods and fields. const should be used in the following cases:
- Methods that do not change the value of any variable in the class should be declared const methods.
- If a function is supposed to just read information from a class, pass a const pointer or reference to this function. The called function would be restricted to calling const methods and using the classes fields only on the right side of an expression.
Proper and consistent use of const will help you catch several bugs at compile time. So start using const from day one of your project. If const is not used extensively from the beginning of a project, it will be close to impossible to add it later.
Restrict header file level dependency
Complex software requires a careful header file management even when programming in C. When developers move to C++, header file management becomes even more complex and time consuming. Reduce header file dependency by effective use of forward declarations in header files. Sometimes to reduce header file dependency you might have to change member variables from values to pointers. This might also warrant changing inline functions to out-of-line functions. Every time you use a #include make sure that you have an extremely good reason to do so.
For details refer to the header file include patterns article.
Don't reinvent the wheel; use STL
The C++ standard template library is extremely powerful. It can save countless hours of coding and testing of complex containers and queues.
- Get started with STL with the STL design patterns.
- Become a master in STL with these video lecture series:
- Object Oriented Tips II
- Hierarchical State Machines
- Header File Include Patterns
- STL Design Patterns
Class with just get-set methods points to missed delegation
Many times while developing classes you might find that a particular class you have developed has just get and set based methods. There are no methods to perform any operations on the object. In many cases, this points to inadequate delegation of work by the caller. Examine the caller of the get-set methods. Look for operations that could be delegated to the class with just get-set methods.
The example below shows a DSP class that has get and set methods. The Message Handler class was doing most of the processing.
The above classes have been transformed to assign most of the DSP queue management to the DSP class itself. This has simplified the design of the Message Handler class. The interfaces of the DSP class have also been simplified.
Replace an array of structures with an array of objects
Whenever you end up with an array of structures in your class, consider if you should convert the array of structure into an array of objects. Initially the structure array might be simple with only one or two fields. As coding progresses, more and more fields are added to the structure. At that time it might be too late to treat the structure as a class.
Delegate work to helper class
If you find that one of the classes in your design has too many methods and the code size for the class is much greater than your average class, consider inventing helper classes to handle some of the functionality of this class. This will simplify the design of the huge class, making it more maintainable. More importantly, you might be able to split the work amongst different developers.
Consider the following class:
The above class can be made more maintainable by adding private helper classes SignalingHandler and ErrorHandler.
Multi-dimensional arrays point to incomplete class identification
If your design contains multi-dimensional arrays, this might point to missed class identification. The following example should clarify this:
The above two dimensional array points to missed identification of SignalProcessingCard class. This has been fixed in the following code fragment:
Multiple nested loops point to incomplete delegation
Many times, nested loops point to incomplete delegation. May be the inner nesting of the loop should have been delegated to a lower level object. Consider the above example of SignalProcessingCard and DSP.
The inner loop in the above code should be replaced with a Initialize method at SignalProcessingCard. Code operating on SignalProcessingCard initialization should not worry about DSP level initialization. This should be delegated to the Initialize method of the SignalProcessingCard.
Class with very large numbers of methods points to incomplete class identification
A class with very large number of methods typically means that fine grain object identification has been missed. At this stage, have a hard look at your design to identify more classes.
Don't go overboard with inheritance
This is a very common mistake made by designers new to object oriented design. Inheritance is such a wonderful concept that its easy to go overboard and try to apply it every where. This problem can be avoided by using the litmus test:
X should inherit from Y only if you can say that X is a Y. By this rule its easy to see that Circle should inherit from Shape as we can make the statement "Circle is a Shape".
Inheritance is the most tightly coupled of all the relationships. Every inheritance relationship causes the derived class to strongly depend upon the base class. That dependency is hard to manage.
Also note that the biggest benefit of object oriented design are obtained from composition and not inheritance. In our earlier example, programmers can develop SignalProcessingCard and DSP objects as if there was only one instance of the object. The multiplicity is achieved by just declaring an array of the objects.
Prefer delegation to inheritance
Many times, relationships are better modeled as delegation than inheritance. When in doubt, always consider delegation as an alternative. Sometimes commonality in classes that do not meet the "is a" rule is better implemented by using a common helper class which implements the common functionality. This class can then be included as a member in both the classes.
Consider two classes TerminalAllocator and DSPAllocator which use similar resource allocation algorithms. The two classes have completely different type of interfaces. You might be tempted to model this as both the classes inheriting from a common Allocator class which implements the common parts of the allocation algorithm. In many cases, it might be better to model TerminalAllocator and DSPAllocator as standalone classes with a helper class Allocator included as a member.
Don't scatter the abstraction
This is a common mistake when multiple developers are working on a project. Each developer implements his or her part by designing objects that they need, without considering if other developers have similar objects. This scatters the abstraction of an object into several different objects which implement pieces of the whole objects functionality. In our example, this would mean that the design contains several objects that represent the SignalProcessingCard and DSP objects in different portions of the code. Each developer implement parts of the SignalProcessingCard and DSP functionality that is needed in their domain. This results in scattering the functionality of an object over several incomplete objects.
Needless to say, such code would be difficult to understand and hard to maintain.
Consider group of objects to split work amongst team members
Embedded software developers often split work amongst team members by dividing the functionality into several tasks. With object oriented design, work can be divided in a much more fine grain way by assigning a group of classes to a developer. In many cases you can implement all the functionality in a single task, thus greatly reducing the effort in designing intra-processor communication.
Use nested classes for lightweight helper classes
Many times you will encounter a situation where a small class might be useful in capturing some of functionality of a large class. Often developers avoid adding such classes as they would result in a new set of header and source files. This brings its associated changes like makefile updates, checking in new elements. Another problem with the this approach is that you end up with simply too many classes. There is no way to isolate the important classes from the simple helper classes.
The solution to this problem is to use small nested classes that are declared within the parent class. With this approach, the nested class does not appear amongst the top level classes in your design. This greatly simplifies the total number of high level classes you have to deal with. (If you are using a tool like Microsoft Visual Studio, the nested classes would appear as tree nodes inside the parent class. Thus adding a new class does not increase the number of classes visible in the outermost nodes of the tree).
Nested classes can be made even more lightweight by letting developers write the code for the nested classes in the parent class source files. This lightweight mechanism would improve the readability of complex classes. The developers can now model the complex class as a set of lightweight helper classes.
Use templates to improve type safety and performance
Do not restrict yourself to using templates as defined in STL. Templates can be used to provide type safe and efficient code in the following cases:
- Classes have common functionality but differ in the size of data structures. Such classes can be modeled in base template class that takes the data structure sizes as template parameters.
- Preprocessor macros are type independent but type unsafe. C++ inline functions are type safe but type dependent. Template functions can be used to replace macros as well as regular inline functions. Template functions are both type safe and type independent.
- Pointer and reference based classes where the functionality is the same in classes but the type to operate on is different. In most such cases declaring template base class with a generic type would solve this problem in an elegant fashion.
Divide your code into framework and application parts
When developing a new application consider dividing the total application into core application code and framework code. The core application code performs operations that are very specific to the application at hand. All the other code that is needed to support the core application should be modeled as an application framework. This has several benefits:
- The application framework developed here might get reused in developing similar applications. The application framework can be reused much more readily than the core application.
- Lower layers of the application framework might be reused in applications that are quite different from the original core application.
- The core application can to be ported to a different platform by just changing the application framework.
- Often developing the core application and framework requires different skills. This application- framework can simplify staffing the project.
Here are a few examples of possible frameworks:
- Tracing framework
- Memory management framework
- Message management framework
- Call processing framework
- Operator interface management framework
- Fault handling framework