|
Here is an assortment of tips to keep in mind when using object oriented
design in embedded systems:
- 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
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.
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:
- CircuitController
- DigitalCircuit
- AnalogCircuit
- DSP
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.
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.
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:
- Activate
- Deactivate
- Block
- Unblock
- ChangeStatus
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.
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 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.
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.
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:
| Circuit
Status (Multiple Get-Set) |
void CircuitManager::GetStatus(const CircuitStatusMsg *pMsg) const
{
for (int i= 0; i < MAX_CIRCUITS; i++)
{
pMsg->circuitInfo[i].circuitId = m_pCircuit[i]->GetId();
pMsg->circuitInfo[i].circuitType = m_pCircuit[i]->GetType();
pMsg->circuitInfo[i].circuitStatus = m_pCircuit[i]->GetStatus();
pMsg->circuitInfo[i].circuitCallId = m_pCircuit[i]->GetCallId();
pMsg->circuitInfo[i].circuitState = m_pCircuit[i]->GetState();
}
}
|
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.
| Circuit
Status (Single Operation) |
void CircuitManager::GetStatus(const CircuitStatusMsg *pMsg) const
{
for (int i= 0; i < MAX_CIRCUITS; i++)
{
m_pCircuit[i]->UpdateStatus(pMsg->circuitInfo[i]);
}
}
void Circuit::UpdateStatus(CircuitInfo &circuitInfo) const
{
circuitInfo.circuitId = m_id;
circuitInfo.circuitType = m_type;
circuitInfo.circuitStatus = m_status;
circuitInfo.circuitCallId = m_callId;
circuitInfo.circuitState = m_state;
}
|
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.
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.
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.
The C++ standard template library is extremely powerful. It can save
countless hours of coding and testing of complex containers and queues. Details
can be found in the STL design
patterns and STL design
patterns II articles.
|