What is the best approach to access concrete sub class specific methods using java?

121 Views Asked by At

Say I have the following trivial example:

// This is provided to me from another jar so cannot be changed.
public interface Shape {
    double calculateArea();
    double calculatePerimeter();
}

public class Triangle implements Shape {
    private double side1;
    private double side2;
    private double side3;

    public Triangle(double side1, double side2, double side3) {
        this.side1 = side1;
        this.side2 = side2;
        this.side3 = side3;
    }

    @Override
    public double calculateArea() {
        // Heron's formula to calculate area of a triangle
        double s = (side1 + side2 + side3) / 2;
        return Math.sqrt(s * (s - side1) * (s - side2) * (s - side3));
    }

    @Override
    public double calculatePerimeter() {
        return side1 + side2 + side3;
    }
}

public class Square implements Shape {
    private double side;

    public Square(double side) {
        this.side = side;
    }

    @Override
    public double calculateArea() {
        return side * side;
    }

    @Override
    public double calculatePerimeter() {
        return 4 * side;
    }

    public void squareSpecificMethod(){
        // Does something specific for squares only
    }
}


public class Circle implements Shape {
    private double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    @Override
    public double calculateArea() {
        return Math.PI * radius * radius;
    }

    @Override
    public double calculatePerimeter() {
        return 2 * Math.PI * radius;
    }
}


public class Main {
    public static void main(String[] args) {
        // Example usage
        Shape triangle = new Triangle(3, 4, 5);
        System.out.println("Triangle Area: " + triangle.calculateArea());
        System.out.println("Triangle Perimeter: " + triangle.calculatePerimeter());

        Shape square = new Square(5);
        System.out.println("Square Area: " + square.calculateArea());
        System.out.println("Square Perimeter: " + square.calculatePerimeter());

        Shape circle = new Circle(3);
        System.out.println("Circle Area: " + circle.calculateArea());
        System.out.println("Circle Perimeter: " + circle.calculatePerimeter());
    }
}

If I want to access the method squareSpecificMethod()
I can do the following but would potentially have to do this multiple times in multiple places:

        ((Square) square).squareSpecificMethod();

or Likewise would have to do this in multiple places


if (square instanceof Square) {
    ((Square) square).squareSpecificMethod();
}

or change this to:

  Shape square = new Square(5);

to

  Square square = new Square(5);

Or use the visitor pattern but I cannot change the shape interface.

How can I avoid having to cast or use instanceOf as from what I've read this can be a potential code smell?

I'm struggling to see the best way of getting the concrete class at runtime when I may need to call implementation specific methods but may not know the implementation until runtime.

I would hit the same problem if I added specific methods to other implementations. And it would seem cumbersome to check instanceof or cast in multiple places

Constrained to Java 11

2

There are 2 best solutions below

2
David Tonhofer On BEST ANSWER

Keep it clean, if you need te test for subclasses and cast to subclasses, something is deeply wrong.

"Object orientation" is about making objects interact. Instead of to find out the subclass of object A and then calls some method specific to A, we create an an annex object that "knows more" by virtue of having been constructed together with A and calls A ("In Soviet Russia, object calls YOU" etc...)

Here we go. We use an annex interface ShapeMangler to which we "give" the Shape. The implementation of that annex interface "knows" how to handle the Square extras. We pass the appropriate implementation of the annex interface together with Shape at the topmost call.

As a side-bonus, we use it to eliminate code duplication made for the printing to stdout -- by having it carry the name of the shape.

// Interface given by the library

interface Shape {
    double calculateArea();
    double calculatePerimeter();
}

// Triangle 

class Triangle implements Shape {

    private final double side1,side2,side3;

    public Triangle(double side1, double side2, double side3) {
        this.side1 = side1;
        this.side2 = side2;
        this.side3 = side3;
    }

    @Override
    public double calculateArea() {
        // Heron's formula to calculate area of a triangle
        double s = (side1 + side2 + side3) / 2;
        return Math.sqrt(s * (s - side1) * (s - side2) * (s - side3));
    }

    @Override
    public double calculatePerimeter() {
        return side1 + side2 + side3;
    }
}

// Square

class Square implements Shape {
    
    private final double side;

    public Square(double side) {
        this.side = side;
    }

    @Override
    public double calculateArea() {
        return side * side;
    }

    @Override
    public double calculatePerimeter() {
        return 4 * side;
    }

    public void squareSpecificMethod(){
        // Does something specific for squares only
        System.out.println("I AM A SQUARE");
    }
}

// Circle

class Circle implements Shape {
    private final double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    @Override
    public double calculateArea() {
        return Math.PI * radius * radius;
    }

    @Override
    public double calculatePerimeter() {
        return 2 * Math.PI * radius;
    }
}

// Something which can mangle a Shape in Shape-specific ways

interface ShapeMangler {

   String header();
   void mangle(Shape shape);

}

// Something which can mangle a Square

class SquareMangler implements ShapeMangler {

  public String header() { return "square"; }

  public void mangle(Shape shape) {
     // This is the one and only cast.
     // If 'shape' is not a Square, this will raise an
     // exception, and rightly so.
     ((Square)shape).squareSpecificMethod(); 
  }

}

// Something which can mangle anything in a default way

class DefaultMangler implements ShapeMangler {

  private final String header;
  
  public DefaultMangler(String header) {
      this.header = header;
  }
  public String header() { return header; }
  public void mangle(Shape shape) { }

}

// Let's roll

class Main {

    // Do something very general which only depends on the
    // interface Shape and ShapeMangler and absolutely does not
    // subclass testing or type casting!

    public static void compute(Shape shape, ShapeMangler mangler) {
        // One should use logging instead.
        // Note that we use the mangler already to get the shape's
        // name.
        final String txt1 = mangler.header() + " area: " + shape.calculateArea();
        final String txt2 = mangler.header() + " perimeter: " + shape.calculatePerimeter();
        System.out.println(txt1);
        System.out.println(txt2);
        // do sth specific that cannot be expressed as a method 
        // of the Shape interface but about which the mangler
        // knows more!
        mangler.mangle(shape);
    }

    public static void main(String[] args) {
        compute(new Triangle(3, 4, 5),new DefaultMangler("triangle"));
        compute(new Circle(3),new DefaultMangler("circle"));
        compute(new Square(5),new SquareMangler());
    }
}

We get:

triangle area: 6.0
triangle perimeter: 12.0
circle area: 28.274333882308138
circle perimeter: 18.84955592153876
square area: 25.0
square perimeter: 20.0
I AM A SQUARE
4
Naman On

As QBrute pointed out in the comment, if you could have made use of later versions of Java, pattern matching possibly would have simplified invoking implementation-specific methods within the construct to something like:

void invokeShapeMethod(Shape shape) {
    System.out.println(shape.getClass().getSimpleName() + " Area: " + shape.calculateArea());
    System.out.println(shape.getClass().getSimpleName() + " Perimeter: " + shape.calculatePerimeter());
    switch (shape) {
        case Square s -> s.squareSpecificMethod();
        // specific methods to other implementations
        default -> {
        }
    }
}

this calls the default methods across the implementation of the interface while at the same time switching over the types of shapes to invoke their specific method. If you invoke this method on each of your types:

List<Shape> shapes = List.of(new Triangle(3, 4, 5), new Square(5), new Circle(3));
shapes.forEach(shape -> invokeShapeMethod(shape));
Triangle Area: 6.0
Triangle Perimeter: 12.0
Square Area: 25.0
Square Perimeter: 20.0
Some Square Specific Method.
Circle Area: 28.274333882308138
Circle Perimeter: 18.84955592153876