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:

  1. Class with just get-set methods points to missed delegation
  2. Replace an array of structures with an array of objects
  3. Delegate work to helper class
  4. Multi-dimensional arrays point to incomplete class identification
  5. Multiple nested loops point to incomplete delegation
  6. Class with very large numbers of methods points to incomplete class identification
  7. Don't go overboard with inheritance
  8. Prefer delegation to inheritance 
  9. Don't scatter the abstraction
  10. Consider group of objects to split work amongst team members
  11. Use nested classes for lightweight helper classes
  12. Use templates to improve type safety and performance
  13. 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.

Overworked Message Handler class and a simple Get-Set DSP class
class DSP
{
public:
   Queue *GetWriteBufferQueue();
   Queue *GetReadBufferQueue();

   void SetStatusRegister(int mask);
   void SetCongestionLevel();
   
   void SetWriteBufferQueue(Buffer *pQueue);
   void SetReadBufferQueue(Buffer *pQueue);   
};   

Status MessageHandler::SendMessage(Buffer *pBuffer)
{
    int dspId = pBuffer->GetDSP();
    
    DSP* pDSP = m_dsp[dspId];
    
    Queue *pQueue = pDSP->GetWriteBufferQueue();
    
    int status = pQueue->Add(pBuffer);    
    
    pDSP->SetStatusRegister(WRITTEN_MESSAGE);
    
    if (pQueue->GetLength() > CONGESTION_THRESHOLD)
    {
       pDSP->SetCongestionLevel();
    }
    
    return status;
}

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
class DSP
{
public:
    void WriteBuffer(Buffer *pBuf);
    Buffer *ReadBuffer();   
};   

Status MessageHandler::SendMessage(Buffer *)
{
    int dspId = pBuffer->GetDSP();
    
    pDSP = m_dsp[dspId];
    
    int status = pDSP->WriteBuffer();
    
    return status;
}

Status DSP::WriteBuffer(Buffer *)
{    
    int status = m_pQueue->Add(pBuffer);    
    
    IOWrite(WRITTEN_MESSAGE);
    
    if (m_pQueue->GetLength() > CONGESTION_THRESHOLD)
    {
       m_bCongestionFlag = true;
    }
    
    return status;
}

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:

Monolithic Digital Trunk Class
class DigitTrunk
{
    Status m_status;
    Timeslot m_timeslot[MAX_TIMESLOT_PER_TRUNK];

    
    int m_signalingTimeslot;
    int m_signalingStatus;
    . . .
    
    int m_errorThreshold;
    int m_localErrorRate;
    int m_remoteErrorRate;
    . . .
public:

    . . .
    void HandleSignalingRequest();
    void SendSignalingIndication();
    . . .
    
    void HandleRemoteError();
    void HandleLocalError();    
    . . .
};    

The above class can be made more maintainable by adding private helper classes SignalingHandler and ErrorHandler.

Digital Trunk Class With Helper Classes
class DigitTrunk
{
    Status m_status;
    Timeslot m_timeslot[MAX_TIMESLOTS_PER_TRUNK];

    class SignalingHandler
    {
    
       int m_signalingTimeslot;
       int m_signalingStatus;
       . . .
     public:
       void HandleSignalingRequest();
       void SendSignalingIndication();
    };
    
    class ErrorHandler
    {
       int m_errorThreshold;
       int m_localErrorRate;
       int m_remoteErrorRate;
       . . .       
    public:
       void HandleRemoteError();
       void HandleLocalError();
    };
    
   // Helper classes
   SignalingHandler m_signalingHandler;
   ErrorHandler m_errorHandler;
    
public:

    . . .
    void HandleSignalingRequest()
    { m_sigalingHandler.HandleSignalingRequest(); }
    
    void SendSignalingIndication()
    { m_signalingHandler.SendSignalingIndication(); }
    
    . . .
    
    void HandleRemoteError()
    { m_errorHandler.HandleRemoteError(); }
    
    void HandleLocalError()
    { m_errorHandler.HandleLocalError(); }
    
    . . .
};    

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:

Two dimensional DSP array declaration
. . .
  // Each Signal Processor Card contains 32 DSPs. 
  // This is represented by a two dimensional
  // array of DSP objects. The first dimension 
  // is the Signal Processor Card Id and the second
  // dimension is the DSP id on the card
  
  DSP m_dsp[MAX_SIGNAL_PROCESSOR_CARDS][MAX_DSPS_PER_CARD];
  

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
   . . .
   // The two dimensional array is replaced. We identify 
   // SignalProcesingCard as an object. This
   // object contains 32 DSPs
    
   class SignalProcessingCard
   {
      DSP m_dsp[MAX_DSPS_PER_CARD];
    public:
      . . .
   };
    
   // Array of signal processing card objects, indexed by signal processing card id  
   SignalProcessingCard m_signalProcessingCard[MAX_SIGNAL_PROCESSOR_CARDS];
   . . .
  

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.

Initializing all DSPs (Nested Loops)
   . . .
   for (card=0; card < MAX_SIGNAL_PROCESSING_CARDS; card++)
   {
      for (dsp=0; dsp < MAX_DSPS_PER_CARD; dsp++)
      {
         m_signalProcessingCard[card].GetDSP(dsp)->Initialize();
      }
   }  

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)
   . . .
   for (card=0; card < MAX_SIGNAL_PROCESSING_CARDS; card++)
   {
      m_signalProcessingCard[card].Initialize();
   }
   
void SignalProcessingCard::Initialize()
{
   for (dsp=0; dsp < MAX_DSPS_PER_CARD; dsp++)
   {
      m_dsp[dsp].Initialize();
   }
}

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.

Modeled as Inheritiance
class TerminalAllocator : public Allocator
{
   . . .
};      

class DSPAllocator : public Allocator
{
   . . .
};  

 

Modeled as Delegation
class TerminalAllocator
{
   Allocator m_allocator;
   . . .
};      

class DSPAllocator
{
   Allocator m_allocator;
   . . .
};  

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.

DigitalTrunk.h : Nested class declaration
class DigitalTrunk
{
private:
      // Timeslot allocator is a private class used by DigitalTrunk. The full
      // name for the class is DigitalTrunk::TimeslotAllocator.
   class TimeslotAllocator

      {
      public:
         int Allocate();
         void Free(int timeslot);   
      }
      
      // Timeslots in transmit and receive direction can be allocated
      // independently
      TimeslotAllocator m_transmitAllocator;
      TimeslotAllocator m_receiveAllocator;
      
public:
   . . .
};      

 

DigitalTrunk.cpp : Nested class methods
// Nested classes methods are also contained
// in the the parent class's CPP file
    
int DigitalTrunk::TimeslotAllocator::Allocate()
{

}      
void DigitalTrunk::TimeslotAllocator::Free()
{

}      

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:

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:

Here are a few examples of possible frameworks: