Re: Abstract public member variales?
- From: Brendan Guild <dont@xxxxxxx>
- Date: Thu, 21 Sep 2006 23:50:59 GMT
H. S. Lahman wrote in WcAQg.2346$zh.1364@trnddc06:">news:WcAQg.2346$zh.1364@trnddc06:
Responding to Guild...
If R3 is not instantiated on demand, then I can see three
possibilities:
1) The relationship is instantiated in the factory and remains
forever. In this case, the SavingClient will be obligated to make
every object that has ever come out of the factory persistent,
even if that object is no longer part of the system state. Under
ordinary conditions, objects which are no longer needed are
deleted, but in this design then can never be deleted; even when
the state is written out to disk and reloaded, the unneeded
objects remain. (It is not a serious problem, but from a
black-box perspective it is humorously impractical.)
This would be the normal approach. If only some members of
[ObjectA] need to be persisted, then the factory would need to
understand those rules because its job is instantiation of the
object and its relationships.
Deletion is a different problem. When a member of [ObjectA] is
explicitly deleted the R3 collection needs to be told. If one is
using a language with GC, then one needs to tell the collection
specifically when the object's life cycle is done to remove the
reference.
I am having difficulty in determining what has gone wrong. I laid out
three options for dealing with objects that are no longer part of the
state of the system and I thought that they were mutually exclusive
and exhaustive. In fact, I was very eager to discover your opinion on
which was most appropriate, but somehow you seem to misunderstand. I
am disappointed because of how much respect I have for your opinion
and the care with which I laid out those options.
I am still eager for my answer, so I will have to try again with a
different approach. First, I will explain why what you gave did not
answer my question.
Even if the factory were given an understanding of which objects need
to be persistent, it would be little use because the factory only has
control of an object when it is being created. All objects would need
to be persistent just after they are created because they are part of
the state of the system. If they were not, then they would not need
to be created at all.
I am sure you would not be surprised that during the course of
execution, some objects cease to be important. Since these objects
are already created, I seriously doubt that the factory can be held
responsible for removing the relationship with the SaveClient.
Perhaps I am mistaken in that, but if so I hope you would have made
it more explicit, since that is not the intuitive role of a factory.
Now I will consider the second part of your response, where deletion
needs to be handled differently. This is very promising, since an
object that is no longer useful can likely be safely deleted from the
set of domain objects (ignoring persistence). Whether we are using
explicit deletion or GC seems to have no impact on the final word:
the SaveClient must be told. My options number (2) and (3) involved
talking to the SaveClient, but you did not choose them, so I can only
imagine that you have some fourth option for talking to the
SaveClient.
Deleting objects and making object so they are no longer persistent
are not so different issues as you think. Either way you need a
strategy for dealing with their persistence. You chose option 1, in
which every object that is ever persistent is persistent forever,
which should mean that a persistent object is never deleted. It is
the cleanest of the 3 options since it involves the least coupling,
so I am not surprised. But your have spoiled your answer by talking
about deletion, indicating that one of us is misunderstanding
something.
There are really only two options, since options (2) and (3) are so
similar: Either I let the factory be solely responsible for the
relationship between the SaveClient and the persistent objects,
meaning that this relationship only changes when the object is
created, making persistent objects persistent forever, or I introduce
some mechanism for taking away the persistence of some object,
requiring messages to be passed from either the persistent objects or
the OrdinaryClient.
If it is possible to choose both, as you seem to be attempting,
please make that more explicit. Otherwise I would very much
appreciate a clear explanation of which you would choose and how you
would deal with the resulting coupling.
One always invokes ObjectA.saveIt() so there is no substitution
possible for the call. Saving a member of [ObjectB] requires a
call to ObjectB.saveIt(), which is a different collaboration
(call) both syntactically and semantically.
Surely you must admit that it is possible to call
Serializable.saveIt() when using the Serializer pattern. There is
nothing actually preventing the use of polymorphic dispatch. I
find it mysterious that you are writing as if it were literally
impossible to do something like:
((Serializable*)myObjectA)->saveIt();
I have already said several times explicitly that calling
Serializer.saveIt() WOULD be polymorphic dispatch.
But I also spent significant discussion time about why that would
normally not be done. One does not use polymorphic dispatch just
because it is there; one uses it because one needs it. That's
because there is /always/ LSP risk in polymorphic dispatch.
And you also said that it can never happen, that it is not possible.
You said just a short distance above, "Saving a member of [ObjectB]
requires a call to ObjectB.saveIt()." That is all I was really
disputing, since I have needed it myself. Since the condition has be
downgraded from 'never' to 'normally not', I think we are in
agreement.
* contains
[Graphic]---------------------+
+ draw() |
A |
| | R1
+------+-+-------+-------+ |
| | | | 1 |
[Line][Rectangle][Text][Picture]----+
This is an example that the GoF use for the Composite pattern
and I intend to use it for the strategy objects in the game
which I am designing. And this is not the only way I will be
using polymorphism.
Aha! Now I see what the disconnect is. You are mixing patterns.
The polymorphic dispatch here is on [Graphic], not [Serializer].
That does not make the dispatch any less polymorphic. The fact
remains that I am using polymorphic dispatch to do persistence
and because of it I can easily avoid dynamic casts.
Of course it is polymorphic dispatch in Composite. But that
polymorphic dispatch has nothing to do with Serializer.
My point is only that with Serializer we can use polymorphic dispatch
where a dynamic cast is required by many other persistence
techniques. There are three points of connection between the
Serializer pattern and the polymorphic dispatch:
1) We are doing the persistence when the polymorphic dispatch occurs
2) The polymorphic dispatch is in service of the persistence
3) The polymorphism of the Composite pattern does not lead to
polymorphic dispatch in any persistence technique other than
Serializer.
But these things only support the connection in my mind, to form my
way of thinking on the issue, and others are free to think of it in a
different way. Since the polymorphic dispatch is there, choosing who
gets credit for it is really unimportant. It was only brought up
because it was a source of misunderstanding between us, but now I
think that misunderstanding is past.
What [Graph] demands is that you have a saveIt() method
implemented in every leaf subclass. To do that you have to
provide that implementation is every class. That is exactly what
Serializer does and requires. So if you just define saveIt() as a
property of [Graph] you don't need Serializer at all.
Whether you need Serializer or not, you are using Serializer. You
have merely renamed the 'Serializable' class to 'Graph'.
The polymorphic dispatch only exists because there is subclassing
below the class where Serializer is inherited. That subclassing
is due to Composite, not Serializer.
I agree with that.
But each object is persisted individually and whoever is invoking
saveIt() knows exactly which individual is being saved (unless
saveIt() is being invoked at a superclass).
This is a pretty big parenthetical 'unless', since it is certainly
central to what we are talking about. I was merely pointing out that
saveIt() is invoked at a superclass all the time, and if that never
occurred then the Serializer pattern would be effectively useless. I
would never even consider the Serializer pattern if I did not have
references to objects by superclass that I wanted to be persistent.
But that is not the point at all. I am sure that you are correct
that eliminating that dependency is good OOA/D, but the point of
the Strategy pattern is only to eliminate the dependency of
Context upon any one particular algorithm of a class of related
algorithms. Context is supposed to depend on the interface of the
Strategy, and the client is not supposed to depend upon it.
Some client triggers the need for a particular Context algorithm
to be executed based on dynamic context.
The point of Strategy is to delegate the algorithms to another
object(s) than Context so that they can be easily substituted
based on context.
Once one defines that delegation those algorithms are no longer
responsibilities of Context; they are responsibilities of the
Strategy objects.
I think this is the core of our misunderstanding on this issue. There
is a difference between algorithms and responsibilities that is being
understated, so I will attempt to solve that.
An algorithm is not a responsibility and a responsibility is not an
algorithm. The Strategy pattern causes Context to delegate algorithms
to another object, and that object gains the responsibility for
producing the results that the algorithm produces. If producing those
results is part of the responsibility of Context then you can (and
should) use delegation of responsibility to conform to good OOA/D
practices.
But here is a critical point: the responsibility of the algorithm is
not necessarily a responsibility of the Context. And I mean this is
the case even before delegation is used, even when the algorithm is
directly contained inside the Context.
Let me give an example that I know well: An entity has the
responsibility of deciding where it should move from moment to
moment. An important algorithm for making such decisions is a path-
finding algorithm which finds a path through an environment from one
point to another. Notice that the entity has no responsibility to
find paths, there are no postconditions for the entity which involves
a path, there are no expectations that an entity will find paths, but
that does not mean that the entity will not find a path as part of
fulfilling a different responsibility. If the entity uses a path-
finding algorithm, then it can delegate that algorithm to another
object. Despite the presence of delegation, it is impossible to
simply create a responsibility delegation because the important
responsibilities to no exist in the Context object.
I apologize for not making that clear earlier, but I am still
learning about many of these issues.
I agree that it is a bit messy, though your way of describing the
problem seems strange to me. The issue is that the semantics of
the strategy become part of the semantics of Context.
The second sentence is the problem. The algorithm
responsibilities have been moved from one object to another by the
delegation. The responsibilities NO LONGER BELONG TO CONTEXT
AFTER THE DELEGATION.
My point has always been that the responsibilities usually are not in
the context even before the delegation, though I admit that I have
not expressed that well until now.
That seems reasonable. In delegation one is delegating
responsibilities from one object to another. But then the
Strategy pattern is not an example of delegation, since the
responsibilities of Context and the responsibilities of the
Strategy are completely independent. In that case I have
difficulty seeing how this relates to the Strategy pattern.
Check out page 20 of the GoF book. Delegation is fundamental to
almost all of their patterns. The problem, which page 20 makes
clear, is that they are also assuming the old Agent role of
functional decomposition for the collaboration with the Client
where the original object remains a middleman (high level node in
the decomposition tree). IOW, they see the delegation in terms of
composition.
Thank you. I should have noticed the relevance of that part of the
introduction earlier. It seems that my respect for the GoF must fall
slightly, because it looks like the person who wrote that
introduction and the person who wrote the description of the Strategy
pattern were not communicating as well as they should have been.
Let me illustrate what I am talking about with quotes.
The intent of the Strategy pattern is: "Define a family of
algorithms, encapsulate each one, and make them interchangeable.
Strategy lets the algorithm vary independently from the clients that
use it."
That makes it quite clear that what is being delegated is strictly
algorithms and not any sort of responsibility. I am aware that it is
bad practice, but this is not merely removing responsibilities from
the Context then needlessly using the Context as a mediator to access
its old responsibilities. There is no intent in the Strategy pattern
to remove responsibilities from the Context.
However, that conflicts with this from part 6 of the introduction:
"In the Strategy pattern, an object delegates a specific request to
an object that represents a strategy for carrying for carrying out
that request."
I hope you see the conflict between these two quotes. The Strategy
pattern is not intended to delegate requests, it is intended to
delegate algorithms. I believe that the correct way to resolve this
conflict is in favor of the intent of the pattern. In other words, in
a particular implementation of the pattern the Context might delegate
requests directly to the Strategy object, but that is not fundamental
to the Strategy pattern, merely something which can occur.
Because the Client is talking to Context directly rather than
Strategy, the postcondition of contextinterface must include (be a
superset of) the postcondition of Strategy.
I think that is a bit strong. If Context were breaking good OOA/D
practices and both waiting for and depending upon a response from the
Strategy, then any postcondition of Strategy would only have to be
true at the moment that the Strategy responds. After that moment, the
Context is free to violate the Strategy's postcondition in any way
that it needs to satisfy its own postcondition.
What is the purpose in forbidding contextinterface from waiting?
The short answer is decoupling. The main purpose is to ensure
that behaviors are self-contained (i.e., have no dependencies on
other behaviors). If the behavior in hand can't wait for an
answer, it makes no sense to construct it to need the answer.
I must agree that is a good point. When there is a chain of functions
depending upon each other, if any function fails to satisfy its
postconditions then everything fails.
<snip description of traditional functional decomposition>
This does not sound like how functional decomposition is supposed
to work. You make it seem like an exercise in pattern
recognition, where repeated sequences of function calls are
factored out for no other reason than that they represent a
repeating pattern that can be factored out.
It is in a way. To avoid redundancy one needs to recognize the
repeated sequences.
It is not repeated sequences that need to be recognized; it is
actually a very similar thing, but with a critical difference. What
needs to be recognized is repeated purposes. In other words,
sequences of function calls that serve the same purpose in multiple
places and can then be factored into a function definition.
One must not replace a sequence by a function that happens to contain
the same sequence if the purposes do not match. The source of the
problems you describe come from that mistake, not from functional
decomposition.
You certainly can specify the contract for any function without
knowing the contracts for any other function. You can specify the
contract for a function even before the function is defined. One
almost always does that, especially in top-down decomposition.
The contract and purpose of a function is important, but the
function calls involved in the definition are mere implementation
details, so the contract determines the implementation, not the
other way around.
No, you can't. The client is contracting for a single <high
level> behavior. The low level behaviors are extensions of the
high level behavior by definition and the high level behavior
cannot produce the correct results for the client without them.
Therefore one cannot specify the high level behavior without also
specifying what all the lower level behaviors do.
procedure X(a)
temp1 = Y(a + 2)
z = temp1 * 5
One cannot specify what value 'z' should have for a given 'a'
input without knowing what the Y function does.
I think the issue is being confused by the abstract nature of the
example. Can you use a slightly more practical example?
Let me try giving a proper name to X:
procedure factorial(a)
temp1 = Y(a + 2)
z = temp1 * 5
Now watch me specify what value 'z' should have for a given 'a'
without knowing anything about Y: The value of 'z' should be 'a!' for
any value of 'a'.
Now I can guess what Y(x) does: (x - 2)!/5, but even if I could not,
it would be no more difficult to create the above specification. In
good functional decomposition a function has a purpose and that
purpose determines its specification. The actual definition of the
function has no role at all in determining the specification of the
function, doing that would be incorrect in top-down or even bottom-up
design.
In top-down design, the specification of the function comes first
based on what is needed of the function in pre-existing function
calls. In bottom-up design, the specification of the function comes
first still, but it is based upon what is expected to be needed.
Notice how crazy the function Y becomes in the above example, Y(x) =
(x - 2)!/5. That is the direct result of trying to create the purpose
of the function based upon the implementation. In reality, the
specification of X has nothing at all to do with the specification of
Y, because Y does not need to be used inside X and very likely would
not be. The set of functions called in X is determined by the
specification of X, not the other way around.
The client contract specification for X must be a superset of the
specification of Y. [One can't even unit test X without a valid
implementation of Y. Using stubs to provide a return value
corresponding to 'a' is just self-delusion; one is testing the
test harness, not X.]
Surely you cannot test X until after X is implemented any more than
you can implement X until after the results of X have been specified.
First we specify, then we implement, then we test.
The problem was that when requirements changed for
one of the contexts, the best place to implement the change might
be in one of its very low level descending nodes. But some other
context where the node was reused might be unaffected by the
change in requirements and would still want the original
behavior. That is, for that client the specification of the
higher level node should be unchanged.
I think there is some sort of mistake here. If the requirements
of a function change then the function must change. It would be
foolish to try to alter the requirements of other functions in an
attempt to avoid reimplementing the one function that needs to
change, that only leads to the problem you are describing.
Instead, one must change the function definition as necessary to
meet the new requirements, including defining new functions if
needed, but not redefining the old.
I postulated that the requirements on the function only changed
for one client of several. That is the problem with the reuse of
higher level nodes in different contexts; one has a lattice with
multiple clients when one eliminates the redundancy.
The requirements of a function cannot actually change for one client
of several. A function is required to do a certain thing and that is
the foundation of everything about that function. If you need the
function to do something else then you need a new function.
Of course, you can incorrectly believe that the requirements of a
function have changed when they have not and then when you modify the
function you will end up breaking it, but that is certainly not
fundamental to functional decomposition. What you are describing
seems like a way to deliberately cause trouble. No matter what design
you use there will always be ways to intentionally break it.
At some high level one computes, say, employee benefits that depend
on (among other things) base salary. Let's say there are several
different types of benefits needing computations the differ in
detail. However, let's also say one such computation is shared
among them: net after tax income. So we have
Benefit1 Benefit2 Benefit3
\ | /
\ | /
\ | /
\ | /
ComputeAfterTaxIncome
But to compute after tax income one needs to subtract federal,
state, and local taxes from base salary. To get each of those we
have to access the DB for the relevant fields from the Employee
record first:
ComputeAfterTaxIncome
/ \
/ \
/ \
GetDBInfo Compute
So far so good since all three benefits use the same data and
compute after tax income the same way. So we can use a single
higher level function, ComputeAfterTaxIncome, for all of them.
Now suppose requirements change for Benefit3 such that one needs
to use the fully burdened salary rather than the base salary while
the Benefit1 and Benefit2 still use base salary. The maintainer
"walks" the call stack from Benefit3 and realizes that GetDBInfo
needs to change its query so that it accesses the burdened salary
field rather than the base salary field.
This is where the maintainer shoots himself in the foot. There should
be no walking, that is the entire source of the problem. I suppose
that it is ironic that if the maintainer actually had shot himself in
the foot, that would have solved the problem.
Each function has its own requirements and when you want to make a
change you find the function or functions whose requirements include
whatever you are changing, then you make the change.
'ComputeAfterTaxIncome' is not responsible for Benefit3. If there is
a change to the benefit that Benefit3 represents, then Benefit3 is
the function to change. Surely that is not surprising!
If the maintainer knew what he was doing he would only change
ComputeAfterTaxIncome in the event that the meaning of 'after tax
income' had changed, then that change would correctly affect the
entire program. What you are describing is the introduction of a new
kind of after tax income which should be represented by a new
function.
.
- Follow-Ups:
- Re: Abstract public member variales?
- From: H. S. Lahman
- Re: Abstract public member variales?
- References:
- Abstract public member variales?
- From: Brendan Guild
- Re: Abstract public member variales?
- From: Brendan Guild
- Re: Abstract public member variales?
- From: H. S. Lahman
- Re: Abstract public member variales?
- From: Brendan Guild
- Re: Abstract public member variales?
- From: H. S. Lahman
- Re: Abstract public member variales?
- From: Brendan Guild
- Re: Abstract public member variales?
- From: H. S. Lahman
- Re: Abstract public member variales?
- From: Brendan Guild
- Re: Abstract public member variales?
- From: H. S. Lahman
- Re: Abstract public member variales?
- From: Brendan Guild
- Re: Abstract public member variales?
- From: H. S. Lahman
- Re: Abstract public member variales?
- From: Brendan Guild
- Re: Abstract public member variales?
- From: H. S. Lahman
- Re: Abstract public member variales?
- From: Brendan Guild
- Re: Abstract public member variales?
- From: H. S. Lahman
- Re: Abstract public member variales?
- From: Brendan Guild
- Re: Abstract public member variales?
- From: H. S. Lahman
- Re: Abstract public member variales?
- From: Brendan Guild
- Re: Abstract public member variales?
- From: H. S. Lahman
- Re: Abstract public member variales?
- From: Brendan Guild
- Re: Abstract public member variales?
- From: H. S. Lahman
- Re: Abstract public member variales?
- From: Brendan Guild
- Re: Abstract public member variales?
- From: H. S. Lahman
- Re: Abstract public member variales?
- From: Brendan Guild
- Re: Abstract public member variales?
- From: H. S. Lahman
- Re: Abstract public member variales?
- From: Brendan Guild
- Re: Abstract public member variales?
- From: H. S. Lahman
- Abstract public member variales?
- Prev by Date: Re: Is this too much OOP?
- Next by Date: Re: Is this too much OOP?
- Previous by thread: Re: Abstract public member variales?
- Next by thread: Re: Abstract public member variales?
- Index(es):
Relevant Pages
|
Loading