Kiedy powinienem rozszerzyć klasę Java Swing?

35

Moje obecne rozumienie implementacji dziedziczenia jest takie, że należy rozszerzać klasę tylko wtedy, gdy istnieje relacja IS-A . Jeśli klasa nadrzędna może dodatkowo mieć bardziej szczegółowe typy podrzędne o różnych funkcjach, ale będzie miała wspólne elementy wyodrębnione w obiekcie nadrzędnym.

Kwestionuję to zrozumienie z powodu tego, co zaleca nam mój profesor Java. Zalecił, aby w przypadku JSwingaplikacji budowanej w klasie

Należy rozszerzyć wszystkich JSwingklas ( JFrame, JButton, JTextBox, etc) do oddzielnych klas niestandardowych i określić dostosowanie GUI powiązane w nich (podobnie jak wielkość składnika, etykiety komponentów, etc)

Jak dotąd jest tak dobry, ale dalej radzi, aby każdy JButton miał własną niestandardową rozszerzoną klasę, chociaż jedynym wyróżnikiem jest jej etykieta.

Na przykład jeśli GUI ma dwa przyciski OK i Anuluj . Zaleca przedłużenie ich jak poniżej:

class OkayButton extends JButton{
    MainUI mui;
    public OkayButton(MainUI mui) {
        setSize(80,60);
        setText("Okay");
        this.mui = mui;
        mui.add(this);        
    }
}

class CancelButton extends JButton{
    MainUI mui;
    public CancelButton(MainUI mui) {
        setSize(80,60);
        setText("Cancel");
        this.mui = mui;
        mui.add(this);        
    }
}

Jak widać jedyną różnicą jest setTextfunkcja.

Więc czy to standardowa praktyka?

Btw, kurs, w którym to omówiono, nazywa się Najlepszymi praktykami programowania w Javie

[Odpowiedź od prof]

Omówiłem więc problem z profesorem i podniosłem wszystkie punkty wymienione w odpowiedziach.

Jego uzasadnieniem jest to, że podklasa zapewnia kod wielokrotnego użytku przy zachowaniu standardów projektowania GUI. Na przykład, jeśli programista użył przycisków niestandardowych Okayi Cancelprzycisków w jednym oknie, łatwiej będzie umieścić te same przyciski również w innych oknach.

Wydaje mi się, że mam powód, ale wciąż wykorzystuje dziedziczenie i sprawia, że ​​kod jest kruchy.

Później każdy programista mógł przypadkowo wywołać setTextna Okayprzycisk i zmienić go. W takim przypadku podklasa staje się po prostu uciążliwa.

Paras
źródło
3
Po co rozszerzać JButtoni wywoływać metody publiczne w konstruktorze, skoro można po prostu utworzyć JButtoni wywołać te same metody publiczne poza klasą?
17
trzymaj się z dala od tego profesora, jeśli możesz. Jest to dalekie od najlepszych praktyk . Duplikacja kodu, jak pokazano na ilustracji, to bardzo nieprzyjemny zapach.
njzk2
7
Zapytaj go dlaczego, nie wahaj się przekazać tutaj swojej motywacji.
Alex
9
Chcę głosować, bo to okropny kod, ale chcę też głosować, ponieważ to świetnie, że pytasz, a nie ślepo akceptujesz słowa złego profesora.
Pozew Fund Moniki w
1
@Alex Zaktualizowałem pytanie uzasadnieniem prof
Paras

Odpowiedzi:

21

Jest to niezgodne z zasadą substytucji Liskowa, ponieważ OkayButtonnie można go zastąpić w żadnym miejscu, w którym Buttonmożna się spodziewać. Na przykład możesz zmienić etykietę dowolnego przycisku, jak chcesz. Ale robienie tego przy OkayButtonnaruszeniu wewnętrznych niezmienników.

Jest to klasyczne niewłaściwe użycie dziedziczenia do ponownego użycia kodu. Zamiast tego użyj metody pomocniczej.

Innym powodem, aby tego nie robić, jest to, że jest to po prostu skomplikowany sposób osiągnięcia tego samego, co zrobiłby kod liniowy.

