Yes, but _which_ object? According to you (and I agree) the ordering
should be the responsibility of the collection.
The problem is that the collection classes for the individual
associations can only capture said:
No. The _ordering relation_ is not exposed by the dynamic_cast. The
client has no reason to know whether it's "less" or "greater", or
something more complicated calculated by an oracle from multiple
properties of the object, or even some abstraction not present _in_ the
object at all, such as its position in an external database. All the
client needs to know is that the objects will be presented in the
correct order.
I see your concern, but I don't see it as a significant problem. (There
is also a way to address the concern directly that I'll get to later.)
The client has relationships with two different sorts of objects and
that, quite naturally, are abstracted as two separate binary
associations among peer classes in the OOA. That's because the real
collaboration is the processing that [Client] does with the [ClassA] or
[ClassB] object in hand. Each of those associations is necessarily
ordered in order to solve the problem in hand but that ordering only
applies to the simple binary association.
The fact that both sets of objects also need to be somehow ordered
*together* is a separate problem constraint than the ordering of the
individual collections. That joint constraint spans the associations so
it can (at most) only be partially implemented within the collections
(e.g., as an alternative ordering as suggested in my example). It is
that coordination _between collections_ that appears in the client
because the client logically owns any coordination among its
participation in its associations.
Now one can get around that by providing yet another class to act as a
collection of collections to encapsulate that synchronization:
[Client]
| 1
|
| R1 <<ordered>>
|
| accesses
| 1 1
[HCollection] ------------------------+
| 1 |
| |
| R2 <<ordered>> | R3 <<ordered>>
| |
| coordinates with | coordinates with
| * | *
[ClassA] [ClassB]
While we have conveniently encapsulated the overall ordering constraint,
there are several problems with this. We have introduced [HCollection]
as a peer object (i.e., at a higher level of abstraction than the R2 and
R3 collections) that has no counterpart in the customer domain; it is a
pure OOP implementation entity from the computing domain. So it has no
business being in the OOA solution where functional requirements are
resolved.
The second problem is that for the OP's situation we have obscured the
access to just [ClassA] or [ClassB] objects. That is especially
troublesome when the ordering for those situations is different that the
overall ordering. We can solve that by providing additional direct
associations between [Client] and [ClassA] and [ClassB], but that
complicates the design and introduces more collections to be managed at
the OOP level.
The third problem is that [Client] now receives a stream of both
[ClassA] and [ClassB] objects from [HCollection] but the client needs to
process objects from each class differently. The [Client] could do that
in a typesafe manner if it were navigating R2 and R3 directly, but it
has no way of knowing which type the next object from [HCollection] is.
Since C++ has no builtin facility for managing heterogeneous collections
in a typesafe manner, the convenient way to do this is via dynamic_cast.
But to do that [Client] must understand that [HCollection] manages the
[ClassA] and [ClassB] collections; it must know what types [HCollection]
manages just to properly use dynamic_cast. IOW, [Client] must know who
[HCollection] collaborates with (i.e., [Client] must understand the R2
and R3 associations) and that knowledge is hard-wired into [Client]'s
implementation.
The point is that as soon as one introduces [HCollection] as a peer
problem space entity, one potentially opens a can of worms that creates
implementation dependencies in [Client]'s implementation on software
structure that is removed from its immediate context (i.e., it depends
on [HCollection]'s associations rather than its own associations). Using
dynamic_cast just manifests that more fundamental OOA/D problem.
To put it a different way, the overall ordering constraint needs to be
implemented somewhere. It can't by fully implemented within the binary
associations to [ClassA] and [ClassB] that are defined in the problem
domain. IOW, one must encapsulate the coordination somewhere and the
problem space entity with both associations in common seems like a good
choice.
One could argue that the deficiency of C++ is the root problem. If one
had a language that has builtin, typesafe support of heterogeneous
collections (e.g., Ada), one can encapsulate the synchronization in a
collection. But lacking that, having the client coordinate its binary
associations explicitly is the better choice.
Having said all this, there is a way to encapsulate the overall ordering
in [HCollection] to satisfy your concerns and not use dynamic_cast. If
we had additional, conditional relationships:
0..1 current for R4 0..1
[Client] ----------------------------- [ClassA]
0..1 current for R5 0..1
[Client] ----------------------------- [ClassB]
to capture the notion of the current object for [Client] to process,
then only one relationship would be "live" (instantiated) at a time.
Then HCollection::getNext() doesn't return an object. Instead it removes
any existing instantiation of R3 and R4 and instantiates the appropriate
relationship based on comparing the sizes of the next object in each
collection. When that action returns the [Client] object then looks to
see which conditional association is instantiated.
This removes any responsibility from [Client] for knowing which object
to process is next. It also serializes the concerns of a heterogeneous
object stream into managing relationship instantiation for the "current"
object.
I might use this solution if I could identify an entity in the problem
space that naturally has the responsibility for managing disparate
entities. For example, the notion of a Queue Manager might be a relevant
concept for a domain expert for doing something like managing messages
in different formats from different sources in a FIFO manner. Then one
rationalizes the R2 and R3 collections as particular source queues and
one renames [HCollection] to be [QueueManager].
However, another good OOA/D practice is to try to minimize conditional
associations because they require additional executable code to manage
and they tend to be more fragile during maintenance. So for something as
simple as the OP's example, I would still probably let [Client] do the
interleaving.
<aside>
Note that this would be even better if one decided to break up [Client]
into different objects for the processing around [ClassA] and [ClassB]
objects. Continuing the example above, that might be the case if the
message processor is subclassed by format, which just happens to map
directly to message source.
Now [QueueManager] instantiates the association to the right subclass
client. This changes the flow of control design because now
[QueueManager] would be the one to trigger the processing by also
sending a message to announce a message was ready to the right format
processor that, in turn, would navigate the relationship. That is, when
it is time to process another message, a <OO> message is sent to
[QueueManager] who selects the right message from among the queues,
instantiates the relationship, and sends a <OO> message to the right
client to do its thing. That's actually a much more OO-like way to
connect the dots of flow of control than telling the client to do its
thing and having the client, in turn, tell [QueueManager] to get the
next widget.
Making flow of control decisions based on problem space properties
will always be more robust during maintenance than making such
decisions on 3GL implementation properties. The goal is to make the
application more maintainable and avoid foot-shooting; not elegance,
reduced keystrokes, minimizing static structure, or even being
convenient for the developer.
Are you really suggesting that there's no correlation between "elegance,
[...] convenient for the developer" and "more maintainable"?
Not quite. I am suggesting that violating good OOA/D practice to provide
elegance, reduced keystrokes, or developer convenience tends to reduce
long-term maintainability. IOW, when the choice is between misusing
dynamic_cast in order to have the convenience of a heterogeneous
collection vs. maintainability, maintainability wins.
--
There is nothing wrong with me that could
not be cured by a capful of Drano.
H. S. Lahman
(e-mail address removed)
Pathfinder Solutions
http://www.pathfindermda.com
blog:
http://pathfinderpeople.blogs.com/hslahman
"Model-Based Translation: The Next Step in Agile Development". Email
(e-mail address removed) for your copy.
Pathfinder is hiring:
http://www.pathfindermda.com/about_us/careers_pos3.php.
(888)OOA-PATH