Spis zadań w tym module

  1. Hierarchia pojazdów (Podstawy dziedziczenia)
  2. System kadrowy (Nadpisywanie metod i super())
  3. Problem diamentowy (MRO w praktyce)
  4. Uniwersalne narzędzia (Wielodziedziczenie i Mixiny)
  5. Budowa komputera (Kompozycja vs Dziedziczenie)

1. Dziedziczenie (is-a)

Dziedziczenie pozwala tworzyć nowe klasy na podstawie już istniejących. Klasa pochodna przejmuje wszystkie atrybuty i metody klasy bazowej. Stosujemy je, gdy możemy powiedzieć, że obiekt B "jest" (is-a) rodzajem obiektu A (np. Pies jest Zwierzęciem).

2. Klasa bazowa i pochodna

W Pythonie dziedziczenie zapisujemy podając nazwę rodzica w nawiasie przy definicji klasy: class Dziecko(Rodzic). Dziecko automatycznie zyskuje dostęp do zachowań Rodzica, co pozwala uniknąć powtarzania kodu.

01
Hierarchia pojazdów (Podstawy dziedziczenia)
Czego student się nauczy

Tworzenia prostych hierarchii klas, wykorzystywania metod klasy bazowej w klasach pochodnych oraz rozumienia relacji "jest" (is-a).

Scenariusz

Jako architekt systemów w globalnej korporacji transportowej, musisz zaprojektować fundamenty pod nowy system zarządzania zróżnicowaną flotą maszyn. Kluczowym wyzwaniem jest stworzenie struktury, która pozwoli na efektywne współdzielenie cech wspólnych przy jednoczesnym zachowaniu unikalnych właściwości poszczególnych rodzajów transportu. Twoim zadaniem jest opracowanie hierarchii klas, gdzie nadrzędny model pojazdu będzie definiował markę i model, a klasy pochodne rozszerzą go o specyficzne dane techniczne. Samochody w Twoim systemie muszą posiadać informację o liczbie drzwi, podczas gdy motocykle wymagają precyzyjnego określenia typu napędu przekazywanego na koło. Dzięki zastosowaniu dziedziczenia, unikniesz uciążliwego powielania kodu i zapewnisz spójny interfejs dla wszystkich elementów floty. Każdy pojazd, niezależnie od typu, powinien potrafić przedstawić swoją pełną specyfikację w sposób ujednolicony. Gotowy moduł będzie stanowił bazę dla przyszłych systemów monitorowania zużycia paliwa czy planowania przeglądów technicznych. Poprawnie zaimplementowana relacja "is-a" pokaże Ci, jak budować logiczne powiązania między obiektami w świecie rzeczywistym.

Wymagania techniczne
  • Zdefiniuj klasę bazową Pojazd przechowującą uniwersalne atrybuty: marka oraz model.
  • Dodaj w klasie bazowej metodę info() prezentującą podstawowe dane techniczne maszyny.
  • Opracuj klasę pochodną Samochod, stosując poprawną składnię dziedziczenia po klasie Pojazd.
  • Rozszerz klasę Samochod o specyficzny atrybut instancji określający liczba_drzwi.
  • Zaimplementuj klasę Motocykl jako kolejną specjalizację nadrzędnego modelu pojazdu.
  • Wprowadź w klasie Motocykl dodatkowy atrybut techniczny opisujący typ_napedu.
  • Upewnij się, że obie klasy pochodne mają dostęp do metody info() zdefiniowanej u rodzica.
  • Przetestuj hierarchię, tworząc instancje obu typów i wywołując na nich odziedziczone funkcjonalności.
