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:
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.
Overworked Message Handler class and a simple Get-Set DSP class |
|
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.
Here the DSP class does most of the work |
|
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.
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:
Monolithic Digital Trunk Class |
|
The above class can be made more maintainable by adding private helper classes SignalingHandler and ErrorHandler.
Digital Trunk Class With Helper Classes |
|
If your design contains multi-dimensional arrays, this might point to missed class identification. The following example should clarify this:
Two dimensional DSP array declaration |
|
The above two dimensional array points to missed identification of SignalProcessingCard class. This has been fixed in the following code fragment:
SignalProcessingCard class eliminates two dimensional array |
|
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.
Initializing all DSPs (Nested Loops) |
|
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.
Initializing all DSPs delegated to SignalProcessingCard class (no nested loop) |
|
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.
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.
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.
Modeled as Inheritiance |
|
Modeled as Delegation |
|
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.
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.
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.
DigitalTrunk.h : Nested class declaration |
|
DigitalTrunk.cpp : Nested class methods |
|
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:
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:
Here are a few examples of possible frameworks: