Object Oriented Design Tips II
We have already covered object oriented design tips in a previous article. Here we will look at more tips that will help you improve your object oriented design skills:
- 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
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 SignalProcesingCard 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