Search code examples
javaaopaspectjaspectprivate-methods

How to add method and intercept private fields from a different class


I am trying to introduce a Shape class as a parent interface for the classes Circle and Rectangle. I have to implement getName() method that will return Circle for the Circle object and Rectangle for the Rectangle object. Also, I have to override the toString() method in both the Circle and Rectangle classes. The toString methods will call the getName() method and will generate a string representing the object as follows:

  • Circle with radius of 2 is represented as "Circle(2)"
  • Rectangle with width of 2 & height of 10 is represented as "Rectangle(2, 10)".

Also, I cannot modify the classes Shape, Rectangle, Circle or Main, for which you will find the codes below. I'm not sure about how to do this. Can someone please help me?

Here is what I have done so far:

Shape.java

public interface Shape {
    String getName();
    double getPerimeter();
    double getArea();
}

Rectangle.java

public class Rectangle {
    private double width, height;
    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }
    public double getPerimeter() {
        return 2 * (this.width + this.height);
    }
    public double getArea() {
        return this.width * this.height;
    }
}

Circle.java

public class Circle{
    private double radius;
    public Circle(double radius) {
        this.radius = radius;
    }
    public double getPerimeter() {
        return 2 * Math.PI * this.radius;
    }
    public double getArea() {
        throw new RuntimeException("Oops, I don't know how to calculate this :(");
    }
}

Question.aj

public aspect Question {
    declare parents: Rectangle implements Shape;
    declare parents: Circle implements Shape;
    
    public String Rectangle.getName(){
        return "Rectangle";
    }
    
    public String Circle.getName(){
        return "Circle";
    }
    
    public String Rectangle.toString(){
        return Rectangle.getName()+"(" + this.width +", " + this.height +")";
    }
    
    public String Circle.toString(){
        return Circle.getName() + "(" + this.radius + ")";
    }
}

Main.java

public class Main {
    public static void main(String[] args) {
        try {
            Shape s;
            s = (Shape) new Rectangle(2, 10);
            System.out.println("The area of " + s + " is " + s.getArea());
            s = (Shape) new Rectangle(-2, 10);
            System.out.println("The perimeter of " + s + " is " + s.getPerimeter());
            s = (Shape) new Circle(-2);
            System.out.println("The perimeter of " + s + " is " + s.getPerimeter());
            s = (Shape) new Circle(2);
            System.out.println("The area of " + s + " is " + s.getArea());
        }
        catch(Exception e) {
            System.out.println("Error: " + e.getMessage());
        }
    }
}

Solution

  • I am trying to introduce a Shape class as a parent interface for the classes Circle and Rectangle

    For that you need to use inter-type declarations an AspectJ static crosscutting feature that allows to change the structure of a class, namely adding new methods, make the class implement interfaces and so. Which you have done correctly:

    declare parents: Rectangle implements Shape;
    declare parents: Circle implements Shape;
    

    I have to implement getName() method that will return "Circle" for the Circle object and "Rectangle" for the Rectangle object.

    You also did that correctly:

    public String Rectangle.getName(){
        return "Rectangle";
    }
    
    public String Circle.getName(){
        return "Circle";
    }
    

    Also, I have to override the toString() method in both the Circle and Rectangle > classes.

    Which you also did it correctly:

    public String Rectangle.toString(){ (...)}
    
    public String Circle.toString(){ (...)}
    

    The toString methods will call the getName() method and will generate a string representing the object as follows: Circle with radius of 2 is represented as "Circle(2)" Rectangle with width of 2 & height of 10 is represented as "Rectangle(2, 10)".

    This step you did it wrongly:

    public String Rectangle.toString(){
        return Rectangle.getName()+"(" + this.width +", " + this.height +")";
    }
    
    public String Circle.toString(){
        return Circle.getName() + "(" + this.radius + ")";
    }
    

    You have two problems,

    1. the method getName() is not static, therefore change both Rectangle.getName() and Circle.getName() to this.getName();

    2. the fields width, height, and radius are private. And therefore you cannot simply access them from the aspect like that. From source:

    Code written in aspects is subject to the same access control rules as Java code when referring to members of classes or aspects. So, for example, code written in an aspect may not refer to members with default (package-protected) visibility unless the aspect is defined in the same package.

    While these restrictions are suitable for many aspects, there may be some aspects in which advice or inter-type members needs to access private or protected resources of other types. To allow this, aspects may be declared privileged. Code in priviliged aspects has access to all members, even private ones.

    To solve this, you have (at least) 3 options:

    1. Make those fields public;
    2. Make the Aspect Question privileged;
    3. Make getters for those private fields.

    From an OOP encapsulation point of view, the third option is the best. Which would look like this:

    To the class Rectangle add the getters for the width and height fields:

    public double getWidth() {return this.width;}
    
    public double getHeight() {return this.height;}
    

    and to the class Circle add the getter for the radius field:

    public double getRadius() {return this.radius;}
    

    Finally, adapt the aspect Question accordingly:

    public String Rectangle.toString(){
        return this.getName()+"(" + this.getWidth() +", " + this.getHeight() +")";
    }
    
    public String Circle.toString(){
        return this.getName() + "(" + this.getRadius()+ ")";
    }
    

    Also, I cannot modify the classes Shape, Rectangle, Circle or Main, for which you will find the codes bellow.

    Okey, this excludes the approaches 1) (which was bad anyway) and 3) (which as the best IMO).

    Consequently, you are left with making the aspect Question privileged:

     privileged public aspect Question
    

    I share the same opinion of some authors:

    • J. D. Gradecki and N. Lesiecki. in the book Mastering AspectJ: Aspect-Oriented Programming in Java.
    • R. Laddad. in the book AspectJ in Action: Enterprise AOP with Spring Applications.

    that privileged aspects should be avoided as much as possible since they add an
    intrinsic dependency between aspects and classes, and they circumvent the visibility rules that where applied to that target class.