Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Fluent setters with inheritance in java

Tags:

java

I've recently seen some code (amazon hadoop code) that utilizes this type of syntax

Foo bar = new Foo().setX(10).setY(11);

I thought that was sweet so I decided to give it a go. made my setX() type functions return Foo instead of void and put return this; in all of them. this worked well. until I tried it with inheritance, which produced some strage results.

I'll give a concrete example: I have two classes, Location class that has two fields, x and y. and another class Location3D that inherits from Location and adds a third field, z. all the fields use the method described above for their setters.

now I want to create a new location3D instance and set its fields, what happens is

new Location3D().setZ(7).setY(6).setX(5)

works while

new Location3D().setX(7).setY(6).setZ(5)

doesn't.

by doesn't work I mean that what returns from setY(6) is a Location object and not a location3D object and therefore does not have a setZ() method!

after this long intro my question is: can this form of "setter stringing" be made to work with inheritance without forcing the caller to cast objects ? if so how ?

also I'm sure there's a term for this better then "setter stringing", what is it ?

like image 514
Oren Avatar asked Dec 31 '12 19:12

Oren


2 Answers

As you pointed out, the reason that new Location3D().setX(7).setY(6).setZ(5) doesn't work is because setX() and setY() return instances of Location and not Location3D.

You can get around this using generics (though the solution isn't particularly pretty) by adding a generic type parameter to your Location class:

public class Location<T extends Location<T>> {
    protected int x, y;

    @SuppressWarnings("unchecked")
    public T setX(int x) {
        this.x = x;
        return (T) this;
    }

    @SuppressWarnings("unchecked")
    public T setY(int y) {
        this.y = y;
        return (T) this;
    }
}

Your subclass Location3D would then have set itself as the generic type parameter so that the superclass returns instances of Location3D instead of Location:

public class Location3D extends Location<Location3D> {
    protected int z;

    public Location3D setZ(int z) {
        this.z = z;
        return this;
    }
}

Unfortunately, there's no way that I know of to avoid the warnings produced by the superclass, hence the @SuppressWarnings("unchecked") annotations.

It's also worth noting that if you define your subclass such that the generic type parameter is a different class type, then you could wind up with ClassCastException, hence you should document that restriction in your superclass for anyone who might want to create their own subclass.

Finally, chaining together method calls in the way you describe is usually referred to as method chaining. The style of setter methods that you're describing is closely related to the builder pattern.

like image 173
Mansoor Siddiqui Avatar answered Nov 20 '22 10:11

Mansoor Siddiqui


The problem is that Location returns Location, as Location3D would return Location3D. To solve this, override the methods you want to use in Location3D and change the return type:

public class Location {
    private int x;
    private int y;

    public Location setY(int y){
        this.y = y;
        return this;
    }

    public Location setX(int x){
        this.x = x;
        return this;
    }
}

And Location3D:

public class Location3D extends Location {

    private int z;

    public Location3D setY(int y){
        super.setY(y);
        return this;
    }

    public Location3D setX(int x){
        super.setX(x);
        return this;
    }

    public Location3D setZ(int z){
        this.z = z;
        return this;
    }
}

Composition approach:

public class Location {
    private int x;
    private int y;

    public Location setY(int y){
        this.y = y;
        return this;
    }

    public Location setX(int x){
        this.x = x;
        return this;
    }
}

And Location3D:

public class Location3D {

    private Location location = new Location();
    private int z;

    public Location3D setY(int y){
        location.setY(y);
        return this;
    }

    public Location3D setX(int x){
        location.setX(x);
        return this;
    }

    public Location3D setZ(int z){
        this.z = z;
        return this;
    }
}
like image 25
John Ericksen Avatar answered Nov 20 '22 11:11

John Ericksen