Re: Abstract public member variales?



Responding to Guild...

Perhaps 'a good move' is too loose a requirement in practice, but
even if the requirement were extremely strict, there will always
be more than one way to satisfy it in practice. Very often,
finding a path will be used to choose a good move, but it would
be design suicide to actually require that path-finding be used,
since there are other ways to do it.

The problem lies in 'good move'. You have to define /exactly/
what that means in terms of problem requirements when the
responsibility is identified.


This could use more justification. Why do I need to specify exactly what a good move is? I do not need to know what a good move is to write the client, that is certain. I can easily write the client without the burden of knowing how to make good moves, I will merely pass off that burden and make it a responsibility of the entity. As far as the client is concerned, a 'good move' is a precise technical description of the requirements of the entities. That is the level of abstraction that the client works at.

The client does care if there are preconditions that must prevail when the move is executed. Those preconditions will be determined by the requirements.

There are two other reasons. One is that eventually a method needs to be implemented for the responsibility. To write that method properly one must have a complete, precise, and unambiguous specification. That specification does not define How the method will work but it must do a very good job of describing exactly What it should do.

Perhaps more important, the developer needs to know what the responsibilities are before defining flow of control. That is, once the responsibilities are defined the developer needs confidence that all of the problem requirements have been accounted for. That confidence is only possible if each responsibility is defined in a manner that describes what requirements it resolves.

Recall that I said that in the OO paradigm one abstracts the problem space by abstracting all the intrinsic entity properties _that are necessary to solve the problem in hand_. One cannot know that one has done that properly unless one can trace the problem's explicit requirements and the problem domain's implicit structural requirements to those responsibilities.

In practice OO developers rarely run through some sort of requirements checklist to validate responsibilities. What they actually do is a two-fold process. First they keep one eye on the problem when they extract responsibilities and anthropomorphize. Because responsibilities are intrinsic to entities, the relevant requirements addressed are logically related and can be evaluated. So if one decides that the Entity.move responsibility will require finding a path it becomes clear the requirements that define path selection criteria and/or methodology are related to the responsibility and must be included in the responsibility definition.

The second phase of the process occurs when the developer defines collaborations in the overall flow of control of the solution. The developer validates that the collaboration works by double checking that all the requirements' implied contexts have been accounted for. A variation on this is the DbC approach I described for determining where messages are generated. Either way one must have a complete, precise, and unambiguous specification of what the responsibility does to evaluate the collaboration.

IOW, self-contained, intrinsic, logically indivisible responsibilities ensure that one can define the solution flow of control by simply connecting "atomic" dots with messages. But to decide which messages go where and when they go one needs to be able to map the problem requirements into the solution flow of control. One can only do that if one knows exactly What each dot does.

What you seem to be arguing is that you have the first case where
the choice is dependent purely on Entity's internal state. I am
very skeptical of that, but let's see where that takes us.


You are right to be skeptical, because I am not arguing that at all. The choice of algorithm is not the entities responsibility, that is specifically removed from the entity and must be taken over by someone else, such as the factory or the client. It can change with changing conditions outside the entity.

I am arguing that the way in which the algorithm is used is dependent purely on the Entity's internal state. By that I mean, if we have a path-finding algorithm, the Entity chooses which two points to give to the algorithm and the Entity chooses what the result means and how to use it. A particularly smart entity might be using it to guess where the avatar is going, for example.

I think your 'move' behavior is far from logically indivisible. I see at least the following distinct and largely orthogonal responsibilities

(1) The Entity picks the points on the path (as opposed to the player or the AI). IOW, the Entity decides where to go.

(2) From prior discussion a path may or may not need to be selected. That selection may not be done by Entity but it is important because of the decision in (4). That is, (4) depends on whatever decision is made.

(3) A path finding algorithm is selected. That may be done by someone else apparently, but it is important because (4) depends upon it.

(4) The path, if provided, must be followed. That implies the Entity does something else if no path is necessary (e.g., hops on the Magic Carpet).

What this screams to me is that this responsibility must be broken up. (1) is clearly a quite different task than (4). I would argue that (4) is also a compound responsibility because two distinct actions are required depending on whether a path is provided or not. In addition, in the overall solution flow of control (2) and (3) are preconditions for executing (4). Also, at least (3) and possibly (2) have (1) as a precondition of their execution. Assuming (2) and (3) are done by other objects, that yields at least three distinct collaborations with the outside world by Entity.

In addition, on the basis of object cohesion I would argue that (1) and (4) are complex enough to make Entity a god object and (1) should allocated (1) to some other object. That would simplify things for Entity because it is only left with (4) as the 'move' responsibility. (The choices might then be encapsulated, by private methods if nobody else cares how Entity gets from A to B.)

My point is that problem space abstraction will lead to a quite different allocation of responsibilities. In addition, one will arrive at Strategy-like constructs quite naturally, which was one of the points of my subsequent description.

Apropos of the current context, I would also argue that looking at each of these individual tasks closely enough to identify individual requirements will make the lack of cohesion and logical indivisibility of your description of Entity.move more clear. So one can view creating a proper specification of the responsibility as a tool for ensuring one has correctly abstracted the problem space.

You expressed a concern over micromanagment. Breaking up the responsibility is not micromanagement. It is simply divide-and-conquer or separation of concerns. That is necessary to make the application more robust in the face of volatile requirements. For example, my discussion of the preconditions above has an implied vision of how the overall solution will work. IOW, it assumes a specific sequence of operations that need to be enforced.

What happens if that order changes? Let's assume that currently (2) depends only on where the Entity is but not on what the path end points are. So (2) could be executed before (1). Now suppose some later requirements change makes the path selection depend on the end points as well. That requires the order of execution to be changed. If Entity.move is monolithic one has to change the internal implementation of the method, which always entails risk when internal flow of control is modified.

If the responsibilities are broken up as I have described, all one needs to do is change where the messages are generated and who they go to. That will be less risky and usually rather easy to do because one is just connecting the dots differently without touching them. (In UML would make that change in an Interaction Diagram without touching the object definitions or implementations.)