usr
źródło
Nie narusza to LSP, chyba że OkayButtonma niezmiennik, o którym myślisz. OkayButtonNie może twierdzić, że jakiekolwiek dodatkowe niezmienniki, może jedynie rozważyć tekst jako domyślne i zamierza w pełni wspierać zmiany w tekście. Nadal nie jest to dobry pomysł, ale nie z tego powodu.
hvd
Nie narusza tego, ponieważ możesz. To znaczy, jeśli metoda activateButton(Button btn)oczekuje przycisku, możesz z łatwością podać mu instancję OkayButton. Zasada nie oznacza, że ​​musi mieć dokładnie taką samą funkcjonalność, ponieważ dzięki temu dziedziczenie byłoby praktycznie bezużyteczne.
Sebb
1
@Sebb, ale nie można go używać z funkcją, setText(button, "x")ponieważ narusza to (zakładane) niezmiennik. To prawda, że ​​ten konkretny OkayButton może nie mieć żadnego interesującego niezmiennika, więc chyba wybrałem zły przykład. Ale może. Na przykład, co jeśli moduł obsługi kliknięć mówi if (myText == "OK") ProcessOK(); else ProcessCancel();? Następnie tekst jest częścią niezmiennika.
usr
@usr Nie myślałem, że tekst jest częścią niezmiennika. Masz rację, to wtedy zostało naruszone.
Sebb
50

Jest to całkowicie okropne pod każdym możliwym względem. W najwyżej użyj funkcji fabrycznej, aby utworzyć JButtons. Powinieneś dziedziczyć po nich tylko wtedy, gdy potrzebujesz poważnych rozszerzeń.

DeadMG
źródło
20
+1. Więcej informacji można znaleźć w hierarchii klas Swing w AbstractButton . Następnie spójrz na hierarchię JToggleButton . Dziedziczenie służy do konceptualizacji różnych typów przycisków. Ale nie służy do różnicowania koncepcji biznesowych ( Tak, Nie, Kontynuuj, Anuluj ... ).
Laiv
2
@Laiv: nawet o różnych rodzajów dla, na przykład JButton, JCheckBox, JRadioButtonjest tylko ustępstwo twórców zaznajamiania się z innymi narzędziami, jak zwykły AWT, gdzie takie typy są standardowe. Zasadniczo wszystkie mogą być obsługiwane przez klasę jednego przycisku. To połączenie modelu i delegata interfejsu użytkownika robi różnicę.
Holger
Tak, można je obsłużyć za pomocą jednego przycisku. Jednak faktyczną hierarchię można ujawnić jako dobre praktyki. Utworzenie nowego przycisku lub po prostu przekazanie kontrolera zdarzenia i behaivora innym komponentom jest kwestią preferencji. Gdybym to ja, rozszerzyłbym komponenty Swing, aby wymodelować mój przycisk owm i zawrzeć jego zachowania, działania, ... Aby ułatwić jego implementację w projekcie i ułatwić młodszym. W środowisku sieciowym wolę komponenty sieciowe niż zbyt dużą obsługę zdarzeń. Tak czy inaczej. Masz rację, możemy oddzielić interfejs użytkownika od Behaivors.
Laiv
12

Jest to bardzo niestandardowy sposób pisania kodu Swing. Zazwyczaj rzadko tworzysz podklasy składników Swing UI, najczęściej JFrame (w celu skonfigurowania okien potomnych i programów obsługi zdarzeń), ale nawet ta podklasa jest niepotrzebna i wielu odradza. Zapewnianie dostosowywania tekstu do przycisków i tak dalej jest zwykle wykonywane przez rozszerzenie klasy AbstractAction (lub interfejsu Action, który zapewnia). Może to zapewnić tekst, ikony i inne niezbędne dostosowania wizualne oraz połączyć je z rzeczywistym kodem, który reprezentują. Jest to o wiele lepszy sposób pisania kodu interfejsu użytkownika niż pokazywane przykłady.

(przy okazji, Google Scholar nigdy nie słyszał o cytowanym przez siebie artykule - czy masz bardziej precyzyjne odniesienie?)

Jules
źródło
Jaki dokładnie jest „standardowy sposób”? Zgadzam się, że napisanie podklasy dla każdego komponentu jest prawdopodobnie złym pomysłem, ale oficjalne samouczki Swing nadal sprzyjają tej praktyce. Myślę, że profesor po prostu postępuje zgodnie ze stylem pokazanym w oficjalnej dokumentacji frameworka.
PRZYJEDŹ OD
@COMEFROM Przyznaję, że nie przeczytałem całego samouczka Swing, więc może przegapiłem, gdzie ten styl jest używany, ale strony, na które patrzyłem, zwykle używają klas podstawowych, a nie kierują podklasami, np. Docs.oracle .pl / javase / tutorial / uiswing / components / button.html
Jules
@Jules przepraszam za zamieszanie, miałem na myśli kurs / artykuł, który wykłada profesor, nazywa się Best Practices in Java
Paras
1
Zasadniczo prawda, ale jest jeszcze jeden ważny wyjątek - JPanel. W rzeczywistości bardziej przydatne jest podklasowanie tego JFrame, ponieważ panel jest tylko abstrakcyjnym kontenerem.
Zwyczajny
10

