Jaki jest lepszy sposób na wywołanie metody, która jest dostępna tylko dla jednej klasy, która implementuje interfejs, ale nie dla drugiej?

11

Zasadniczo muszę wykonywać różne działania, biorąc pod uwagę określony warunek. Istniejący kod jest zapisywany w ten sposób

Interfejs podstawowy

// DoSomething.java
interface DoSomething {

   void letDoIt(String info);
}

Wdrożenie pierwszej klasy robotniczej

class DoItThisWay implements DoSomething {
  ...
}

Wdrożenie drugiej klasy robotniczej

class DoItThatWay implements DoSomething {
   ...
}

Główna klasa

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

Ten kod działa dobrze i jest łatwy do zrozumienia.

Teraz, ze względu na nowy wymóg, muszę przekazać nową informację, która ma sens DoItThisWay.

Moje pytanie brzmi: czy następujący styl kodowania jest dobry, aby poradzić sobie z tym wymogiem.

Użyj nowej zmiennej klasy i metody

// 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 {
     ...
   }
 } ;

}

Jeśli zrobię to w ten sposób, muszę wprowadzić odpowiednią zmianę do osoby dzwoniącej:

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

Czy to dobry styl kodowania? Czy mogę to po prostu rzucić

   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)
     }
Anthony Kong
źródło
19
Dlaczego nie przejść qualitydo konstruktora DoItThisWay?
David Arno,
I prawdopodobnie trzeba zmienić moje pytanie ... ponieważ z przyczyn wydajności budowę DoItThisWayi DoItThatWaysą wykonywane raz w konstruktorze Main. Mainjest klasą długowieczną i doingItnazywa się ją wiele razy.
Anthony Kong
Tak, w takim przypadku dobrym pomysłem byłoby zaktualizowanie pytania.
David Arno,
Czy chcesz powiedzieć, że setQualitymetoda będzie wywoływana wiele razy podczas życia DoItThisWayobiektu?
jpmc26
1
Nie ma istotnej różnicy między dwiema prezentowanymi wersjami. Z logicznego punktu widzenia robią to samo.
Sebastian Redl,

Odpowiedzi:

6

Zakładam, że to qualitymusi być ustawione obok każdego letDoIt()połączenia na DoItThisWay().

Problem widzę wynikające tutaj jest taka: Ty wprowadzamy sprzęgło czasowej (czyli co happends jeśli zapomnisz zadzwonić setQuality()przed wywołaniem letDoIt()na zasadzie DoItThisWay?). A implementacje są rozbieżne DoItThisWayi DoItThatWayrozbieżne (jedno trzeba setQuality()zadzwonić, drugie nie).

Chociaż może to nie powodować problemów w tej chwili, może w końcu cię prześladować. Warto spojrzeć jeszcze raz letDoIt()i zastanowić się, czy informacje jakościowe mogą być częścią infoprzekazywanej przez ciebie informacji; ale to zależy od szczegółów.

CharonX
źródło
15

Czy to dobry styl kodowania?

Z mojego punktu widzenia żadna z twoich wersji nie jest. Najpierw trzeba zadzwonić, setQualityzanim letDoItbędzie można go wywołać, to połączenie czasowe . Utkniesz oglądanie DoItThisWayjako pochodna DoSomething, ale to nie jest (przynajmniej nie funkcjonalnie), to raczej coś w rodzaju

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

To by było Mainraczej coś w rodzaju

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

Z drugiej strony możesz przekazać parametry bezpośrednio do klas (zakładając, że jest to możliwe) i przekazać decyzję, której z nich użyć do fabryki (sprawiłoby to, że instancje byłyby wymienne i oba można wyprowadzić DoSomething). To by Mainwyglądało tak

