Paul Freeman COM3205 Software Engineering Homework 3 Solution October 11, 2002 NOTE: To avoid infinite recursions due to calls made to methods outside the lawOfDemeter package, it is necessary to ammend the pointcut "scope" to: private pointcut scope(): !within(lawOfDemeter..*) && !cflow(execution(* lawOfDemeter..*(..))); // not in this package or within the // controlflow of the execution of a method in this package For example, the method "Supplier.at(JoinPoint jp)" produces a string by concatinating "jp.getThis()" to a String. The call to "jp.getThis()", when used in a string concatination, is actually translated to "jp.getThis().toString()" by the java compiler. If we are checking a class Foo with our LoDChecker and the class Foo overrides the method "toString()" it inherited from "Object", it is possible to create an infinite recursion if we allow the "toString()" method execution in Supplier.at() to be examined by the Checker aspect that extends Supplier. Part 1: Question: Write a paragraph or two that argues for or against the following statement: Any Java or AspectJ program can be simulated by an equivalent AspectJ program where all Java method bodies are empty. The AspectJ program is less than 4 times the length of the Java program. Answer: Most Java or AspectJ programs can be simulated by an equivalent AspectJ program where all Java method bodies are empty. I say "most" because a problem arises when inheritance is introduced. More on that later... disregarding inheritance, a programming style where all methods are declared with empty bodies in one location, while in another location method signatures are repeated, would seem to be more error prone than one in which the majority or all method signatures were only written once - especially since, in its current state, AspectJ does not notify the user when a pointcut does not define a single join point in a program. Mistyping a signature in a pointcut could cause extreme frustration for the user. Unfortunately inheritance makes it impossible to simulate all Java or AspectJ programs. This is due to the fact that AspectJ uses "Type Patterns" when applying pointcuts to join points in the execution of a program. For example: class A{ public void Foo(){ System.out.println("in A"); } public static void main(String[] args){ A a = new A(); B b = new B(); a.Foo(); b.Foo(); } } class B extends A { public void Foo(){ System.out.println("in B"); } } output: in A in B If one were to rewrite this in an AspectJ style where method bodies reside in advice, the overridden method Foo in class B would never be executed. class A{ public void Foo(){ } public static void main(String[] args){ } } privileged aspect AExecutions { void around(): execution(void A.Foo()){ System.out.println("in A"); } void around(String[] args): execution(void A.main(String[])) && args(args){ A a = new A(); B b = new B(); a.Foo(); b.Foo(); } } class B extends A{ public void Foo(){ } } privileged aspect BExecutions { void around(): execution(void B.Foo()){ System.out.println("in B"); } } output: in A in A NOTE: to run this test, compile the enclosed classes with ajc using the enclosed "A.lst" file. To run the test call "java testcase.Foo" This is due to the fact that it is impossible, using the type patterns currently available in AspectJ, to exclude subtypes from being examined in advice. You can explicitly declare advice to examine a class and all of its subclasses with "+", e.g. A+ declares that the user is refering to A and all of A's subtypes (oddly enough this is identical to using A alone). However, there is no designation to exclude subtypes. Another interesting side-effect of inheritance regarding AspectJ programs is that Aspects cannot inherit from non-abstract Aspects. Therefore, since advice must be placed in an Aspect and we can't convert all classes to aspects as some classes may be superclasses, a style should be promoted where "monitoring aspects" are created. "Monitoring aspects" would contain the advice that performs the function of the empty method bodies. "Monitoring aspects" should also be declared as "privileged" so that they may gain access to any of the private members of the class they are monitoring. Following this principle, in the above case I created a "monitoring" aspect for A called AExecutions and a monitoring aspect for B called BExecutions. Constructors also pose a problem when trying to write a program with empty method bodies, that is if you consider constructors to be methods - emptying their method bodies, moving the body to the monitoring aspect. A call to a super constructor can only occur in the subclass's constructor. Therefore, since we cannot move calls to superconstructors out of a class, if that class is extended we cannot technically make all methods, completely empty. For example: class A{ void A(int x){ super(x); } } class B extends A { } Can't be translated to (because classes can't contain advice): class A{ void A(int x){ } void around(int x): execution(A.new(int)) && args(x){ super(x); } } class B extends A{ } AND can't be translated to (because non abstract aspects can't be extended) aspect A{ void A(int x){ } void around(int x): execution(A.new(int)) && args(x){ super(x); } } class B extends A{ } AND can't be translated to (because you can't call a super constructor from within a method that is not a constructor) class A{ void A(int x){ } } privileged aspect AExecutions { void around(int x): execution(A.new(int)) && args(x){ super(x); } } class B extends A{ } It should be argued then that constuctors do not need to be completely empty, in much the same way that empty non-void methods must contain a return statement to compile. But then there needs to be an added constraint placed on the advice around the construction of an object. Specifically, since the super() call must be first in a java constructor, to translate a method correctly into advice, the advice must execute after the constructor executes. NOTE: To run the Trip.java program with the method bodies removed, compile the enclosed classes with ajc using the Trip.lst file. Then call "java trip". To run the Supplier program with the method bodies removed, comile the enclosed classes with the Supplier.lst file. Then call "java testcase.Foo". ** END PART 1 PART 2: // UNKNOWN 1: // INSERT YOUR CODE for // print the allowed target objects for the current method call. // Call method printHashMap. printHashMap(targets); // END INSERT YOUR CODE // UNKNOWN 2: // INSERT YOUR CODE for // check whether target is contained in the allowed target objects (arguments). // Call method contains. return super.contains(target); // END INSERT YOUR CODE // UNKNOWN 3: // INSERT YOUR CODE for // making the program work correctly Stack argStack = new Stack(); Object around(): Any.Execution(){ // before a method executes, we store the arguments of the method argStack.push(targets); targets = new HashMap(); addAll(thisJoinPoint.getArgs(), argument + at(thisJoinPoint)); // arguments include this/self - store the object currently executing add(thisJoinPoint.getThis(), receiver + at(thisJoinPoint)); // then we proceed with the method execution Object obj = proceed(); // after the method executes, we discard the arguments targets = (HashMap)argStack.pop(); return obj; } // END INSERT YOUR CODE (see "part2-code_solution.zip" for working code.) PART 3: Problem: Write a program that flags all calls where the target object is either an immediate part or an argument. Solution: I implemented my partial Law of Demeter checker by combining and modifing the two solutions provided by Dr. David Lorenz, which solved the above problems separately. The original solutions used: 1. an abstract class called Supplier that provided basic storage functionality 2. an aspect called Checker which extended supplier and: a. provided advice that captured "supplier objects" - objects that our partial LoDChecker was attempting to examine b. verified that the stored values met the requirements of a specific part of the LoD 3. a class Any which contained all of the general pointcuts that needed to examined I modified the original designs to work together by abstracting the "gathering" portion of the Checker aspect from both solutions to two separate Aspects called: 1. ArgumentBin - gathers arguments to methods 2. ImmediatePartBin - gathers "immediate part objects", or instance variables Each "bin" aspect extends Supplier and contains advice identical to what was supplied in the solutions to the individual problems. An additional public accessor method called printBin() was added to each bin object. The method simply wraps around the method "printHashMap(HashMap targets)" inherited from supplier. The portion of the Checker aspect common to both solutions, i.e the advice that checks each method call in the executing program, was left in a single aspect still called Checker. However it needed a minor adjustment to function correctly, i.e. be able to check if an object was in either bin object. To correctly make the "contains" examination, we must get a reference to the "global" ArgumentBin aspect - query it, and get a reference to the ImmediatePartBin aspect associated with the object currently executing, and query it. These actions can be performed with the following respective statements: 1. ArgumentBin argBin = ArgumentBin.aspectOf(); 2. ImmediatePartBin partBin = ImmediatePartBin.aspectOf(thisJoinPoint.getThis()); (NOTE: The actual code is slightly different as there must also be a check to determine if an ImmediatePartBin exists for the currently executing object.) Once we have a reference to both bin objects, it is simply a matter of querying their "contains(Object target)" methods to determine if the target of the method call is a valid object, i.e. does not violate either portion of the Law of Demeter. Finally, the Any class that was provided in both solutions was included unaltered in my final solution. (see "part3-code_solution.zip" for working code.) Test Class: package testcase; class Foo { Foo f ; public Foo(Foo f) { this.f=f; } public int add(int a, int b, int c) { return a+b+c; } public Foo() { super(); } static void bar() { } void test() { f = new Foo(); String s = f.toString(); } Foo getF() { return f; } Object compute() { return new Object(); } public void test(Integer i){ int x = i.intValue(); Integer i2 = new Integer(10); x = i2.intValue(); } public void run(Object x) { add(1,2,3); x.toString(); f.test(); Foo.bar(); getF().getF().test(); new Foo().test(); this.compute(); this.compute().toString(); compute().toString(); } public static void main(String[] args) { Foo aFoo = new Foo(new Foo(new Foo())); aFoo.run(new Foo()); aFoo.toString(); // the following test fails due to the current implimentation of the supplier // class - the HashMap creates hash indexes based on the toString() method // of the object supplied as the key. aFoo.test(new Integer(10)); } } OUTPUT: Checking--call(void testcase.Foo.run(Object)) at Foo.java:85:7 this null target testcase.Foo@42719c !! LoD Violation !! Checking--call(int testcase.Foo.add(int, int, int)) at Foo.java:72:7 this testcase.Foo@42719c target testcase.Foo@42719c target found OK--Receiver at Foo.java:71:4 this testcase.Foo@42719c target testcase.Foo@42719c object id testcase.Foo@42719c Checking--call(String java.lang.Object.toString()) at Foo.java:73:7 this testcase.Foo@42719c target testcase.Foo@30c221 target found OK--Argument at Foo.java:71:4 this testcase.Foo@42719c target testcase.Foo@42719c object id testcase.Foo@30c221 Checking--call(void testcase.Foo.test()) at Foo.java:74:7 this testcase.Foo@42719c target testcase.Foo@10385c1 target found OK--Instance variable at Foo.java:45:13 this testcase.Foo@42719c target testcase.Foo@42719c object id testcase.Foo@10385c1 Checking--call(String java.lang.Object.toString()) at Foo.java:57:18 this testcase.Foo@10385c1 target testcase.Foo@1e5e2c3 target found OK--Instance variable at Foo.java:56:7 this testcase.Foo@10385c1 target testcase.Foo@10385c1 object id testcase.Foo@1e5e2c3 Checking--call(void testcase.Foo.bar()) at Foo.java:75:7 this testcase.Foo@42719c target null !! LoD Violation !! Checking--call(Foo testcase.Foo.getF()) at Foo.java:76:6 this testcase.Foo@42719c target testcase.Foo@42719c target found OK--Receiver at Foo.java:71:4 this testcase.Foo@42719c target testcase.Foo@42719c object id testcase.Foo@42719c Checking--call(Foo testcase.Foo.getF()) at Foo.java:76:6 this testcase.Foo@42719c target testcase.Foo@10385c1 target found OK--Instance variable at Foo.java:45:13 this testcase.Foo@42719c target testcase.Foo@42719c object id testcase.Foo@10385c1 Checking--call(void testcase.Foo.test()) at Foo.java:76:19 this testcase.Foo@42719c target testcase.Foo@1e5e2c3 !! LoD Violation !! Checking--call(String java.lang.Object.toString()) at Foo.java:57:18 this testcase.Foo@1e5e2c3 target testcase.Foo@c3c749 target found OK--Instance variable at Foo.java:56:7 this testcase.Foo@1e5e2c3 target testcase.Foo@1e5e2c3 object id testcase.Foo@c3c749 Checking--call(void testcase.Foo.test()) at Foo.java:77:6 this testcase.Foo@42719c target testcase.Foo@150bd4d !! LoD Violation !! Checking--call(String java.lang.Object.toString()) at Foo.java:57:18 this testcase.Foo@150bd4d target testcase.Foo@1bc4459 target found OK--Instance variable at Foo.java:56:7 this testcase.Foo@150bd4d target testcase.Foo@150bd4d object id testcase.Foo@1bc4459 Checking--call(Object testcase.Foo.compute()) at Foo.java:78:6 this testcase.Foo@42719c target testcase.Foo@42719c target found OK--Receiver at Foo.java:71:4 this testcase.Foo@42719c target testcase.Foo@42719c object id testcase.Foo@42719c Checking--call(Object testcase.Foo.compute()) at Foo.java:79:6 this testcase.Foo@42719c target testcase.Foo@42719c target found OK--Receiver at Foo.java:71:4 this testcase.Foo@42719c target testcase.Foo@42719c object id testcase.Foo@42719c Checking--call(String java.lang.Object.toString()) at Foo.java:79:20 this testcase.Foo@42719c target java.lang.Object@12b6651 !! LoD Violation !! Checking--call(Object testcase.Foo.compute()) at Foo.java:80:6 this testcase.Foo@42719c target testcase.Foo@42719c target found OK--Receiver at Foo.java:71:4 this testcase.Foo@42719c target testcase.Foo@42719c object id testcase.Foo@42719c Checking--call(String java.lang.Object.toString()) at Foo.java:80:6 this testcase.Foo@42719c target java.lang.Object@4a5ab2 !! LoD Violation !! Checking--call(String java.lang.Object.toString()) at Foo.java:86:7 this null target testcase.Foo@42719c !! LoD Violation !! Checking--call(void testcase.Foo.test(Integer)) at Foo.java:87:7 this null target testcase.Foo@42719c !! LoD Violation !! Checking--call(int java.lang.Integer.intValue()) at Foo.java:66:13 this testcase.Foo@42719c target 10 target found OK--Argument at Foo.java:65:4 this testcase.Foo@42719c target testcase.Foo@42719c object id 10 Checking--call(int java.lang.Integer.intValue()) at Foo.java:68:9 this testcase.Foo@42719c target 10 target found OK--Argument at Foo.java:65:4 this testcase.Foo@42719c target testcase.Foo@42719c object id 10