Методы интерфейса с переменными типами аргументов


У меня есть Java-интерфейс и реализации классов, которые нуждаются в различных аргументах при вызове аналогичного поведения. Какой из следующих вариантов наиболее уместен?

В первом варианте у меня разные классы наследуют общее поведение от базового интерфейса, и все различия реализуются только непосредственно в классах, а не в интерфейсе. Этот вариант кажется наиболее подходящим, но я должен выполнить ручное приведение типа в коде.

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() {}
}

Во втором варианте я переместил методы один уровень до интерфейса, но теперь мне нужно реализовать поведение с UnsupportedOpException. Это похоже на запах кода. В коде мне не нужно выполнять ручное приведение, но у меня также есть возможность вызывать методы, которые будут создавать исключение во время выполнения - без проверки времени компиляции. Это не такая уж большая проблема - только эти методы с исключением, которые выглядят как запах кода. Является ли этот способ реализации лучшей практикой?

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);
    }
}

Здесь я использовал дженерики, чтобы иметь общий метод в интерфейсе, и тип параметра определяется в каждой реализации класса. Проблема здесь в том, что у меня есть непроверенные звонки для рисования. Это более-менее похоже на задачу прямого литья в варианте 1. Но здесь у меня также есть возможность вызывать методы, которые я не должен уметь!

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);
    }
}

Итак, вопрос - какой из этих 3 вариантов является лучшим (или если есть какой-то другой вариант, который я не нашел)? С точки зрения дальнейшего обслуживания, изменения и т.д.

Обновить

Я обновил вопрос и теперь у меня есть метод paint, который может быть вызван только после того, как объект построен.

Пока это выглядит как лучший вариант, как это предлагается в посте ниже:

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);
    }
}
2 4

2 ответа:

Лучшим вариантом было бы избавиться от перегруженных версий метода drive и передать вместо этого любую информацию, требуемую подклассами в конструкторе:

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);
    }
}

Обращаясь к вашему комментарию относительно решения paint во время выполнения, вы можете добавить метод paint к транспортному средству, который принимает переменные аргументы:

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

Как обсуждалось в комментариях, если число аргументов, используемых в методе paint, изменяется для разных типы транспортных средств, определите класс под названием PaintSpecification, содержащий такие атрибуты, как vehcileColor, trailerColor и измените метод paint, чтобы вместо него был аргумент типа PaintSpecification.

interface Vehicle{
    void drive();
    void paint(PaintSpecification spec);
}
Преимущество всех вышеперечисленных подходов заключается в том, что все реализации Vehicle придерживаются единого контракта, позволяющего выполнять такие операции, как добавление всех ваших экземпляров Vehicle в List и вызов метода paint на них один за другим независимо от их типа.

Но я должен выполнить ручное приведение типа в коде.

Это потому, что вы потеряли информацию о типе, которая вам, очевидно, нужна. Ваш клиентский код зависит от конкретной информации о типе, потому что ваш метод рисования зависит от конкретных типов. Если ваш клиентский код не должен знать о конкретных типах Vehicle, интерфейс Vehicle должен быть спроектирован таким образом, чтобы не нуждаться в конкретной информации о типе. Например,
public void paint();

Это также означает, что каждый экземпляр Vehicle должен иметь всю информацию, необходимую ему для рисования самого себя. Таким образом, вы должны дать реализации color свойства .

public class Car implements Vehicle {

  private int color = 0; // Default color

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

  public void setColor(int color){
     // maybe some validation first
     this.color = color;
  }
}

Что еще ты можешь сделать?

Если вы хотите сохранить код таким, как он есть, вы должны каким-то образом воссоздать информацию о типе.

Я вижу следующие решения:

  • instanceof проверяет с помощью downcast (вы уже пробовали что)
  • Адаптер-Шаблон
  • Посетитель-Шаблон

Адаптер-Шаблон

interface Vehicle {
    public <T extends Vehicle> T getAdapter(Class<T> adapterClass);
}

class Car implements Vehicle {

    @Override
    public <T extends Vehicle> T getAdapter(Class<T> adapterClass) {
        if(adapterClass.isInstance(this)){
            return adapterClass.cast(this);
        }
        return null;
    }
}

Ваш клиентский код будет выглядеть следующим образом:

Vehicle vehicle = ...;

Car car = vehicle.getAdapter(Car.class);
if(car != null){
    // the vehicle can be adapted to a car
    car.paint(1);
}

Плюсы адаптера-Pattern

  • Вы перемещаете проверки instanceof из клиентского кода в адаптер. Таким образом, клиентский код будет более безопасным для рефакторинга. Например, представьте себе следующий клиентский код:

    if(vehicle instanceof Car){
       // ...
    } else if(vehicle instanceof TruckWithTrailer){
       // ...
    }
    

    Подумайте о том, что произойдет, если вы рефакторинг кода в TruckWithTrailer extends Car

  • Адаптер не должен возвращаться сам. Конкретный Vehicle может создать экземпляр другого объекта, который будет выглядеть как тип адаптера.

    public <T extends Vehicle> T getAdapter(Class<T> adapterClass) {
        if(Car.class.isAssignableFrom(adapterClass)){
            return new CarAdapter(this)
        }
        return null;
    }
    

Минусы адаптера-Pattern

Посетитель-Шаблон

interface Vehicle {
    public void accept(VehicleVisitor vehicleVisitor);
}

interface VehicleVisitor {
    public void visit(Car car);
    public void visit(TruckWithTrailer truckWithTrailer);
}

В реализации car затем решат, какой метод VihicleVisitor должен быть вызван.

class Car implements Vehicle {

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

    @Override
    public void accept(VehicleVisitor vehicleVisitor) {
        vehicleVisitor.visit(this);
    }
}

Ваш клиентский код должен затем предоставить VehicleVisitor

    Vehicle vehicle = ...;
    vehicle.accept(new VehicleVisitor() {

        public void visit(TruckWithTrailer truckWithTrailer) {
            truckWithTrailer.paint(1, "1");

        }

        public void visit(Car car) {
            car.paint(1);
        }
    });

Плюсы визитера-паттерна

  • разделение специфической логики типа на отдельные методы

Минусы визитера-паттерна

  • новые типы требуют изменения интерфейса посетителя, и все реализации посетителя должны быть изменены как хорошо.

PS: с дополнительной информацией о контексте вашего кода могут быть и другие решения.