Re: C++ design question

From: H. S. Lahman (h.lahman_at_verizon.net)
Date: 10/05/04


Date: Tue, 05 Oct 2004 18:02:10 GMT

Responding to Elliott...

>>>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.
>
>
> In C++, this can restrict the ways in which an object can be used.
> Choice of language can affect the design.

The goal of OO development is to provide maintainable applications. One
way to do that in OOA/D is via separation of concerns and encapsulation.
  Once one has done that properly, it can always be implemented in an
OOPL (or even a non-OOPL).

The OOPLs, especially C++, are a poor way to understand OOA/D because
they necessarily make compromises with the computing environment. (C++
makes far more than, say, Smalltalk, which is why C++ is generally
regarded as the most technically deficient OOPL.) However, if the OOA/D
is done properly, then those compromises should be fairly benign. For
example, the OOPLs do not separate message and method due to procedural
message passing. But when the OOA/D is constructed assuming a
separation, then the way method responsibilities are defined and the
sequencing of peer-to-peer messages will ensure a robust application
even though message and method are not separated in the OOPLs.

The point I was <eventually> getting to in opening this subtopic was
that if one does the encapsulation of instantiation properly in OOA/D,
one really doesn't need to worry that much about whether a reference is
unset during OOP.

>
>
>>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.]
>
>
> As in a factory or static creator function?

Yes.

>
>
>>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.
>
>
> OK, understood.
>
>
>>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.
>
>
> But then don't you introduce lifetime issues? The sender has sent a
> reference to an object in order to instantiate the referential
> attribute, but the receiver doesn't own that object. If the object is
> destroyed then the receiver suddenly has a dangling reference. Or am I
> missing something here? In the same way that you suggest that
> references should be instantiated in a single method, should the
> reverse be true? Should there be a single method which is always
> invoked for decoupling and destroying?

This brings up another OOA/D issue. Behavior and knowledge
collaborations are treated differently in OOA/D. Because of the
separation of message and method in OOA/D, the collaboration model is
inherently asynchronous for behaviors. That is, there could be an
arbitrary delay between when a message is generated and when the
response method is actually executed. That's because the asynchronous
model is the most general model of behavior; synchronous is just a
special case of asynchronous. (It also allows the OOA models to be
independent of specific implementations like concurrent threads that
address nonfunctional requirements.) Therefore in the OOA/D it is
usually not a good practice to pass much data around in messages -- it
could be out of date by the time it is processed. (There are
exceptions, like "snapshots" of sensor values that must all be from a
particular time slice.)

OTOH, knowledge access is assumed to be synchronous in the OOA/D.
Otherwise one's mind would turn to mush trying to ensure data integrity
in an asynchronous behavior model. [If the knowledge cannot be accessed
synchronously, as in a distributed application, one has to provide
OOP-level infrastructure via threads, semaphores, interface
serialization, or whatever to ensure that it /appears/ to be synchronous
when a behavior accesses knowledge. Fortunately that is relatively easy
to do for attribute access.] Thus a method accesses the knowledge it
needs on an as-needed basis _within its own scope_.

Referential attributes are, conceptually, knowledge responsibilities
because of the static nature of relationships. Therefore, when a method
needs to access knowledge in another object, it navigates directly to it
via whatever reference it currently has in hand. Since that reference
was assigned synchronously prior to the method's execution, it should be
the "current" one.

So how does one ensure that the behavior setting the referential
attribute is executed in a timely fashion before the behavior that
navigates the relationship? The answer lies in DbC, which is crucial to
dealing with asynchronous behavior in OOA/D. For a method to execute
there is some set of preconditions that must prevail. One of those is
that the relationship is properly instantiated. When the developer
issues the message all of those preconditions must already prevail.
Thus the developer is required to make sure there is a daisy-chain of
messages in place that will ensure those preconditions prevail.

[This becomes much more obvious if one employs object state machines to
implement behavior responsibilities. One generates the event where all
the preconditions of the target state prevail. Those will conveniently
be the same as the postcondition for the state issuing the event (a
state is, by definition, a condition -- where a particular suite of
rules and policies prevail). Queuing the events ensures the proper
daisy-chaining for the conditions.]

Thus in the OOA/D one actually has a rather systematic method for
ensuring that life cycle issues are properly resolved, though it is a
tad more formal than most developers usually think about. (In practice,
experienced OOA/D modelers only resort to formal precondition vs.
postcondition analysis in situations that inherently tricky.)

As a trivial example, suppose the client really should own the
responsibility of instantiating the relationship. Then the code changes
from:

Object3::doSomething()
{
    Object1* myObject1
    Object2* myObject2
    ...
    myObject1->doIt(myObject2);
    ...
}

to:

Object3::doSomething()
{
    Object1* myObject1
    Object2* myObject2
    ...
    myObject1->setRelationship(myObject2); // OOA/D synchronous
    myObject1->doIt(); // OOA/D asynchronous
    ...
}

OTOH, if the relationship is instantiated elsewhere, all the developer
needs to ensure is that the instantiating behavior is called before
Object3::doSomething. One way to do that might be:

Client::doIt()
{
    ... // other client behavior
    myObject3 = myFactory=>createObject3(myObject2);
    myObject3->doSomething()
    ... // more client behavior
}

However, this introduces its own set of dependencies. In the OOA/D
Client already has a relationship or myObject2 that the Factory can
access. In addition, it is "hard-wiring" the proper sequence _in its
implementation_. So a better way would be:

Client::doIt1()
{
     ... // other client behavior
     myFactory->createObject3();
}

Client::doIt2()
{
     myObject3->doSomething();
     ... // more client behavior
}

