Re: Abstract public member variales?



Responding to Guild...

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 thought you were using deletion as a basis for another disadvantage of [Deconstructor] vis a vis Serializer.

To answer your question about deletion, I think that has pretty rigid rules that are driven by referential integrity concerns. Whenever an object is deleted, referential integrity demands that one also de-instantiate any relationships that it has with other objects. Otherwise when the relationship is navigated one will encounter nasty stuff like dangling references.

Let's look at the non-GC case first. There are three ways to de-instantiate a relationship, depending on how the relationship was implemented. When the relationship is implemented by passing an object as a message argument, one has to prevent that. Usually that is trivial because to pass the reference the message sender needs the reference in hand and it shouldn't have access to it after the object is deleted. IOW, whatever else one does to de-instantiate the object's relationships will prevent the message sender getting it in hand.

When the relationship is implemented by an explicit identity search, one has to make sure the identity is no longer available to the search method. Typically that will be in a static lookup table where {identity, address} is provided by constructors. One will then need to invoke a corresponding class-level delete to remove the entry from the table.

When the relationship is implemented as a single reference attribute, one has to NULL the referential attribute. [The corollary is that there must be code around each navigation that tests whether the relationship is instantiated and the owning object must behave properly if it isn't. (That is why one strives to eliminate conditional relationships when it is feasible to do so.)]

If an object's relationship is '*', then there will usually be a collection object that needs to have the reference removed from its internal list to de-instantiate the relationship for the deleted object.

Since relationships can be navigated from either side, one needs to ensure that it is properly de-instantiated on both sides.

The tricky part about de-instantiating relationships is that one must be careful about scope. The golden rule of thumb is that one should de-instantiate the object's relationships within the same method as one de-instantiates the object itself. Otherwise one needs to take special pains to ensure that the relationship will not be navigated between the time the object is deleted and the time the relationship is deleted.

How does GC affect this? In some ways GC makes things easier because the object is not removed until there are no more references to it s one <supposedly> doesn't need to worry about deleting objects and the relationships will take care of themselves. However, that tends to make the developer less wary about some rather nasty problems around using GC. IMO, GC causes more problems than it is worth and to be safe one should always invoke an explicit 'delete' method when an object's life cycle is done regardless of whether the language provides GC or not. One has to look out for several things...

One needs a specific delete whenever the relationship is implemented as an explicit identity search because the {identity, address} lookup table is not updated for deleted objects in normal processing. So one will effectively have a memory leak since the object can't be removed by the GC because not all references go away.

A similar argument applies to any '*' relationship. The collection class must have a specific 'remove' method to clean up or the object will never actually be deleted. [Not always necessary. If the object on the 1-side is deleted, the entire collection object with its internal list of references will be deleted.]

The insidious problem with GC is that objects may be still around AND accessible long after they should be removed from the problem solution. The result is that an object may be accessed and collaborations may take place that are incorrect because the object should not have existed at the time the access took place.

[I know of a case where a large company's payroll was a week late because of this sort of problem. And then they had to completely back out the most recent version of the payroll software to run payroll. It took more that a month of parallel processing to find the problem. There were even worse problems related to alternating the way some things were done between the old and new version over multiple accounting periods that eventually had to be resolved. In all, it was a major mess because a developer relied on GC.]

The GC enthusiasts correctly argue that this problem only arises when the developer does not recognize the danger and does not explicitly remove references when an object has a restricted life cycle in the requirements. IOW, if requirements say an object cannot be accessed after a certain point in the solution, the developer has to enforce that by explicitly removing references.

My problem with that view is that it isn't always so easy to see that, especially if the convenience of GC has lulled the developer into not worrying about object deletions until there is a demonstrated memory leak. So it is much safer to /always/ determine the life cycle of an object and explicitly delete it even if one's language has GC, which kind of defeats the purpose of GC.

My point in all this is that deletion is really treated exactly the same way as instantiation. When it is time to do it, one does it. That includes explicitly removing all references to the object -- preferably within the same scope as the object itself is deleted. The original factory object used to create the object is usually used for this because it already had to know what relationships to instantiate.

[Conditional relationships are inherently dynamic so they are not usually instantiated by factories. However, the de-instantiation is directly tied to object deletion so one would still put the deinstantiation in the factory. That may require some checking by the factory for whether the object is currently referenced in a particular reference (i.e., one doesn't want to NULL references that currently point to another object). One can optimize this with tricks like collection classes having a 'remove' method that is a no-op if the indicated reference isn't there at the moment.]


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.

Note that the factory doesn't think in terms of persistence. It simply knows that if it creates an [ObjectA] instance, there are certain relationships that also need to be instantiated. Any decisions to be made for conditional relationships will usually be abstracted as parametric context inputs or attributes. IOW, the factory just needs to know what to do if isPersisted is TRUE or not, not what 'persisted' means. Conversely, any deletion will be similarly abstracted.


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.

When an object dies /all/ of its relationships must die to avoid referential integrity problems. Since the practical issues for referential integrity strongly suggest killing the relationships in the same scope as the object, the logical place to do it is the 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.

Remember that relationships are largely orthogonal to class semantics. They can be treated pretty much as class-cutting aspects. The object is essentially just a placeholder for the relationship implementation. So the notion of "talking to" is quite different. To de-instantiate a relationship implemented with a reference attribute, one needs to access the object to NULL the reference. So one is "talking to" the object. But I regard that conversation as quite different than the conversations involved the collaborations.


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.

Whenever one creates an object, one has to ask the question: When, if ever, does this object die? If it does not exist for the remainder of the current execution session, then one has to determine exactly what condition prevails to end its life cycle. Then one has trigger its deletion and de-instantiate all its relationships as soon as that condition prevails.

That really has nothing to do with whether the objects need to be persisted to a DB. The persistence issues only determine what relationships the object may be involved in.

* 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

You keep saying that a dynamic cast is needed if one doesn't use Serializer. That is just not true. I do not understand why you keep making this assertion when I have already offered several examples of implementations of [Deconstructor] where no dynamic cast is required and much safer techniques based on static structure are used.

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

No, it is not. The polymorphic dispatch is in service of the Composite pattern if saveIt() is defined as a property of [Graphic]. How [Graphic] got that property is not relevant to the pattern's polymorphic dispatch.

Contrast:

[Serializer]
A
|
+----...
|
[ObjectA]

with

[Serializer]
A
|
+----...
|
[ObjectA]
A
|
+----...
|
[ObjectA1]

where one invokes ObjectA.saveIt() in both cases.

In the second case there is clearly polymorphic dispatch because [ObjectA1] and its siblings may have different implementations that are transparently substituted. But in the first case there is no polymorphic dispatch when one invokes ObjectA.saveIt() because one can't get any other implementation that [ObjectA]'s from that syntax.

The second case is exactly the same thing that is happening with the [Graphic] Composite pattern. It is the additional subclassing introduced by the pattern that leads to polymorphic dispatch, not Serializer.

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'.

Exactly my point. But [Graph] exists for reasons related to the problem you are solving, not persisting to a database. The polymorphic dispatch is inherent in the Composite subclassing, not Serializer inheritance.

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.

<Boggled Mode>

Then I don't understand what you have been disagreeing with when I argue that there is no polymorphic dispatch when invoking ObjectA.saveIt() so long as [ObjectA] is itself not a superclass.

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.

My objection is to the third sentence. Why does an entity that has no responsibility for finding a path have a path finding algorithm? If it does not have that responsibility it has no need whatsoever of such an algorithm. Responsibilities exist to resolve requirements; we abstract responsibilities that we need to solve the problem. If there is no requirement for finding a path, then there is no reason to have a path finding algorithm.

Note that the requirement of finding a path may be an explicit requirement (e.g., always use the shortest path) or an implied requirement in the problem domain (e.g., there are obstacles one must go around or a specific route is required for the movement to determine whether there will be interactions with other entities along the route). Either way, that requirement must be explicitly addressed in some responsibility (or combination of responsibilities).

From the client's perspective I think the entity's responsibility is really more like, "Determine the best path from where you are to B and move along it to B." When you delegate the path finding responsibility, that responsibility changes to just, "Move along path P to B." and the client has to establish path P by collaborating with the new owner of the path finding responsibility (e.g., "Find the best path from A to B.").

[BTW, I would argue that even when the same entity owns both the path finding and movement responsibilities, those would probably be separate responsibilities. That's because responsibilities should be logically indivisible at the subsystem's level of abstraction. As soon as there are conjunctions in the definition of a responsibility, it is usually a sign of poorly formed responsibilities. It is that logical indivisibility that allows one to connect the solution dots independently (or at least at a higher level of abstraction) than individual method implementations.]

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.

Then that is a major problem since they must be in the context because responsibilities just recast requirements.

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.

Not quite. An algorithm computes something. The responsibility defines what needs to be computed. In an OO context, substituting algorithms is simply providing different implementations of the computation itself. Thus providing an ascending sort is a responsibility (sort()) while Quicksort and Insertion sort are different <algorithmic> implementations.

What one does when delegating to Strategy is that one moves the sort() responsibility to [Strategy] and the [Strategy] subclasses provide the substitutable <algorithmic> implementations. IOW, sort() must be moved to [Strategy] in order to allow the polymorphic dispatch that enables the substitution. But then there is no reason for [Context] to know about the sort() responsibility any more (unless it is an agent, as in the GoF implementations).


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."

Here I think they are talking about the agent view where [Context] has a pass-through responsibility. That is really more about the implementation of relationship navigation (i.e., who the original client talks to) than about what the pattern actually accomplishes. That's the part I don't like.


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.

Your second sentence here is exactly my position. I would just make the last sentence stronger: one should /always/ provide decoupling through peer-to-peer collaboration rather than using the indirection of request forwarding in the implementation.

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.

I don't think it has anything to do with OOA/D per se. My statement applies for any set of nested functions. The specification of the outermost function must always be a superset specification of all of the nested functions' specifications. [Here I am using 'function' in the restricted sense of a procedure that returns values for the caller's use.] Those nested operations are an extension of the outermost operation and the outermost operation is not complete without them. Therefore whatever they do must be reflected in the specification of the outermost operation.

It is exactly that chain of hierarchical dependency that the OO paradigm seeks to break with peer-to-peer collaboration and self-contained responsibilities.

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.

That observation supports my view immediately above that a chain of specification causality exists. The low level failure is manifested as the outermost operation fails to meet its DbC contract.

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.

You are just using the superset requirements to define the subset requirement that 'factorial' depends upon. IOW, you are using the fact that the specification of 'factorial' is a superset specification to create the specification of Y. That pretty much proves my point that the Y specification is a subset of the 'factorial' specification and vice versa. B-)

The issue is not how one constructs the tree. The issue is that there is a chain of dependency where 'factorial' depends on what Y does _in the existing tree_. That chain of dependency is what matters for maintenance. If one substitutes a Y that does something else, then 'factorial' will be broken because it depends upon what Y does. That dependency chain was broken when I rewrote the example in an OO style (i.e., one could change Y without breaking X1 or X2).

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.

I am not sure what your point is here. One cannot unit test 'factorial' without a properly working implementation of Y. However, when I recast the example in an OO style, I could fully unit test X1, X2, or Y without any implementation of the others (other than a test harness stub for Y to log the arguments when testing X1).

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.

That is pretty much my point. If a function has multiple clients and the requirements change for one of them but not the rest, one has a problem. One needs a new function for the changed client while still using the old function for the other clients.

However, due to the multi-level decomposition the actual repair might be at a much lower level with everything else the same. But the chain of dependencies migrates all the way up the tree like a cancer to break things at the highest level. So one doesn't need just one new function; one needs <at least> a whole new limb full of <largely redundant> functions. That was the core problem of Spaghetti Code.

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.

How can there be no "walking" in a functional decomposition? The maintainer has to find the right place to make the change and that is pretty much the only way to do it. The problem for the maintainer is that GetDBInfo is obtaining the wrong value of "salary" for the new Benefit3 requirements. But the only way that becomes clear is by "walking" down the decomposition limbs.


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.

How can the maintainer do that? ComputeAfterTaxIncome gets the value used for "salary" in the computation from GetDBInfo, which is currently hard-wired to return 'base salary'. At that, ComputeAfterTaxIncome is just a pass through that doesn't even do the computation; it is just coordinating other functions that do the grunt work. To make the change as you suggest, ComputeAfterTaxIncome needs to get a different value from GetDBInfo.

For example, you could modify ComputeAfterTaxIncome from:

ComputeAfterTaxIncome ()
double salary, income;
salary = GetDBInfo ();
income = Compute (salary);
return income;

to:

ComputeAfterTaxIncome (context)
double salary, income;
salary = GetDBInfo (context);
income = Compute (salary);
return income;

However, this still means that the following functions need to be modified:

Benefit1, Benefit2, Benefit3 -- to supply the context parameter
ComputeAfterTaxIncome -- as shown
GetDBInfo -- to get the right value from the DB.

IOW, five of the six relevant functions need to be touched to make the change. Not good. Things get a whole lot worse if the root change needs to be made several levels below ComputeAfterTaxIncome.

Alternatively you could provide:

ComputeAfterTaxIncomeNew ()
double salary, income;
salary = GetDBInfoNew ();
income = Compute (salary);
return income;

that is called just for Benefit3. However, you still need to create a new GetDbInfo to extract burdened salary. In effect this just duplicates the entire descending limb, which would be more obvious if there were more levels between ComputeAfterTAxIncome and GetDBInfo.



*************
There is nothing wrong with me that could
not be cured by a capful of Drano.

H. S. Lahman
hsl@xxxxxxxxxxxxxxxxx
Pathfinder Solutions
http://www.pathfindermda.com
blog: http://pathfinderpeople.blogs.com/hslahman
"Model-Based Translation: The Next Step in Agile Development". Email
info@xxxxxxxxxxxxxxxxx for your copy.
Pathfinder is hiring: http://www.pathfindermda.com/about_us/careers_pos3.php.
(888)OOA-PATH



.



Relevant Pages

  • Re: Question on LSP
    ... not have to be explicit attributes, ... Since objects in memory usually don't move, the language can largely hide the identity mapping. ... My issue here is that whatever semantic meta model the language uses, there must be some way for the developer to unambiguously express the OOA/D is-a semantics for some problem space entity. ... Note that the language allows us to use a name like 'T' on the reference as a mnemonic so that the developer can keep track of what is happening with the indirection. ...
    (comp.object)
  • Re: Design Question
    ... You could advocate that all public methods should be declared in an interface, such that you would have no concrete class that did not implement at least one interface. ... You will have to add new getters and setters to the interface just as you would to the class, and factory method signatures will change just like constructor signatures would. ... In the approach I was advocating, the Record class would have no knowledge of persistence. ...
    (comp.lang.java.programmer)
  • OO Factory
    ... The fact that FACTORY exists in COBOL does no harm, even if you don't use it. ... Your reference to Gang of Four - obviously nothing to do with the folks in Bejing who are all in a mausoleum by now. ... SECTION rather than the current WORKING-STORAGE SECTION - so I got into ... invoke self "new" returning lnk-self End Method "new". ...
    (comp.lang.java.help)
  • Re: Odd (undocumented?) behavior of RAM file within a loop
    ... use strict; use warnings; ... close statement (on AS Perl 5.10 on Win32). ... Note that the explicit close results in the reference to the same ...
    (comp.lang.perl.misc)
  • Re: Static vs. Dynamic typing (big advantage or not)---WAS: c.programming: OOP and memory management
    ... > reference from the dictionary). ... For the core editing functionality, GC seems to have no advantages. ... persistence in general solves some of the same ... problems as garbage collection. ...
    (comp.object)