IMHO, najlepsze praktyki programowania w Javie są zdefiniowane w książce Joshua Blocha „Effective Java”. To wspaniale, że twój nauczyciel daje ci ćwiczenia OOP i ważne jest, aby nauczyć się czytać i pisać style programowania innych ludzi. Ale poza książką Josha Blocha opinie na temat najlepszych praktyk różnią się znacznie.

Jeśli zamierzasz rozszerzyć tę klasę, równie dobrze możesz skorzystać z dziedziczenia. Utwórz klasę MyButton, aby zarządzać wspólnym kodem i podklasuj go dla części zmiennych:

class MyButton extends JButton{
    protected final MainUI mui;
    public MyButton(MainUI mui, String text) {
        setSize(80,60);
        setText(text);
        this.mui = mui;
        mui.add(this);        
    }
}

class OkayButton extends MyButton{
    public OkayButton(MainUI mui) {
        super(mui, "Okay");
    }
}

class CancelButton extends MyButton{
    public CancelButton(MainUI mui) {
        super(mui, "Cancel");
    }
}

Kiedy to jest dobry pomysł? Kiedy korzystasz z typów, które stworzyłeś! Na przykład, jeśli masz funkcję tworzenia wyskakującego okna, a sygnatura to:

public void showPopUp(String text, JButton ok, JButton cancel)

Te typy, które właśnie utworzyłeś, nie przynoszą żadnego pożytku. Ale:

public void showPopUp(String text, OkButton ok, CancelButton cancel)

Teraz stworzyłeś coś przydatnego.

  1. Kompilator sprawdza, czy showPopUp przyjmuje OkButton i CancelButton. Ktoś czytający kod wie, jak ma być używana ta funkcja, ponieważ tego rodzaju dokumentacja spowoduje błąd kompilacji, jeśli przestanie być aktualny. Jest to GŁÓWNA korzyść. W 1 lub 2 badaniach empirycznych dotyczących korzyści bezpieczeństwa typu stwierdzono, że zrozumienie kodu ludzkiego było jedyną wymierną korzyścią.

  2. Zapobiega to również błędom w przypadku odwrócenia kolejności przycisków przekazywanych do funkcji. Błędy te są trudne do wykrycia, ale są również dość rzadkie, więc jest to MNIEJSZA korzyść. Zostało to wykorzystane do sprzedaży bezpieczeństwa typu, ale nie zostało udowodnione empirycznie, że jest szczególnie przydatne.

  3. Pierwsza forma funkcji jest bardziej elastyczna, ponieważ wyświetla dwa dowolne przyciski. Czasami jest to lepsze niż ograniczanie, jakiego rodzaju przycisków zajmie. Pamiętaj tylko, że musisz przetestować funkcję dowolnego przycisku w większej liczbie przypadków - jeśli działa ona tylko wtedy, gdy przekażesz jej dokładnie odpowiedni rodzaj przycisków, nie zrobisz nikomu przysługi, udając, że zajmie ona jakikolwiek przycisk.

Problem z programowaniem obiektowym polega na tym, że stawia wózek przed koniem. Musisz najpierw napisać funkcję, aby wiedzieć, co ma wiedzieć podpis, jeśli warto tworzyć dla niego typy. Ale w Javie musisz najpierw utworzyć typy, aby móc napisać podpis funkcji.

Z tego powodu możesz chcieć napisać kod Clojure, aby zobaczyć, jak wspaniale może być napisanie najpierw twoich funkcji. Możesz pisać szybko, myśląc o asymptotycznej złożoności tak bardzo, jak to możliwe, a założenie o niezmienności Clojure zapobiega błędom tak bardzo, jak robią to typy w Javie.

Nadal jestem fanem typów statycznych dla dużych projektów, a teraz używam narzędzi funkcjonalnych, które pozwalają mi najpierw pisać funkcje, a później nazwać moje typy w Javie . Tylko myśl.

