Re: C++ design question
From: H. S. Lahman (h.lahman_at_verizon.net)
Date: 10/01/04
- Previous message: Mark Nicholls: "Re: Infos about components and code reuse"
- In reply to: Simon Elliott: "Re: C++ design question"
- Next in thread: Simon Elliott: "Re: C++ design question"
- Reply: Simon Elliott: "Re: C++ design question"
- Messages sorted by: [ date ] [ thread ] [ subject ] [ author ]
Date: Fri, 01 Oct 2004 16:46:26 GMT
Responding to Elliott...
>>I think a simpler solution would be to create the Bar instance
>>separately and initialize a pointer to it in Foo:
>>
>>class Foo
>>{
>>private:
>>Bar* myBar;
>>...
>>public:
>>void DoStuffWithBarBase ()
>>...
>>}
>>
>>fooDeriveN::DoStuffWithBarBase()
>>{
>>...
>>this.myBar.doIt();
>>...
>>}
>>
>>Creator::createFooDeriveN(...)
>>{
>>Bar* myBar = new (barDeriveN(...));
>>fooDerive* myFoo = new (fooDeriveN(myBar,...));
>>...
>>}
>
>
> Is the intention here for DoStuffWithBarBase() to be virtual, and for
> each fooDerivedN to implement its own instance of this? This is a
> perfectly valid way of approaching my cut down example, but I don't
> want to do this in my real-world design because barBase knows how to do
> a significant amount of complex tasks and must maintain a significant
> state. I don't want this to be re-implemented (or even called down to)
> from the barDerivedN as this would add significantly to the complexity.
I assumed that the Foo subclasses each had specialized responsibilities,
only one of which was to invoke Bar.doIt. That was the intent of the
"..." around the call. Whether DoStuffWithBarBase needs to be virtual
or not depends on whether there are such specializations.
Sorry, but I am confused by the rest of this paragraph. I assumed from
the DoStuffWithBarBase name that there wasn't any choice about invoking
barBase services. In any case, I don't see any added complexity. In
the end DoStuffWithBarBase is just sending barBase a message (doIt) for
a collaboration.
>
> And fooDeriveN::DoStuffWithBarBase() still only has a pointer to the
> Bar base class to work with, so it can't access anything that
> Foo::DoStuffWithBarBase() can't access.
True; one is limited to polymorphic dispatch. But I thought using that
interface was why you needed the reference to barBase in the first
place. [If you were planning on downcasting it to the correct subclass
in the DoStuffWithBarBase implementation, I would argue: Don't Do That!
B-) If you need the actual specializations of the Bar subclasses as
in my diagram, then the individual relationships need to be instantiated
and directly navigated rather than using barBase superclass access.]
Note, though, that doesn't preclude each Foo implementation from
invoking different responsibilities or providing different parametric
data to barBase through that interface:
fooDerive1::DoStuffWithBarBase
{
...
this->mybar->doIt(15)
...
}
fooDerive2::DoStuffWithBarBase
{
...
this->mybar->doIt(87)
this->myBar->doSomethingElse(...)
...
}
>
> However, in your example, we don't necessarily need
> fooDeriveN::DoStuffWithBarBase() at all because we have a Bar* in Foo*.
> Assuming that Bar is an abstract class which defines all the public
> methods we'll need (ie the barDerivedN classes don't expose any more
> functionality) then we can do everything we need from this Bar*.
To clarify, I was assuming DoStuffWithBarBase implemented other Foo
semantics and sending a message to Bar was peripheral to the semantics.
That is, the name was just a convenience of the example.
>
>
>>DoStuffWithBarBase now navigates to the Bar with confidence that it
>>will do the right thing simply because it is at the end of the
>>relationship. The rules for instantiating the relationship are
>>encapsulated in createFooDeriveN or whoever creates Foos. Note that
>>to invoke your Foo constructor, whoever does that has to know exactly
>>the same things to provide the initializer list.
>>
>>This may seem like a trivial difference in encoding the constructor
>>code, but it actually represents a more robust approach. That's
>>because Foo doesn't need to know anything about the relationship
>>rules or even what sort of Bar is on the other end of the
>>relationship. One way that is manifested is that the Foo code only
>>needs the Bar reference. [It also reduces the complexity of the
>>constructor so that you safety problem goes away. B-)]
>>
>>A more important benefit is that if things change so that Bar is
>>created separately for some reasons (e.g., because other objects need
>>to access it or one decides one can optimize size by eliminating
>>redundant embedded Bar instances or whatever) it is highly unlikely
>>that the Foo constructor will have to change nor any other internals
>>of Foo.
>
>
> I don't think it would be possible to safely uncouple things quite that
> much via this technique. If there's a Bar* in the Foo object, there's
> always the risk that it might not be set, and that someone will then
> try to access it. You could set Bar* myBar to zero in the constructor,
> and then test for zero before you did anything that accessed the
> pointer.
>
> The advantage of the C++ reference is that it can't be unset: it always
> must "point" to something.
Now we have a real OOA/D issue. B-) In OOA/D one treats the rules and
policies for object and relationship instantiation (object
participation) as logically distinct from the rules and policies of
relationship navigation (message passing for collaborations between
objects). That is, one separates the concerns of instantiation and
encapsulates them, often in a dedicated object like the GoF factory
patterns.
An OOA/D relationship is either conditional or not. If it is
conditional one can't implement it with a reference. If it is
unconditional, then it /must/ be instantiated within the scope that
where the participating objects are created. [Usually that is a single
method scope. If it is spread over multiple methods, then the developer
has the responsibility of explicitly guaranteeing referential integrity,
which tends to be a pain. So most developers look for ways to
encapsulate in a single method.]
The point here is that in OOA/D instantiating unconditional
relationships and object instantiation are inextricably linked to the
point of being an idiom. Explicitly encapsulating those rules and
policies ensures that the developer thinks about them. In those rare
cases where the relationship can't be instantiated in the same method
scope as the participants, that is painfully obvious and the developer
realizes some additional care is required. Therefore the concern that
leads to using references really isn't a concern because referential
integrity should have already been worked out in the OOA/D.
I would also point out that encapsulation of instantiation in OOA/D has
additional benefits. Typically relationships are navigated many times
from different contexts compared to their instantiation. Therefore
encapsulating instantiation supports a form of one-fact-one-place to
eliminate redundancy. For example, in OOA/D it is considered a very bad
practice to pass object references in message data packets during
collaboration (other than a setter to instantiate the referential
attribute). The relationship is between the passed object and the
receiver, yet the message sender must know which object the message
receiver must collaborate with. The rules and policies for that are
usually not related to the rules and policies of collaboration, so one
has trashed the sender's cohesion. In addition, selecting the right
object must be done in every collaboration context for every navigation
of the relationship.
A final note. Consider the situation where a relationship is
unconditional but the participants are swapped dynamically during the
execution. This is quite common when applying parametric polymorphism
in the form of specification objects and dynamically assigned
relationships. In that case referential integrity depends upon the
developer ensuring the participants are swapped _at the right time_. In
that case it doesn't make any difference whether one uses pointers or
references because the referential attribute is always "set".
The OOA/D practice of encapsulation ensures that this situation is
treated in exactly the same way as non-dynamic instantiation. That is,
the instantiation is still done in one place; one just has to send a
message to whoever does that whenever the context changes. The place
where the context change prevails doesn't need to know anything about
the relationship dynamics; all it needs to do is announce that the
context has changed. So one also gets better logical decoupling.
However, that consistency and decoupling is ultimately driven by the
OOA/D paradigm of encapsulating instantiation.
[BTW, it is considered good OOA/D practice to eliminate conditionality
in relationships wherever feasible in the OOA/D, even at the cost of
adding additional classes. So one ends up in the same place as using
references. However, the reasons are quite different. Eliminating
conditionality enforces business rules and policies in terms of static
structure rather than the dynamic description. That reduces executable
code size and, consequently, improves reliability.]
>
>
>>If you look at the way full UML code generators work, they invariably
>>implement relationship navigation with pointers and collections of
>>pointers because that allows very generic, aspect-like implementation
>>that does not depend upon the class semantics at all. (They even use
>>naming conventions that reflect relationship discriminators.) Among
>>other things that allows the code generator to generate code for one
>>class without even "looking" at the head files for other classes
>>involved in the navigation.
>
>
> I've not used UML very much as yet.
It's probably time to learn. B-)) In a conference keynote address
around '01 Jacobson predicted that in a decade writing 3GL code would be
as rare as writing Assembly is today. (This is somewhat ironic since
Jacobson and the other Amigos engaged Mellor in a series of conference
debates in the mid-'90s where he defended the position that translation
was not feasible.) I don't see it happening quite that fast, but it is
an inevitable automation of the computing space.
Translation technology has been around since the early '80s but the tool
engineering problems weren't fully overcome until the late '90s. Now
that UML has an action semantic meta-model it is a true general purpose
4GL for functional requirements. With the MDA initiative we have
standardization for the tools that automate the resolution of
nonfunctional requirements in the computing space. In addition, the
major software houses (IBM, CA, Mentor, etc.) have been buying up
translation vendors to establish strategic positions (Pathfinder and
Kennedy-Carter are the only remaining independent old-timers). There
are also new MDA tools popping up on a monthly basis now. IOW, the
Early Adopter Stage is over and the software development paradigm is
shifting.
>
>
>>That abstraction and separation of
>>concerns is not possible in your reference approach because Foo needs
>>to know the specific Bar subclass type even though it actually
>>accesses through the Bar superclass.
>
>
> I don't think it does. fooBase is implemented strictly in terms of
> barBase. Or am I missing something here?
But you had to have a specific barDerived reference in hand to which the
barBase reference was assigned. Providing that in the fooDerived
subclass creates the explicit coupling in the implementation. In my
example there was no declaration of barDerived /anywhere/ in Foo or its
derived classes.
*************
There is nothing wrong with me that could
not be cured by a capful of Drano.
H. S. Lahman
hsl@pathfindermda.com
Pathfinder Solutions -- Put MDA to Work
http://www.pathfindermda.com
blog (under constr): http://pathfinderpeople.blogs.com/hslahman
(888)-OOA-PATH
- Previous message: Mark Nicholls: "Re: Infos about components and code reuse"
- In reply to: Simon Elliott: "Re: C++ design question"
- Next in thread: Simon Elliott: "Re: C++ design question"
- Reply: Simon Elliott: "Re: C++ design question"
- Messages sorted by: [ date ] [ thread ] [ subject ] [ author ]
Relevant Pages
|