You get a bonus - 1 coin for daily activity. Now you have 1 coin

8.3. Evolution of the application's development

Lecture



Release planning

Having considered several scenarios of the system's operation and having satisfied ourselves that the strategic decisions are correct, we can begin planning the development process. Let us divide the work into a number of stages, the result of each of which will serve as the basis for the next:

  • Developing a program possessing minimal functional properties and monitoring only one sensor.
  • Creating the sensor hierarchy.
  • Creating the classes responsible for managing the image on the screen.
  • Creating the classes responsible for the operation of the user interface.

In principle, the order of the stages could be changed, but we chose precisely this sequence, proceeding from the assumption that the most complex and risky part of the work should be carried out first.
Developing a minimal version of the program forces us first of all to model the architecture "vertically", implementing in truncated form practically all the key abstractions. This task carries within it the main risk, for in the process of solving it we in effect verify the correctness of our choice of key abstractions, their roles and functions. The successful creation of an early prototype plays a very large role in building the system. As was already noted in Chapter 7, it gives us a number of technical (and not only technical) advantages. In particular, we immediately reveal any mismatches between the hardware and software parts. Moreover, future users get the opportunity, already at the early stages of the project, to evaluate the appearance and operation of the system.

We will not dwell in detail on the implementation of this version, since it is to a greater degree a tactical task; instead we will move straight on to the later releases. In doing so we will discover for ourselves some interesting features of the development process.

The sensor mechanism

We have already seen how, during the development of the system's architecture, its key abstractions, including the sensor classes, were gradually filled with content and acquired stable forms. Guided by an evolutionary approach to development, we will build the next version on the basis of the first, minimal one.

At this stage of development the hierarchy of sensor classes presented in Figure 8-4 remains unchanged. We must, however, refine the placement of some polymorphic operations in order to achieve as high a degree of generality of the classes in the hierarchy as possible. Earlier, for example, we described the requirements for the currentValue operation belonging to the abstract base class Sensor. The construction of this class can be defined more fully in C++ as follows:

class Sensor {
public:

Sensor(SensorName, unsigned int id = 0);
virtual ~Sensor();
virtual float currentValue = 0;
virtual float rawValue() = 0;
SensorName name() const;
unsigned int id() const;

protected:
...
};

This class includes pure virtual member functions, and is therefore abstract.

Note that the class constructor tells the instance its name and number. This is done to provide the ability to determine the sensor type dynamically, and also to satisfy one of the requirements for the system, according to which each of the sensors has a fixed access address in main memory. These implementation details of the system can be hidden by computing the address in memory from the sensor type and its identification number.

After we have added new properties to the sensor class, we can go back a little and simplify the declaration of the function DisplayManager::display, which can now have only one argument, namely a reference to an object of the class Sensor. The remaining arguments can be dispensed with, since an object of a class derived from sensor will itself supply information about its type and identification number.

This seemingly insignificant change is extremely desirable, for if we do not strive to simplify the external interface of classes, then over time our system will suffer more and more from the overloading of the interaction protocols among them.

The declaration of the subclass CalibratingSensor is based on the base class Sensor:

class CalibratingSensor : public Sensor {
public:

CalibratingSensor(SensorName, unsigned int id = 0);
virtual ~CalibratingSensor();
void setHighValue(float, float);
void setLowValue(float, float);
virtual float currentValue();
virtual float rawValue() = 0;

protected:
...
};

This class includes two new operations (setHighValue and setbowValue), and implements the virtual function currentValue of the base class.

Now let us consider the declaration of the subclass HistoricalSensor, based on the class CalibratingSensor:

class HistoricalSensor : public CalibratingSensor {
public:

HistoricalSensor(SensorName, unsigned int id = 0);
virtual ~HistoricalSensor();
float highValue() const;
float lowValue() const;
const char* timeOfHighValue() const;
const char* timeOfLowValue() const;

protected:
...
};

Four new operations are defined in this class, the implementation of which requires interaction with the class TimeDate. Note also that HistoricalSensor is still an abstract class, since we have not defined in it an implementation of the pure virtual function rawValue, which will be defined in the next subclass.

The class TrendSensor is derived from HistoricalSensor; one new property has been added to it:

class TrendSensor : public HistoricalSensor {
public:

TrendSensor(SensorName, unsigned int id = 0);
virtual ~TrendSensor();
float trend() const;

protected:
...
};

One new function, trend, is defined in this class. Like certain other operations added in the intermediate classes, it is not marked as virtual, since we do not want inheriting classes to override it.

And now, at last, we come to the concrete class TemperatureSensor:

class TemperatureSensor : public TrendSensor {
public:

TemperatureSensor(unsigned int id = 0);
virtual ~TemperatureSensor();
virtual float rawValue();
float currentTenperature();

protected:
...
};

Note that the signature of the constructor for this class is defined in a new way. Here the concrete type of the sensor is known to us, so there is no need to specify its name when creating the object. Let us also pay attention to the new operation currentTemperature. Its presence is logically quite justified; however, if we return to the results of our analysis, we will discover that an analogous operation is performed by the polymorphic function currentValue. Nevertheless, we have included both functions in the description, since the currentTemperature operation is safer from the point of view of typing.

After we have successfully completed the implementation of all the classes of this hierarchy and integrated them with the previous release, we can move on to the next level of system functionality.

The mechanism for displaying information on the screen