Wskazówki wykonania
  • Klasę bazową zdefiniuj jako class Pojazd:, a w jej __init__ przypisz markę i model.
  • Metoda info(self) powinna zwracać podstawowy opis tekstowy, np. f"{self.marka} {self.model}".
  • Przy definicji class Samochod(Pojazd): pamiętaj o nawiasach wskazujących na rodzica.
  • W konstruktorze Samochod wywołaj super().__init__(marka, model) przed przypisaniem drzwi.
  • Nadpisz metodę info() w klasach pochodnych, aby dodać unikalne cechy do opisu bazowego.
  • Możesz użyć zapisu return super().info() + f", Drzwi: {self.liczba_drzwi}".
  • Pamiętaj, że super() oszczędza Ci ponownego pisania logiki inicjalizacji tych samych pól.
  • Przetestuj, czy motocykl poprawnie wyświetla swój typ napędu obok danych odziedziczonych.
  • Sprawdź, czy ewentualna zmiana metody info() w rodzicu zostanie odzwierciedlona w obu dzieciach.
  • Zwróć uwagę, że każda klasa pochodna dziedziczy wszystkie metody publiczne klasy Pojazd.
Przykładowy ekran
>>> s = Samochod("Ford", "Focus", 5) >>> m = Motocykl("Yamaha", "R1", "Łańcuch") >>> s.info() Pojazd: Ford Focus, Drzwi: 5 >>> m.info() Pojazd: Yamaha R1, Napęd: Łańcuch
Wnioski do opracowania
  • Wyjaśnij precyzyjnie, dlaczego zrezygnowaliśmy z ponownego definiowania atrybutów marka i model w klasach pochodnych.
  • Opisz fundamentalną rolę relacji "is-a" (jest-rodzajem) w logicznym porządkowaniu struktury danych w Twoim programie.
  • Omów kluczowe zalety stosowania klasy bazowej do bezpiecznego przechowywania cech wspólnych dla całej grupy obiektów.
  • Przeanalizuj techniczny proces przekazywania argumentów do konstruktora rodzica za pomocą instrukcji super().__init__.
  • Zastanów się i opisz, w jaki sposób dodanie nowej metody do klasy Pojazd natychmiastowo wpłynie na wszystkie jej klasy pochodne.
  • Wnioskuj o poziomie reużywalności kodu wynikającym z kategorycznego braku konieczności powtarzania tych samych definicji pól.
  • Porównaj mechanizm dziedziczenia do tradycyjnego kopiowania fragmentów kodu pod kątem łatwości późniejszego utrzymania całego systemu.
  • Opisz, w jaki sposób metoda info() w klasie pochodnej rozszerza, a nie całkowicie zastępuje, opis pochodzący bezpośrednio z klasy bazowej.
  • Sprawdź i udowodnij, czy instancja klasy Samochod jest technicznie traktowana jednocześnie jako instancja klasy Pojazd.

Rozwiązanie

3. Nadpisywanie metod (Overriding)

Klasa pochodna może posiadać metodę o tej samej nazwie co klasa bazowa. Wtedy metoda z dziecka "przesłania" metodę rodzica. Pozwala to na specyficzne zachowanie różnych podklas przy wywołaniu tego samego polecenia.

4. Wykorzystanie super()

Funkcja super() pozwala na odwołanie się do metod klasy bazowej z wnętrza klasy pochodnej. Jest to kluczowe zwłaszcza w konstruktorach __init__, aby zapewnić poprawną inicjalizację atrybutów rodzica.

02
System kadrowy (Nadpisywanie metod i super())
Czego student się nauczy

Modyfikowania zachowań odziedziczonych, używania super() do rozszerzania logiki oraz inicjalizacji wielopoziomowej.

Scenariusz

