Observing a container

J

jacob navia

The observer interface
----------------------
In its general form, the observer design pattern can be defined as a
one-to-many dependency between objects so that when one object changes
state, all its dependents are notified and updated automatically.
When a container changes its state, specifically when elements are added
or removed, it is sometimes necessary to update relationships that can
be very complex. The observer interface is designed to simplify this
operation by allowing the container to emit notifications to other
objects that have previously manifested interest in receiving them by
subscribing to them. In general notifications are sent only when one of
the defined operations for a container occur, mostly operations that
change the number of elements.
This interface then, establishes a relationship between two software
entities:
1. The container, that is responsible for sending the notifications when
appropriate
2. The receiver, that is an unspecified object represented by its
callback function that is called when a change occurs that matches the
notifications specified in the subscription.
Since this relationship needs both objects, it will be finished when
either object goes out of scope or breaks the relationship for whatever
reason. Both objects can unsubscribe (terminate) their relationship.
Caveats
• It is in general a bad idea to modify the object being observed during
a notification since this could trigger other notification messages.
Implementations are not required to avoid this situation that is the
responsibility of the programmer. Contrary to the iterator interface no
error is issued when a possible infinite loop is started.
Implementations may catch the error by limiting the number of recursive
invocations of this interface but they are not required to do so.
Since all messages sent by the containers have different type of
information in the same two arguments that each message is associated
with, there is no possible compile time control of the usage of the
received pointers or numbers. The observer function must correctly
discriminate between the different messages it can receive.
An alternative design would have been to specify not one type of
observer function but to define a different functio n type for each
possible message the containers could send. We would have then a
SubscribeAdd SubscribeErase SubscribeReplace functions, combined with
NotifyAdd, NotifyErase, NotifyReplace functions. That design would have
been easier to control at compile time. It was rejected because of the
increased complexity of the interface and the necessity for the user to
define a lot of functions just to know when something as simple as ”Was
this container modified?” happened
The interface
-------------
typedef void (*ObserverFunction)(const void *ObservedObject,
unsigned Operation,
void *ExtraInfo[]);
typedef struct tagObserverInterface {
int (*Subscribe)(void *ObservedObject,
ObserverFunction callback, unsigned
Operations);
int (*Notify)(const void *ObservedObject,unsigned operation,
void *ExtraInfo1,void *ExtraInfo2);
size_t (*Unsubscribe)(void *ObservedObject,
ObserverFunction callback);
} ObserverInterface;
extern ObserverInterface iObserver;
ObserverFunction
----------------
typedef void (*ObserverFunction)(void *ObservedObject,
unsigned Operation, void *ExtraInfo[]);
Description:
This function will be called by the interface when a notification is
received for an observed object. The call happens after all arguments
have been processed, the actual work of the function is finished (when
adding an object) or not yet done (when destroying an object). The
container is in a consistent state. For the callbacks that are
called when an object is deleted from a container the call happens
before any call to free() and before any call to a destructor (if any)
is done. For the calls that add an object the callback is called after
the container has been modified.
Arguments:
1. ObservedObject: Specifies the object that sends the notification,
i.e. the container that has the subscription. It is assumed that this
container conforms to the iGeneric interface.
2. Operation: The operation that provoked the notification. Since it is
possible to subscribe to several operations with only one callback
function, this argument allows the callback to discriminate between the
operation notifications.
3. ExtraInfo: This argument is specific to each operation and conveys
further information13 for each operation.
None of the arguments will be ever NULL or zero.
Subscribe
---------
int (*Subscribe)(void *ObservedObject, ObserverFunction callback,
unsigned Operations);
Description: This function establishes the relationship between the
observed object (argument 1) and the observer, represented by its
callback (argument 2). The third argument establishes which operations
are to be observed. This operation performs an allocation to register
the relationship in the observer interface tables, therefore it can fail
with an out of memory condition.
Errors:
CONTAINER ERROR BADARG The observed object pointer is NULL , the
callback function pointer is NULL , or the operations argument is zero.
CONTAINER ERROR NOMEMORY There is not enough memory to proceed.
Returns:An integer greater than zero if the relationship was
established, a negative error code otherwise.
Notify
------
int (*Notify)(void *ObservedObject,unsigned Operation,
void *ExtraInfo1,void *ExtraInfo2);
Description:
This function will be used by the container to send a message to the
receiver callback. The arguments correspond roughly to the arguments the
callback function will receive. ”Notify” will call all the objects that
are observing ObservedObject and that have subscribed to one of the
operations specified in the Operation argument. This implies a search
through the observer interface table, and possibly several calls, making
this function quite expensive. The time needed is roughly proportional
to the number of registered callbacks and the complexity of the
callbacks themselves.

