Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Interface methods with variable argument types

I have java interface and class implementations that have need for different arguments when invoking similar behavior. Which of the following is mostly appropriate?

In first option I have different classes inherit common behavior from base interface and all differences are only implemented directly in the classes and not in interface. This one seems most appropriate, but I have to do manual type-cast in the code.

public class VaryParam1 {

    static Map<VehicleType, Vehicle> list = new HashMap<>();

    static List<Car> carsList = new ArrayList<>();
    static List<TruckWithTrailer> trucksList = new ArrayList<>();

    public static void main(String[] args) {
        list.put(VehicleType.WITHOUT_TRAILER, new Car());
        list.put(VehicleType.WITH_TRAILER, new TruckWithTrailer());

        //violates LSP?
        ((Car)list.get(VehicleType.WITHOUT_TRAILER)).paint(1); //ok - but needed manual cast
        ((TruckWithTrailer)list.get(VehicleType.WITH_TRAILER)).paint(1, "1"); //ok - but needed manual cast

        carsList.add(new Car());
        trucksList.add(new TruckWithTrailer());

        //Does not violate LSP
        carsList.get(0).paint(1);
        trucksList.get(0).paint(1, "1");
    }
}

enum VehicleType {
    WITHOUT_TRAILER,
    WITH_TRAILER;
}

interface Vehicle{
    //definition of all common methods
    void drive();
    void stop();
}

class Car implements Vehicle {

    public void paint(int vehicleColor) {
        System.out.println(vehicleColor);
    }

    @Override
    public void drive() {}

    @Override
    public void stop() {}
}

class TruckWithTrailer implements Vehicle {

    public void paint(int vehicleColor, String trailerColor) {
        System.out.println(vehicleColor + trailerColor);
    }

    @Override
    public void drive() {}

    @Override
    public void stop() {}
}

In second option I have moved methods one level up to the interface, but now I need to implement behavior with UnsupportedOpException. This looks like code smell. In code, I don't have to do manual casting, but I also have possibility to call methods that will produce exception in run time - no compile time checking. This is not that big problem - only this methods with exception that look like code smell. Is this way of implementation best practice?

public class VaryParam2 {

    static Map<VehicleType, Vehicle> list = new HashMap<>();

    public static void main(String[] args) {
        list.put(VehicleType.WITHOUT_TRAILER, new Car());
        list.put(VehicleType.WITH_TRAILER, new TruckWithTrailer());

        list.get(VehicleType.WITHOUT_TRAILER).paint(1); //works
        list.get(VehicleType.WITH_TRAILER).paint(1, "1"); //works

        list.get(VehicleType.WITHOUT_TRAILER).paint(1, "1"); //ok - exception - passing trailer when no trailer - no compile time check!
        list.get(VehicleType.WITH_TRAILER).paint(1); //ok - exception - calling trailer without trailer args - no compile time check!
    }
}

enum VehicleType {
    WITHOUT_TRAILER,
    WITH_TRAILER;
}

interface Vehicle{
    void paint(int vehicleColor);
    void paint(int vehicleColor, String trailerColor);    //code smell - not valid for all vehicles??
}

class Car implements Vehicle {

    @Override
    public void paint(int vehicleColor) {
        System.out.println(vehicleColor);
    }

    @Override
    public void paint(int vehicleColor, String trailerColor) {    //code smell ??
        throw new UnsupportedOperationException("Car has no trailer");
    }
}

class TruckWithTrailer implements Vehicle {

    @Override
    public void paint(int vehicleColor) {  //code smell ??
        throw new UnsupportedOperationException("What to do with the trailer?");
    }

    @Override
    public void paint(int vehicleColor, String trailerColor) {
        System.out.println(vehicleColor + trailerColor);
    }
}

Here I used generics in order to have common method in interface, and parameter type is decided in each class implementation. Problem here is that I have unchecked calls to paint. This is more-less similar to problem of direct casting in option 1. Bur here I also have possibility to call methods that I should not be able to!

public class VaryParam3 {

    static Map<VehicleType, Vehicle> list = new HashMap<>();


    public static void main(String[] args) {
        list.put(VehicleType.WITHOUT_TRAILER, new Car());
        list.put(VehicleType.WITH_TRAILER, new TruckWithTrailer());

        list.get(VehicleType.WITHOUT_TRAILER).paint(new VehicleParam());    //works but unchecked call
        list.get(VehicleType.WITH_TRAILER).paint(new TruckWithTrailerParam());    //works but unchecked call

        list.get(VehicleType.WITHOUT_TRAILER).paint(new TruckWithTrailerParam()); //works but should not!
        list.get(VehicleType.WITH_TRAILER).paint(new VehicleParam());   //ClassCastException in runtime - ok but no compile time check
    }
}