Zostałeś poproszony o przygotowanie zaawansowanego modułu płacowego dla prężnie rozwijającej się firmy technologicznej, która stawia na sprawiedliwy system wynagrodzeń. W Twoim modelu każdy pracownik posiada określoną pensję podstawową, jednak struktura organizacyjna przewiduje specjalne zasady dla kadry zarządzającej. Managerowie, oprócz standardowego wynagrodzenia, otrzymują stały, motywacyjny bonus za odpowiedzialność związaną z prowadzeniem zespołów projektowych. Twoim celem jest wykorzystanie mechanizmu nadpisywania metod, aby w sposób elegancki i automatyczny obliczać całkowitą wypłatę dla każdego szczebla kariery. Musisz użyć funkcji super(), aby bezpiecznie rozszerzyć logikę klasy bazowej, nie tracąc przy tym dostępu do jej pierwotnych funkcjonalności. Takie rozwiązanie gwarantuje, że wszelkie globalne zmiany w sposobie naliczania podstawy zostaną automatycznie uwzględnione również w profilach managerów. System powinien być na tyle elastyczny, aby w przyszłości łatwo dodawać kolejne grupy zawodowe z unikalnymi systemami premiowymi. Dzięki takiemu podejściu, Twój kod stanie się wzorcem poprawności architektonicznej i łatwości w utrzymaniu.

Wymagania techniczne
  • Skonstruuj klasę Pracownik posiadającą atrybuty nazwisko oraz miesięczną pensja podstawową.
  • Zaimplementuj metodę oblicz_wyplate(), która w klasie bazowej zwraca jedynie kwotę bazową.
  • Zdefiniuj klasę Manager dziedziczącą wszystkie cechy i zachowania po klasie Pracownik.
  • Rozszerz konstruktor managera o dodatkowy parametr reprezentujący stały, kwotowy bonus.
  • Wykorzystaj funkcję super().__init__() do poprawnej inicjalizacji atrybutów odziedziczonych po rodzicu.
  • Nadpisz metodę oblicz_wyplate(), aby uwzględniała sumę pensji podstawowej oraz dodatku funkcyjnego.
  • Wywołaj super().oblicz_wyplate() wewnątrz nadpisanej metody dla zachowania czystości kodu.
  • Zademonstruj polimorfizm, wywołując metodę obliczeń na liście obiektów różnych typów pracowników.
Wskazówki wykonania
  • Klasa Pracownik powinna posiadać metodę oblicz_wyplate(self) zwracającą wartość self.pensja.
  • W klasie Manager(Pracownik) konstruktor musi przyjmować nazwisko, pensję oraz kwotę bonusu.
  • Użyj instrukcji super().__init__(nazwisko, pensja), aby nie powtarzać przypisań w managerze.
  • Metoda oblicz_wyplate w managerze powinna zwracać super().oblicz_wyplate() + self.bonus.
  • Dzięki super(), każda globalna zmiana w liczeniu pensji bazowej zostanie automatycznie uwzględniona.
  • Przetestuj polimorfizm: stwórz listę zawierającą obiekty typu Pracownik oraz Manager.
  • Użyj pętli for p in lista: print(p.oblicz_wyplate()), aby sprawdzić wyniki dla obu ról.
  • Zauważ, że pętla traktuje oba obiekty w ujednolicony sposób, mimo że wykonują one różne wersje metody.
  • To zadanie pokazuje, jak mechanizm super() ułatwia późniejsze utrzymanie i rozwój kodu.
  • Upewnij się, że specyficzny atrybut bonus jest zdefiniowany wyłącznie w klasie Manager.