[Have you noticed that I keep coming up with examples of maintenance benefits for existing contexts that weren't in the original context? B-) I assert that is no accident. Here being able to change the flow of control without touching method implementations (at least in OOA/D) is a direct result of abstracting the problem space with "atomic" responsibilities that separate concerns. IOW, the OO benefits come "for free" if one properly applies an OO methodology. So I should always be able to come up with some kind of maintenance benefit examples in any context where one has well-formed OOA/D constructs.]

The other alternative is to do some delegation in the problem
space by somehow breaking up [Entity] into identifiable standalone
objects at a lower level of abstraction. When you do that, each
will have /intrinsic/ properties at that lower level of
abstraction. Those will be manifested as unique responsibilities
for each object. One of those is likely to be determining a path
from A to B, which you break out as a peer object like PathFinder.
(Note the clever way I sneaked in a commercial plug.) Since
there are several ways to do that, you subclass it.


And where do you put the various ways in thich the path finding algorithm can be used? Which object has to decide when and where paths should be found? In my current design I have a class for that, subclassed in all the various possibilities (including ignoring the path-finder all tegother) and it is called Entity.

That is a design issue at the core of my argument for breaking up Entity above. Exactly who that object should be I can't say because I don't know enough about your vision of the problem space. But I can say it very likely should not be Entity that decides whether paths should be found. That's because whether one needs a path is very likely to depend on external context that Entity shouldn't need to know about.

BTW, I would argue that your two questions represent quite different responsibilities. Whether they are different enough (or their resolution is complex enough) to warrant their being in different objects depends on the problem space. But I would almost certainly make them different responsibilities.

Now you have come full circle back to your [Strategy], right? Superficially Yes. But on closer inspection it is significantly different because the responsibilities of [PathFinder] are
/intrinsic/.


I am not sure how you intend the word 'intrinsic' to be interpreted here. Surely the only responsibility of [PathFinder] is finding paths and I would call that an intrinsic responsibility of a path-finding algorithm under any circumstances.

By intrinsic I mean that (A) the responsibility is uniquely ties to the object identity and (B) that the nature of the responsibility does not depend in any way on the overall problem solution's flow of control.

Caveat 1. As I indicated one abstracts responsibilities that are needed for the problem solution. In addition, one tailors the abstraction to suit the problem in hand. But that applies to What the responsibility is, not How it does it. Thus for a given set of inputs a behavior responsibility should still produce deterministic outputs regardless of the current context in the overall solution because...

Caveat 2. Behaviors need knowledge inputs and those inputs often reside in other objects, which is context external to the responsibility. However, those inputs are just state variables so they imply nothing about the sequence of behaviors that set them. That's why knowledge access is assumed to be synchronous in OOA/D; one gets state variable values as needed as inputs.

Now clearly finding paths is an intrinsic responsibility of [PathFinder]. But it may not be the only one. It may need to create a [Path] object, format the path information in a useable form, establish relationships so Entity can access the information, or any number of other activities related to managing paths in the problem context. IOW, there are several requirements and design issues we haven't touched on yet that might logically belong to (be intrinsic to) a PathFinder in the problem context.

That means you have to define the responsibility of [Entity] differently so that finding a path isn't implicitly included.


But Entity was never responsible for finding paths, even when it happens to actually find paths. A responsibility is an activity that one is compelled to do for the sake of others, just as one function does its task so that the whole software can work. You talk of path-
finding as it were impossible to move without finding a path, but I am thinking in a more strict sense.

In your original statement of what Entity did you included finding paths. In this context I was simply demonstrating how problem space delegation is used to separate out that responsibility and that such delegation would naturally lead to the Strategy pattern, but without the agent request forwarding.

So the point is that /after/ one delegates the responsibility that was originally in a monolithic Entity, it is no longer in that Entity.

However, those small differences suddenly make other issues you
are concerned about more manageable. What if you don't always
need to find a path? Easily handled by making the relationship
between [Entity] and [Pathfinder] conditional.


So the client needs to check if the relationship exists before navigating, and so it must deal with the possibility of needing path finding or not needing it separately.

That's one possibility. Another is that the same state variable used to determine whether the relationship should be instantiated might be used to determine which processing branch (path navigation or flying carpet) should be taken. Then the processing branch where a path was needed would use the relationship while the other branch would not.

In this case I suspect the second is more likely since there will be two distinct sets of processing involved and that is really the choice to be made. One could use the existence of an active relationship for that but I would prefer using the same state variable.

What if the need to find a path turns out to be dependent on
something outside [Entity]? No problem. Just have whoever
understands that context instantiate the relationship or not.


And what if path finding is needed on several occasions, such as finding a path to multiple points to discover which is closest? Then I presume that the client would need to be responsible for that also. Would the entity have an interface that allows the client to report the results, or would you recommend that the client simply be charged with making all the decisions for the entity to spare a complicated interface for Entity.

The decision here is whether one needs a path or not (i.e., whether PathFinder needs to be invoked or not). If not having a path depends on PathFinder discovering there is no suitable path, one has a different situation. But that would be identified in PathFinder's responsibility and PathFinder would then the one to decide whether Entity follows a predefined path or not.

Four key points here:

(1) Starting with a god object one gets to the Strategy pattern
with basic problem space abstraction even if one never read the
GoF book.

(2) The problem space-based delegation leads one to a solution
where the delegatee is naturally decoupled from the delegator.


I am sure it is simply a misunderstanding, but the decoupling does not seem so natural to me. The GoF wanted the client to be decoupled from the algorithm, but that is certainly not happening. You talk about delegating responsibilities from the context to the strategy, but it seems more like responsibilities are be delegated from the context to the client as nicely abstract operations are becoming strict and concrete.

I think it is quite natural in the problem space. Once one chooses the lower level of abstraction for the delegation, then one and only one identifiable entity in the problem space owns the responsibility. At the higher level of abstraction we have a single identifiable entity:

[Car]
+ horsepower
+ color

After lowering the level of abstraction we have two identifiable entities:

[Car]
+ color

[DriveTrain]
+ horsepower

Now only a DriveTrain owns horsepower. If one has decomposed Car to produce DriveTrain, why would Car still have DriveTrain's properties? I can't imagine a situation where a domain expert like an auto assembly line manager would think of Car owning horsepower when there are a bunch for drive trains on the line.


In the Strategy pattern as the GoF wrote it, the client has no knowledge of how, when, or why the algorithm is used. I am putting effort into seeing how breaking down that decoupling is making anything better, but I have yet to find any rewards.

Actually, in the GoF pattern the Context object does understand the semantics of all of those things, at least at the level of specification. That's the core problem of the agent request forwarding approach. That's because a client who needs the responsibility talks only to [Context]. Therefore the [Context] responsibility specification must implicitly capture all the requirements that the Client expects to be satisfied.

Note that the entire client/service metaphor of DbC is based upon the fact that an object's responsibilities exist to satisfy requirements defined externally. That is, the client of the responsibility defines the requirements that the service responsibility is obligated to provide. Thus the /obligation/ implicit in a responsibility refers to doing what the problem solution -- manifested in the abstract notion of an arbitrary "client" -- needs to have done in any given collaboration context. So if a Client talks to a Context directly, then the [Context] responsibility specification must accurately reflect all of the requirements defined by the solution in that collaboration context.

(3) Collaboration is constructed around the objects and
responsibilities one got from problem space abstraction, not some
preconceived notion of a sequence of operations one needs to solve
the problem. IOW, get the abstractions right and the flow of
control will take care of itself.


From my problem space I get that the entity is responsible for making its own movement decisions. That is how creatures work in real life. There is no clear path-finder object in the problem space. I agree that separating the algorithm into its own objects greatly increase the flexibility and simplicity of the Entity class, but any interactions between the client and the path-finder are completely inappropriate from a domain perspective.

What you seem to be saying is that there is not such thing as a 'concept' in your problem space. I don't buy that. The PathFinder is a conceptual entity that is going to exist in any problem space where one has to select paths. That's because the notion of "selecting" is already conceptual. Note that the need to extract such conceptual entities from the problem space is especially important to OO abstraction because we need to anthropomorphize.

(4) The decoupling in (2) allows one to easily handle a wide
variety of variations on what one needs to do with the delegation.
IOW, get the basic problem space abstractions right and one can
easily deal with variations.


I am sure you are right that it is relatively easy to arrange for variations by manipulating a complex system of peer objects with clearly divided responsibilities, but when there are no such objects in the domain, when the responsibilities all rest on a single domain object, then I am not sure that theses strictly defined responsibility boundaries will not cause difficulty with any slight shift in the domain.


You have a notion of 'Entity' some of whose actions are determined based upon applying rules and policies that are related to context outside Entity. Those rules and policies are not intrinsic to Entity because they involve context beyond entity, particularly dynamic run-time game context. You also have a need to simplify Entity in order to better manage complexity and ensure better maintainability of the software. So you have to find some entity in the problem space that would logically own those responsibilities. One technique is to reduce the level of abstraction with which you view the 'Entity' to decompose it via delegation. Another is to make use of concepts in the problem domain.

But somehow you are going to have to abstract more problem space entities from the problem space. You don't have a choice about that. You have already identified a highly abstract and conceptual entity as Entity. Just look for some more at a lower level of abstraction.

[One can also argue that you are not limited to a single problem space. Clearly the UI and persistence are different problem spaces than the game's fantasy realm. One could postulate relevant domains based upon mathematics, physics, or logic that are relevant to how your fantasy realm maps to player and computing reality. IOW, there is potentially an enormously rich suite of conceptual domains from which you can abstract objects.]


That is especially troubling when it has not yet been made clear why the obvious approach suggested by the GoF is harmful.

I don't think what is harmful about the GoF approach has anything to do with the abstractions. It is only related to how a client navigates to a service -- given that one has already provided abstractions and relationships for the delegation.

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.

Surely you do not mean that you have never used any algorithm
that was not required, where your current bit of code had no responsibility for the task the algorithm performed. That
suggests that it is bad OOA/D to sort a list before removing
duplicates. I hope that it is not bad OOA/D to look ahead and
plan a path even though all that is technically required is just
a single step.

That's not what I am saying. I am saying one solves the problem
in hand and only the problem in hand. By implication I am also
saying that the requirements need to be unambiguously defined.

So I am saying that if there is no requirement to remove
duplicates, then one does not have a responsibility to remove
duplicates, so one should not implement code to remove duplicates
before sorting.


I regret that it seems you have gotten my example backwards and perhaps missed the point as well. I would normally abandon it and choose another example, but in this case I think the example would be highly effective if used correctly. Therefore, I will put more effort and detail into it now.

If you have ever implemented a function to remove duplicates from a list, you will surely have encountered this issue. When a list is unsorted, finding and removing duplicates tends to take O(n^2) time, where n is the length of the list. However, if the list is sorted, then it can be done in no more than O(n) time.

Let us call a duplicate removing function 'removeDups()' where removeDups(myList) is a list containing all the elements of myList but with each element appearing at most once. I hope you will agree with that as the requirements for removeDups().

One thing that is not amoung the requirements of removeDups() is that removeDups() must sort the list. In fact, it is entirely possible to implement it without sorting the list. removeDups() would fulfill its purpose equally perfectly with or without sorting.

I can predict from your words two possible opinions about removeDups. The first is that since sorting the list is not required for removeDups to behave appropriately, removeDups should not sort the list. Just as you said: "If there is no requirement for finding a path, then there is no reason to have a path finding algorithm." So since there is no requirement for sorting, we have no reason to have a sorting algorithm.

First note that sorting is already a given in your original example specification.

You are making a very big assumption here that is not warranted by the requirements: that removeDups is invoked /before/ the list is sorted. What we need to do is focus on what the requirements are. In this case the implied requirements are that duplicates are possible (in the input) and are not allowed (in the output).

The most efficient way to meet those requirements is to sort the list with duplicates and then invoke removeDups to eliminate any that might be there. But that is just a specific implementation vs. nonfunctional requirements (performance) that has nothing to do with the basic case of sorting AND any removing duplicates.

However, I think this is all beside the point. The issue is whether I need to provide code for dealing with duplicates. If the requirements indicate there will never be duplicates in the input list, I don't need to provide any sort of code for duplicate removal. If duplicates are possible but don't have to be removed, I still don't need code to remove duplicates but I do need code to ensure duplicates are sorted properly

The real point here is that if the requirements do not specify which of the three situations prevail, those requirements are incomplete and I cannot specify what sort() does properly.

But not precluding duplicates does not imply that
there will be duplicates. So the requirements need to be quite
specific about the possible situations:

(1) duplicates are possible, but not allowed
(2) duplicates are possible and allowed
(3) duplicates are not possible.


When you chose to make the with or without duplicates as the part of the function at issue, you took the easy way out because the presence or absence of duplicates is clearly a part of the output of the function and therefore likely should be part of the requirements. However, when sorting may or may not be part of the implementation, there is no such easy answer, since the sorting is entirely internal to the function.

In your example sorting was a given. The issue was whether code also needed to be included to deal with removing duplicates. My response was that the requirements determine whether such code is needed. The requirements /must/ specify which of these three cases prevails before that decision can be made.

[Technically, the requirements need to specify more than that. But let's not go there.]

Similarly, if Entity has a responsibility to move from A to B and
that may require finding a path, then the definition of the
responsibility must identify the circumstances when a path must be
found AND the circumstances governing the choice of algorithm if a
path must be found. Now those choices may be based solely on the
internal state of Entity, but they need to be specified before the
responsibility is implemented because they reflect problem space
rules and policies that the responsibility must implement.


I can accept that I must choose the specifics of when and what algorithm before I can implement the entity, but I find it very counter-intuitive that such decisions have anything to do with the problem space rules. The problem space only cares about where entities move, it cannot possibly care how or why the entities chose to move that way.

There is a decision to be made when deciding whether finding a path is necessary. To make that decision there are decision variables and some set of rules for evaluating them. In this case those rules are necessarily part of how the basic game works. As such they are game requirements.

They may be trivial (e.g., a path was already found that can be reused) or enormously complex (e.g., a complex analysis of terrain, line of sight distance, fog of war, etc.). But they are requirements that define how the game works.

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


In my case that is certainly not the entity's responsibility. The
entity has absolutely no responsibility that looks anything like finding a path. The entity is only responsible for making the
best move that it can to challenge the player. In other words,
the entity itself chooses B and it chooses the path, or it does
not choose at all and moves randomly because that is harder for
the player to predict, or it does something else.

All you are doing is abstracting the responsibility at an even
higher level of abstraction. However, the level of abstraction
should be determined by what needs to be done to solve the
problem. Requirements defined at a higher level than that are
inherently ambiguous. To actually do the move a path may have to
be found and, if so, the correct algorithm must be selected and
the Entity must follow that path. Those are inherent requirements
in the fantasy realm problem space that your responsibility must
honor.


I wonder if you are confusing practical requirements with theoretical requirements. Theoretically the entity might choose its path by reading astrology predictions over the internet which will tell it where it should move. Practically it will need a path-finding algorithm.

I don't care how it is done. That is just another set of game rules. But as you have described movement, YOU have already defined much more detailed invariant processing than some abstract notion of 'move'. Whatever that processing is and how any decisions are made depend upon how your game works. Those requirements need to be worked out before you can design, much less code.

You cannot have ambiguous requirements at the level of abstraction that the game works. Since you are defining how the game works, it is you who has to do that specification of requirements.

Note that you can make a requirement that says you should be able to add arbitrary properties to an Entity in the game context, such as skills. But when you do so you will also need to carry through on the implications, such as how such added properties interact with other game entities in all game contexts. In doing that I would bet you will find that added properties can't be truly arbitrary and there will need to be some rules about what kind of properties can be added. IOW, for things to play together there will be constraints all over the place that limit how the game realm works.

A resolution of those requirements cannot be implemented until
you: (A) define exactly when finding a path is necessary and when
it isn't; (B) define how the right algorithm is selected if
finding a path is required; and (C) what the entity does to move
from point A to point B (which will probably depend on whether it
is following a path or not). You can't write a line of code until
you have unambiguous specifications for those things. The only
place those requirements are going to be defined is in the
specification of the responsibility.


Here is where I took that quote which I used above. I think I know what you mean, but I would like to point something out: With the Strategy from the GoF I cannot write a line of code for the Context until those specifics are decided. With the technique you are advocating, I cannot write a line of code of the client or the context until those specifics are decided. It seems clear to me which is the preferable situation.

Au contraire. In my version [Context] has nothing to do with the strategy responsibilities except as a placeholder on the navigation bath between Client and Strategy. So you don't need to know anything about the strategy semantics to write [Context].

Similarly, I can write the Client code completely because in OOA/D, all it is doing is sending an announcement message, which doesn't change.

In either case I agree one cannot write a line of [Strategy] without understanding the requirements. In addition, I cannot identify the collaboration (i.e., Client's announcement should go to Strategy) until I know what Strategy does.

The entity encapsulates that sort of detail from the client
because making the client hold the path and direct the entity
along it would be horrible micro-management. It is especially
important to avoid that kind of coupling because many entities
would not follow any sort of path.

It's not micromanagement; it is decoupling and separation of
concerns. Finding a path is a quite different thing than following
a path. In the OO paradigm one isolates concerns and encapsulates
them so that one can deal with them in a controlled fashion within
limited scope. One then decouples the solutions so that they are
largely independent mini solutions.


I am not so sure that finding a path and choosing a move can be separated. Of course it is possible to ask the client to regulate the finding a path and give the path to the entity so that the entity does not have to collaborate with the path-finder, but what if I discover during implementation that I need to find a second path to make the decision? Using the GoF Strategy I would simply make another call to the path finder as needed. With the peer-to-peer strategy, I would need a serious redesign that would involve the client and introduce new relationships between the entity and the path finder.

For the first sentence, maybe I've been at this too long but it seems axiomatic that they can be separated because I've done it a gazillion times. The collaborations (communications) necessary for them to play together are usually easy to work out once they have been semantically isolated and encapsulated.

One quick test. Visualize the code for moving along a predefined path. Visualize the code for identifying alternative paths and selecting between them. Could you reuse one code block for the other purpose? I think not. The first is follow-the-dots down the yellow brick road while the second is some fancy operations research algorithm. When the implementation approaches are that different the subject matters are different.

As far as the collaborations are concerned, what do you mean by, "during the implementation"? If you mean implementing the path-walking algorithm, I submit you should have realized that as soon as you specified what path-walking needs to do. How can you specify the preconditions (i.e., two paths might be needed) if you don't know that?

Choosing where to move and finding paths are so tightly connected in the domain that I worry that putting walls between them will only force me to find more complicated routes to solving the problem.

The goal is to make different concerns less tightly connected so that the application is more maintainable. The walls are certainly there as one separates concerns, but they exist to provide decoupling.

The processing will be the same or simpler, though. Separation of concerns tends to promote thinking in terms of encoding invariants while dealing with details in parametric data. That can greatly simplify the executable code. In addition, such separation often allows one to captures rules and policies in static structure so one does not have to think about them on every collaboration or provide executable code to enforce them.

OTOH, OO programs do tend to be bigger because of all that static structure. [They also tend to be slower if done in an OOPL because of all the behind-the-scenes work to support higher level abstractions likes classes. Hiding that grunt work comes at the prices of overhead to support generality.] But if all we wanted to do was write compact, elegant programs very quickly, we would all be doing functional programming. B-)

BTW, the collaboration between Client and PathFinder is pretty
minimal. Basically it is something like:

Client: Hey, Pathfinder, Entity needs a path between A and B.

PathFinder: Hey, Entity, I've got a path for you to follow.

Entity: Thanks. I'm off to follow it. I'll let anyone who cares
know when I get there.

Thus Client doesn't need to know anything about the path.


The client does not need to know anything about how the path is represented, but the client needs to know everything about how and when the entity needs a path. Unless of course we use the Strategy pattern as the GoF intended, in which case the client is decoupled from such details.

Work backwards from Entity in a DbC fashion:

Precondition to execute Entity.move: there must be a path to follow

Precondition to execute PathFinder.findPath: it is time for Entity to move

So what does Client really need to know? Just that it did something that someone else cares about. The developer connected the dots knowing that what Client did ultimately means it is time for Entity to move. But the developer also knows that for Entity to move, a path must be available to follow. Hence the developer daisy-chains the messages by addressing them to the right place ("Hey, Xxxx").

Does Client know a path will be found? No. Does Client know that Entity will move? No. Does Pathfinder know why a path is needed? No. Does Pathfinder know what Entity will do with a path? No. IOW, nobody knows anything about the context as they do their own thing. The developer connected the semantic dots in the overall solution by addressing the announcement messages properly.

I think you should be really sure you understand this collaboration scenario and it implications because it demonstrates why decoupling, encapsulation, self-containment, logical indivisibility, and peer-to-peer collaboration are so important to maintainability. If one does things this way there are no "hard-wired" solution sequences and no dependencies on what other methods do.

In particular, note that Client does not tell Entity to move even though whatever it did modified the state of the solution in a manner that triggers Entity to move. This provides an insight into peer-to-peer collaboration that I haven't mentioned before. There is a subtle difference between the notion of 'client' in the generic sense of some external source of DbC requirements and the notion of 'client' in a specific collaboration. [One of these days I am going to have to come up with a synonym for one of the uses so they can be readily distinguished in discussions like this.]

When we think of what a responsibility is during problem space abstraction, we do that in terms of an imaginary generic client that defines what the requirements are that the responsibility is obligated to resolve. That "client" is a surrogate for the overall problem requirements that we must resolve. The notion of a "client" just provides a convenient mapping to the subset of requirements resolved by the responsibility.

The client in a collaboration, though, is simply the one who satisfies the precondition for executing whatever needs to be done next in the solution. That client doesn't care what happens and was probably not remotely related to defining the requirements on what needs to happen. IOW, the client in a collaboration is merely raises a condition that triggers an action.

So peer-to-peer collaboration comes into play by ensuring that the trigger talks directly to whoever is being triggered. There are specific reasons why this is important in concurrent processing, but the general reason is because we want to ensure direct causality between the condition where the precondition for executing prevails and the method that actually executes.

So if Client in the example raises the condition for Entity to move, why isn't it talking directly to Entity? The answer lies in the chain of preconditions. There are actually two preconditions for Entity.move. One is that the condition in Client prevails. But the other is that a path be available. In this case Client provides just one clause of a compound precondition. So it is up to the developer to define the minimum chain of causality between the various conditions and Entity.

Note that lacking other requirements, PathFinder could have been invoked first, then Client, and finally Entity. One would still have a minimal chain of causality insofar as Entity was concerned. One could accommodate that be reorganizing the messages without changing anything in object semantics. In point of fact, though, it is highly unlikely that finding a path would be a prerequisite to whatever Client did that happened to change the state of the application to trigger an Entity move.

That can work in many situations, but I think this particular situation is a very bad example. The only reason that a Context
would want a sorting algorithm would be for some greater task,
such as removing duplicates from a list, or looking up items in a
list by sorted key.

I agree because a sort() behavior would likely be a fundamental operation of a collection that depended very strongly on the
internal implementation of the list. In fact, it would normally
be implemented with realized code from a library so the sort()
behavior would just select which library routine to invoke and one
wouldn't need Strategy at all. I was just trying to describe the
responsibility semantics of the delegation with as simple an
example as possible.


Assume that you were using strategy, since it is not entirely impractical to use it in that case. There are many sorting algorithms of all different kinds, so it would be appropriate to use the Strategy pattern if the choice of sorting algorithm were important at run time.

Actually, it is impractical. The sort algorithms, however many there may be, exist beyond the domain of the problem in hand. In OO applications we only address the rules and policies that are unique to the problem space. Any code that is defined in terms of well-known mathematical algorithms would /always/ be realized code beyond the scope of the OO solution. That would be true even if one had to encode them for the application in hand. That's because the requirements aren't going to change for such algorithms, so one doesn't need OO.

[Been there, done that. I've worked with OO applications where as much as 60% of the executable code was realized outside the OOA/D. In one case I had to encode an entire linear programming package because we didn't have one available that could be licensed to our customers on a run-time basis. Buy I did that in FORTRAN while the application itself was done in C++. It was encapsulated in a single C++ method call.]

Using Strategy as the GoF intended is trivial. Just instantiate the relationship when the Context is created so that it has the appropriate sorting algorithm for whatever will be stored in it, then the Context will sort as it needs to in its implementation.

Using Strategy as you suggest would be much more difficult. You would ask the client to invoke the sort, meaning that the client must know when sorting is needed, and therefore the client knows the details of the collection's implementation.

If I follow the GoF, I have a client that is dependent upon the interface of the Context and a Context that is dependent upon the interface of the algorithm. There are no other dependencies.

If I try to do peer-to-peer, then my client is dependent upon the interface of the algorithm, and the interface of the Context, and the implementation of the Context. It is true that I have gained an independence of the Context from the algorithm, but why do I want that? All I ever wanted to do was sort a list to make searching it more efficient. I would rather search it unsorted than go to all the trouble it seems I would have to in a peer-to-peer design.

The Client invokes Strategy.sort() just like Context would. The relationship, which needs to be instantiated properly in either case, takes care of the polymorphic dispatch substitution.

The only difference is that in my case Client invokes Strategy.sort() directly without any sort() property on [Context] while in the GoF implementation Client invokes Context.sort(). IOW the only difference lies in whether [Context] duplicates the [Strategy] interface.

Now suppose the requirements change and Strategy.sort can now do both ascending and descending sorts. So we need a new interface: Strategy.sort (direction). [Client] needs to change because only it knows which direction the sort should be done. In my approach [Context] does not need to change but in the GoF implementation it would.

No matter how many different examples we use it always comes back to the same thing. If [Context] acts as an agent providing request forwarding, [Context] will have to change whenever the requirements on the forwarded request change. It doesn't matter what else [Context] does or who instantiates the relationship or how many subclasses there are or how the relationship is instantiated. It is about the dependency where the responsibility requirements on Strategy.whatever() are duplicated on Context.whatever().

IOW, for whatever nontrivial example you want to cite where [Context] acts as an agent I can /always/ come up with a requirements change that will force [Context] to be modified. However, if the Client talks to directly to the Strategy service, [Context] never needs to be modified for that same change.

global pointer to int i;

int length(pointer to int list) {
Take a pointer to an int and treat it as a null-terminated list
of ints. It returns the length and modifies i to point to the
last element of the list. }

pointer to int find(int target, pointer to int list) {
int size = length(list);
Treat the list as sorted and search for target in the list by
using the length of the list and modifying i during the search.
At the end i must point to the target value or else be null.
return i; }

I am not suggesting that the pseudo code should actually be used,
it is merely to point out that the specification of length() is
not and cannot be a subset of the specification of find(), because
the specification of length() would include 'i' pointing to the
end of the list, but the specification of find() cannot include
that. By definition, one is not a superset of the other.

'find' has a specification that is a superset of the specification
of 'length'. The 'find' behavior will not work correctly for its specification unless 'length' works correctly for its
specification. That means the 'find' specification necessarily
includes the specification of 'length'. Nor can one unit test
'find' without a correct implementation of 'length'.


I think I see where the problem is coming from. There is a terminology confusion over 'specification'. The specification of a function is a mapping from inputs to results, or preconditions to postconditions. A specification of a 'factorial()' function could be written simply as 'factorial(x) = x!', assuming that the meaning of 'x!' is understood. The specification does not change if factorial happens to use a function called Y, or not.

You can specify it that way so long as everyone is agreed about what "!" means. That is the problem here. When you refer to "length of the list" in your 'find' specification you have to have a particular notion of what that means. That semantics just happens to be the specification of what value 'length' should return for a given array of the sort that 'find' operates upon.

Now you can argue that your specification of 'find' is really something vague like "find an element in a list" that doesn't explicitly depend on 'length' or even the notion of "length of the list". I submit such a specification is too vague to implement unless one agrees on what 'find', 'element', and 'list' mean. But to define what those terms mean you get into issues like whether the list is terminated by a null because you need to be precise about how the search is terminated. So it still comes back to the specification of 'find' implicitly including the specification of 'length'.


That is how I use the word 'specification'. You are using the word 'specification' to have the same meaning as 'implementation' would have for me. 'length' and it's specification play no part in the specification of 'find' in my usage of the word. I did not give a specification of 'find', I only gave a pseudo code implementation.

No, I don't think so. I agree both your specifications were clearly polluted with implementation. But what I am talking about is how one would specify what the 'length' function does in an implementation-independent manner. For example, "'length' returns a count of the number of contiguous non-zero integers starting at the beginning of the list." [Even that has problems because of notions like "beginning". But the specific definition doesn't matter. All that matters is that 'length' must have one.]

The point is that whatever the implementation-independent specification of 'length' is, that specification will be identical to the specification of what "length of the list" means in your specification of 'find' (or what 'list' means in a more abstract specification of 'find').

The key phrase in the 'find' specification is "by using the length
of the list". IOW, the "length of the list" must be correct in
some unambiguously defined manner. The specification of that
correctness is the same as the specification of correctness for
'length'. (In effect, 'find' is defining requirements of 'length'
by its notion of what "length of the list" means.) Therefore the
specification of 'find' includes the specification of 'length'.

In effect, you use the phrase "by using the length of the list" as
a shorthand so you don't have to repeat the specification that you
provided for 'length'.


I was not writing the specification for find(), I was writing the implementation (though a bit vaguely). The specification of find() has nothing at all to do with the length of the list, it is strictly about locating a certain element within the list.

Then I don't understand the point of your example. We are talking about the /specification/ of higher level nodes in a functional decomposition depending on the /specifications/ of the lower level nodes in a direct line of descent. My whole argument about Spaghetti Code resulting from reuse in functional decomposition is about maintaining consistency in the specifications.

Each lower level node is an extension of the higher level node since it decomposes some aspect of the higher level node's specification, so there must be a chain of causality among the specifications.

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


That does make sense as a potential source of maintenance
trouble, though I do not think it as serious as you do. When you
break a function or a method, that will always have effects that
cause the entire system to stop working, whether we are using OOD
or functional decomposition.

It had enormous implications for maintenance in the two ways I described. It was difficult to evaluate how a change in a low
level function would affect clients.


It is easy if the function is properly documented with a clear purpose and requirements. If you have that, then you know that you will introduce bugs if and only if you violate the requirements. So long as factorial(x) computes 'x!', you can change it as wildly as you desire. You can call any function and perform any task,

When a change is made to a low level function in the decomposition, that change affects /every/ higher level function for which it is a descendent. That's because changing the specification of a lower level node changes the specification of every higher level node from which it descends. Therefore any client of any such higher level function could be broken. Therefore the maintainer must inspect every client of every higher level node from which the lower level node descends.

The proof is in the pudding. Modify your 'length' function so it always returns 2. I don't care how you specify 'find', that function will no longer produce correct results according to its specification in all cases. That's because the implementation of 'find' that resolves its specification depends upon a specification for what 'length' does that is consistent with (a subset of) its own specification.

Essentially the maintainer had to "walk" up the tree from the point
of the proposed change and check if every client for every node in
the line of ascent was broken.


If that were actually necessary, then something far more serious was already broken, either the maintainer, the documentation, or the function that got changed was used in an inappropriate manner somewhere, such as if getSalary(employee) were being used to find the age of the employee. It works fine when the two number happen to be the same, but it will inevitably break in maintenance.

That was /always/ the case when a lower level function was modified. That's why it was known as Spaghetti Code!

The Strategy pattern only uses one layer of functional
decomposition. There is only one dependency created for each
instance of the pattern, not a deep lattice of complex
dependencies. Surely the benefits of the Strategy pattern
outweigh the maintenance risks.

But there are no hierarchical dependencies if the collaboration is
peer-to-peer. The fix can generally be made in one place without breaking other methods because there is no chain of dependencies. In the example, one can provide GetDBInfoNew that extracts the
right value of salary from the DB. Then one can call that version
directly in the context where the value is needed.

Note that in a well formed OO application, we would have something
like

[Benefit]
- ComputeAfterTaxIncome()
A
|
+------------+---------------+
| | |
[Benefit1] [Benefit2] [Benefit3]

where ComputeAfterTaxIncome would be a private method. Initially
it would have a single implementation but after the change each
subclass would have its own implementation -- all isolated to the
definition of [Benefit] semantics. That implementation would call
the right version of GetDBInfo.


Yes, I can see that OOD is better in this case. You surely do not have to convince me that OOD is usually better. In fact, it is OOD that allows me to use the Strategy pattern at all, even though it is frowned upon for being contaminated with functional decomposition.

But all that really matters is the question: Is functional decomposition always disastrous, or can it be beneficial in certain specialized situations? You say that the issue is minor when there are only two layers. That is the case in the Strategy pattern, so I will not worry about more than that.

There is nothing wrong with functional decomposition. Spaghetti code only occurred when functional decomposition was combined with high level node reuse to eliminate redundancy. Of course without that reuse one then had the problem of multiple edits when a change really did affect multiple contexts where limbs were duplicated.

The OO paradigm solves both problems by eliminating the hierarchical dependencies.


I hope I have clearly explained how it is possible to work with functional decomposition without a nightmare of spaghetti code (including what I wrote below). Since it is possible, perhaps the benefits of GoF-style Strategy might outweigh the costs.

I'm afraid not. I spent the first two decades of my career dealing with the spaghetti code resulting from hierarchical specification dependencies. It was a nightmare precisely for the reasons I've explained.

A related perspective is that OO responsibilities are defined as /intrinsic/ properties of the underlying entity. That isolation
from solution context makes it a whole lot easier to think about
what a responsibility needs to do.


You can get that just as well by designing the functional decomposition carefully. Choose the responsibility of each function and then stick to it like glue. As long as you can keep yourself from trying to change the responsibilities afterwards, you will have all the same benefits of isolation from the solution context. Your function just needs to play its own well-defined part.

Only if you allow duplication of limbs, which has its own set of problems.

The context-independence also helps ensure specification
consistency because the specification is not linked to context.


It is as if you were describing functional decomposition. Once a function's purpose is defined, it can be used wherever that particular task is needed. The purpose of a function is not linked to context either, so long as it is always used with the same purpose in every context.

Only so long as it is self-contained and its specification does not depend hierarchically on other functions.

If you move down the lattice to make something which you might
call a 'repair' in some place other than where the
responsibilities lie, then your repair is actually just breaking
things. What you are describing is not the way to deal with
maintenance in a functional decomposition design. I will try to
explain in more detail below.

Again, that is the point. The requirements on the higher level
node are not serviced in a single function. Instead, they could
be serviced anywhere in the descending tree. Every function in
the descending tree is an extension of the high level function. Corollary: every function in the descending tree resolves some
unique requirement in the overall set of requirements for the
higher level function.


The requirements on a higher level function are not and must not be described in terms of the lower level functions; you have made that very clear by all the problems you have illustrated coming from that. Fortunately, that is not an fundamental part of functional decomposition. I think it is not any part of good functional decomposition.

They ALWAYS are in a functional decomposition. That is what functional decomposition is, by definition. Each descendent lower level function resolves some subset of requirements on the higher level function. Conversely the specification of the higher level function is the union of every descending function's specification.

That's why it is called 'decomposition'. One decomposes the higher level function's requirements into subsets that are resolved in the lower level functions. That is how the tree was constructed in the first place.

The problem is not that the maintainer did some walking to find
the the right place to make the change, the problem is that he had
the right place to make the change, Benefit3, and then kept on
walking so that he could make the change in a place that breaks
everything.

But the only place where the change can be made is in GetDBInfo --
that's where the wrong (from the new Benefit3 requirement's
perspective) value of "salary" is read. That's because that is
the way the original functional decomposition was structured. The
maintainer doesn't have any choice unless he wants to completely
reorganize the tree or duplicate the entire limb.


Remember that you were the one that pointed out that this is a lattice, not a tree. The concept of limbs seems to be confusing this issue, because it sounds as if all the functions used by Benefit3 (directly or indirectly) would have to be duplicated, but that is not even close to true. All that is required is that new nodes have to be put into the lattice with the correct connections to pre-existing nodes. Each node is a small thing and you only need one for every level between Benefit3 and the DB.

The tree for Benefit3 is:

[Benefit3]
|
|
[ComputeAfterTaxIncome]
/ \
/ \
/ \
[GetDBInfo] [Compute]

When the requirements change for [Benefit3] the tree needs to be modified in GetDBInfo. That is trivial to diagnose and repair. The problem arises because of reuse. The lattice comes in because the limbs are reused:

[Benefit1]
|
|
[ComputeAfterTaxIncome]
/ \
/ \
/ \
[GetDBInfo] [Compute]

[Benefit2]
|
|
[ComputeAfterTaxIncome]
/ \
/ \
/ \
[GetDBInfo] [Compute]

Now changing GetDBInfo breaks [Benefit1] and [Benefit2], which is what the point about identifying changes above comes in. One has to "walk" up the tree and examine every client of every higher level node to find that [Benefit1] and [Benefit2] are now broken.

If the maintainer is lucky enough to identify that problem, by your approach one has to provide a new limb that is not shared by [Benefit1] or [Benefit2]:

[Benefit3]
|
|
[NewComputeAfterTaxIncome]
/ \
/ \
/ \
[NewGetDBInfo] [Compute]

That might not be too painful in this simple example but it will lead to major redundancy if [GetDBInfo] is several levels lower than [ComputeAfterTaxIncome] in the decomposition tree.

If the domain changes so that the benefit represented by Benefit3
needs to change, then the function Benefit3 is the one to make
the modifications in. No function that Benefit3 calls is
responsible for that specific benefit: one you find the function
responsible for the behaviour you want to change, you must stop
walking. That simple rule seems to eliminate all of the spaghetti
code problems.

Tell me how you make the change in Benefit3 when Compute uses the
value from GetDBInfo. Show me the code in Benefit3 with the
change that doesn't require changes to ComputeAfterTaxIncome,
Compute, or GetDBInfo.


I have no example code for how Benefit3 might work, but from what I know it would probably look something like this:

Benefit3 ()
double incomeFromFullyBurdenedSalary, benefit;
incomeFromFullyBurdenedSalary = ComputeAfterTaxIncomeNew();
benefit = ComputeBenefit3(incomeFromFullyBurdenedSalary);
return benefit;

And of course if ComputeAfterTaxIncomeNew() does not already exist then it must be written. Especially if bottom-up design is used, there is a good chance that it does already exist, but even if it does not, thanks to functional code reuse, there will be little involved in its definition.

That's my point. You also have to provide a NewGetDBInfo as well because that is where the change actually needs to be made. IOW, you need to duplicate the limb. But in so doing you duplicate ComputeAfterTaxIncome, which is identical to the old one except for replacing the GetDBInfo call with NewGetDBInfo.

There is no way to simply change Benefit3 because it has delegated the resolution of /its/ requirements to ComputeAfterTaxIncome and GetDBInfo. So you have to make the change where the affected requirement (what flavor of "salary" must be used) is actually implemented in GetDBInfo. But because of the chain of specification dependencies, that means you have to duplicate the entire intervening limb, which leads to redundancy because those nodes deal with different requirements.

BTW, apropos of the point above, note that the problem is in GetDBInfo, but one recognizes it because Benefit3 no longer produces a correct result. If Benefit3 fails against its specification even though one doesn't have to touch its implementation to make things right, that just demonstrates that the specification of GetDBInfo is derived from that specification.

[BTW I find this (making a change in Benefit3) inconsistent with
your statement below that my second example of changing
ComputeAfterTaxIncome and GetDBInfo is the correct way to do it.]


Perhaps I misunderstood your example. To me it seemed that you were not changing ComputeAfterTaxIncome and GetDBInfo at all and since you should not be, I presumed that you were correct. I thought that ComputeAfterTaxIncomeNew() was a new function, not a replacement for ComputeAfterTaxIncome(). If you actually change ComputeAfterTaxIncome, you would be breaking your software, which is why it is a bad idea.

No, I provided both a new ComputeAfterTaxIncome and a new GetDBInfo in the example you said was the right way to do it.

I suspect you are getting hung up on how the specification is documented. In a functional decomposition one documents the specification at each point in the tree _where the requirement is resolved_. That's because, by definition, every descending
function is an extension of the higher level function. Thus the
specification of the higher level function is a union of the lower
level specifications. So it would be redundant (not to mention
insufferably tedious) to duplicate the documentation of the
specifications at every ascending level of the limb.


I cannot imagine the duplication you are describing here. The specification of a function is not shared by any function it calls. You seem to be suggesting that the text of the specification of getDBInfo would have a place in the text of the specification of Benefit3, just because one is indirectly called from the other.

It would. Have you ever looked at 2167B documentation? Or formal methods specifications? B-)