Factory::createObject3()
{
     myObject2 = myClient->getObject2(); // synchronous
     myObject3 = new (Object3()); // synchronous create
     myObject3->setObject2Ref(myObject2); // synchronous initialization
     myClient->doIt2() // asynchronous behavior
}

Now we have completely encapsulated all the instantiation and we have
ensured, by splitting up Client::doIt, that everything gets done in the
right sequence. Yet we have no data passing, no dependence upon
returned values from behaviors (no one except getters return a value),
and no "hard-wired" sequences in Client's implementation.

One way the benefits are manifested is that Client::doIt1 and
Client::doIt2 can be fully unit tested without implementing any other
behaviors. All we have to ensure is that each routine did its client
behavior properly and made the indicated call. However, to test the
original Client::doIt, we would have to have a correct implementation of
Factory::createObject3 available.

This is also more robust because we can change the sequencing without
touching Client's implementation. So if something new must be done
between creating myObject3 and invoking Object3::doSomething(), we just
insert that behavior call between Factory and Client. That is, Factory
sends a message to the new behavior rather than to Client::doIt2 and the
new behavior invokes Client::doIt2. That restructuring of the message
path is exactly where it should be -- between Factory and Client --
rather than in Client's implementation.

[BTW, I provided this example in code because you don't do much UML and
I assume you don't do things like state machines and abstract action
language. So the code represents an OOA/D that I did in my head and
then transferred to 3GL code. All of the separation of concerns,
encapsulation, and logical indivisibility of the Client methods would
have been virtually forced by good OOA/D practice. Thus the code really
just reflects that if one gets the OOA/D right, the code will pretty
much take care of itself. B-)]

>
>
>>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".
>
>
> In design terms perhaps, but surely in language semantics (specifically
> C++) it does matter?

To the extent that C++ does not allow references to be reassigned, Yes.
  However, C++ does provide pointers to perform the equivalent
operations. I see that as a purely tactical issue. The OOA/D will look
exactly the same whether one uses references or pointers. In addition,
the OOA/D is so much more abstract that it must be implementable at the
OOPL level or else the OOPL is seriously flawed.

The fact that participants must be swapped is a problem space reality
and one has to deal with it somehow in the solution. The tactical
mechanics will vary from one implementation language to the next, but
the problem doesn't. IOW, get the OOA/D right and then it has to be
tactically implementable in the OOPL de jour.

>>>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.
>
>
> I recall the same debate about 3GL's versus assembly. I hardly ever
> write any assembler code these days, and even the embedded developers I
> work with are mostly using C now.

Ah, yes. I remember those debates as well and, for the times, the
paradigm shift was a major dislocation in the way software was
developed. Alas, too many developers today weren't around then and they
don't see history repeating itself.

>
>
>>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.
>
>
> It's not an area I know much about, but I'd suspect that one area where
> UML really scores is that it would be much more possible to logically
> prove the correctness of a design? (Whether it meets the spec must
> still be a different matter...)

Right. In translation the models themselves are executable. They
represent a full solution for functional requirements. (Non functional
requirements like size, performance, and reliability are automated in
the transformation process.) In fact, it is standard practice to run
exactly the same test suite for functional requirements against the
generated code as one ran against the UML models; only the test harness
changes.

Also, because of the focus on only functional requirements and an
inherently more compact representation, one has better requirements
traceability and the solution is easily to validate by inspection.
Where I worked before retiring we did an extensive evaluation of the
technology. The most surprising result was that our defect rates
dropped by 50%, which none of expected. I can't account for that by
cause and effect against specific practices so my speculation is that it
is due to the overall functional focus and compactness of representation.

*************
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



Relevant Pages

  • Re: Singletons
    ... There are four basic ways to implement a relationship: embedding an object in the implementation of another object; employing a referential pointer; passing an object reference as a message argument; and using an RDB-style search of instances by explicit identifier. ... Relationships are always implemented and navigated when addressing collaboration messages. ... By modifying the context I meant that one defines the solution flow of control differently so that the instantiation can be done in one place rather than in several. ...
    (comp.object)
  • Re: UML inner/nested class associations
    ... The OOA/D model needs to be unambiguously mappable into the 3GL implementation, but it does not need to be literally mapped into it. ... If one instantiates objects AND their relationships within a procedure, then in a synchronous implementation one gets referential integrity "for free" because only one procedure executes a time. ... there is nothing to prevent the OOP programmer from putting the instantiation of objects in one method and the instantiation of their relationships in another. ...
    (comp.object)
  • Re: Yet another design question, C++ oriented : better approach
    ... Do you have an author you read who says that references are ... > In OOA/D relationship ends are either unconditional or conditional. ... > the developer's responsibility to ensure that referential integrity is ... > instantiation and navigation of relationships. ...
    (comp.object)
  • Re: Managing multiple instances
    ... That will probably mean that you have to have some sort of lookup table somewhere in the implementation. ... You will also need some way to describe the client context so that it can be mapped to the actual object identity (e.g., an index into the lookup table that yields a reference). ... All you have to provide is an enumeration variable that is named for the models. ... Unless instantiation is trivial and unlikely to become more complicated, it is generally good practice to encapsulate the rules and policies for instantiation away from the rules and policies of collaboration. ...
    (comp.object)
  • Re: Abstract public member variales?
    ... Since the direction of these relationships is now clear, if I am correct then it must be that every Deconstructor object has references to ObjectA, ObjectB, and ObjectC, then the various concrete deconstructors choose the appropriate reference to save so that ConcreteA saves ObjectA and ConcreteB saves ObjectB, etc. ... that instantiation is a separate responsibility than the one triggering the actual collaboration activity of saving the object. ... I think that saying this technique uses the Strategy pattern is misleading. ...
    (comp.object)