Przykładowy ekran
>>> p = Pracownik("Kowalski", 3000) >>> m = Manager("Nowak", 3000, 1000) >>> print(p.oblicz_wyplate()) 3000 >>> print(m.oblicz_wyplate()) 4000
Wnioski do opracowania
  • Szczegółowo wyjaśnij, co by się stało z obiektem klasy Manager, gdybyśmy w jego konstruktorze zapomnieli wywołać super().__init__.
  • Opisz mechanizm nadpisywania metod (overriding) jako elegancki sposób na dostarczanie specyficznej implementacji dla ogólnych zachowań.
  • Omów techniczne zalety użycia super().oblicz_wyplate() zamiast sztywnego i ryzykownego odwołania do Pracownik.oblicz_wyplate(self).
  • Przeanalizuj, jak polimorfizm pozwala na ujednolicone i masowe przetwarzanie różnych typów pracowników wewnątrz jednej pętli for.
  • Zastanów się nad potencjalnym ryzykiem niespójności danych przy próbie ręcznej inicjalizacji atrybutów rodzica bezpośrednio w konstruktorze dziecka.
  • Wnioskuj o elastyczności systemu, który umożliwia błyskawiczne dodawanie nowych ról zawodowych (np. Stażysta) z unikalną logiką płacową.
  • Porównaj podejście oparte na dziedziczeniu do stosowania rozbudowanych i trudnych w utrzymaniu instrukcji warunkowych if/else.
  • Opisz kluczową rolę funkcji super() w zachowaniu poprawnego łańcucha wywołań metod przy wielopoziomowej hierarchii klas.
  • Sprawdź eksperymentalnie, czy specyficzny atrybut bonus jest w jakikolwiek sposób dostępny dla obiektów klasy bazowej Pracownik.

Rozwiązanie

5. Wielodziedziczenie i MRO

Python jako jeden z niewielu języków pozwala na dziedziczenie po wielu klasach naraz. Kolejność wyszukiwania metod określa algorytm MRO (Method Resolution Order). Możesz ją sprawdzić poleceniem Klasa.mro().

03
Problem diamentowy (MRO w praktyce)
Czego student się nauczy

Analizowania hierarchii wielodziedziczenia, rozumienia kolejności wywołań w strukturze diamentowej oraz unikania błędów z nią związanych.

Scenariusz

W świecie zaawansowanego programowania obiektowego w Pythonie, wielodziedziczenie otwiera ogromne możliwości, ale niesie też ze sobą ryzyko skomplikowanych konfliktów nazw. Twoim wyzwaniem jest zbadanie i zrozumienie tzw. problemu diamentowego, który pojawia się, gdy dwie klasy dziedziczą po jednym rodzicu, a następnie stają się podstawą dla kolejnej, wspólnej podklasy. Musisz zaprojektować strukturę modelującą osobę, która jednocześnie pełni rolę studenta oraz pracownika uczelni, łącząc obowiązki obu tych grup. Kluczowym elementem zadania jest obserwacja, jak system operacyjny języka Python radzi sobie z kolejnością wywoływania metod w tak złożonej sieci powiązań. Wykorzystaj algorytm MRO, aby prześledzić ścieżkę poszukiwania definicji i upewnić się, że wspólny przodek jest inicjalizowany dokładnie jeden raz. Takie doświadczenie pozwoli Ci świadomie projektować wielopoziomowe hierarchie klas, unikając pułapek związanych z niejednoznacznością kodu. Zrozumienie tego mechanizmu jest niezbędne przy pracy z dużymi frameworkami, takimi jak Django czy SQLAlchemy. Gotowy projekt będzie dowodem Twojej biegłości w poruszaniu się po najbardziej wymagających obszarach programowania obiektowego.

Wymagania techniczne
  • Zbuduj cztery klasy tworzące graf diamentu: Osoba (rodzic), Student, Pracownik (dzieci) oraz StudentPracownik.
  • Zaimplementuj w klasie Osoba metodę przedstaw_sie() z podstawowym komunikatem identyfikacyjnym.
  • Nadpisz metodę przedstaw_sie() w klasach Student oraz Pracownik, rozszerzając informację o roli.
  • Użyj instrukcji super().przedstaw_sie() w każdej z klas pochodnych, aby zachować łańcuch wywołań.
  • Zdefiniuj klasę końcową StudentPracownik, dziedziczącą jednocześnie po obu klasach pośrednich.
  • Wywołaj metodę prezentacji na obiekcie łączącym obie role i przeanalizuj kolejność komunikatów.
  • Skorzystaj z atrybutu klasowego __mro__ lub metody mro(), aby wyświetlić ścieżkę poszukiwania metod.
  • Potwierdź, że klasa bazowa Osoba została zainicjalizowana tylko raz dzięki algorytmowi C3 Linearization.
