Какой лучший способ вызвать метод, который доступен только одному классу, который реализует интерфейс, но не другой?

11

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

Базовый интерфейс

// DoSomething.java
interface DoSomething {

   void letDoIt(String info);
}

Реализация первого рабочего класса

class DoItThisWay implements DoSomething {
  ...
}

Реализация второго рабочего класса

class DoItThatWay implements DoSomething {
   ...
}

Основной класс

   class Main {
     public doingIt(String info) {
        DoSomething worker;
        if (info == 'this') {
          worker = new DoItThisWay();
        } else {
          worker = new DoItThatWay();
        }
        worker.letDoIt(info)
     }

Этот код работает нормально и его легко понять.

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

Мой вопрос: хорош ли следующий стиль кодирования для удовлетворения этого требования.

Используйте новую переменную класса и метод

// Use new class variable and method

class DoItThisWay implements DoSomething {
  private int quality;
  DoSomething() {
    quality = 0;
  }

  public void setQuality(int quality) {
    this.quality = quality;
  };

 public void letDoIt(String info) {
   if (quality > 50) { // make use of the new information
     ...
   } else {
     ...
   }
 } ;

}

Если я делаю это таким образом, мне нужно внести соответствующие изменения в вызывающей стороне:

   class Main {
     public doingIt(String info) {
        DoSomething worker;
        if (info == 'this') {
          int quality = obtainQualityInfo();
          DoItThisWay tmp = new DoItThisWay();
          tmp.setQuality(quality)
          worker = tmp;

        } else {
          worker = new DoItThatWay();
        }
        worker.letDoIt(info)
     }

Это хороший стиль кодирования? Или я могу просто бросить это

   class Main {
     public doingIt(String info) {
        DoSomething worker;
        if (info == 'this') {
          int quality = obtainQualityInfo();
          worker = new DoItThisWay();
          ((DoItThisWay) worker).setQuality(quality)
        } else {
          worker = new DoItThatWay();
        }
        worker.letDoIt(info)
     }
Энтони Конг
источник
19
Почему бы просто не перейти qualityв конструктор для DoItThisWay?
Дэвид Арно
Мне, вероятно, нужно пересмотреть мой вопрос ... потому что по соображениям производительности конструирование DoItThisWayи DoItThatWayвыполняется один раз в конструкторе Main. Mainэто долгоживущий класс и doingItвызывается много раз снова и снова.
Энтони Конг
Да, обновление вашего вопроса было бы хорошей идеей в этом случае.
Дэвид Арно
Вы хотите сказать, что setQualityметод будет вызываться несколько раз за время существования DoItThisWayобъекта?
jpmc26
1
Между двумя представленными вами версиями нет существенной разницы. С логической точки зрения они делают то же самое.
Себастьян Редл

Ответы:

6

Я предполагаю, что это qualityдолжно быть установлено рядом с каждым letDoIt()вызовом на DoItThisWay().

Возникающая здесь проблема заключается в следующем: вы вводите временную связь (то есть, что произойдет, если вы забудете позвонить, setQuality()прежде чем вызывать letDoIt()на DoItThisWay?). И реализации для DoItThisWayи DoItThatWayрасходятся (один должен был setQuality()позвонить, другой нет).

Хотя это может не вызывать проблем прямо сейчас, оно может снова преследовать вас. Возможно, стоит взглянуть еще раз letDoIt()и подумать, может ли информация о качестве быть частью той, которую infoвы проходите через нее; но это зависит от деталей.

CharonX
источник
15

Это хороший стиль кодирования?

С моей точки зрения ни одна из ваших версий не является. Во- первых , чтобы звонить , setQualityпрежде чем letDoItможно назвать это временное сцепление . Вы застряли, рассматривая DoItThisWayкак производную от DoSomething, но это не (по крайней мере, не функционально), это скорее что-то вроде

interface DoSomethingWithQuality {
   void letDoIt(String info, int quality);
}

Это сделало бы Mainчто-то вроде

class Main {
    // omitting the creation
    private DoSomething doSomething;
    private DoSomethingWithQuality doSomethingWithQuality;

    public doingIt(String info) {
        DoSomething worker;
        if (info == 'this') {
            int quality = obtainQualityInfo();
            doSomethingWithQuality.letDoIt(info, quality);
        } else {
            doSomething.letDoIt(info);
        }
    }
}

С другой стороны, вы можете передать параметры классам напрямую (при условии, что это возможно) и делегировать решение, какое из них использовать фабрике (это сделает экземпляры взаимозаменяемыми, и оба могут быть получены из DoSomething). Это будет Mainвыглядеть так

class Main {
    private DoSomethingFactory doSomethingFactory;

     public doingIt(String info) {
         int quality = obtainQualityInfo();
         DoSomething doSomethingWorker = doSomethingFactory.Create(info, quality);
         doSomethingWorker.letDoIt();
     }
}

Я знаю, что вы написали, что

потому что по соображениям производительности конструирование DoItThisWay и DoItThatWay выполняется один раз в конструкторе Main

Но вы можете кэшировать детали, которые дорого создавать на фабрике, и передавать их конструктору.

Пол Керчер
источник
Ты шпионишь за мной, пока я печатаю ответы? Мы делаем подобные
замечания
1
@DocBrown Да, это спорно, но так как DoItThisWayтребует качества , чтобы быть установлен перед вызовом letDoItон ведет себя по- разному. Я ожидаю, что DoSomethingбудет работать одинаково (не устанавливая качество раньше) для всех производных. Имеет ли это смысл? Конечно, это немного размыто, так как если бы мы вызывали setQualityметод фабрики, клиент не знал бы, нужно ли устанавливать качество или нет.
Пол Керчер
2
@DocBrown Я бы предположил, что контракт для letDoIt()(без какой-либо дополнительной информации) «Я могу правильно выполнить (делать все, что я делаю), если вы предоставите мне String info». Требование setQuality()предварительного вызова усиливает предварительное условие вызова letDoIt()и, таким образом, будет нарушением LSP.
CharonX
2
@CharonX: если, например, будет контракт, подразумевающий, что " letDoIt" не будет выбрасывать какие-либо исключения, а если забыть вызвать "setQuality", то возникнет letDoItисключение, тогда я согласен, что такая реализация нарушит LSP. Но это выглядит как очень искусственные предположения для меня.
Док Браун
1
Это хороший ответ. Единственное, что я хотел бы добавить, это некоторые комментарии о том, как катастрофически плохая временная связь для кодовой базы. Это скрытый контракт между, казалось бы, отделенными компонентами. При нормальной связи, по крайней мере, это явно и легко идентифицируется. Делать это, по сути, копать яму и покрывать ее листьями.
JimmyJames
10

Давайте предположим, что qualityнельзя передать в конструктор, и setQualityтребуется вызов .

В настоящее время фрагмент кода, как

    int quality = obtainQualityInfo();
    worker = new DoItThisWay();
    ((DoItThisWay) worker).setQuality(quality);

слишком мал, чтобы вкладывать в него слишком много мыслей. ИМХО это выглядит немного некрасиво, но не очень сложно понять.

Проблемы возникают, когда такой фрагмент кода растет и из-за рефакторинга у вас получается что-то вроде

    int quality = obtainQualityInfo();
    worker = CreateAWorkerForThisInfo();
    ((DoItThisWay) worker).setQuality(quality);

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

Тем не менее, я бы на самом деле дал tmpлучшее имя:

      int quality = obtainQualityInfo();
      DoItThisWay workerThisWay = new DoItThisWay();
      workerThisWay.setQuality(quality)
      worker = workerThisWay;

Такое именование помогает сделать неправильный код неправильным.

Док Браун
источник
TMP может быть даже лучше назвать что-то вроде "workerThatUsesQuality"
user949300
1
@ user949300: ИМХО, это не очень хорошая идея: предположим, DoItThisWayполучаются более конкретные параметры - в зависимости от того, что вы предлагаете, имя нужно менять каждый раз. Лучше использовать имена vor переменных, которые проясняют тип объекта, а не то, какие имена методов доступны.
Док Браун
Для пояснения, это будет имя переменной, а не имя класса. Я согласен, что помещать все возможности в название класса - плохая идея. Поэтому я предлагаю workerThisWayстать чем-то вроде usingQuality. В этом небольшом фрагменте кода безопаснее быть более конкретным. (И если в их домене есть фраза лучше, чем «usingQuality», используйте ее.
user949300
2

Общий интерфейс, где инициализация изменяется в зависимости от данных времени выполнения, обычно поддается фабричной схеме.

DoThingsFactory factory = new DoThingsFactory(thingThatProvidesQuality);
DoSomething doSomething = factory.getDoer(info);

doSomething.letDoIt();

Затем DoThingsFactory может беспокоиться о получении и установке информации о качестве внутри getDoer(info)метода перед возвратом конкретного объекта DoThingsWithQuality, приведенного к интерфейсу DoSomething.

Грег Бургардт
источник