Errors:
CONTAINER ERROR BADARG The ObservedObject pointer is NULL or the
Operation argument is zero.

Returns:A positive number with the number of objects that received the
notifications, zero if there was no match for the combination of
observed object and operations specified, or a negative error code.

Unsubscribe
-----------
size_t (*Unsubscribe)(void *ObservedObject, ObserverFunction callback);
Description:
This function breaks the relationship between the observed object and
the observer. There are several combinations of both arguments:
o The ObservedObject argument is NULL . This means that the callback
object wants to break its relationship to all objects it is observing.
The observer interface will remove all relationships that contain this
callback from its tables.
o The callback argument is NULL . This means that the given
ObservedObject is going out of scope and wants to break all
relationships to all its observers. The interface removes from its
tables all relationships that have this object as the observed object.
This happens normally immediately after the notification FINALIZE is sent.
o If both callback and ObservedObject are non NULL , only the matching
relationship will be removed from the tables.
Here is a complete example that demonstrates some of the above functions.

Example:
include "containers.h"
static void fn(void *ObservedObject, unsigned operation,
void *extraInfo[])
{
printf("Object is %p, operation is %d\n",ObservedObject,operation);
}
int main(void)
{
ValArrayInt * vInt = iValArrayInt.CreateSequence(24,0,1);
printf("Original array: \n");
iValArrayInt.Fprintf(vInt,stdout,"%d ");
iObserver.Subscribe(vInt,fn,CCL_ADD|CCL_FINALIZE);
printf("Adding an integer\n");
iValArrayInt.Add(vInt,4096);
iValArrayInt.Fprintf(vInt,stdout,"%d ");
iValArrayInt.Finalize(vInt);
}
OUTPUT:
Original array:
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
Adding an integer
Object is 0x100100080, operation is 1
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 4096
Object is 0x100100080, operation is 16

We setup our observer function calling the Subscribe API. We request to
be notified when there is an addition and when the object finalizes. Our
callback function does nothing but print some of its arguments. We see
that we get called when the requested operations are performed.
 
M

Malcolm McLean

The observer interface
My view on this is that it's a programming paradigm best avoided. A list
supports insert / delete, not arbitrary operations triggered on insert/
delete. Those operations are quite likely to themselves involve insert/
delete (If you're not interested in the list, why attach a notify on it?
If you are interested in the list, as often as not you'll want to modify
it again in response to a modification). But the killer is we can stack up
two observers on the same list, written by different people, who don't know
about the other observer. If they're both modifying in response to
modifications, or even if they're just modifying third party structures,
you've got the possibility of all sorts of complex, hard to understand,
undesireable interactions.

So I'd tend to cut through the problem by discouraging it altogether.
But if you're writing generic code, there's a strong case that you have to
support everything that a calling programmer may want to do. That
includes attaching observers to containers. By proving an interface,
you're in a sense saying "write the code this way". I know not really.
The question is how to provide something, but make it clear that in most
circumstances, it shouldn't be used. There are other examples, eg you can
globally override the new / delete operators in C++. Very rarely do people
do this, but occasionally it's highly useful.
 
J

jacob navia

Le 10/12/2013 23:46, Malcolm McLean a écrit :
My view on this is that it's a programming paradigm best avoided. A list
supports insert / delete, not arbitrary operations triggered on insert/
delete. Those operations are quite likely to themselves involve insert/
delete (If you're not interested in the list, why attach a notify on it?
If you are interested in the list, as often as not you'll want to modify
it again in response to a modification). But the killer is we can stack up
two observers on the same list, written by different people, who don't know
about the other observer. If they're both modifying in response to
modifications, or even if they're just modifying third party structures,
you've got the possibility of all sorts of complex, hard to understand,
undesireable interactions.

In principle you are right. It can get messy. But it is very usefull if
you avoid inserting/deleting stuff within a callback.

For instance, you have a pointer to the list in some data structure.
If the object that owns the pointer is notified of the list destruction
it just sets the pointer to NULL.

Or, you want to be notified of deletions since you have just stored
pointers in the list, not the data the pointers point to, and you
want to call free when an object is deleted.

Or you have a window list, and you want to redraw some part of your user
interface when an object is added or deleted.

All this can be very useful and it is perfectly safe.

C is a "close to the hardware" language and C programmers aren't Java
programmers where the programmer is protected from its own mistakes by a
software layer that doesn't allow any pointers or dangerous constructs.

Here the dangers of this construct are documented, so they are clear to
the programmer.

In any case the list serializes the messages to the registered
observers, so there is only ONE observer being notified at a time.
There is no "broadcast"
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,769
Messages
2,569,580
Members
45,054
Latest member
TrimKetoBoost

Latest Threads

Top