Czy nadrzędne metody to zapach kodu?

31

Czy to prawda, że ​​nadrzędnymi konkretnymi metodami jest zapach kodu? Ponieważ uważam, że jeśli chcesz zastąpić konkretne metody:

public class A{
    public void a(){
    }
}

public class B extends A{
    @Override
    public void a(){
    }
}

można go przepisać jako

public interface A{
    public void a();
}

public class ConcreteA implements A{
    public void a();
}

public class B implements A{
    public void a(){
    }
}

a jeśli B chce ponownie użyć a () w A, można go przepisać jako:

public class B implements A{
    public ConcreteA concreteA;
    public void a(){
        concreteA.a();
    }
}

który nie wymaga dziedziczenia, aby zastąpić metodę, czy to prawda?

grgrr
źródło
4
Głosowane, ponieważ (w komentarzach poniżej), Telastyn i Doc Brown zgadzają się na zastąpienie, ale udzielają przeciwnych odpowiedzi tak / nie na główne pytanie, ponieważ nie zgadzają się co do znaczenia „zapachu kodu”. Tak więc pytanie, które miało dotyczyć projektowania kodu, zostało mocno powiązane z fragmentem slangu. I myślę, że nie jest to konieczne, ponieważ pytanie dotyczy konkretnie jednej techniki przeciwko drugiej.
Steve Jessop,
5
Pytanie nie jest oznaczone Java, ale kod wydaje się być Java. W języku C # (lub C ++) należy użyć virtualsłowa kluczowego do obsługi zastąpień. Tak więc problem jest bardziej (lub mniej) niejednoznaczny ze względu na szczegóły języka.
Erik Eidt,
1
Wygląda na to, że prawie każda funkcja języka programowania nieuchronnie zostaje sklasyfikowana jako „zapach kodu” po wystarczającej liczbie lat.
Brandon
2
Wydaje mi się, że finalmodyfikator istnieje dokładnie w takich sytuacjach. Jeśli zachowanie metody jest takie, że nie jest przeznaczone do zastąpienia, oznacz ją jako final. W przeciwnym razie spodziewaj się, że programiści mogą go zastąpić.
Martin Tuskevicius

Odpowiedzi:

34

Nie, to nie jest zapach kodu.

  • Jeśli klasa nie jest ostateczna, można ją zaklasyfikować do podklasy.
  • Jeśli metoda nie jest ostateczna, można ją zastąpić.

Odpowiedzialność każdej klasy leży w starannym rozważeniu, czy podklasowanie jest właściwe i które metody można zastąpić .

Klasa może zdefiniować siebie lub dowolną metodę jako ostateczną, lub może nałożyć ograniczenia (modyfikatory widoczności, dostępne konstruktory) na sposób i miejsce jej podziału.

Zwykłym przypadkiem zastąpienia metod jest domyślna implementacja w klasie bazowej, którą można dostosować lub zoptymalizować w podklasie (szczególnie w Javie 8 wraz z pojawieniem się domyślnych metod w interfejsach).

class A {
    public String getDescription(Element e) {
        // return default description for element
    }
}

class B extends A {
    public String getDescription(Element e) {
        // return customized description for element
    }
}

Alternatywa dla nadrzędnym jest zachowanie wzoru Strategia , w której zachowanie jest wydobywane jako interfejs i implementacja może być ustawiony w klasie.

interface DescriptionProvider {
    String getDescription(Element e);
}

class A {
    private DescriptionProvider provider=new DefaultDescriptionProvider();

    public final String getDescription(Element e) {
       return provider.getDescription(e);
    }

    public final void setDescriptionProvider(@NotNull DescriptionProvider provider) {
        this.provider=provider;
    }
}

