具有可变参数类型的接口方法

Interface methods with variable argument types

我有 java 接口和 class 实现,它们在调用类似行为时需要不同的参数。以下哪项最合适?

在第一个选项中,我让不同的 classes 从基本接口继承了共同的行为,所有差异仅直接在 classes 中实现,而不是在接口中实现。这个似乎最合适,但我必须在代码中进行手动类型转换。

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

这里我使用泛型是为了在接口中有通用的方法,参数类型在每个class实现中决定。这里的问题是我有未经检查的绘画调用。这与选项 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 方法。

到目前为止,这看起来像是下面 post 中建议的最佳选择:

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

更好的选择是摆脱 重载 版本的 drive 方法,并传递子 classes 所需的任何信息在构造函数中改为:

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 的 class,其中包含 [=17] 等属性=], trailerColor 并将 paint 方法更改为具有类型 PaintSpecification 的参数。

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

上述所有方法的优点是所有 Vehicle 实现都遵循单一契约,允许您执行诸如将所有 Vehicle 实例添加到 List 和无论它们的类型如何,都对它们一一调用 paint 方法。

but I have to do manual type-cast in the code.

这是因为您丢失了显然需要的类型信息。

您的客户端代码取决于具体类型信息,因为您的绘制方法取决于具体类型。

如果您的客户端代码不应该知道具体的 Vehicle 类型,则 Vehicle 接口应该设计成不需要具体类型信息的方式。例如

public void paint();

这也意味着每个 Vehicle 实例必须具有绘制自身所需的所有信息。因此你应该给实现 color properties.

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(你已经试过了)
  • Adapter-Pattern
  • Visitor-Pattern

Adapter-Pattern

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

Adapter-Pattern

的优点
  • 您将 instanceof 检查从客户端代码移到适配器中。因此客户端代码将更多refactoring-safe。例如。想象以下客户端代码:

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

    想想如果将代码重构为 TruckWithTrailer extends Car

  • 会发生什么
  • 适配器不能 return 本身。具体 Vehicle 可能会实例化另一个对象,让它看起来像适配器类型。

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

Adapter-Pattern

的缺点
  • 当您添加越来越多的 Vehicle 实现(很多 if-else 语句)时,客户端代码的 cyclomatic complexity 会增加。

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

Visitor-Pattern

的优点
  • 在不同的方法中分离类型特定的逻辑

Visitor-Pattern

的缺点
  • 新类型需要更改访问者界面,并且访问者的所有实现也必须更改。

PS:有了有关代码上下文的更多信息,可能会有其他解决方案。