Wskazówki wykonania
  • Zdefiniuj class Osoba: z metodą __init__ i podstawową wersją przedstaw_sie.
  • Klasy Student(Osoba) i Pracownik(Osoba) muszą wywoływać super().__init__() w konstruktorach.
  • W definicji class StudentPracownik(Student, Pracownik): podaj obie klasy w nawiasie po przecinku.
  • Pamiętaj o kolejności: super() podąża za MRO, odwiedzając klasy od lewej do prawej.
  • W każdej metodzie przedstaw_sie wypisz komunikat i wywołaj super().przedstaw_sie().
  • Przetestuj obiekt końcowy i policz, ile razy pojawił się komunikat z klasy bazowej Osoba.
  • Wyświetl StudentPracownik.mro(), aby zrozumieć, jak Python spłaszcza strukturę diamentu.
  • Zauważ, że bez super() wspólny przodek mógłby być zainicjalizowany nielogicznie wielokrotnie.
  • To zadanie uczy poprawnego łączenia cech z wielu źródeł przy zachowaniu integralności hierarchii.
  • Spróbuj zamienić kolejność rodziców w nawiasie i zobacz, jak zmienia się kolejność komunikatów.
Przykładowy ekran
>>> sp = StudentPracownik() >>> sp.przedstaw_sie() Jestem Studentem. Jestem Pracownikiem. Jestem Osobą. >>> print(StudentPracownik.mro()) [StudentPracownik, Student, Pracownik, Osoba, object]
Wnioski do opracowania
  • Wyjaśnij szczegółowo, dlaczego mimo istnienia dwóch ścieżek do klasy Osoba, komunikat "Jestem Osobą" pojawia się w konsoli tylko jeden raz.
  • Opisz precyzyjne działanie algorytmu MRO (Method Resolution Order) w procesie spłaszczania grafu dziedziczenia do czytelnej, liniowej listy wywołań.
  • Omów znaczenie algorytmu C3 Linearization w skutecznym zapobieganiu wielokrotnej i błędnej inicjalizacji wspólnych przodków.
  • Przeanalizuj ścieżkę poszukiwania metody przedstaw_sie przy użyciu systemowego polecenia help(StudentPracownik).
  • Zastanów się, w jaki sposób kolejność podania klas w definicji (np. Student, Pracownik) wpływa na priorytet wywoływania metod.
  • Wnioskuj o bezwzględnej konieczności stosowania super() w każdej klasie pośredniej dla zapewnienia poprawnego działania diamentu.
  • Porównaj strukturę diamentową do tradycyjnego dziedziczenia jednobazowego pod kątem złożoności i łatwości debugowania błędów.
  • Opisz, w jaki sposób Python automatycznie rozwiązuje konflikty nazw w sytuacji, gdy obie klasy pośrednie posiadają atrybut o identycznej nazwie.
  • Sprawdź i opisz skutki uboczne, które wystąpią, gdy dokładnie jedna z klas w całym łańcuchu pominie wymagane wywołanie super().

Rozwiązanie

6. Mixiny (Klasy domieszki)

Mixin to klasa, która nie służy do tworzenia samodzielnych obiektów, ale dostarcza konkretną funkcjonalność innym klasom poprzez wielodziedziczenie (np. logowanie do pliku, zamiana na JSON).

04
Uniwersalne narzędzia (Wielodziedziczenie i Mixiny)
Czego student się nauczy

Projektowania reużywalnych modułów (Mixins) i wstrzykiwania ich do różnych, niepowiązanych ze sobą klas.

Scenariusz