Typically top down developers had no stomach for the necessary redundancy so they only specified detailed requirements in the decomposition nodes where the requirements were actually resolved. That left higher level node specifications like "perform the benefit computation" with near zero information content.

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.


Just to be clear, the above is the correct way to do it.

And it would be a disaster if the limb had several levels between ComputeAfterTaxIncome() and GetDBInfo(). You would have to
duplicate the entire limb down to the actual GetDBInfo() call.


But you do not have to duplicate anything that is shared, which means that in practice you will be duplicating nothing. The things which you are thinking of duplicating are those things which change, and since they are changing one cannot properly call it duplication. You create a new version of ComputeAfterTaxIncome which finds a different value by changing the DB access. All the work of doing the computation is not duplicated, it is still in the 'Compute(salary)' function. It is not duplicated because it does not change, and if it were to change, then I would hardly call that duplication either.

ComputeAfterTaxIncome had to be duplicated here because it had to call a different GetDBInfo than the original. Whoever calls NewGetDBInfo has a new call in it, so one needs a new function. But then whoever calls that new function in the [Benefit3] limb needs to have a different call. So it needs to be a new function. And so on all the way back up to [Benefit3]. The entire limb needs to be duplicated even though no other functionality in the limb has changed; the duplication is solely because the functional decomposition call stack has changed, which is "hard-wired" into each function's implementation.