class B extends A {
    public B() {
        setDescriptionProvider(new CustomDescriptionProvider());
    }
}
Peter Walser
źródło
Rozumiem, że w prawdziwym świecie zastąpienie konkretnej metody może dla niektórych być zapachem kodu. Ale podoba mi się ta odpowiedź, ponieważ wydaje mi się bardziej oparta na specyfikacji.
Darkwater23
Czy w ostatnim przykładzie nie należy Aimplementować DescriptionProvider?
Zauważył
@Spotted: A może implementować DescriptionProvider, a zatem być domyślnym dostawcą opisu. Ale chodzi tutaj o rozdzielenie problemów: Awymaga opisu (niektóre inne klasy też mogą), podczas gdy inne implementacje je zapewniają.
Peter Walser,
2
Ok, brzmi bardziej jak wzorzec strategii .
Zauważył
@Spotted: masz rację, przykład ilustruje wzór strategii, a nie wzór dekoratora (dekorator dodałby zachowanie do komponentu). Poprawię swoją odpowiedź, dzięki.
Peter Walser,
25

Czy to prawda, że ​​nadrzędnymi konkretnymi metodami jest zapach kodu?

Tak, ogólnie rzecz biorąc, nadrzędnymi konkretnymi metodami jest zapach kodu.

Ponieważ metoda podstawowa jest powiązana z zachowaniem, które programiści zwykle szanują, zmiana ta będzie prowadzić do błędów, gdy twoja implementacja zrobi coś innego. Co gorsza, jeśli zmienią zachowanie, poprzednio poprawna implementacja może zaostrzyć problem. Ogólnie rzecz biorąc, dość trudne jest przestrzeganie zasady substytucji Liskowa w przypadku nietrywialnych konkretnych metod.

Teraz są przypadki, w których można to zrobić i jest to dobre. Półtrywialne metody można nieco bezpiecznie ominąć. Podstawowe metody, które istnieją tylko jako „rozsądne” zachowanie, które ma być nadrzędne, mają dobre zastosowanie.

Dlatego wydaje mi się to zapachem - czasem dobrze, zwykle źle, spójrz, aby się upewnić.

Telastyn
źródło
29
Twoje wyjaśnienie jest w porządku, ale doszedłem do dokładnie przeciwnego wniosku: „nie, nadpisywanie konkretnych metod nie jest ogólnie zapachem kodu” - jest to tylko zapach kodu, gdy nadpisuje się metody, których nie zamierzano zastąpić. ;-)
Doc Brown
2
@DocBrown Dokładnie! Widzę też, że wyjaśnienie (a zwłaszcza wniosek) sprzeciwiają się wstępnemu stwierdzeniu.
Tersosauros 11.04.16
1
@Spotted: Najprostszym kontrprzykładem jest Numberklasa z increment()metodą, która ustawia wartość na następną prawidłową wartość. Jeśli podklasujesz to w miejsce, EvenNumbergdzie increment()dodaje dwa zamiast jednego (a ustawiający wymusza równość), pierwsza propozycja OP wymaga zduplikowanych implementacji każdej metody w klasie, a jego druga wymaga pisania metod otoki w celu wywołania metod w elemencie. Częścią dziedziczenia jest, aby klasy dziecięce wyjaśniły, czym różnią się od rodziców, podając tylko te metody, które muszą być różne.
Blrfl 11.04.16
5
@DocBrown: bycie zapachem kodu nie oznacza, że ​​coś jest koniecznie złe, tylko że jest prawdopodobne .
mikołak
3
@docbrown - dla mnie jest to zgodne z „faworyzowaniem kompozycji zamiast dziedziczenia”. Jeśli przesadzasz z konkretnymi metodami, jesteś już w krainie o wąskim zapachu, by mieć dziedzictwo. Jakie są szanse, że przerosniesz coś, czym nie powinieneś być? Na podstawie mojego doświadczenia sądzę, że jest to prawdopodobne. YMMV.
Telastyn
7

jeśli chcesz zastąpić konkretne metody [...], możesz je przepisać jako