Pracujesz nad dużą aplikacją korporacyjną, która przetwarza tysiące różnorodnych dokumentów, zamówień oraz profili użytkowników w czasie rzeczywistym. Każdy z tych obiektów, mimo że należy do zupełnie innego obszaru biznesowego, musi posiadać wspólną funkcjonalność automatycznego raportowania swojego stanu do plików dziennika. Twoim zadaniem jest stworzenie uniwersalnej klasy domieszki, zwanej Mixinem, która "wstrzyknie" umiejętność logowania do dowolnej klasy bez zaburzania jej głównej hierarchii dziedziczenia. Dzięki temu rozwiązaniu, nie musisz kopiować skomplikowanego kodu zapisu danych do każdego modułu aplikacji z osobna. Mixin powinien w sposób inteligentny analizować wewnętrzną strukturę obiektu i generować czytelne raporty tekstowe na żądanie. Takie podejście promuje zasadę reużywalności kodu i sprawia, że system jest niezwykle łatwy w rozbudowie o nowe typy danych. Twoje rozwiązanie pokaże, jak za pomocą wielodziedziczenia można budować modułowe i elastyczne narzędzia pomocnicze działające w tle aplikacji. Gotowy projekt będzie doskonałym przykładem profesjonalnego wykorzystania wzorca domieszki w architekturze systemów rozproszonych.

Wymagania techniczne
  • Opracuj klasę domieszkę LogMixin przeznaczoną do rozszerzania funkcjonalności diagnostycznych.
  • Zaimplementuj wewnątrz Mixina metodę log_to_file() generującą tekstowy zrzut stanu obiektu.
  • Wykorzystaj atrybut self.__dict__ do automatycznego odczytu wszystkich nazw i wartości pól instancji.
  • Zdefiniuj klasę User reprezentującą profil klienta, dziedziczącą wyłącznie po LogMixin.
  • Stwórz klasę Order dla zamówień sklepowych, również wykorzystującą wielodziedziczenie z domieszką.
  • Zapewnij unikalność atrybutów w obu klasach biznesowych, aby zademonstrować uniwersalność narzędzia.
  • Wywołaj funkcję logowania dla obiektów obu typów, sprawdzając poprawność formatowania wyników.
  • Wyjaśnij, dlaczego Mixiny nie powinny posiadać własnego stanu w metodzie __init__.
Wskazówki wykonania
  • Klasa LogMixin nie powinna posiadać własnej metody __init__ ani stanu wewnętrznego.
  • Metoda log_to_file(self) powinna pobierać nazwę aktualnej klasy przez self.__class__.__name__.
  • Użyj self.__dict__, aby w pętli wygenerować tekstowy opis wszystkich zmiennych instancji obiektu.
  • Definiując class User(LogMixin):, wskazujesz, że użytkownik zyskuje nową umiejętność logowania.
  • Możesz dodać inne klasy, np. Order(LogMixin), które nie są ze sobą powiązane w hierarchii biznesowej.
  • Pamiętaj, że Mixiny dodajemy zazwyczaj jako pierwsze klasy w liście dziedziczenia wielokrotnego.
  • Przetestuj funkcjonalność logowania na obiektach posiadających zupełnie różne zestawy atrybutów danych.
  • Zwróć uwagę, że Mixin jest całkowicie generyczny i nie "wie" nic o szczegółach klasy, do której jest dołączany.
  • To podejście skutecznie eliminuje powielanie kodu przy implementacji zadań pomocniczych w systemie.
  • Sprawdź, czy metoda log_to_file poprawnie radzi sobie z wyświetlaniem danych w konsoli.
