2017-02-18 4 views
4

У меня есть интерфейс 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. Bur здесь у меня также есть возможность вызывать методы, которые я не мог бы!

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 вариантов является лучшим выбором (или, если есть какой-то другой вариант, я не нашел)? С точки зрения дальнейшей эксплуатации, изменение и т.д.

UPDATE

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

До сих пор это выглядит как самый лучший вариант, как это предлагается в следующей почте:

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

Почему количество дверей, имеющих отношение к * вождению * транспортное средство? Наверное, это что-то определено во время строительства? –

+0

Я расскажу пример с помощью метода paint(), в котором автомобиль имеет краску (carcolor) и грузовик имеет боль (carcolor, trailercolor). Цвета определяются во время выполнения, после того, как транспортные средства уже сконструированы. – bojanv55

+0

В первом примере вы подразумеваете, что вы уже знаете тип «Vehicle», в зависимости от значения перечисления, которое вы передаете. Это правильно? –

ответ

8

Лучшим вариантом было бы, чтобы избавиться от перегруженных версий метода 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); 
    } 
} 

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

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

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

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

Преимущество всех описанных выше подходов является то, что все Vehicle реализации придерживаются одного контракта позволит вам выполнять такие операции, как добавление все Vehicle экземпляры в List и вызывая paint метод на них один за другим, независимо от их тип.

+0

Что делать, если параметры не известны во время строительства? – bojanv55

+2

@ bojanv55 Тогда вы не создадите объект, пока не получите требуемые параметры? – CKing

+0

Вместо drive() можно сказать, что это метод paint(). Но цвет будет определяться впоследствии. У вас уже есть транспортные средства в списке, а затем вы решили их нарисовать. у вас есть для автомобиля: краска (carcolor) и для грузовика: краска (carcolor, trailercolor). – bojanv55

1

, но я должен сделать ручной тип-лить в коде.

Это потому, что вы потеряли информацию о типе, которая вам явно нужна.

Код вашего клиента зависит от конкретной информации о типе, так как ваш метод окраски зависит от конкретных типов.

Если код вашего клиента не должен знать о конкретных типах 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 чеки с опущенными (вы уже пробовали это)
  • Adapter-Pattern
  • посетителей-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); 
} 

Доводы адаптерной-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; 
    } 
    

минусах Adapter-Pattern

  • cyclomatic complexity кода клиента увеличивается при добавлении больше и больше реализаций Vehicle (много если это-то еще заявления).

посетители-Pattern

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

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

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

class Car implements Vehicle { 

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

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

Ваш клиентский код должен затем обеспечивают

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

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

     } 

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

Доводы визитера-Pattern

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

Против Посетитель-Pattern

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

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

+0

Как вы передаете параметры 1 и «1» в шаблон «VehicleVisitor» посетителя, так как они всегда могут быть переменными? Затем вы возвращаетесь к источнику проблемы. –

+0

@MarkoKraljevic Я бы либо создал собственный класс и передал их через конструкторы args, либо в случае использования анонимного класса final vars. Я бы предпочел создать собственный класс в любом случае, потому что он делает код более надежным. Я просто использовал анонимный класс в моем примере, чтобы он был легким и коротким. –

Смежные вопросы