Preparing the next release, in which the classes DisplayManager and LCDDevice are to be finally defined, does not require any new design decisions from us. All that remain are a few tactical steps connected with the signature and semantics of certain member functions. Combining the decisions made during analysis with our first architectural prototype, in which we made some important assumptions about the protocol for displaying values, we can define the following interface in C++:

class DisplayManager {
public:

DisplayManager();
~DisplayManager();
void clear();
void refresh();
void display(Sensor&);
void drawStaticItems(TemperatureScale, SpeedScale);
void displayTime(const char*);
void displayDate(const char*);
void displayTemperature(float, unsigned int id = 0);
void displayHumidity(float, unsigned int id = 0);
void displayPressure(float, unsigned int id = 0);
void displayWindChill(float, unsigned int id = 0);
void displayDewPoint(float, unsigned int id = 0);
void displayWindSpeed(float, unsigned int id = 0);
void displayWindDirection(unsigned int, unsigned int id = 0);
void displayHighLow(float, const char*, SensorName, unsigned int id = 0);
void setTemperatureScale(TemperatureScale);
void setSpeedScale(SpeedScale);

protected:
// ...
};

None of the operations given is virtual, since the creation of a hierarchy of classes for displaying information on the screen is not planned, and DisplayManager will have no descendants.

Note that this class contains several fairly primitive operations (such as DisplayTime and refresh), but at the same time possesses a composite operation, display, whose presence in many ways simplifies the interaction of clients with an instance of the class DisplayManager.

DisplayManager ultimately makes use of the resources of the class LCDDevice, which, as we have already determined, serves as a software wrapper for the hardware. DisplayManager raises the abstraction to the level of the concepts of the problem domain.

The user interface mechanism

The last main element of our system is the user interface mechanism, which must be implemented with the help of the classes Keypad and InputManager. Similarly to LCDDevice, the class Keypad serves as the link with the hardware, relieving InputManager of the need to adapt each time to new "iron". Separating these two abstractions greatly eases the process of adapting the system to other hardware input devices and increases the degree of resilience of its architecture.

Let us begin by defining the vocabulary of the problem domain:

enum Key {kRun, kSelect, kCalibrate, kMode, kUp, kDown, kLeft, kRight, kTemperature, kPressure, kHumidity, kWind, kTime, kDate, kUnassigned};

We have to use the prefix k so as not to duplicate the type names already defined for SensorName.

Next, let us define the Keypad class as follows:

class Keypad {
public:

Keypad();
~Keypad();
int inputPending() const;
Key lastKeyPress() const;

protected:
...
};

The protocol for this class was already largely defined during the analysis. We have added only the inputPending operation; this was done so that a client can find out whether there is a new, as yet unprocessed user command.

The class InputManager has a largely analogous interface:

class InputManager {
public:

InputManager(Keypad&);
~InputManager();
void processKeyPress();

protected:

Keypad& repKeypad;

};

As we shall see, the behavior of this class is described almost exhaustively by a finite state machine.

Figure 8-13 illustrates the interaction of the classes Sampler, InputManager and Keypad in processing user commands. To integrate them, the interface of the class Sampler must be somewhat modified, by including in its description a new object repInputManager:

class Sampler {
public:

Sampler(Sensor&, DisplayManager&, inputManager&);
...

protected:

Sensors& repSensors;
DisplayManager& repDisplayManager;
InputManager& replnputManager;

};

Now the link between instances of the classes Sensors, DisplayManager and InputManager is established at the moment an object of the class Sampler is created. The use of references guarantees that each instance of Sampler will receive the appropriate set of sensors, display manager, and input manager. Another scheme, in which pointers are used instead of references, would provide a rather weak connection, allowing the creation of a Sampler object that lacked some important components.

The key function Sampler::sample must be modified as follows:

void Sampler::sample(Tick t)
{

repInputManager.processKeyPress();
for (SensorName name = Direction; name <= Pressure; name++)

for (unsigned int id = 0; id < repSensors.numberOfSensors(name); id++)

if (!(t % samplingRate(name)))

repDisplayManager.display(repSensors.sensor (name, id));

}

At the beginning of each frame we have added a call to the processKeyPress method. The processKeyPress operation is the entry point into the finite state machine that controls the operation of instances of the class InputManager. There are two approaches to implementing any finite state machine: one can represent the states of the system as objects and rely on their polymorphic behavior, or simply introduce an enumeration of the states, denoting them by literals.

For finite state machines with a relatively small number of states, among which is the class InputManager, it is sufficient to use the second approach. First let us define the names of the enclosing states of the class:

enum InputState {Running, Selecting, Calibrating, Mode);

Then let us define some protected functions of the class:

class InputManager {
public:
...
protected:

Keypads repKeypad;
InputState repState;
void enterSelecting();
void enterCalibrating();
void enterMode();

};

And, finally, let us begin implementing the transitions between states (see Figure 8-11):

void InputManager::process Keypress() {

if (repKeypad.inputPending()) {

Key key = repKeypad.lastKeyPress();
switch (repState) {
case Running:

if (key == kSelect)

enterSelecting();

else if (key == kCalibrate)

enterCalibrating();

else if (key == kMode)

enterMode();

break;case Selecting: break;
case Mode: break;
}}

}

Thus, the implementation of this function reflects the content of the state transition diagram in Figure 8-11.

Comments

To leave a comment

If you have any suggestion, idea, thanks or comment, feel free to write. We really value feedback and are glad to hear your opinion.
To reply

Lectures and tutorial on "Object Oriented Analysis and Design"

Terms: Object Oriented Analysis and Design