Przykładowy ekran
>>> u = User("admin") >>> o = Order(105, 250.0) >>> u.log_to_file() [LOG - User]: {'name': 'admin'} >>> o.log_to_file() [LOG - Order]: {'id': 105, 'total': 250.0}
Wnioski do opracowania
  • Wyjaśnij, jakie są strategiczne zalety stosowania Mixinów w porównaniu do budowania jednej, ogromnej klasy bazowej ze wszystkimi funkcjami.
  • Uzasadnij technicznie, dlaczego Mixiny kategorycznie nie powinny posiadać własnej metody __init__ ani przechowywać stanu wewnętrznego.
  • Opisz mechanizm "wstrzykiwania" nowej funkcjonalności (tzw. plug-in pattern) do zupełnie niepowiązanych ze sobą klas biznesowych.
  • Omów sposób kreatywnego wykorzystania atrybutu self.__dict__ do budowania uniwersalnych i generycznych narzędzi diagnostycznych.
  • Zastanów się nad ryzykiem kolizji nazw metod w sytuacji dołączania wielu różnych i niezależnych Mixinów do jednej klasy docelowej.
  • Wnioskuj o poprawie czystości architektury, w której zadania pomocnicze (np. logowanie) są całkowicie odseparowane od logiki głównej.
  • Porównaj wzorzec Mixin do koncepcji interfejsu z domyślną implementacją, znanej z innych nowoczesnych języków programowania.
  • Opisz, dlaczego Mixiny dodaje się zazwyczaj na samym początku listy dziedziczenia wielokrotnego w definicji klasy w Pythonie.
  • Sprawdź, czy klasa LogMixin może być użyta samodzielnie do stworzenia funkcjonalnego obiektu i wyjaśnij, czy ma to sens projektowy.

Rozwiązanie

7. Polimorfizm

Polimorfizm ("wielopostaciowość") pozwala traktować obiekty różnych klas w ten sam sposób, o ile posiadają one wspólną metodę. Dzięki temu możemy napisać jedną funkcję obsługującą listę różnych zwierząt wywołując na każdym daj_glos().

8. Kompozycja (has-a)

Kompozycja polega na budowaniu klasy z innych obiektów (np. Samochód ma Silnik). Często jest to rozwiązanie lepsze i bardziej elastyczne niż dziedziczenie, ponieważ pozwala na wymianę części składowych w czasie działania programu.

9. Zasada Podstawienia Liskov (LSP)

To zasada mówiąca, że wszędzie tam, gdzie program oczekuje obiektu klasy bazowej, powinniśmy móc podłożyć obiekt klasy pochodnej i wszystko powinno działać bez błędów.

10. Podsumowanie Relacji

Zrozumienie kiedy stosować dziedziczenie (is-a), a kiedy kompozycję (has-a) to klucz do zostania architektem systemów. Dziedziczenie porządkuje strukturę, a kompozycja daje elastyczność.

05
Budowa komputera (Kompozycja vs Dziedziczenie)
Czego student się nauczy

Rozróżniania dwóch najważniejszych relacji w OOP: dziedziczenia i kompozycji oraz łączenia ich w jednym projekcie.

Scenariusz

Jednym z najtrudniejszych dylematów architekta oprogramowania jest wybór pomiędzy dziedziczeniem a kompozycją podczas modelowania złożonych systemów technicznych. Twoim zadaniem jest rozstrzygnięcie tego problemu na przykładzie symulatora budowy komputera stacjonarnego. Musisz zauważyć, że komputer jako całość jest rodzajem urządzenia elektronicznego, co naturalnie sugeruje użycie relacji dziedziczenia w projekcie. Z drugiej strony, każda jednostka centralna składa się z konkretnych, wymiennych komponentów, takich jak procesor czy pamięć RAM, co najlepiej oddaje relacja kompozycji. Twoim celem jest połączenie obu tych podejść w jedną, spójną całość, która wiernie odwzoruje strukturę i działanie rzeczywistego sprzętu. Musisz zaimplementować mechanizm, w którym włączenie głównego urządzenia powoduje automatyczną aktywację i sprawdzenie statusu wszystkich jego podzespołów. Taka hybrydowa architektura zapewnia najwyższą elastyczność, pozwalając na łatwą wymianę procesora na mocniejszy model bez zmiany definicji samej klasy komputera. Poprawna realizacja tego zadania nauczy Cię, jak budować trwałe i skalowalne systemy obiektowe, odzwierciedlające skomplikowane zależności konstrukcyjne.