Jeśli można go przepisać w ten sposób, oznacza to, że masz kontrolę nad obiema klasami. Ale wtedy wiesz, czy zaprojektowałeś w ten sposób klasę A, a jeśli tak, to nie jest to zapach kodu.

Jeśli z drugiej strony nie masz kontroli nad klasą podstawową, nie możesz przepisać w ten sposób. Więc albo klasa została zaprojektowana w taki sposób, aby wyprowadzić ją w ten sposób, w takim przypadku po prostu śmiało, nie jest to zapach kodu, albo nie było, w którym to przypadku masz dwie opcje: poszukaj innego rozwiązania w ogóle, lub idź naprzód , ponieważ atuty robocze projektują czystość.

Jan Hudec
źródło
Gdyby klasę A zaprojektowano do wyprowadzenia, czy nie byłoby sensowniej mieć abstrakcyjną metodę jasnego wyrażania intencji? Skąd ten, kto zastępuje metodę, może wiedzieć, że metoda została zaprojektowana w ten sposób?
Zauważył
@Spotted, istnieje wiele przypadków, w których można wprowadzić domyślne implementacje, a każda specjalizacja musi tylko zastąpić niektóre z nich, albo w celu rozszerzenia funkcjonalności, albo w celu zwiększenia wydajności. Ekstremalnym przykładem są goście. Użytkownik wie, w jaki sposób metody zostały zaprojektowane na podstawie dokumentacji; musi tam być, aby wyjaśnić, czego oczekuje się od zastąpienia w obu przypadkach.
Jan Hudec
Rozumiem twój punkt widzenia, ale uważam, że to niebezpieczne, że jedynym sposobem, aby programista, aby poznać metodę, którą można zastąpić, jest przeczytanie dokumentu, ponieważ: 1) jeśli musi być zastąpiona, nie mam gwarancji, że zostanie wykonana. 2) jeśli nie można go zastąpić, ktoś zrobi to dla wygody 3) nie każdy czyta dokument. Poza tym nigdy nie spotkałem się z przypadkiem, w którym musiałem zastąpić całą metodę, ale tylko części (i skończyłem na szablonie).
Zauważył
@Spotted, 1) jeśli musi być zastąpione, powinno być abstract; ale w wielu przypadkach nie musi tak być. 2) jeśli nie można go przesłonić, powinien być final(nie-wirtualny). Ale wciąż istnieje wiele przypadków, w których metody mogą zostać zastąpione. 3) jeśli dalszy programista nie czyta dokumentów, postrzelą się w stopę, ale zrobią to bez względu na zastosowane środki.
Jan Hudec
Ok, więc nasz punkt widzenia rozbieżność polega tylko na 1 słowie. Mówicie, że każda metoda powinna być abstrakcyjna lub ostateczna, a ja mówię, że każda metoda musi być abstrakcyjna lub ostateczna. :) W jakich przypadkach zastąpienie metody jest konieczne (i bezpieczne)?
Zauważył
3

Po pierwsze, chciałbym tylko zaznaczyć, że to pytanie ma zastosowanie tylko w niektórych systemach pisania. Na przykład, w typowania strukturalnych i kaczka wpisywanie systemów - klasa po prostu mając metodę o tej samej nazwie i podpis będzie oznaczać , że klasa była wpisać kompatybilny (przynajmniej dla tego konkretnego sposobu, w przypadku Duck wpisywanie).

Jest to (oczywiście) bardzo różne od dziedziny języków o typie statycznym , takich jak Java, C ++, itp. Jak również charakter silnego pisania , używanego zarówno w Javie i C ++, jak i C # i innych.


Domyślam idziesz z tła Java, gdzie metody muszą być wyraźnie oznaczone jako final jeśli projektant kontrakt zamierza dla nich nie do zmiany. Jednak w językach takich jak C # ustawienie domyślne jest odwrotne. Metody (bez dekoracji, szczególne słowa kluczowe), „ zamknięte ” (C # 's wersja final) i musi być jawnie zadeklarowane jako virtualjeśli są one dopuszczone do być pominięte.

