Методы интерфейса с переменными типами аргументов
У меня есть 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 ответа:
Лучшим вариантом было бы избавиться от перегруженных версий метода
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
- цикломатическая сложностьклиентского кода возрастает, когда вы добавляете все больше реализаций
Vehicle
(множество операторов if-else).Посетитель-Шаблон
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: с дополнительной информацией о контексте вашего кода могут быть и другие решения.