Wymagania techniczne
  • Zdefiniuj klasę nadrzędną UrzadzenieElektroniczne z podstawową logiką zarządzania zasilaniem.
  • Opracuj niezależne klasy Procesor oraz PamiecRAM, każda z unikalnymi parametrami wydajnościowymi.
  • Skonstruuj klasę Komputer, stosując dziedziczenie po UrzadzenieElektroniczne (relacja is-a).
  • Zaimplementuj wewnątrz klasy Komputer relację kompozycji poprzez tworzenie instancji podzespołów (relacja has-a).
  • Przypisz obiekty procesora i pamięci do atrybutów instancji komputera wewnątrz jego konstruktora.
  • Nadpisz metodę wlacz(), aby oprócz startu zasilania wyświetlała pełną specyfikację składowych części.
  • Sprawdź, czy obiekty składowe (has-a) poprawnie komunikują się z obiektem nadrzędnym podczas pracy.
  • Uzasadnij wybór kompozycji dla części wymiennych zamiast stosowania głębokiego dziedziczenia sprzętowego.
Wskazówki wykonania
  • UrzadzenieElektroniczne powinno posiadać atrybut stanu czy_wlaczone = False i metodę wlacz().
  • W klasie Komputer wewnątrz konstruktora stwórz instancje: self.cpu = Procesor(...).
  • To jest przykład kompozycji – komputer posiada (has-a) procesor jako swoją integralną część składową.
  • Metoda wlacz(self) w komputerze powinna najpierw wywołać super().wlacz() dla startu zasilania.
  • Następnie wywołaj metody informacyjne bezpośrednio z obiektów self.cpu i self.ram.
  • Pamiętaj, że podzespoły nie muszą dziedziczyć po komputerze – są odrębnymi bytami logicznymi.
  • Przetestuj tworzenie komputera, przekazując parametry podzespołów bezpośrednio przy inicjalizacji.
  • Zastanów się: dziedziczenie komputera po procesorze byłoby błędem, bo komputer nie "jest" procesorem.
  • Dzięki kompozycji możesz łatwo podmienić procesor w locie: moj_pc.cpu = Procesor("Ryzen 9", 3.8).
  • To zadanie uczy, jak budować elastyczne systemy poprzez poprawne dobieranie relacji między obiektami.
Przykładowy ekran
>>> laptop = Komputer("Intel i7", 16) >>> laptop.wlacz() Zasilanie ON. Start systemu... Procesor: Intel i7, RAM: 16 GB. Status: Gotowy.
Wnioski do opracowania
  • Wyjaśnij precyzyjnie, dlaczego klasa Komputer dziedziczy po UrzadzenieElektroniczne, ale posiada Procesor jako atrybut.
  • Opisz fundamentalną różnicę między relacją "is-a" (dziedziczenie) a relacją "has-a" (kompozycja) na przykładzie budowy sprzętu.
  • Omów zalety stosowania kompozycji przy modelowaniu złożonych systemów fizycznych składających się z wielu wymiennych modułów.
  • Przeanalizuj mechanizm delegacji zadań z klasy nadrzędnej do jej obiektów składowych (np. wywołanie self.cpu.info()).
  • Zastanów się i opisz, jakie błędy projektowe i logiczne mogłyby wyniknąć z próby dziedziczenia klasy Komputer bezpośrednio po Procesorze.
  • Wnioskuj o ogromnej elastyczności architektury wynikającej z możliwości łatwej wymiany podzespołów w czasie działania programu.
  • Porównaj sztywność hierarchii dziedziczenia do swobody, jaką daje dynamiczne łączenie wielu obiektów w relacji kompozycji.
  • Opisz, w jaki sposób metoda wlacz() w klasie nadrzędnej koordynuje i synchronizuje pracę wielu niezależnych obiektów składowych.
  • Sprawdź, czy cykl życia obiektów składowych (części) jest technicznie i logicznie nierozerwalnie związany z cyklem życia obiektu nadrzędnego.

Rozwiązanie