Jeśli interfejs API lub biblioteka została zaprojektowana w tych językach przy użyciu konkretnej metody, którą można zastąpić, wówczas (przynajmniej w niektórych przypadkach) musi mieć sens zastąpienie tej metody . Dlatego nie jest to zapach kodu zastępujący niezamkniętą / wirtualną / nie finalkonkretną metodę.     (Zakłada się oczywiście, że projektanci API przestrzegają konwencji i albo oznaczają jako virtual(C #), albo oznaczają jako final(Java) odpowiednie metody do tego, co projektują.)


Zobacz też

Tersozaury
źródło
Czy w typowaniu kaczym dwie klasy posiadające tę samą sygnaturę metody są wystarczające, aby były kompatybilne z typem, czy też konieczne jest, aby metody miały taką samą semantykę, aby można je było zastąpić pewną domyślną klasą bazową?
Wayne Conrad,
2

Tak, ponieważ może to prowadzić do wywołania super -wzorca. Może również denaturować początkowy cel metody, wprowadzać skutki uboczne (=> błędy) i powodować niepowodzenia testów. Czy użyłbyś klasy, której testy jednostkowe są czerwone (wypełnianie)? Nie będę

Pójdę jeszcze dalej, mówiąc, że początkowy zapach kodu występuje, gdy metoda nie jest abstractani final. Ponieważ pozwala to zmienić (poprzez przesłonięcie) zachowanie skonstruowanej istoty (to zachowanie może zostać utracone lub zmienione). Z abstracti finalintencja jest jednoznaczna:

  • Streszczenie: musisz zapewnić zachowanie
  • Finał: jesteś nie wolno modyfikować zachowanie

Najbezpieczniejszym sposobem na dodanie zachowania do istniejącej klasy jest to, co pokazuje twój ostatni przykład: użycie wzoru dekoratora.

public interface A{
    void a();
}

public final class ConcreteA implements A{
    public void a() {
        ...
    }
}

public final class B implements A{
    private final ConcreteA concreteA;
    public void a(){
        //Additionnal behaviour
        concreteA.a();
    }
}
Cętkowany
źródło
9
To czarno-białe podejście nie jest aż tak przydatne. Pomyśl o Object.toString()Javie ... Gdyby tak było abstract, wiele domyślnych rzeczy nie działałoby, a kod nawet nie skompilowałby się, gdyby ktoś nie przesłonił toString()metody! Gdyby tak było final, sytuacja byłaby jeszcze gorsza - brak możliwości konwersji obiektów na ciąg znaków, z wyjątkiem Java. Jak mówi odpowiedź @Peter Walser, ważne implementacje domyślne (takie jak Object.toString()) . Zwłaszcza w domyślnych metodach Java 8.
Tersosauros
@Tersosauros Masz rację, jeśli toString()było albo abstractczy finalbyłoby to ból. Moja osobista opinia jest taka, że ​​ta metoda jest zepsuta i lepiej byłoby w ogóle jej nie mieć (jak equals i hashCode ). Pierwotnym celem domyślnych ustawień Javy jest umożliwienie ewolucji API z retrokompatybilnością.
Zauważył
Słowo „retrocompatibility” ma strasznie dużo sylab.
Craig,
@Spotted Jestem bardzo rozczarowany ogólną akceptacją konkretnego dziedziczenia na tej stronie, spodziewałem się lepszego: / Twoja odpowiedź powinna mieć o wiele więcej głosów pozytywnych
TheCatWhisperer
1
@TheCatWhisperer Zgadzam się, ale z drugiej strony tak długo zajęło mi odkrycie subtelnych zalet korzystania z dekoracji nad konkretnym dziedzictwem (w rzeczywistości stało się jasne, kiedy zacząłem pisać testy), że nie można za to winić nikogo . Najlepiej jest uczyć innych, dopóki nie odkryją Prawdy (żartu). :)
Zauważyłem