Dlaczego Go nie ma dziedziczenia
Część sekcji Programowanie obiektowe ścieżki GO w Coddy. Lekcja 34 z 107.
Tradycyjne języki obiektowe, takie jak Java i C++, używają dziedziczenia do współdzielenia kodu między typami. Klasa potomna rozszerza klasę nadrzędną, dziedzicząc wszystkie jej pola i metody. Go celowo pomija tę funkcję.
Dziedziczenie powoduje ścisłe powiązanie typów. Gdy zmienia się klasa nadrzędna, zmiany dotyczą wszystkich klas potomnych.
Głębokie hierarchie dziedziczenia stają się trudne do zrozumienia i utrzymania. „Problem kruchej klasy bazowej” występuje, gdy modyfikacje klasy bazowej nieoczekiwanie powodują błędy w klasach pochodnych.
Projektanci Go wybrali inną ścieżkę: kompozycja zamiast dziedziczenia. Zamiast mówić „Dog jest Animal”, Go zachęca do stwierdzenia „Dog ma zachowania podobne do Animal”. Ta subtelna zmiana prowadzi do bardziej elastycznego i łatwiejszego w utrzymaniu kodu.
Go umożliwia ponowne wykorzystanie kodu za pomocą dwóch mechanizmów, które zostały już omówione:
- Interfejsy definiują kontrakty zachowania bez szczegółów implementacji
- Osadzanie struktur pozwala typom zawierać inne typy i ponownie wykorzystywać ich metody
Rozważmy takie porównanie. W tradycyjnym programowaniu obiektowym możesz napisać class Dog extends Animal. W Go osadzasz strukturę Animal wewnątrz Dog i implementujesz wspólne interfejsy. Rezultatem jest podobna funkcjonalność przy słabszym sprzężeniu między typami.
W tym rozdziale szczegółowo omówimy osadzanie struktur i pokażemy, jak Go zapewnia korzyści dziedziczenia bez jego wad.
Wyzwanie
ŁatwyZbudujmy system powiadomień, który pokaże podejście Go oparte na kompozycji zamiast dziedziczenia. Utworzysz typy współdzielące zachowanie za pomocą interfejsów i osadzania struktur, a nie hierarchii klas.
Rozmieścisz kod w trzech plikach:
notifier.go: Zdefiniuj interfejsNotifierz jedną metodąNotify(message string) string. Utwórz też strukturęBaseNotifierz polemName, która będzie osadzana w innych typach. Dodaj doBaseNotifiermetodę o nazwieFormat(message string) string, która zwraca wiadomość poprzedzoną nazwą powiadamiającego w nawiasach kwadratowych.channels.go: Utwórz dwa typy kanałów powiadomień, które osadzająBaseNotifieri implementują interfejsNotifier:EmailNotifierz dodatkowym polemAddressSMSNotifierz dodatkowym polemPhone
Notifykażdego typu powinna używać osadzonej metodyFormati uwzględniać w wyniku informacje dotyczące danego kanału.main.go: Utwórz funkcję o nazwieSendAlert, która przyjmuje dowolnyNotifieri wiadomość, a następnie zwraca wynik wywołaniaNotify. Odczytaj szczegóły powiadomień z wejścia, utwórz oba typy powiadamiających i pokaż, jak można używać ich zamiennie za pośrednictwem interfejsu.
Dane wejściowe będą następujące:
- Wiersz 1: Nazwa powiadamiającego dla wiadomości e-mail
- Wiersz 2: Adres e-mail
- Wiersz 3: Nazwa powiadamiającego dla SMS
- Wiersz 4: Numer telefonu
- Wiersz 5: Treść alertu
Metoda Format w BaseNotifier powinna zwracać:
[Name] messageMetody Notify powinny zwracać:
- EmailNotifier:
Email to [Address]: [formatted message] - SMSNotifier:
SMS to [Phone]: [formatted message]
Na przykład dla danych Alerts, user@mail.com, Urgent, 555-1234 i Server down wynik powinien wyglądać następująco:
Email to user@mail.com: [Alerts] Server down
SMS to 555-1234: [Urgent] Server downZwróć uwagę, że oba typy powiadamiających ponownie wykorzystują metodę Format z BaseNotifier dzięki osadzaniu, a każdy z nich zapewnia własną implementację Notify. Funkcja SendAlert działa z dowolnym Notifier, nie znając jego konkretnego typu. To właśnie kompozycja zamiast dziedziczenia w praktyce.
Spróbuj swoich sił
package main
import (
"bufio"
"fmt"
"os"
)
// SendAlert przyjmuje dowolny Notifier i wiadomość oraz zwraca wynik wywołania Notify
// TODO: Zaimplementuj funkcję SendAlert
func main() {
scanner := bufio.NewScanner(os.Stdin)
// Odczytaj nazwę powiadamiacza e-mail
scanner.Scan()
emailName := scanner.Text()
// Odczytaj adres e-mail
scanner.Scan()
emailAddress := scanner.Text()
// Odczytaj nazwę powiadamiacza SMS
scanner.Scan()
smsName := scanner.Text()
// Odczytaj numer telefonu
scanner.Scan()
phoneNumber := scanner.Text()
// Odczytaj treść alertu
scanner.Scan()
alertMessage := scanner.Text()
// TODO: Utwórz EmailNotifier z emailName i emailAddress
// TODO: Utwórz SMSNotifier z smsName i phoneNumber
// TODO: Użyj SendAlert, aby wysłać alertMessage przez oba powiadamiacze
// i wypisz wyniki
// Wyłącz ostrzeżenia o nieużywanych zmiennych (usuń te instrukcje, gdy zaczniesz używać zmiennych)
_ = emailName
_ = emailAddress
_ = smsName
_ = phoneNumber
_ = alertMessage
}
Ta lekcja zawiera krótki quiz. Zacznij lekcję, żeby na niego odpowiedzieć i śledzić swoje postępy.
Wszystkie lekcje w sekcji Programowanie obiektowe
1Podstawy OOP w Go
Pliki zewnętrzneObszar roboczy i moduły GoPakiety i importyNazwy eksportowane i nieeksportowaneWprowadzenie do OOP w GoStruktury jako klasyDefiniowanie metod dla strukturOdbiorniki wskaźnikowe i wartościoweInicjalizacja strukturFunkcje konstruktoraPodsumowanie – prosty kalkulator4Interfejsy
Wprowadzenie do interfejsówImplementacja niejawnaInterfejs jako kontraktPusty interfejs (any)Asercja typuPrzełącznik typówKompozycja interfejsówInterfejsy Stringer i ErrorPowtórka — kalkulator figur7Hermetyzacja
Pola eksportowane i nieeksportowaneHermetyzacja na poziomie pakietuMetody pobierające i ustawiająceUkrywanie informacji w GoPowtórka – dane studentów10Typy generyczne (Go 1.18+)
Wprowadzenie do typów generycznychParametry typówOgraniczenia typówStruktury generyczneObejście ograniczeń metod generycznychPodsumowanie — generyczna kolekcja2Typy i struktury w praktyce
Typy proste i złożoneDefiniowanie własnych typówTagi pól strukturStruktury anonimoweStruktury zagnieżdżoneWartości zerowe i domyślnePodsumowanie – książka kontaktów5Kompozycja zamiast dziedziczenia
Dlaczego Go nie ma dziedziczeniaPodstawy osadzania strukturPromowanie metodOsadzanie wielu strukturOsadzanie a agregacjaPrzesłanianie osadzonych metodPodsumowanie — hierarchia pracowników8Obsługa błędów i programowanie obiektowe
Interfejs błęduWłasne typy błędówOpakowywanie błędów (fmt.Errorf)Błędy wartowniczeerrors.Is() i errors.As()Panic, Defer i RecoverPodsumowanie — parser plików3Wskaźniki i pamięć
Podstawy wskaźników w GoWskaźniki do strukturPrzekazywanie przez wartość a przez referencjęFunkcja new()Odśmiecanie pamięci w GoPowtórka — kreator listy wiązanej6Polimorfizm w Go
Polimorfizm za pomocą interfejsówDuck typing w GoZasady spełniania interfejsówKolekcje polimorficzneWstrzykiwanie zależnościPowtórka — procesor płatności9Współbieżność i OOP
Podstawy goroutinesKanały i komunikacjaKanały buforowane i niebuforowaneInstrukcja selectsync.Mutex i sync.RWMutexsync.WaitGroupProjektowanie struktur bezpiecznych wątkowoPowtórka – pula workerów12Zaawansowane zagadnienia programowania obiektowego
Wzorzec opcji funkcyjnychWzorzec Builder w GoŁańcuchowe wywoływanie metodAliasy typów a definicje typówPodstawy refleksjiPrzegląd generowania koduPoćwicz samodzielnie: Kompilator Go online