More interesting software systems are complex, and the implementation of complex software typically requires multiple developers. The introduction of additional developers increases software complexity, which requires the software design to provide for software components that are resilient against change. Functional behaviors or component dependencies, such as the need for one component to recognize that another component has been acted upon, are sometimes recognized. This is an opportunity for exercising good design.
When encountering a relationship where it is necessary for one component to know that an operation has been performed on another component, there may exist a temptation to simply couple one component with the other. This coupling can be implemented in C by the use of a global flag or a hard-coded function call from the “observed” component to the specific “listener” or “observer” component. There are at least two problems with these approaches.
Global variables, including global flags, are generally a bad idea. The use of a global flag to convey that an event has occurred in the observed component to an observer component raises concerns over the management of the global flag. In the simplified case of an observed component being observed by a single observer component, the responsibilities of setting and clearing the flag can be distributed between the observed and observer. The distribution of the responsibilities becomes problematic when there are potentially many observers. In a situation where the order of the observer components that inspect the flag is potentially random, a problem may arise where an observer component clears or causes the observed component to clear the flag before other observer components are able to inspect the flag and act on its value.
A hard-coded call from the observed component to a function that is provided by the listening component requires that the observed component is knowledgeable of the listening component’s interface. Though recognition of the observed component’s need of a mechanism to notify an observer component may have resulted through the design of an initial observer component, it is worthwhile to consider the possibility that there may be other software components that want to observe the observed component. Some observer components may be designed after the observed component has been implemented and finalized. A hard-coded function call from an observed component to specific observer components cannot support the addition of new observer components.
Recognizing during design that a software component needs to be observable is important. This allows the designer to employ the Observer Pattern as described in Design Patterns: Elements of Reusable Object-Oriented Software. The designer of an observable software component shall provide an interface for observing software components to subscribe to the observable software component’s notification mechanism.
An example of an observable component and two observer components are presented here in C. One observer component will act on the notification from the observable component immediately, while the other observer component will wait until one of its interface functions is called.
/* observed component */
typedef struct
{
int listener_count;
/* array of function pointers */
void (*listener_list[10])(unsigned,unsigned);
} Button;
void button_Init( Button *pButton )
{
pButton->listener_count = 0;
}
void button_RemoveClickListener( Button *pButton,
void (*fp)(unsigned,unsigned) )
{
unsigned i;
void (*temp)(unsigned,unsigned);
for( i = 0; i < pButton->listener_count; ++i )
{
if( pButton->listener_list[i] == fp )
{
pButton->listener_count--;
pButton->listener_list[i] =
pButton->listener_list[pButton->listener_count];
}
}
}
void button_AddClickListener( Button *pButton,
void (*fp)(unsigned,unsigned) )
{
button_RemoveClickListener( pButton, fp );
pButton->listener_list[ pButton->listener_count++ ] = fp;
}
void button_Click( Button *pButton, unsigned x, unsigned y )
{
unsigned i;
for( i = 0; i < pButton->listener_count; ++i )
{
pButton->listener_list[i](x,y);
}
}
/* observer component 1 */
void observer_OnClick( unsigned x, unsigned y )
{
printf( "observer_OnClick( 0x%X, 0x%X )\r\n", x, y );
}
/* observer component 2 */
bool observer2_isClickEventHandled = true;
void observer2_OnClick( unsigned x, unsigned y )
{
observer2_isClickEventHandled = false;
}
void observer2_Action()
{
if( observer2_isClickEventHandled )
{
printf( "observer2_Action(): no click event to handle\r\n" );
}
else
{
printf( "observer2_Action(): handling click event\r\n" );
}
observer2_isClickEventHandled = true;
}
/* system */
int main()
{
Button button;
/* system initialization */
button_Init( &button );
button_AddClickListener( &button, observer_OnClick );
button_AddClickListener( &button, observer2_OnClick );
/* system operation */
observer2_Action();
/* more system operation */
button_Click( &button, 1,3);
/* and more system operation */
observer2_Action();
observer2_Action();
}
The Button component in the example code is observable. The Button component provides an interface that allows observer software components to add themselves as the Button component’s observers. To be a compatible observer in this example, a software component must provide a function that returns void and takes only two unsigned integers as parameters. The overall software system will be responsible for associating the observing components with the observed components, or the observing components can register themselves with the observed components during the observing components’ initialization routines.
Some software development shops may consider function pointers taboo. Instead of passing a function pointer to the registration function (“button_AddClickListener” in the example), a pointer to a flag variable that resides within an observing software component can be used. In this way, each software component is able to have its own copy of the notification, which it can manage in a way that is solely dependent on its implementation. This mechanism is more robust to change and the effects of other software components as it overcomes the problems of using a global flag or hard-coded function call to implement a method that allows a component to notify other components of an event.