*************
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: Attaching a behavior only to multiple classes.
    ... > so there are 2 levels of abstraction in the example, we may be agreeing, my ... It is about abstracting the problem space. ... entity then that entity owns the responsibility. ... is explicitly abstracted as a peer of [Context]. ...
    (comp.object)
  • Re: Abstract public member variales?
    ... To be a polymorphic dispatch it must be possible for the different implementations to be substituted transparently for the client depending on context. ... saved to the appropriate deconstructor. ... The lookup table is logically a responsibility of so whoever is assigning the reference doesn't need to know how the mapping is implemented. ...
    (comp.object)
  • Re: Attaching a behavior only to multiple classes.
    ... >> Each is logically indivisible at its level of abstraction. ... If the responsibility belongs to ... of the constituent elements for a particular application context. ... it isn't delegation per se. ...
    (comp.object)
  • Re: Abstract public member variales?
    ... I can easily write the client without the burden ... You are creating preconditions by using the peer-to-peer design. ... I don't think defining the requirements on a responsibility has anything to do with peer-to-peer access. ... But the level of abstraction for doing that is determined by the level of abstraction of the subsystem. ...
    (comp.object)
  • Re: Abstract public member variales?
    ... compared to even the slightest modification to the client. ... For that reason I regard language-based information hiding mechanisms as more of a message to future maintainers that says, "I had a reason for restricting access in this context. ... What knowledge the behavior needs is driven by what the behavior does, which is defined by that behavior's responsibility specification. ... If I change the property of a gnome's height from 4 feet tall to ...
    (comp.object)