From mira@ccs.neu.edu Wed Nov 12 12:45:54 1997 Received: from taboor.ccs.neu.edu (mira@taboor.ccs.neu.edu [129.10.112.115]) by amber.ccs.neu.edu (8.8.6/8.7.3) with ESMTP id MAA25086 for ; Wed, 12 Nov 1997 12:39:53 -0500 (EST) From: Mira Mezini Received: (mira@localhost) by taboor.ccs.neu.edu (8.8.6/8.6.4) id MAA14269 for lieber; Wed, 12 Nov 1997 12:39:52 -0500 (EST) Message-Id: <199711121739.MAA14269@taboor.ccs.neu.edu> Subject: examples To: lieber@ccs.neu.edu (Karl Lieberherr) Date: Wed, 12 Nov 1997 12:39:52 -0500 (EST) X-Mailer: ELM [version 2.4 PL23beta2] MIME-Version: 1.0 Content-Type: text/plain; charset=US-ASCII Content-Transfer-Encoding: 7bit Status: R Hi Karl: I have attached the file with the Counting, Average and Container examples below. Please note that there might be inconsistencies or repetitions, or not very precise formulations, and that the material might be not very well structured. The reason is that I tried to write down all my thoughts to ba able to reconsider them again. But, I hope it will be good enough as a basis for discussing. -- Mira ------------- cut here ------------ The following adjuster would implement counting all objects of a certain class included in a certain object structure. adjuster Counting [ adjustment CountingEntry { (@int total_count; @) init (@ total_count = 0; @) return (@ total_count @) // When attached to a strategy change will modify the traversal code // generated in the base class for the strategy at hand -- the generated // traversal code is denoted by super in the change method above. } adjustment Count(int total_count) or adjustment Count { change (@ total_count++; super(); @) {(@ int total_count; @) } change (@ ... @) } ] The existence of two alternatives for Count is related to the fact that Count is supposed to manipulate a variable (object) which is passed to it as a parameter, i.e. which it does not initialize. In the first alternative this is made explicit. In the second alternative Count declares, but does not create and initialize total_count, which again means that this variable will be bound later when the adjustment gets activated. The adjuster above represents an abstract implementation of counting certain elements of an object structure. The CountingEntry adjustment will adjust the behavior of the "root" object in the structure, while Count will adjust the behavior of those part-objects of the root which need to be counted. The basic class structure does not need even have a method called count. We can attach this adjuster to several strategies. Suppose: strategies S1 = from Foo to Y; S2 = from Foo to X; We could say: (A) (*) Foo f = new Foo(){count_Y = Counting during S1} f.count_Y(); (**) f +:: {count_X = Counting during S2} f.count_X(); At this point the "change" method in the Count adjustment gets renamed to "count_Y" for the object f. One can even imagine not to mention explicitly the name "count_Y" to be given to the change part of adjustments. This information can actually be filtered out by the generator. It nows (a) that something is getting counted because of the name of the adjuster, and (b) that actually Y-objects are being counted because of the source vertex of the strategy S1. Thus one can imagine the generator to rename the "change" method to "counting_Y". One can go even further and apply the same technique for naming subobjects (I call this way the sum of instance variables) declared by adjustments. For example, the programmer call them uniformely "myObject". In the counting adjuster above, total_count would be substituted with myObject. In a similar way to renaming "change" as "counting_Y", "myObject" could be renamed transparently to "Y_Count" -- Y is now attached to the name of the adjustment Count within the adjuster Counting, which (Count) actually modifies the behavior of target objects. So, after (**), the object f would have two additional methods (in addition to those implemented in Foo), named counting_Y and counting_X and two additional instance variables, Y_count, and X_count. In a similar way, the return methods could also be renamed to return_Y_count and return_X_count, respectively. Note that theses renamings are done individually for the object f, since the modification happens only in connection with its instantiation. We could also say something like (B) Y_Counting_Foo (or even Foo) = Foo with Counting during S1. In the second case, we statically add new functionality to that implemented in Foo and store the result in the new class Y_Counting_Foo (or the old Foo). In this case renaming happens at the class level, i.e. all objects of Y_Counting_Foo (or Foo) will be created with the additional methods and variables. This corresponds to the current implementation of visiting functionality in DEM/J (right?). Advantage of this style as compared to DEM/J: --------------------------------------------- 1. There is no need for explicitly mentioning all possible visiting functionalties in the basic class. The idea of the visitor pattern is to add new functionality to an existing base class. So, in the more general case, the basic class structure "does not know" about future functionality that might be added to it. 2. New functionality can be attached at the level of individual objects, i.e. attached to object rather than class graphs. The question is however, does individual attachment makes sense, what is it good for? 3. There is no need for before methods within visitors and for explicitly mentining the name of the class that will be visited. Above we use the same adjuster to count both X and Y classes. We could also think of counting both of them simultaneously: Foo f = new Foo() {Counting during S1, S2} // S2 < S1 f.counting() // should return both the number of X-instances and Y-instances included in f. In order for that to work, Counting adjuster will be activated twice The generator will rename total_count twice: once as total_count_X and once as total_count_Y. Also the implementation of the return method will be transformed to : return {@ this.return_X_count; this return_Y_count @} return_X_count {@ System.out.printlin("Y:" + total_count_Y @} return_Y_count {@ System.out.printlin("Y:" + total_count_X @} Notice: this is a kind of implicit parametrization of the adjusters with strategies. The explicit variant would be something like: adjuster Counting(Strategy s) [ adjustment CountingEntry { (@int total_count; @) init (@ total_count = 0; @) return (@ s.target: + total @) } It should be noted however, that avoiding the explicit existence of base class names is meaningful for generic adjusters (or visiting functionalities) like counting, inventoring, etc. There are however other visitors, which are inherently as generic. For example, the pricing visitor as in the example from pattern book assumes that there are two kinds of objects in the structure for which the price has to be computed differently. That means that except for the entry adjustment there will be two target adjustments. In this case, we need to explicitly attach these adjustments to the particular classes in the class graph they belong to. This however could be done outside the adjustment code, again at the time an adjuster is attached (bound) to a strategy: S3 = from Computer to Equipment. adjuster Pricing [ adjustment PricingEntry { ... } adjustment Pricing1 { ... + this.discountPrice() ... }; adjustment Pricing2 { ... + this.netPrice() ...}; Pricing modify {Computer, CompositeEquipment, Equipment} during S3 The advantage is that the same Pricing adjuster could be used with several base class structures that implement discountPrice and netPrice. ---------------- Now, what does that all means for the generator. ------------------------------------------------ (B) In the second case (the case denoted by B above) not very much changes as compared to the current workings of the Dem/J generator (my understanding of it). Suppose we modify the class Foo itself. In this case, two methods called counting and counting_Y will be added into the class Foo. Counting simply calls this.counting_Y. The generated code for counting_Y will delegate a method with the same name to that part of Foo in the concrete class graph that is included in the computed S1-graph -- this is the traversal related part of the generated code. In addition, the code included in the change method in CountingEntry will be atached to the implementation of counting_Y before the generated code for traversing. A counting_Y method will be added to all classes between Foo and Y. The implementation of counting in most of the classes between is simply traversing and delegating the same method down the object structure. The attachement of the change method in the Count adjustment to the generated counting_Y in Y, is similar to the attachment of change part to the generated traversal code for counting_Y in Foo. Q: So if only class level atachment of visitors is considered, what is gained by this rondo style implementation? A: 1- There are no before and after methods. So, why were they there? What has been the rationale behind them? 2- There are no additional visitor objects created. So, what was the rationale behind modeing visiting functionality as objects? Was it the influence of the visitor pattern? (A) Consider now the realization of attching adjusters to strategies individually for particular objects. There are two possibilities to realize that: 1. Automatically create dummy subclasses of the classes included in the strategy graph. Put code generated as described above in the implicitly created subclasses instead of adding it to the classes in the strategy graph. Create the object structure for the instance of Foo supposed to provide the visiting functionality by instantiating the implicitly created subclasses rather than the original classes. In this way, we get the desired effect of having the classes Foo and Y (and those between them) and their modifications not being put in a single static relation at a higher level of abstraction i.e. from the programmer's point of view and let the generator clone the modification (and adapt the clone to the particular strategy the modification is being attached to) and establish the static relation only between the clones and the base classes. Advantage: efficiency Disadvantage: the program size increases. Restrictions: - No dynamic modifications, i.e. only for implementing visitor-related behavior modifications, as they are actually implemented in Dem/J. Q: (technical) How do you tell Y objects to be created as instances of a subclass of Y? However, this is a problem only if we want to have visiting functionality added individually to particular objects. In the case we want only the attachement at the class level as it works currently in Dem/J, there is no problem. A: 2. Add code that creates the subclass to be a combiner -- as described in Rondo/Java <---- look at that. Advantage: dynamic modifications Disadvantage: Expensive when no dynamicity ???? ----------------- After having discussed the new style based on the simple counting example here are the average and container examples. Average: -------- adjuster Summing [ adjustment SummingEntry { (@ int total_sum; @) init (@ total_sum = 0; @) return (@ total_sum @) } adjustment Sum(int total_sum) { change (@ total_sum += this.value; super(); @)} ] adjustment Average connects Summing Counting { change (@ super(); System.out.print(this.total_sum/this.total_count) @) } // this suppose that accessing methods such as total_sum and total_count // are automatically generated. (***) Foo f = Foo new{Averaging during S1}; f.averaging; //returns the average value of Y objects In encountering (***), the generator will create traversing methods called average_Y. In addition, when the dummy subclasses of Foo and Y will be created the definitions in Summing, Counting and Average are copied in this subclasses under the names summing_Y, counting_Y, and averaging_Y respectively. Example 2: Container -------------------- For the container example the summing adjuster above can be reused. The strategy now is S1 = from Container to Weight; adjuster Summing [ adjustment SummingEntry { (@ int total_sum; @) init (@ total_sum = 0; @) return (@ total_sum @) } adjustment Sum(int total_sum) { change (@ total_sum += this.value; super(); @)} ] asdjustment Initial modifies Summing { (@ int initial; Stack stack; @) change (@ stack.push(this.total_sum); super(); initial = (Integer) stack.pop(); @) } adjustment Checking modifies Initial { (@ int violations;@) change (@ System.out.println(" start new container "); super(); integer initial = this.initial; int cap = this.get_capacity().get_i().intValue(); int diff = total_sum.intValue() - initial.intValue(); if (diff > cap) { violations = new Integer(violations.intValue() + 1); // this.set_violations(new Integer(violations.intValue() + 1)); System.out.println(" total weight " + diff + " but limit is = " + cap + " OVER CAPACITY "); }; System.out.println(" end container "); } Container c = new Container{Checking during S1}. c.checking(). This will produce the same output as the corresponding implementation in Dem/J. I have already tested it (paper-based testing) on the following example (taken from http://www.ccs.neu.edu/research/demeter/sources/ DemeterJava/examples/j-check-capacity-stack/): // input ( //begin container 1 apple 1 //item ( //begin container 2 pencil 1 //item ( //begin container 3 apple 1 //item 1) //capacity orange 1 1) //capacity orange 1 kiwi 1 5) //capacity This example demonstrates how the same summing adjuster was reused as part of two different visiting functionalities, Averaging and Checking, respectively, without being changed. The reason is that it does not contain any "before XXX". The XXX is bound at strategy application (attachment) time. "before" is not needed anymore, since super-calls plays the same role at the level of connecting different pieces of implementations for the same message that is simulated by delegating before methods to the visitor objects in the original implementation.