| Register | FAQ | Calendar | Search | Today's Posts | Mark Forums Read |
|
#21
| |||
| |||
| H. S. Lahman wrote in news:3OhQg.7146$o1.960{}trndny04: > Responding to Guild... > > There will already be a relationship with whoever is running the > persistence show and the objects that need saving. That's because > what needs to be saved is a requirement on the save semantics for > the particular problem. Recall this diagram from a few messages > ago: > > [OrdinaryClient] > | 1 > | > | R2 > | > | related to > | * * manages persistence for > [ObjectA] -------------------------------+ > | 1 | R3 > | saves | > | | > | R1 | > | | > | 1 1 invokes 1 | 1 > [Deconstructor] ------------------ [SavingClient] > > In effect there is an R3 instantiation when every [ObjectA] > instance is instantiated (presumably by some factory object). So > [SavingClient] always knows the type of [ObjectA]. If > [SavingClient] saves members of other classes, there will be > similar relationships to those classes. So in the worst case of > multiple classes [SavingClient] will need a cast based on which > relationship it is being navigated (which we've talked about > before). I had presumed that the R3 relationship would be instantiated only as needed, when it was decided that the current state should be written to some persistence mechanism. The reason for this is that keeping that relationship alive all the time causes certain problems that I thought were too serious. 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.) 2) The relationship is instantiated in the factory but can be removed by the OrdinaryClient as needed when objects cease to be useful. I am sure this would cause a large amount of coupling between OrdinaryClient and SaveClient and I doubt that you would suggest that approach. 3) An attribute of ObjectA indicates whether each object is still useful or should be deleted. In that way, the SaveClient can avoid saving useless objects. Once again, we end up with ObjectA knowing about its own persistence. And of course the fourth option is to instantiate R3 on instances of ObjectA that we know we need to save an no others, which requires that we wait until the last moment before doing it. I honestly cannot guess which of the above techniques you would recommend. They all have serious issues. I am very eager to get your opinion, and it would be especially nice if you were to suggest something unlike anything I have thought of. > 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(); >> * 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. > The Composite pattern itself introduces the polymorphic dispatch > the same way as it introduces polymorphic dispatch for draw(). > IOW, the Composite pattern itself /requires/ polymorphic dispatch > for /all/ properties. The fact that [Picture] happens to get > saveIt() from Serializer is no longer relevant. At that point > Picture.saveIt() is no different than Picture.draw(); it is just > another behavior that [Graphic] says must be implemented in every > subclass. I think it is very unfair to the Serializer pattern to give the Composite pattern credit for the polymorphic dispatch on saveIt(). It is true that the Composite pattern introduces the polymorphism, but without the Serializer pattern we would not be able to take advantage of it in doing the persistence. This sort of situation is the entire reason for the existence of the Serializer pattern. If there were no Composite pattern and no situations where polymorphism is used, then we would always know the exact class of every object and persistence would be trivial. > To see this suppose that only [Text] and [Picture] objects are > saved. Now saveIt() cannot be invoked for all [Graphic] objects. > Since it is a specialization relative to [Graphic] one will have > to know specifically whether one has a [Text] or [Picture] object > in hand. IOW, to invoke saveIt() the client will still have to > already know which object is in hand so there will be no need to > invoke Serializer.saveIt(). To put it another way, one does not > have a [Serializer] object in hand; one has a [Graphic] object in > hand. I presume that you mean that you are deciding to only save Text and Pictures despite the fact that you know that some Pictures might contain other components. If you knew that Picture would never contain anything but Text and other Pictures, then you could use polymorphic dispatch as usual. If a picture were to contain a rectangle, then not saving the rectangle would mean not actually saving the picture and it would violate the intended semantics of saveIt(), which means that you are not following the Serializer pattern. Your example is not as illustrative as I would like, because it does not seem like anything that might occur in practice. It is easy for me to imagine that the particular situation you are describing can be safely ignored. >> Suppose that the Graphic class is a subclass of 'Serializable' >> and therefore has a 'saveIt()' method. The implementation of >> saveIt() in Picture is surely going to use polymorphic dispatch. > > That doesn't change anything. Where [Graphic] gets saveIt() does > not change the fact the the polymorphic dispatch is part of the > Composite pattern rather than the Serializer pattern. The fact that it is a part of the Composite pattern does not change the fact that polymorphic dispatch is actually used during persistence. I thank the Serializer pattern for this because all other persistence techniques do not allow this kind of polymorphism. >> If the Strategy semantics were to actually change, the Context >> must change because the semantics of the Context is dependent >> upon the semantics of the strategy. It is less certain that the >> client must change, especially if the semantics of the Context >> does not change, but since the client must supply the Strategy, >> there is likely to have to be a change in some factory at least. > > Picture me jumping up and down screaming, "No, No, NO!!! The > point is to eliminate the dependency in [Context]!!!!" B-) 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. That is the Strategy pattern as the GoF give it. It seems as though Strategy is not good OOA/D, after all, but that does not change what Strategy is. > The difference between contextinterface and strategyinterface is > syntactic (e.g., one can convert data value units and whatnot). > One can always provide multiple syntaxes for accessing a given > semantics. The issue here is that [Context] must somehow provide > the correct [Strategy] semantics for the Client to access. 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 various strategies are not necessarily merely different algorithms for producing certain outputs from certain inputs. The results of the algorithms might be different from strategy to strategy, so the semantics of Context will also vary. However, all of the strategies have an abstract semantics that they all follow. They may do things in different ways and produce different results, but they are all fundamentally required to do the same job. In that sense, the semantics of Context never changes, it is independent of the strategy. In the example that the GoF give, the Composition is the Context and it represents some text, such as an article. It is responsible for keeping the line breaks in the appropriate places. The various strategies are algorithms that the Composition can use to fulfill its responsibility, and depending on which strategy is used, the line breaks will be in different places, but the responsibility of Composition has not really changed, so long as it is specified abstractly. The benefit is that while the factory has to decide which strategy to use, the actual client can be completely oblivious to the strategy. It is likely that the client does not have the slightest care about how the lines are broken, and the Strategy pattern allows us to keep it from worrying about that. >> Context must be a direct client of Strategy if one is following >> the Strategy pattern. In fact, Context is likely to be the only >> direct client of Strategy while the client of the Context is >> certainly not intended to be a client of Strategy. > > The point here is that [Context] should NOT be a client of > [Strategy]. I think it is important to clarify something here: Are you saying that the Context should not be a client of Strategy when using the Strategy pattern, or are you saying that the Strategy pattern should not be used? If you are suggesting the former, then you are going against the definition of the Strategy pattern given by the GoF, which only causes trouble. If you are suggesting the latter, then I agree, though I must admit that I do not fully understand the reason. I am going to continue to speak of the Strategy pattern as the pattern which the GoF described, not as that pattern might be modified to conform to modern OOA/D rules. It is not yet clear to me why those modification must be made, or in other words, why the Strategy pattern is bad. >> It seems that Strategy.algorithminterface() is not allowed to have >> any postconditions that Context can depend upon. But if that is the >> case, then why does Context have postconditions that it is expected >> to fulfill? > > That is true in a delegation. Those properties now belong to > [Strategy]. When doing the delegation one needs to modify the > postconditions on [Context] to remove any related to the > responsibilities that have been transferred to [Strategy] because > [Context] no longer owns those responsibilities. IOW, the > responsibilities of [Context] are not the same before and after the > delegation. 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. >>>However, OOA/D messages are supposed to be announcements that >>>have no expectation about the response. In addition, OOA/D >>>assumes an asynchronous model for behavior access so >>>algorithminterface() might not even start executing by the time >>>contextinterface() completes. >> >> That is only if contextinterface() does not wait for a response >> from algorithminterface(). If contextinterface() must satisfy >> some postcondition contract then I can only imagine that >> algorithminterface() must also have such a contract and that >> contract would theoretically allow Context to have expectations >> about a response, were it good OOA/D to have those expectations. > > As I said, the OOA/D is asynchronous so contextinterface() can't > wait. That is the conundrum. Similarly, OOA/D messages are > announcements, so there can't be any expectation (dependency on) > the response to the message. That is the second conundrum I am > pointing out here. Then what does a postcondition mean? It seems that contextinterface() has a post condition that it must fulfill. Is that not an expection of the response that the client can rely upon? Or do you mean that there is some unspecified fundamental different between the interface of Strategy and the interface of Context that allows Context to have postconditions but forbids it for Strategy? What is the purpose in forbidding contextinterface from waiting? >>>There is a very good reason why the OOA/D model works that way. >>>If contextinterface() depends upon what algorithminterface() >>>does, then Context is nothing but a high level node in a >>>functional decomposition tree. That means there is a >>>hierarchical dependency of contextinterface() on >>>algorithminterface(). Those sorts dependencies were aka >>>Spaghetti Code. One can make a case that the entire OO paradigm >>>is designed to eliminate such hierarchical dependencies. >> >> >> I thought functional decomposition was considered to be the cure >> for spaghetti code when 'spaghetti code' meant using 'goto' >> statements. I agree with you that this is functional >> decomposition and that there is a hierarchical dependency, but I >> would appreciate more details on why this is a bad thing. > > Spaghetti code lived on in Structured Programming long after > gotos. The problem is that once one has defined the functional > decomposition tree, one has a whole flock of "atomic" leaf > functions that appear many, many times along the base of the tree. > It would be enormously tedious to chain those together in a > "flat" sequence of calls to atomic functions. > IOW, the same "atomic" leaf function (usually at the level of > language > operators) appeared many times as a leaf because it was needed at > the base of multiple limbs in the tree. Moreover, whole sequences > of functions tended to be repeated in different contexts. > > The solution was to compose higher level nodes in the tree as > functions that "locked in" entire sequences (i.e., all the limbs > descending below the high level node traversed in order). That in > itself was not a problem so long as one identified a higher level > node independently in each limb where is was needed. Alas, that > isn't what was done because it would have required also > duplicating all the descending limbs. > > So the higher level nodes were only implemented once and were > reused in different limbs to avoid redundancy. Redundancy is bad > because when a change is made it has to be made in multiple > places, thus increasing effort and risk. That effectively turned > the functional decomposition tree into a lattice because each > higher level node now had multiple clients in addition to the > original descending limbs it coalesced. That lattice was the > Spaghetti Code. 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. The process is supposed to work either downwards from the top or upwards from the bottom. If we start at the top, then we first define a single function whose requirements are the purpose of the software at its most abstract, then we imagine how we can break up that task into steps and make functions to represent each step. The requirements of each function are the requirements of each step. If a step repeats then the function is called more than once. A function F can be called from a function G only if the requirements of F match something that G needs done. Even if G were to happen to have a sequence of function calls that precisely matches the calls made by F, you are not allowed to use F unless the sequence of function calls has the same purpose that F is required to fill. That is because there may be more than one way to fill the requirements of F and the actual implementation of F is not to be depended upon. The upwards is similar, but one starts by building simple functions out of primitives and defining their purpose based on what is expected to be needed. Once a function is built you forget about how it was built and look only at its purpose. Because of this, one might have to make two functions that are identical in implementation just because they have distinct purposes. At this point it would be perfect for me to provide a clever example, but instead let us look at the first example I could think of. Suppose you are dealing with geometric figures such as squares and circles. You might have two functions 'diameter(c)' which computes the diameter of a circle, and 'horizontalExtension(s)' which computes the width of a shape. If your domain happened to include only circles, then diameter(c) and horizontalExtension(s) would probably have identical implementations, but for more complex shapes horizontalExtension(s) would need to be greatly more complicated. One might have been tempted to use diameter(c) in place of horizontalExtension(s) when dealing only with circles, but even in that situation that is not to be done because their purposes are distinct. > The reason this was bad was because of the hierarchical > dependencies hidden behind the higher level nodes. One could not > specify the contract for the higher level node without specifying > the contracts for every descending node on the limbs that it > encapsulated. 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. > 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. The source of the issue must be with abstraction. The purpose and contract of a function is described at a higher level of abstraction than the functions that it calls. For example, a geometry procedure might use cartesian coordinates, or it might use polar coordinates, but the purpose of the function 'distance(p1,p2)' does not say one way or another, and no function that uses distance(p1,p2) specifies cartesian or polar in its requirements. If a decision were to be made that polar coordinates must be used, that would not effect the requirements of the function at the top of the lattice, nor would that function have to change in the slightest, it is dealing at a level of abstraction where polar coordinate points and cartesian coordinate points are indistinguishable. Similarly, the purpose of distance(p1,p2) does not change, but since it is built using cartesian coordinates, its definition does have to change. (This is exactly the same way that the Context of a Strategy does not need to change just because the Strategy changes in the Strategy pattern.) When each function has a clearly defined purpose and is not simply a way to factor out regularities, the problems you describe cannot occur. As a result of that, I cannot accept that simply because the Strategy pattern involves functional decomposition it must be bad. >> When I first read about the Strategy design pattern I thought it >> was amazingly clever, just as with all the GoF's design patterns. >> I think it may have been the best book I have ever read, though I >> must admit that it is getting a bit old and it seems that the >> Strategy pattern is showing its age. If we use the delegation fix >> on it, we are effectively ripping out the soul of the pattern and >> perhaps the name 'Strategy' should be laid to rest in that case. > > It is clever. It is a great solution to a lot of problems. > However, one can get much better decoupling than the GoF provides > in their implementations if one honors peer-to-peer collaboration. > To do that the basic concepts of the pattern don't need to change. That sounds very wrong to me. If you take away the functional decomposition of the Strategy pattern then all you are left with is simple delegation of responsibilities, which is so fundamental that I doubt it even deserves to be called a pattern. Everything good about the Strategy pattern comes in the collaboration of the Context object with the Strategy object. |
|
#22
| |||
| |||
| Responding to Guild... >>There will already be a relationship with whoever is running the >>persistence show and the objects that need saving. That's because >>what needs to be saved is a requirement on the save semantics for >>the particular problem. Recall this diagram from a few messages >>ago: >> >>[OrdinaryClient] >> | 1 >> | >> | R2 >> | >> | related to >> | * * manages persistence for >>[ObjectA] -------------------------------+ >> | 1 | R3 >> | saves | >> | | >> | R1 | >> | | >> | 1 1 invokes 1 | 1 >>[Deconstructor] ------------------ [SavingClient] >> >>In effect there is an R3 instantiation when every [ObjectA] >>instance is instantiated (presumably by some factory object). So >>[SavingClient] always knows the type of [ObjectA]. If >>[SavingClient] saves members of other classes, there will be >>similar relationships to those classes. So in the worst case of >>multiple classes [SavingClient] will need a cast based on which >>relationship it is being navigated (which we've talked about >>before). > > > I had presumed that the R3 relationship would be instantiated only as > needed, when it was decided that the current state should be written to > some persistence mechanism. The reason for this is that keeping that > relationship alive all the time causes certain problems that I thought > were too serious. > > 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. This is a common problem in GC languages anyway and deletion in general must be addressed any time one has a * relationship. So this is a basic OOP implementation issue and I don't think it has anything specifically to do with the Serializer problem. (The Serializer just saves the object; it doesn't delete it.) >>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. > > >>> * 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. >>The Composite pattern itself introduces the polymorphic dispatch >>the same way as it introduces polymorphic dispatch for draw(). >>IOW, the Composite pattern itself /requires/ polymorphic dispatch >>for /all/ properties. The fact that [Picture] happens to get >>saveIt() from Serializer is no longer relevant. At that point >>Picture.saveIt() is no different than Picture.draw(); it is just >>another behavior that [Graphic] says must be implemented in every >>subclass. > > > I think it is very unfair to the Serializer pattern to give the > Composite pattern credit for the polymorphic dispatch on saveIt(). It > is true that the Composite pattern introduces the polymorphism, but > without the Serializer pattern we would not be able to take advantage > of it in doing the persistence. 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. The polymorphic dispatch only exists because there is subclassing below the class where Serializer is inherited. That subclassing is due to Composite, not Serializer. > This sort of situation is the entire reason for the existence of the > Serializer pattern. If there were no Composite pattern and no > situations where polymorphism is used, then we would always know the > exact class of every object and persistence would be trivial. The reason Serializer exists is to define an interface for persistence that is common to all objects that need to be persisted. 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). For polymorphic dispatch to be relevant the caller would not know which specific object it is saving. >>To see this suppose that only [Text] and [Picture] objects are >>saved. Now saveIt() cannot be invoked for all [Graphic] objects. >>Since it is a specialization relative to [Graphic] one will have >>to know specifically whether one has a [Text] or [Picture] object >>in hand. IOW, to invoke saveIt() the client will still have to >>already know which object is in hand so there will be no need to >>invoke Serializer.saveIt(). To put it another way, one does not >>have a [Serializer] object in hand; one has a [Graphic] object in >>hand. > > > I presume that you mean that you are deciding to only save Text and > Pictures despite the fact that you know that some Pictures might > contain other components. You're right, [Picture] isn't a leaf so I should have chosen [Text] and [Rectangle]. >>>If the Strategy semantics were to actually change, the Context >>>must change because the semantics of the Context is dependent >>>upon the semantics of the strategy. It is less certain that the >>>client must change, especially if the semantics of the Context >>>does not change, but since the client must supply the Strategy, >>>there is likely to have to be a change in some factory at least. >> >>Picture me jumping up and down screaming, "No, No, NO!!! The >>point is to eliminate the dependency in [Context]!!!!" B-) > > > 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. A fundamental of good OOA/D practice is to eliminate unnecessary dependencies. There is no need for Context to depend on Strategy because the original client can now talk directly to Strategy. > That is the Strategy pattern as the GoF give it. It seems as though > Strategy is not good OOA/D, after all, but that does not change what > Strategy is. The pattern and it goal are fine. The problem lies in implementing the message communication by using Composite as a pass-through agent in the collaboration. That forces the algorithm semantics to be also defined in the Context interface, which creates an unnecessary dependency. >>The difference between contextinterface and strategyinterface is >>syntactic (e.g., one can convert data value units and whatnot). >>One can always provide multiple syntaxes for accessing a given >>semantics. The issue here is that [Context] must somehow provide >>the correct [Strategy] semantics for the Client to access. > > > 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 various > strategies are not necessarily merely different algorithms for > producing certain outputs from certain inputs. The results of the > algorithms might be different from strategy to strategy, so the > semantics of Context will also vary. 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. IOW, the DbC contract between client and service shifts from Client/Context before the delegation to Client/Strategy after the delegation. Therefore there is no need for Context to even know that they exist any more (unless Context itself is also a client of them in the solution). Only the original client still needs to know about the responsibilities. >>>Context must be a direct client of Strategy if one is following >>>the Strategy pattern. In fact, Context is likely to be the only >>>direct client of Strategy while the client of the Context is >>>certainly not intended to be a client of Strategy. >> >>The point here is that [Context] should NOT be a client of >>[Strategy]. > > > I think it is important to clarify something here: Are you saying that > the Context should not be a client of Strategy when using the Strategy > pattern, or are you saying that the Strategy pattern should not be > used? Absolutely. All the GoF patterns address a single basic problem where one has a relationship: * invokes 1 [Client] ------------------ [Service] R1 The basic problem is that [Service] needs to provide different behaviors based on dynamic context. That dynamic complexity cannot be captured properly in a simple static association. The GoF solution in almost all patterns is to delegate the context-dependent behaviors from [Service] to other objects and employ polymorphic dispatch to substitute them. The substitution is achieved by dynamically instantiating a relationship(s) based upon the prevailing context: [Client] | * | | R1 | | is related to | 1 * role of R2 1 [Service] ------------------------ [Delegatee] A | ... Where R2 is instantiated dynamically based on dynamic context. The [Delegatee] behaviors are substituted for the client via that instantiation. Note that the R1 relationship still exists. The circumstances vary with the particular problem space so there are a bunch of different variant patterns. But fundamentally all of them address exactly this basic problem and almost all of them solve it this way, even those as complicated as Visitor. Therefore the Client of the delegated responsibilities still remains the original client of [Service]. The Client just has to navigate the R1 -> R2 path to get to the [Service] behavior rather than just the R1 path. But once the delegation has been defined, [Service] is just a placeholder for the navigation path and does not need to know anything about either the specific collaboration or what service is being provided. Unfortunately the GoF implement that relationship navigation by making [Service] a pass-through agent for the responsibility and that, in turn, creates an unnecessary dependency. >>>It seems that Strategy.algorithminterface() is not allowed to have >>>any postconditions that Context can depend upon. But if that is the >>>case, then why does Context have postconditions that it is expected >>>to fulfill? >> >>That is true in a delegation. Those properties now belong to >>[Strategy]. When doing the delegation one needs to modify the >>postconditions on [Context] to remove any related to the >>responsibilities that have been transferred to [Strategy] because >>[Context] no longer owns those responsibilities. IOW, the >>responsibilities of [Context] are not the same before and after the >>delegation. > > > 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. >>>>However, OOA/D messages are supposed to be announcements that >>>>have no expectation about the response. In addition, OOA/D >>>>assumes an asynchronous model for behavior access so >>>>algorithminterface() might not even start executing by the time >>>>contextinterface() completes. >>> >>>That is only if contextinterface() does not wait for a response >>>from algorithminterface(). If contextinterface() must satisfy >>>some postcondition contract then I can only imagine that >>>algorithminterface() must also have such a contract and that >>>contract would theoretically allow Context to have expectations >>>about a response, were it good OOA/D to have those expectations. >> >>As I said, the OOA/D is asynchronous so contextinterface() can't >>wait. That is the conundrum. Similarly, OOA/D messages are >>announcements, so there can't be any expectation (dependency on) >>the response to the message. That is the second conundrum I am >>pointing out here. > > > Then what does a postcondition mean? It seems that contextinterface() > has a post condition that it must fulfill. Is that not an expection of > the response that the client can rely upon? Or do you mean that there > is some unspecified fundamental different between the interface of > Strategy and the interface of Context that allows Context to have > postconditions but forbids it for Strategy? 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. That's because Client is contracting with a single behavior that can't be specified without knowing what the algorithm does. That creates a hierarchical dependency of Context on Strategy, which is a no-no. > 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. A corollary is that self-contained methods allow one to connect the solution dots on a peer-to-peer basis to eliminate unnecessary dependencies. A secondary reason is that self-contained methods make it much easier to implement the OOA/D design in concurrent or truly asynchronous environments without change. Synchronous behavior is a special case of asynchronous behavior where the sequence of messages is pre-defined. One can always implement an asynchronous design in a synchronous computing environment but one can't always implement a synchronous design in an asynchronous computing environment. > <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. >>The reason this was bad was because of the hierarchical >>dependencies hidden behind the higher level nodes. One could not >>specify the contract for the higher level node without specifying >>the contracts for every descending node on the limbs that it >>encapsulated. > > > 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. 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.] In a properly formed OO application this sort of dependency does not exist because one would not construct the methods that way. Instead one would do something like: method X1 (a) temp1 = a + 2 Y(temp1) // message announcing temp1 has been computed method X2 (a) z = a * 5 // properly assign 'z' method Y (a) temp1 = ... // compute a value from 'a' X2 (temp1) // message announcing temp1 has been computed Now one can unambiguously define exactly what each method does without knowing anything at all about what other methods do. Validation is deterministic from just state inputs, state outputs, and logged announcement messages. One can also unit test each method in complete isolation (one just needs to know the right message was sent, not what happens as a result). >>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. As I said, that has two problems. One is recognizing that other clients might be affected when one is myopically "walking" the decomposition call stack for a single client whose requirements have changed. The other is figuring out how to make changes at low levels that affect many higher level nodes and contexts without reintroducing massive redundancy. > The source of the issue must be with abstraction. The purpose and > contract of a function is described at a higher level of abstraction > than the functions that it calls. For example, a geometry procedure > might use cartesian coordinates, or it might use polar coordinates, but > the purpose of the function 'distance(p1,p2)' does not say one way or > another, and no function that uses distance(p1,p2) specifies cartesian > or polar in its requirements. It's not that kind of issue. 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. At that point the maintainer has no direct way to know that Benefit1 and Benefit2 are also clients of ComputeAfterTaxIncome (i.e., the maintainer would have to proactively look to see if there were other clients). But if the maintainer simply makes the change to GetDBInfo, it will break Benefit1 and Benefit2. Now suppose the maintainer does realize there is a problem. The question is how to fix it so all three benefits get the right computation. In this trivial example it is pretty obvious one needs two versions of GetDBInfo and some mechanism for identifying the benefit context to determine which version to invoke. Alas, when the lower level function that needs to be versioned is 10 levels below the common higher level node among the benefits, that mechanism tends to get very messy. ************* There is nothing wrong with me that could not be cured by a capful of Drano. H. S. Lahman hsl{}pathfindermda.com Pathfinder Solutions http://www.pathfindermda.com blog: http://pathfinderpeople.blogs.com/hslahman "Model-Based Translation: The Next Step in Agile Development". Email info{}pathfindermda.com for your copy. Pathfinder is hiring: http://www.pathfindermda.com/about_us/careers_pos3.php. (888)OOA-PATH |
|
#23
| |||
| |||
| H. S. Lahman wrote in 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. |
|
#24
| |||
| |||
| 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 |