PS muiUczyniłem wskaźnik końcowym - nie musi być zmienny.

GlenPeterson
źródło
6
Z 3 punktów, 1 i 3 można równie dobrze obsłużyć za pomocą ogólnych przycisków J i odpowiedniego schematu nazewnictwa parametrów wejściowych. punkt 2 jest tak naprawdę wadą, ponieważ sprawia, że ​​kod jest ściślej powiązany: co zrobić, jeśli chcesz, aby kolejność przycisków została odwrócona? Co jeśli zamiast przycisków OK / Anuluj chcesz użyć przycisków Tak / Nie? Czy stworzysz zupełnie nową metodę showPopup, która przyjmuje YesButton i Nobutton? Jeśli używasz tylko domyślnych przycisków J, nie musisz najpierw tworzyć typów, ponieważ już istnieją.
Nzall
Rozszerzając to, co powiedział @NateKerkhofs, nowoczesne IDE pokaże formalne nazwy argumentów podczas pisania rzeczywistych wyrażeń.
Solomon Slow
8

Byłbym skłonny założyć się, że twój nauczyciel tak naprawdę nie wierzy, że zawsze powinieneś przedłużać komponent Swing, aby go użyć. Założę się, że wykorzystują to jako przykład, aby zmusić cię do ćwiczenia przedłużania zajęć. Na razie nie martwiłbym się zbytnio najlepszymi praktykami w świecie rzeczywistym.

To powiedziawszy, w prawdziwym świecie wolimy kompozycję niż dziedziczenie .

Twoja reguła „należy rozszerzać klasę tylko wtedy, gdy istnieje relacja IS-A” jest niepełna. Powinien kończyć się „... i musimy zmienić domyślne zachowanie klasy” dużymi pogrubionymi literami.

Twoje przykłady nie spełniają tych kryteriów. Nie masz do extendtej JButtonklasy po prostu ustawić swój tekst. Nie masz do extendtej JFrameklasy tylko dodać elementy do niego. Możesz zrobić te rzeczy dobrze, używając ich domyślnych implementacji, więc dodanie dziedziczenia to tylko niepotrzebne komplikacje.

Jeśli zobaczę klasę extendsinnej klasy, zastanawiam się, co się zmienia . Jeśli niczego nie zmieniasz, nie zmuszaj mnie wcale do przeglądania klasy.

Powrót do pytania: kiedy należy rozszerzyć klasę Java? Kiedy masz naprawdę, naprawdę, naprawdę dobry powód, aby przedłużyć klasę.

Oto konkretny przykład: jednym ze sposobów wykonania niestandardowego malowania (dla gry lub animacji, lub tylko dla niestandardowego komponentu) jest rozszerzenie JPanelklasy. (Więcej informacji na ten temat tutaj .) Można rozszerzyć JPanelklasę, bo trzeba zastąpić na paintComponent()funkcję. W ten sposób zmieniasz zachowanie klasy . Nie można wykonać niestandardowego malowania z domyślną JPanelimplementacją.

Ale tak jak powiedziałem, twój nauczyciel prawdopodobnie używa tych przykładów jako wymówki, aby zmusić cię do ćwiczenia przedłużania zajęć.

Kevin Workman
źródło
1
Och, czekaj, możesz ustawić Bordermalowanie dodatkowych dekoracji. Lub utwórz JLabeli przekaż niestandardową Iconimplementację. Podklasa JPaneldo malowania nie jest konieczna (i dlaczego JPanelzamiast JComponent).
Holger
1
Myślę, że masz dobry pomysł, ale prawdopodobnie możesz wybrać lepsze przykłady.
1
Ale chcę powtórzyć, że twoja odpowiedź jest prawidłowa, a ty masz rację . Myślę, że konkretny przykład i samouczek słabo go wspierają.
1
@KevinWorkman Nie wiem o tworzeniu gier Swing, ale zrobiłem kilka niestandardowych interfejsów użytkownika w Swing i „nie rozszerzanie klas Swing, aby łatwo było łączyć komponenty i renderery przez config” jest tam dość standardowe.
1
„Założę się, że wykorzystują to jako przykład, aby zmusić cię do ...” Jeden z moich głównych wątków! Instruktorzy, którzy uczą, jak robić X, używając strasznie obłąkanych przykładów tego, jak robić X.
Solomon Slow