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 JSwing
aplikacji budowanej w klasie
Należy rozszerzyć wszystkich JSwing
klas ( 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 setText
funkcja.
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 Okay
i Cancel
przyciskó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ć setText
na Okay
przycisk i zmienić go. W takim przypadku podklasa staje się po prostu uciążliwa.
JButton
i wywoływać metody publiczne w konstruktorze, skoro można po prostu utworzyćJButton
i wywołać te same metody publiczne poza klasą?Odpowiedzi:
Jest to niezgodne z zasadą substytucji Liskowa, ponieważ
OkayButton
nie można go zastąpić w żadnym miejscu, w którymButton
można się spodziewać. Na przykład możesz zmienić etykietę dowolnego przycisku, jak chcesz. Ale robienie tego przyOkayButton
naruszeniu 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.
źródło
OkayButton
ma niezmiennik, o którym myślisz.OkayButton
Nie 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.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.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ówiif (myText == "OK") ProcessOK(); else ProcessCancel();
? Następnie tekst jest częścią niezmiennika.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ń.
źródło
JButton
,JCheckBox
,JRadioButton
jest 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ę.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?)
źródło
JPanel
. W rzeczywistości bardziej przydatne jest podklasowanie tegoJFrame
, ponieważ panel jest tylko abstrakcyjnym kontenerem.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:
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:
Te typy, które właśnie utworzyłeś, nie przynoszą żadnego pożytku. Ale:
Teraz stworzyłeś coś przydatnego.
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ą.
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.
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
mui
Uczyniłem wskaźnik końcowym - nie musi być zmienny.źródło
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
extend
tejJButton
klasy po prostu ustawić swój tekst. Nie masz doextend
tejJFrame
klasy 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ę
extends
innej 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
JPanel
klasy. (Więcej informacji na ten temat tutaj .) Można rozszerzyćJPanel
klasę, bo trzeba zastąpić napaintComponent()
funkcję. W ten sposób zmieniasz zachowanie klasy . Nie można wykonać niestandardowego malowania z domyślnąJPanel
implementacją.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ęć.
źródło
Border
malowanie dodatkowych dekoracji. Lub utwórzJLabel
i przekaż niestandardowąIcon
implementację. PodklasaJPanel
do malowania nie jest konieczna (i dlaczegoJPanel
zamiastJComponent
).