10.3 Associations


Associations among objects allow client objects to invoke the operations provided by server objects. There are many ways to implement associations, appropriate to the nature and locality of the objects and their association. Implementations appropriate for objects within the same thread will fail when used across thread or processor boundaries. Accessing objects with multivalued roles must be done differently than with a 1-to-1 association. Some implementation strategies that work for composition don't work for client-server associations. One of the purposes of detailed design is to resolve the management of associations within the objects.

The simplest cases are the 1-to-1 or 1-to-(0,1) association between objects within the same thread. The 1-to-(0,1) is best done with a pointer to the server object, since there are times when the role multiplicity will be zero (i.e., the pointer will be null). A 1-to-1 association may also be implemented with a reference (in the C++ sense) because the association link is always valid.[6] A 1-to-1 composition association may also be used in inline class declaration, which would be inappropriate for the more loosely coupled client-server association. Normal aggregation is implemented in exactly the same way as an association. The following class shows these simple approaches.

[6] C++ requires that references always be valid; that is, a null reference is semantically illegal.

 class testAssoc {     T myT;    // appropriate only for 1-to-1                  composition     T* myT2;  // ok for 1-to-1 or 1-to-(0,1)                  association or composition     T& myT3;  // ok for 1-1 association or composition }; 

As discussed in Chapter 9, multivalued roles are most often resolved using the container pattern. This involves inserting a container class between the two classes with the multivalued role, and possibly iterators as well, as shown in Figure 10-2. Review Chapter 9 for more detail on the container pattern.

Figure 10-2. Detailed Design of Multivalued Roles

graphics/10fig02.gif

Crossing thread boundaries complicates the resolution of associations somewhat. Simply calling an operation across a thread boundary is not normally a good idea because of mutual exclusion and reentrancy problems. It can be done if sufficient care is taken: The target operation can implemented using mutual exclusion guards and both sides must agree on the appropriate behavior if access cannot be immediately granted. Should the caller be blocked? Should the caller be returned to immediately with an indication of failure? Should the caller be blocked, but only for a maximum specified period of time? All of these kinds of rendezvous are possible and appropriate in different circumstances.

Although directly calling an operation across a thread boundary is lightweight, it is not always the best way. If the underlying operating system or hardware enforces segmented address spaces for threads, it may not even be possible. Operating systems provide additional means for inter-task communication, such as OS message queues and OS pipes.

An OS message queue is the most dominant approach for requesting services across a thread boundary. The receiver thread's active object reads the message queue and dispatches the message to the appropriate component object. This approach has a fairly heavy runtime cost, but maintains the inherent asychronicity of the threads.

OS pipes are an alternative to message queues. They are opened by both client and server objects and are a slightly more direct way for the client to invoke the services of the server.

When the service request must cross processor boundaries, the objects must be more decoupled. Common operating services to meet intra-processor communications include sockets and remote procedure calls (RPCs). Sockets usually implement a specified TCP/IP protocol across a network. The common protocols are the Transmission Control Protocol (TCP) and User Datagram Protocol (UDP). The TCP/IP protocol suite does not make any guarantees about timing, but it can be placed on top of a data link layer that does. TCP supports reliable transmission using acknowledgements; it also supports what are called stream sockets. UDP is simpler and makes no guarantees about reliable transmission.

Lastly, some systems use RPCs. Normally, RPCs are implemented using a blocking protocol so that the client is blocked until the remote procedure completes. This maintains the function call like semantics of the RPC, but may be inappropriate in some cases.

Using any of the approaches that cross the thread boundary (with the exception of the direct guarded call), requires a different implementation in the client class. The client must now know the thread ID and a logical object ID to invoke the services. So rather than a C++ pointer or reference, ad hoc operating system dependent means must be used.

These methods can be implemented using the broker pattern from [9] or the observer pattern described in Chapter 9. The observer pattern allows the server to be remote from the client, but requires that the client know the location of the server. The broker pattern adds one more level of indirection, requiring only that the client know a logical name for the target object, which allows the Broker to identify and locate the server.

Note that this discussion has been independent of the underlying physical medium of inter-processor communication. It can be implemented using shared memory, Ethernet networks, or various kinds of buses, as appropriate. Reusable protocols are built using the layered architecture pattern, so that the data link layer can be replaced with one suitable for the physical medium with a minimum of fuss.



Real Time UML. Advances in The UML for Real-Time Systems
Real Time UML: Advances in the UML for Real-Time Systems (3rd Edition)
ISBN: 0321160762
EAN: 2147483647
Year: 2003
Pages: 127

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net