enum VehicleType {
    WITHOUT_TRAILER,
    WITH_TRAILER;
}

class VehicleParam {
    int vehicleColor;
}

class TruckWithTrailerParam extends VehicleParam {
    String trailerColor;
}

interface Vehicle<T extends VehicleParam>{
    void paint(T param);
}

class Car implements Vehicle<VehicleParam> {

    @Override
    public void paint(VehicleParam param) {
        System.out.println(param.vehicleColor);
    }
}

class TruckWithTrailer implements Vehicle<TruckWithTrailerParam> {

    @Override
    public void paint(TruckWithTrailerParam param) {
        System.out.println(param.vehicleColor + param.trailerColor);
    }
}

So question is - which of this 3 options is the best one (or if there is some other option I have not found)? In terms of further maintenance, changing etc.

UPDATE

I updated the question and now I have paint method that can be called only after object is constructed.

So far this looks like the best option as it is suggested in the post below:

public class VaryParam4 {

    static Map<VehicleType, Vehicle> list = new HashMap<>();

    public static void main(String[] args) {
        list.put(VehicleType.WITHOUT_TRAILER, new Car());
        list.put(VehicleType.WITH_TRAILER, new TruckWithTrailer());

        list.get(VehicleType.WITHOUT_TRAILER).paint(new PaintConfigObject());    //works but can pass trailerColor (even if null) that is not needed
        list.get(VehicleType.WITH_TRAILER).paint(new PaintConfigObject());    //works
    }
}

enum VehicleType {
    WITHOUT_TRAILER,
    WITH_TRAILER;
}

class PaintConfigObject {
    int vehicleColor;
    String trailerColor;
}

interface Vehicle{
    void paint(PaintConfigObject param);
}

class Car implements Vehicle {

    @Override
    public void paint(PaintConfigObject param) {
        //param.trailerColor will never be used here but it's passed in param
        System.out.println(param.vehicleColor);
    }
}

class TruckWithTrailer implements Vehicle {

    @Override
    public void paint(PaintConfigObject param) {
        System.out.println(param.vehicleColor + param.trailerColor);
    }
}
like image 711
Bojan Vukasovic Avatar asked Feb 18 '17 15:02

Bojan Vukasovic


People also ask

What is the type of methods and variables in interface?

An interface is a container of abstract methods and static final variables. The interface contains the static final variables. The variables defined in an interface can not be modified by the class that implements the interface, but it may use as it defined in the interface.

Can an interface be a variable type?

Interface types act like class types. You can declare variables to be of an interface type, you can declare arguments of methods to accept interface types, and you can even specify that the return type of a method is an interface type.

What is variable argument method?

Variable Arguments (Varargs) in Java is a method that takes a variable number of arguments. Variable Arguments in Java simplifies the creation of methods that need to take a variable number of arguments.

What types of methods can an interface have?

The interface body can contain abstract methods, default methods, and static methods. An abstract method within an interface is followed by a semicolon, but no braces (an abstract method does not contain an implementation).


1 Answers

A better option would be to get rid of the overloaded versions of the drive method and pass whatever information is required by the subclasses in the constructor instead :

interface Vehicle{
    void drive();
}

class Car implements Vehicle {
    private int numberOfDoors;

    public Car(int numberOfDoors) {
         this.numberOfDoors = numberOfDoors;
     }

    public void drive() {
        System.out.println(numberOfDoors);
    }
}


class TruckWithTrailer implements Vehicle {
    private int numberOfDoors;
    private int numberOfTrailers;

    public TruckWithTrailer(int numberOfDoors,numberOfTrailers) {
          this.numberOfDoors = numberOfDoors;
          this.numberOfTrailers = numberOfTrailers;
    }

    @Override
    public void drive() {
        System.out.println(numberOfDoors + numberOfTrailers);
    }
}

Addressing your comment regarding the paint being decided at runtime, you can add a paint method to vehicle that takes variable arguments :

interface Vehicle{
    void drive();
    void paint(String ...colors);
}

As discussed in the comments, if the number of arguments to be used in the paint method varies for different vehicle types, define a class called PaintSpecification that contains attributes such as vehcileColor, trailerColor and change the paint method to have an argument of type PaintSpecification instead.

interface Vehicle{
    void drive();
    void paint(PaintSpecification spec);
}

The advantage of all the above approaches is that all Vehicle implementations adhere to a single contract allowing you to perform operations such as adding all your Vehicle instances to a List and calling the paint method on them one by one regardless of their type.

like image 90
Chetan Kinger Avatar answered Sep 29 '22 08:09

Chetan Kinger