Hi Karl, I wanted to share with you some thoughts on (a) passing strategies as parameters to adjusters, or visitor classes, or context classes, or whatever term we use for describing the functionality to be attached to a class structure along a traversal on this structure. Let me use the initials C, S, and F for denoting the class struture we operate on, the strategy to we use to traverse the class structure, and the functionality we want to execute during this traversal, respectively. I will use B for denoting the resulting behavior after C, S, and F are composed together. and (b) the composition mechanism used for visitor classes (inheritance vs. Rondo). The main question in (a) is how do we specify and compose C, S, and F, in order to maximize reuse of any of them when the others change, i.e. in order to maximize adaptiveness. To my intuition the more orthogonal the elements on the right side of the equation below are the better the adaptiveness: B = C + S + F That means, the rationale behind any design should be to specify C, S, and B in as a generic way as possible, i.e. each of them should hard-code as less assumptions on the concrete form of the others as possible, and provide means for loosely coupling the generic specifications when needed to create a complete B. My claim is that in the current Dem/J, C, S, and F are still coupled with each other. 1) C -- F coupling. The connection between F and C is "hard-coded" in the implementation of both. Visitors contain names of classes and methods in C. For example: PricingVisitor { before Equipment (@ total=total.add(host.get_netPrice()); @) ^^^^^^^^^ ^^^^^^^^^^^^ before CompositeEquipment(@ total=total.add(host.get_discountPrice()); @) ^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^ } Classes in C contain visitors as parameters to their methods. For example: Computer { traversal allEquip(InventoryVisitor, PricingVisitor) { ^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^ to Equipment; } } The problem: the range of applying a visitor is restricted and C should be edited, if we want to add new vistors afterwards. 2) C -- S coupling. Traversals are part of the implementation of C. 3) S -- F coupling. They are connected to each other by the implementations of the methods in C. 4) F -- F coupling. Nested visitors relay on concrete method or visitor names to access the functionality of each other. For example: InitialVisitor { // stack handling init (@ stack = new Stack(); @) // init (@ stack = new MyStack(); @) before Container (@ stack.push(sV.get_total()); @) ^^^^^^^^^ after Container (@ initial = (Integer) stack.pop(); @) int get_current_sum() to SummingVisitor ^^^^^^^^^^^^^^ { before SummingVisitor (@ return_val = host.get_total().intValue(); @) } ^^^^^^^^^^^^^^ ^^^^^^^^^ } So, what can be done to further decouple. 1) Use generic names + strategies as parameters in the implementation of F. For example: Summing (Strategy S) [ Entry { (@ int total_sum @) init (@ total_sum = 0; @) return (@ total_sum @) } Sum(total_sum) { change target(S) (@ total_sum += this.value; @) ^^^^^^ ^^^^^^^^ } change -- is a generic notation for the method to be modified in C. target(S) -- is a generic notation for the class name in C to be modified. This generic notations are made concrete by the attachment of Summing and a concrete S to a concrete C. This attachment happens outside of C, S, and F, as follows: S = from Foo to Y; Foo = f new Foo{Summing during S}; ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ One can go even further and parameterize F also with "method-valued" variables, denoting generically the methods in C that are used (invoked) within the implementation of F. What that means is as follows. In the Summing implementation above the implementation of the Sum adjustment implies that C implements a method named "value". That restricts the application of Summing only to those C instances that has a method with this name. One could also write: Summing (Strategy S, MethodValue f) [ Entry { (@ int total_sum @) init (@ total_sum = 0; @) return (@ total_sum @) } Sum(total_sum) { change target(S) (@ total_sum += this.f; @) ^^^^^^ } Foo = f new Foo{Summing(S, value)}; (*) ^^^^^^^^^^^^^^^^ In this way, a visitor makes explicit what it expects: the strategy, and the methods in C. In addition, C is also an implicit parameter (this within the code). The actual parameters are provided at the attachment (*). We could call this the Input-Interface of F. 2) 3) These kinds of coupling are eleminated by attaching strategies to C outside the implementation of C. 4) For avoiding this kind of coupling we require elemnts of F (adjusters, visitor classes) to have a well defined "Output-Interface" in addition to the Input-Interface. That means, we require F to make explicit in a generic way what it provides. The "return" method could be used for this purpose. Adjusters that make use of other adjusters are allowed to access the functionality of the latter only by calling "super.return" to get the result of invoking the functionality implemented in their "nested" visitor. The visitors should be implemented such that they access only the return of their immediate nested visitor. Let me illustrate the idea of 4) by comparing two implementations of the capacity checking functionality. The first one, is the one we already discussed: S1 = from Container to Weight; and the involved adjusters and adjustments: adjuster Summing(strategy s) [ adjustment SummingEntry { (@ int total_sum; @) init (@ total_sum = 0; @) return (@ total_sum @) } adjustment Sum(int total_sum) { change target(s) (@ total_sum += this.value; @)} ] adjustment Initial modifies Summing { (@ int initial; Stack stack; @) change source(s) (@ stack.push(this.total_sum); super(); initial = (Integer) stack.pop(); @) } adjustment Checking modifies Initial { (@ int violations;@) change source(s) (@ System.out.println(" start newcontainer"); 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 + " OVERCAPACITY "$ System.out.println(" end container "); } Container c = new Container{Checking during S1}. c.checking(). So, what are the problems of this implementation: 1) Summing could be used only with a C that implements "value" 2) because the "modifies" statements are directly attached to the the implementation of adjustments, these are less reusable. This is true also when using inheritance between visitors. In fact in the above implementationone could freely substitute modifies with extends (use of standard inheritance in Java). The Initial functionality could in fact (if implemented in a sufficient generic way) be reused with different "nested" visitors. Counting could also be wrapped up with the functionality implemented in Initial. 3) Even if the modifes statements were not there but written separately Initial is not really reusable because it directly access "total_sum" in its implementation. An alternative implementation would be the following: adjuster Summing(Strategy s, MethodValue f) [ adjustment SummingEntry { (@ int total_sum; @) init (@ total_sum = 0; @) return (@ total_sum @) } adjustment Sum(int total_sum) { change target(s) (@ total_sum += this.f; @)} ] adjustment Initial (Strategy s){ (@ int initial; @) change source(s) (@ initial = super.return(); super(); @) return (@ super.return() - initial @) } adjustment Checking(Strategy s, MethodValue f) { (@ int violations;@) change source(s) (@ System.out.println(" start newcontainer"); super(); integer initial = this.initial; int cap = this.f().get_i().intValue(); (*) if (super.return > 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 + " OVERCAPACITY "$ System.out.println(" end container "); } Now for the concrete C: -------- // solving the capacity checking problem from hw 2 // without modifying the host // visitor has own stack to keep track of initial value // class dictionary (@ import java.util.*; @) Container = "(" List(Item) Capacity ")". Item : Container | Simple. List(S) ~ {S}. Simple = Ident Weight. Capacity = Integer. Weight = Integer. ------- and asssuming that we have already implemented Container::get_capacity and Weight::value in the .beh file, we could write: Strategy s1 = from Container to Weight; Container c = new Container{Checking(s1, get_capacity) modifies Initial(s1) modifies (Summing(s1, value)} Modifies simply binds the super parameter in the corresponding adjustments for this particular attachment of C, S and F. We could also use some other syntactic form for avoiding making clear the difference of "modifies" a la Rondo from extend a la Java: Container c = new Container{Checking(s1, get_capacity)->Initial(s1)->Summing(s1, value)} Note that there is no need for the stack in the implementation of Initial above. Since the Initial modifies the source of s and it declares a new variable, initial, the latter will extend the straucture of all source objects (containers in the particular attachment below). Thus each container will have its own place for storing initial. Also note that super.return in (*) within the implementation of Checking returns the same result as diff in the original implementation, since super get bound to Initial in the attachment above. (b) The issue Rondo modification vs. inheritance should be clear by now. The idea is that if we would have made Initial a subclass of Summing we were not able to reuse the same Initial with e.g. counting as as the core functionality. In the Rondo model, reuse of a single set of adjustments in different modification arrangement happens by changing substantially the standard object model. There is the set of classes and adjustments which are however not connected in any static inheritance hierarchy which is globally parmanently valid. On top of the set of classes combiners are lists of pointers realizing the different arrangements. It is due to the fact that Rondo has to be realized on top of another language, with a global static inheritance that makes it necessary to dublicate code during the generation process. Otherwise, dispatching on top of the language (Java) dispatcher is required which would result in overhead.