class Main {
    private DoSomethingFactory doSomethingFactory;

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

Wiem, że to napisałeś

ponieważ ze względu na wydajność konstrukcja DoItThisWay i DoItThatWay odbywa się raz w konstruktorze Main

Ale możesz buforować części, które są kosztowne do wytworzenia w fabryce i przekazać je również konstruktorowi.

Paul Kertscher
źródło
Argh, szpiegujesz mnie, pisząc odpowiedzi? Robimy takie podobne punkty (ale twój jest znacznie bardziej wszechstronny i wymowny);)
CharonX
1
@DocBrown Tak, jest dyskusyjna, ale ponieważ DoItThisWaywymaga ustawienia jakości przed wywołaniem letDoIt, zachowuje się inaczej. Spodziewałbym się, że a DoSomethingzadziała w taki sam sposób (wcześniej nie ustawiając jakości) dla wszystkich instrumentów pochodnych. Czy to ma sens? Oczywiście jest to trochę rozmyte, ponieważ jeśli setQualitywywołamy metodę fabryczną, klient będzie agnostyczny, czy jakość musi być ustawiona, czy nie.
Paul Kertscher
2
@DocBrown Zakładam, że umowa letDoIt()dotyczy (brak jakichkolwiek dodatkowych informacji) „Mogę poprawnie wykonać (zrobić wszystko, co robię), jeśli mi to zapewnisz String info”. Wymaganie setQuality()wcześniejszego wezwania wzmacnia warunek wstępny powołania letDoIt(), a zatem stanowiłoby naruszenie LSP.
CharonX,
2
@CharonX: jeśli, na przykład, istniałaby umowa sugerująca, że ​​„ letDoIt” nie wyrzuci żadnych wyjątków, a zapomnienie o wywołaniu „setQuality” spowoduje, że letDoItwyrzuci wyjątek, to zgodzę się, taka implementacja naruszy LSP. Ale te wyglądają dla mnie na bardzo sztuczne założenia.
Doc Brown
1
To dobra odpowiedź. Jedyne, co chciałbym dodać, to komentarz na temat katastrofalnie złego sprzężenia czasowego dla bazy kodu. Jest to ukryta umowa między pozornie oddzielonymi komponentami. Przy normalnym sprzężeniu przynajmniej jest to wyraźne i łatwe do zidentyfikowania. Robi to zasadniczo kopanie dołu i przykrywanie go liśćmi.
JimmyJames
10

Załóżmy, że qualitynie można przekazać do konstruktora, a wywołanie setQualityjest wymagane.

Obecnie fragment kodu podobny do

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

jest o wiele za mały, aby zainwestować w niego zbyt wiele myśli. IMHO wygląda trochę brzydko, ale nie bardzo trudno zrozumieć.

Problemy pojawiają się, gdy taki fragment kodu rośnie, a po refaktoryzacji powstaje coś takiego

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

Teraz nie widzisz od razu, czy ten kod jest nadal poprawny, a kompilator ci nie powie. Tak więc wprowadzenie tymczasowej zmiennej właściwego typu i unikanie rzutowania jest nieco bardziej bezpieczne dla typu, bez żadnego dodatkowego wysiłku.

Jednak tak naprawdę dałbym tmplepszą nazwę:

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

Takie nazewnictwo pomaga sprawić, że zły kod wygląda źle.

Doktor Brown
źródło
tmp może być jeszcze lepiej nazwany „pracownikThatUsesQuality”
user949300
1
@ user949300: IMHO to nie jest dobry pomysł: załóżmy, że DoItThisWayuzyskuje się bardziej szczegółowe parametry - według sugestii nazwa musi być zmieniana za każdym razem. Lepiej jest używać nazw vor zmiennych, które czynią typ obiektu jasnym, a nie nazwy metod, które są dostępne.
Doc Brown,
Aby wyjaśnić, będzie to nazwa zmiennej, a nie nazwa klasy. Zgadzam się, że umieszczenie wszystkich możliwości w nazwie klasy to kiepski pomysł. Więc sugeruję, żeby workerThisWaystało się coś w tym rodzaju usingQuality. W tym małym fragmencie kodu bezpieczniej jest być bardziej szczegółowym. (A jeśli w ich domenie jest lepsze wyrażenie niż „usingQuality”, użyj go.
user949300
2

Wspólny interfejs, w którym inicjalizacja różni się w zależności od danych wykonawczych, zwykle nadaje się do wzorca fabrycznego.

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

doSomething.letDoIt();

Następnie DoThingsFactory może martwić się uzyskaniem i ustawieniem informacji o jakości wewnątrz getDoer(info)metody przed zwróceniem konkretnego obiektu DoThingsWithQuality rzutowanego do interfejsu DoSomething.

Greg Burghardt
źródło