Streszczenie Kompozycja kontra dziedziczenie - relacja "ma-a" i "jest-a"

Ten moduł porównuje dwie fundamentalne techniki projektowania obiektowego: dziedziczenie (relacja "jest-a") oraz kompozycję (relacja "ma-a"). Dziedziczenie implementowane jest przez składnię class Pochodna(Bazowa) z nadpisywaniem metod i wywoływaniem klasy bazowej przez super() . Kompozycja polega na przechowywaniu obiektów jako atrybutów innych obiektów, z delegacją metod i zależnością cyklu życia. Omówiona jest różnica między kompozycją a agregacją (słabszą formą relacji "ma-a", gdzie obiekt składowy może istnieć samodzielnie). Przedstawiona jest zasada "favor composition over inheritance" z praktycznymi przykładami (Pojazd → Samochod → Elektryk dla dziedziczenia; Samochod ma Silnik, Czlowiek ma Adres dla kompozycji). Moduł dostarcza też kryteriów wyboru właściwej techniki oraz przykład systemu zamówień.

Kluczową umiejętnością, którą zdobędziesz po przerobieniu tego modułu, jest świadome projektowanie relacji między klasami. Dziedziczenie pozwala na szybkie ponowne wykorzystanie kodu i budowanie hierarchii pojęciowych, ale może prowadzić do sztywnych i kruchych struktur. Kompozycja z kolei oferuje elastyczność i lepszą enkapsulację, kosztem większej ilości kodu "klejowego". W praktyce dojrzały projektant obiektowy łączy obie techniki, wybierając odpowiednie narzędzie do konkretnej sytuacji. Moduł ten pomoże Ci wypracować intuicję potrzebną do podejmowania tych decyzji projektowych.

  • Relacja "jest-a" (dziedziczenie) - klasa pochodna jako szczególny przypadek klasy bazowej, składnia class Pochodna(Bazowa)
  • super() i nadpisywanie metod - rozszerzanie konstruktora i metod klasy bazowej, przesłanianie działania
  • Relacja "ma-a" (kompozycja) - obiekt jako atrybut, tworzenie w __init__ , delegacja metod
  • Agregacja vs kompozycja - zależność cyklu życia, tworzenie wewnętrzne vs zewnętrzne przekazanie
  • Zasada "favor composition over inheritance" - kryteria wyboru, elastyczność a sztywne hierarchie

Przechowywanie obiektów w kolekcjach, takich jak listy, słowniki czy zbiory, jest podstawowym wzorcem w programowaniu obiektowym. Listy obiektów pozwalają na zbiorcze operacje, takie jak iterowanie, filtrowanie, sortowanie czy agregowanie danych. Python oferuje bogaty zestaw narzędzi do pracy z listami, w tym wyrażenia listowe, funkcje sorted(), filter() oraz sortowanie z kluczem lambda. Ważne jest zrozumienie, że lista przechowuje referencje do obiektów, a nie ich kopie, co ma wpływ na modyfikację danych przez różne części programu. Iterowanie po liście obiektów i wywoływanie ich metod to jeden z najczęstszych wzorców w codziennej pracy.

Kompozycja, czyli umieszczanie jednych obiektów wewnątrz innych, jest podstawową techniką budowania złożonych systemów z prostszych komponentów. W przeciwieństwie do dziedziczenia, które wyraża relację 'jest', kompozycja wyraża relację 'ma' i jest często preferowanym podejściem w projektowaniu obiektowym. Graf obiektów w rzeczywistych systemach może być bardzo złożony, ale nawigacja po nim jest naturalna i czytelna dzięki notacji kropkowej. Kompozycja pozwala na elastyczne budowanie funkcjonalności poprzez łączenie prostych obiektów w bardziej złożone struktury. Wzorzec ten jest fundamentem takich koncepcji jak wstrzykiwanie zależności czy architektura warstwowa.

1Wprowadzenie

Kompozycja kontra dziedziczenie: kiedy stosować, relacja "ma-a" kontra "jest-a"

W tej części poznamy dwie fundamentalne techniki projektowania obiektowego: dziedziczenie (relacja "jest-a") oraz kompozycję (relacja "ma-a"). Zrozumiesz, kiedy stosować każdą z nich i jakie są ich mocne oraz słabe strony.

Nauczysz się również rozpoznawać sytuacje, w których nadmierne dziedziczenie prowadzi do kruchego kodu, oraz poznasz zasadę favor composition over inheritance .

Dziedziczenie i kompozycja to nie są wzajemnie wykluczające się podejścia - w dobrze zaprojektowanym systemie obiektowym często występują obie te techniki. Kluczem jest umiejętność rozpoznania, która relacja naturalnie występuje między modelowanymi pojęciami. Będziesz w stanie nie tylko napisać kod wykorzystujący każdą z tych technik, ale także uzasadnić swój wybór w kontekście konkretnego problemu projektowego.

Moduł ten stanowi pierwszą część kompleksowego kursu programowania obiektowego w języku Python. Jego celem jest wprowadzenie studentów w paradygmat obiektowy oraz przedstawienie fundamentalnych koncepcji, takich jak klasy, obiekty, atrybuty i metody. Materiał został zaprojektowany tak, aby stopniowo budować zrozumienie od prostych analogii do konkretnych implementacji w kodzie. Każdy slajd zawiera przykłady, które można samodzielnie przetestować w środowisku REPL. Zaleca się aktywne uczestnictwo poprzez modyfikowanie przykładów i wykonywanie ćwiczeń. Systematyczna praca z materiałem gwarantuje solidne opanowanie podstaw OOP. W kolejnych modułach wiedza ta będzie rozwijana o bardziej zaawansowane zagadnienia.

Programowanie obiektowe to nie tylko zestaw reguł składniowych, ale przede wszystkim sposób myślenia o problemach programistycznych. Kluczowe jest zrozumienie, że klasy służą do modelowania rzeczywistych bytów i relacji między nimi. Dzięki OOP kod staje się bardziej modularny, łatwiejszy w utrzymaniu i bardziej odporny na błędy. Współczesne aplikacje webowe, systemy bazodanowe i frameworki w dużym stopniu opierają się na paradygmacie obiektowym. Opanowanie OOP otwiera drzwi do zrozumienia zaawansowanych wzorców projektowych. Zachęcamy do cierpliwej i systematycznej nauki - każde nowe pojęcie będzie szczegółowo wyjaśnione i zilustrowane przykładami.

2Cele dydaktyczne

Co nauczysz się w tej części?

Cel główny: Rozróżniać relację "jest-a" (dziedziczenie) od "ma-a" (kompozycja) i świadomie wybierać odpowiednią technikę.
  • Definiować i stosować dziedziczenie jako relację "jest-a"
  • Definiować i stosować kompozycję jako relację "ma-a"
  • Rozumieć różnicę między kompozycją a agregacją
  • Implementować kompozycję z delegacją metod i zależnością życia
  • Stosować zasadę "favor composition over inheritance"
  • Identyfikować problemy nadmiernego dziedziczenia
  • Projektować system zamówień z użyciem kompozycji

Po ukończeniu tej części będziesz potrafił świadomie zaprojektować hierarchię klas, unikając typowych pułapek takich jak zbyt głębokie dziedziczenie czy niewłaściwe użycie relacji "ma-a" zamiast "jest-a". Zdobyta wiedza pozwoli Ci tworzyć kod, który jest łatwiejszy w utrzymaniu, testowaniu i rozwijaniu. Szczególny nacisk położymy na praktyczne rozpoznawanie, która technika jest właściwa w konkretnej sytuacji projektowej - to umiejętność, która odróżnia początkującego programistę od doświadczonego architekta oprogramowania.

Cele dydaktyczne

Przedstawione cele dydaktyczne zostały opracowane zgodnie z zasadą stopniowania trudności, co pozwala na systematyczne budowanie kompetencji. Każdy cel koncentruje się na konkretnym aspekcie programowania obiektowego, od teorii przez praktykę aż po zaawansowane zastosowania. Realizacja wszystkich założeń gwarantuje solidne podstawy do dalszego rozwoju w kierunku bardziej złożonych zagadnień, takich jak wzorce projektowe. Materiał został dostosowany do potrzeb studentów kierunków informatycznych. Oczekuje się, że po ukończeniu modułu student będzie potrafił samodzielnie projektować proste hierarchie klas i implementować je w Pythonie. Regularne sprawdzanie postępów pomoże w identyfikacji obszarów wymagających dodatkowej uwagi i powtórki.

Struktura kursu przewiduje płynne przejście od zagadnień podstawowych do bardziej złożonych, z licznymi przykładami i ćwiczeniami praktycznymi. Zaleca się aktywne uczestnictwo poprzez samodzielne eksperymenty z kodem. Środowisko REPL Pythona jest doskonałym narzędziem do natychmiastowego testowania omawianych koncepcji bez konieczności tworzenia plików projektowych. Warto również korzystać z dodatkowych materiałów, takich jak dokumentacja oficjalna Pythona oraz społecznościowe fora programistyczne. Systematyczna praca i regularne powtórki są kluczem do sukcesu w opanowaniu programowania obiektowego.

3 Relacja "jest-a" (is-a) - dziedziczenie

Dziedziczenie jako "jest rodzajem"

Relacja "jest-a" (ang. is-a ) oznacza, że jedna klasa jest szczególnym przypadkiem drugiej. Klasa pochodna jest klasą bazową, ale z dodatkowymi cechami lub zachowaniem. To najbardziej intuicyjna relacja w projektowaniu obiektowym - odpowiada naturalnemu sposobowi, w jaki kategoryzujemy obiekty w rzeczywistym świecie.

Przykłady:
Pies jest zwierzęciem.
Samochód jest pojazdem.
Pracownik jest osobą.
Kwadrat jest prostokątem.
KontoOszczędnościowe jest KontemBankowym.

W Pythonie relację "jest-a" implementujemy przez dziedziczenie - klasa pochodna dziedziczy wszystkie atrybuty i metody klasy bazowej. Oznacza to, że jeśli mamy klasę Zwierze z metodą oddychaj() , to każda klasa pochodna (np. Pies , Kot ) automatycznie zyskuje tę metodę. Nie musimy jej ponownie implementować - wystarczy, że określimy relację dziedziczenia przez składnię class Pies(Zwierze) .

Ważną konsekwencją tej relacji jest polimorfizm: obiekt klasy pochodnej może być używany tam, gdzie oczekiwany jest obiekt klasy bazowej. Jeśli funkcja przyjmuje argument typu Zwierze , możemy przekazać jej obiekt Pies , Kot czy Ptak - wszystko będzie działać poprawnie, ponieważ każdy z tych obiektów "jest" Zwierzęciem.

Relacja jest-a

Slajd zatytułowany "Relacja "jest-a" (is-a) - dziedziczenie" przedstawia istotne zagadnienie z zakresu programowania obiektowego w Pythonie. Zrozumienie przedstawionych tu koncepcji jest niezbędne do dalszej nauki i praktycznego stosowania OOP w codziennej pracy programisty. Zaleca się dokładne przeanalizowanie przykładów kodu i samodzielne ich przetestowanie w środowisku REPL lub edytorze. Warto również zwrócić uwagę na powiązania między tym tematem a innymi zagadnieniami omawianymi w kursie. Systematyczne budowanie wiedzy krok po kroku to klucz do opanowania programowania obiektowego w Pythonie. Nie pomijaj żadnego slajdu, nawet jeśli wydaje Ci się, że temat jest Ci już znany - powtórka utrwala wiedzę i pozwala dostrzec nowe szczegóły w znajomym materiale.

Zachęcamy do samodzielnego eksperymentowania z omawianymi mechanizmami i modyfikowania przykładowego kodu. Praktyczne ćwiczenia i własne projekty to najskuteczniejsza metoda nauki programowania, ponieważ wymagają aktywnego stosowania wiedzy. Pamiętaj, że błędy są naturalną częścią procesu uczenia się i każda pomyłka przybliża Cię do mistrzostwa. Warto prowadzić własny dziennik błędów, w którym zapisujesz napotkane problemy i ich rozwiązania. Taki zeszyt stanie się z czasem bezcennym źródłem wiedzy i referencją na przyszłość. Korzystaj z dokumentacji oficjalnej Pythona oraz społeczności programistycznych - to nieocenione źródła wiedzy i wsparcia w trudnych momentach.

4 Kod: "Pies jest-zwierzęciem"

Przykład dziedziczenia - Pies rozszerza Zwierze

# Klasa bazowa - definiuje ogolne cechy zwierzecia
class Zwierze:
    def __init__(self, nazwa):
        self.nazwa = nazwa

    def oddychaj(self):
        return f"{self.nazwa} oddycha."

    def jedz(self, pokarm):
        return f"{self.nazwa} je {pokarm}."

# Klasa pochodna - Pies JEST Zwierzeciem
# Dziedziczy oddychaj() i jedz(), dodaje szczekaj()
class Pies(Zwierze):
    def szczekaj(self):
        return f"{self.nazwa} szczeka: Hau! Hau!"

    def pilnuj(self):
        return f"{self.nazwa} pilnuje domu."

# Dowod dzialania - Pies korzysta z metod wlasnych i dziedziczonych
p = Pies("Burek")
print(p.oddychaj())  # dziedziczone z Zwierze
print(p.jedz("karme"))  # dziedziczone z Zwierze
print(p.szczekaj())  # wlasne z Pies
print(p.pilnuj())    # wlasne z Pies

# Polimorfizm - Pies moze byc uzyty jako Zwierze
def wizyta_u_weterynarza(zwierze):
    print(zwierze.oddychaj())
    print("Badanie zakonczone.")

wizyta_u_weterynarza(p)  # dziala, bo Pies jest Zwierzeciem

Slajd zatytułowany "Kod: "Pies jest-zwierzęciem"" przedstawia istotne zagadnienie z zakresu programowania obiektowego w Pythonie. Zrozumienie przedstawionych tu koncepcji jest niezbędne do dalszej nauki i praktycznego stosowania OOP w codziennej pracy programisty. Zaleca się dokładne przeanalizowanie przykładów kodu i samodzielne ich przetestowanie w środowisku REPL lub edytorze. Warto również zwrócić uwagę na powiązania między tym tematem a innymi zagadnieniami omawianymi w kursie. Systematyczne budowanie wiedzy krok po kroku to klucz do opanowania programowania obiektowego w Pythonie. Nie pomijaj żadnego slajdu, nawet jeśli wydaje Ci się, że temat jest Ci już znany - powtórka utrwala wiedzę i pozwala dostrzec nowe szczegóły w znajomym materiale.

Zachęcamy do samodzielnego eksperymentowania z omawianymi mechanizmami i modyfikowania przykładowego kodu. Praktyczne ćwiczenia i własne projekty to najskuteczniejsza metoda nauki programowania, ponieważ wymagają aktywnego stosowania wiedzy. Pamiętaj, że błędy są naturalną częścią procesu uczenia się i każda pomyłka przybliża Cię do mistrzostwa. Warto prowadzić własny dziennik błędów, w którym zapisujesz napotkane problemy i ich rozwiązania. Taki zeszyt stanie się z czasem bezcennym źródłem wiedzy i referencją na przyszłość. Korzystaj z dokumentacji oficjalnej Pythona oraz społeczności programistycznych - to nieocenione źródła wiedzy i wsparcia w trudnych momentach.

5 Kiedy dziedziczenie ma sens

Dziedziczenie stosuj, gdy:

  • Klasa pochodna jest szczególnym przypadkiem klasy bazowej (np. Kot jest Ssakiem)
  • Chcesz rozszerzyć zachowanie klasy bazowej bez modyfikacji jej kodu
  • Istnieje naturalna hierarchia pojęciowa, która nie ulegnie zmianie
  • Klasa pochodna dziedziczy interfejs i implementację (lub tylko interfejs w przypadku klas abstrakcyjnych)
  • Potrzebujesz polimorfizmu - obiekty różnych klas mogą być używane zamiennie

Dziedziczenie sprawdza się najlepiej, gdy hierarchia jest stabilna i nie przewidujemy częstych zmian. Na przykład hierarchia biologiczna (Ssak → Pies) zmienia się rzadko. Z kolei hierarchia w systemie finansowym (Konto → KontoOszczędnościowe) może wymagać modyfikacji przy zmianie przepisów - wtedy lepiej rozważyć kompozycję.

Warto też pamiętać, że dziedziczenie tworzy silne powiązanie między klasą bazową a pochodną. Mówimy o white-box reuse (ponowne użycie przez "przezroczyste pudełko"), ponieważ klasa pochodna często zna i polega na wewnętrznej implementacji klasy bazowej. To może być zaletą (łatwość rozszerzania), ale też wadą (kruchość przy zmianach).

Slajd zatytułowany "Kiedy dziedziczenie ma sens" przedstawia istotne zagadnienie z zakresu programowania obiektowego w Pythonie. Zrozumienie przedstawionych tu koncepcji jest niezbędne do dalszej nauki i praktycznego stosowania OOP w codziennej pracy programisty. Zaleca się dokładne przeanalizowanie przykładów kodu i samodzielne ich przetestowanie w środowisku REPL lub edytorze. Warto również zwrócić uwagę na powiązania między tym tematem a innymi zagadnieniami omawianymi w kursie. Systematyczne budowanie wiedzy krok po kroku to klucz do opanowania programowania obiektowego w Pythonie. Nie pomijaj żadnego slajdu, nawet jeśli wydaje Ci się, że temat jest Ci już znany - powtórka utrwala wiedzę i pozwala dostrzec nowe szczegóły w znajomym materiale.

Zachęcamy do samodzielnego eksperymentowania z omawianymi mechanizmami i modyfikowania przykładowego kodu. Praktyczne ćwiczenia i własne projekty to najskuteczniejsza metoda nauki programowania, ponieważ wymagają aktywnego stosowania wiedzy. Pamiętaj, że błędy są naturalną częścią procesu uczenia się i każda pomyłka przybliża Cię do mistrzostwa. Warto prowadzić własny dziennik błędów, w którym zapisujesz napotkane problemy i ich rozwiązania. Taki zeszyt stanie się z czasem bezcennym źródłem wiedzy i referencją na przyszłość. Korzystaj z dokumentacji oficjalnej Pythona oraz społeczności programistycznych - to nieocenione źródła wiedzy i wsparcia w trudnych momentach.

6 Kod: sprawdzanie relacji issubclass

Sprawdzanie relacji dziedziczenia - issubclass i isinstance

# Definiujemy hierarchię
class Zwierze:
    pass

class Ssak(Zwierze):
    pass

class Pies(Ssak):
    pass

# issubclass() - sprawdza relacje miedzy klasami
print(issubclass(Pies, Zwierze))   # True - Pies jest podklasa Zwierze
print(issubclass(Pies, Ssak))      # True - Pies jest podklasa Ssak
print(issubclass(Ssak, Pies))       # False - Ssak NIE jest podklasa Pies
print(issubclass(Pies, Pies))       # True - kazda klasa jest podklasa samej siebie

# isinstance() - sprawdza przynaleznosc obiektu do klasy
p = Pies()
print(isinstance(p, Zwierze))          # True - obiekt Pies jest instancja Zwierze
print(isinstance(p, Ssak))             # True - obiekt Pies jest instancja Ssak
print(isinstance(p, Pies))             # True - obiekt Pies jest instancja Pies

# isinstance() dziala rowniez z obiektem object
print(isinstance(p, object))            # True - kazdy obiekt w Pythonie dziedziczy po object

# Przydatne w praktyce - sprawdzanie przed wykonaniem operacji
def wykonaj_dzwiek(zwierze):
    if isinstance(zwierze, Pies):
        return "Hau!"
    elif isinstance(zwierze, Ssak):
        return "Cos piszczy"
    else:
        return "Nieznany dzwiek"

print(wykonaj_dzwiek(p))  # Hau!

Quiz podsumowujący to nie tylko sprawdzenie wiedzy, ale także okazja do utrwalenia najważniejszych koncepcji poprzez aktywne przypominanie sobie materiału. Badania z zakresu neurodydaktyki pokazują, że testowanie własnej wiedzy jest jedną z najskuteczniejszych metod uczenia się, znacznie efektywniejszą niż bierne czytanie. Każde pytanie quizu zostało starannie zaprojektowane, aby sprawdzić zrozumienie konkretnego zagadnienia, a nie tylko pamięciowe odtworzenie definicji. Jeżeli któreś pytanie sprawiło Ci trudność, potraktuj to jako sygnał, że dany temat wymaga powtórzenia. Wróć do odpowiedniego slajdu i przeanalizuj go jeszcze raz, tym razem zwracając uwagę na szczegóły, które mogły Ci umknąć.

Sukces w nauce programowania polega na systematycznym budowaniu wiedzy - każda kolejna część opiera się na fundamentach z części poprzednich. Jeżeli masz wątpliwości co do którejkolwiek koncepcji, nie przechodź dalej, dopóki jej nie wyjaśnisz. Wykorzystaj dostępne zasoby: dokumentację Pythona, fora społecznościowe, tutoriale wideo i oczywiście możliwość eksperymentowania we własnym środowisku programistycznym. Pamiętaj, że każdy zaawansowany programista zaczynał od podstaw i pokonał te same trudności co Ty teraz. Gratulujemy ukończenia kolejnego etapu nauki i życzymy powodzenia w dalszej części kursu.

7Podsumowanie "jest-a"

Relacja "jest-a" - kluczowe punkty

  • Dziedziczenie modeluje relację "X jest Y" (np. Pies jest Zwierzęciem)
  • Klasa pochodna dziedziczy wszystkie publiczne składniki klasy bazowej
  • Można przesłonić (override) metody, zmieniając ich działanie
  • Python wspiera wielodziedziczenie (klasa może mieć wiele klas bazowych)
  • Sprawdzanie relacji: issubclass() i isinstance()
  • Zaleta: ponowne użycie kodu, polimorfizm, przejrzysta hierarchia
  • Wada: sztywne powiązanie, problemy przy głębokim dziedziczeniu

Dziedziczenie to potężne narzędzie, ale jak każde potężne narzędzie, wymaga rozwagi. Stosuj je, gdy relacja "jest-a" jest naturalna i stabilna. Unikaj tworzenia hierarchii głębszych niż 2-3 poziomy - każdy dodatkowy poziom zwiększa ryzyko kruchości i utrudnia zrozumienie kodu. Pamiętaj, że w Pythonie wszystko dziedziczy po object , więc nawet niejawnie każda klasa uczestniczy w hierarchii dziedziczenia. Świadome projektowanie relacji między klasami to kluczowa umiejętność, którą będziesz doskonalić przez całą swoją przygodę z programowaniem obiektowym.

Podsumowanie jest-a

Podsumowanie to dobry moment na refleksję nad przyswojonym materiałem i identyfikację obszarów wymagających dodatkowej pracy. Wymienione punkty stanowią esencję przerobionej części kursu - od definicji klasy, przez tworzenie obiektów, aż po kompozycję i wzorzec fabryki. Każdy z tych punktów będzie rozwijany w kolejnych modułach, dlatego warto upewnić się, że są dobrze zrozumiane. Zachęcamy do tworzenia własnych notatek i map myśli, które pomagają w usystematyzowaniu wiedzy. Regularne powtórki są kluczowe dla trwałego zapamiętania materiału.

Mapy myśli są skutecznym narzędziem wizualizacji złożonych koncepcji i relacji między nimi. Przedstawiona mapa obrazuje najważniejsze pojęcia omówione w tej części kursu oraz ich wzajemne powiązania. Tworzenie własnych map myśli podczas nauki programowania aktywuje inne obszary mózgu niż czytanie liniowego tekstu, co przekłada się na lepsze zapamiętywanie. Studenci, którzy regularnie tworzą mapy myśli, osiągają lepsze wyniki w testach koncepcyjnych i szybciej łączą nowe informacje z już posiadaną wiedzą. Zachęcamy do wykorzystania tej techniki.

8 Relacja "ma-a" (has-a) - kompozycja

Relacja "ma-a"

Relacja "ma-a" (ang. has-a ) oznacza, że obiekt jednej klasy zawiera lub składa się z obiektów innych klas. To relacja część–całość , w której całość posiada części, ale części nie są całością - różni się to zasadniczo od relacji "jest-a", gdzie klasa pochodna jest szczególnym przypadkiem klasy bazowej.

Przykłady:
Samochód ma silnik.
Człowiek ma adres.
Książka ma strony.
Szkoła ma uczniów.
Restauracja ma menu.

W Pythonie kompozycję implementujemy przez przechowywanie obiektu jako atrybutu innego obiektu. Obiekt "posiadający" (nadrzędny) zarządza cyklem życia obiektu "posiadanego" (składowego). Oznacza to, że gdy obiekt nadrzędny jest tworzony, tworzy również swoje komponenty, a gdy jest niszczony (traci referencję), jego komponenty również przestają istnieć.

Kompozycja realizuje zasadę black-box reuse (ponowne użycie przez "czarną skrzynkę") - obiekt nadrzędny korzysta z gotowych komponentów, nie wiedząc, jak są one zaimplementowane wewnętrznie. To prowadzi do luźniejszych powiązań między klasami i łatwiejszego testowania.

Relacja ma-a

Przechowywanie obiektów w kolekcjach, takich jak listy, słowniki czy zbiory, jest podstawowym wzorcem w programowaniu obiektowym. Listy obiektów pozwalają na zbiorcze operacje, takie jak iterowanie, filtrowanie, sortowanie czy agregowanie danych. Python oferuje bogaty zestaw narzędzi do pracy z listami, w tym wyrażenia listowe, funkcje sorted(), filter() oraz sortowanie z kluczem lambda. Ważne jest zrozumienie, że lista przechowuje referencje do obiektów, a nie ich kopie, co ma wpływ na modyfikację danych przez różne części programu. Iterowanie po liście obiektów i wywoływanie ich metod to jeden z najczęstszych wzorców w codziennej pracy.

Kompozycja, czyli umieszczanie jednych obiektów wewnątrz innych, jest podstawową techniką budowania złożonych systemów z prostszych komponentów. W przeciwieństwie do dziedziczenia, które wyraża relację 'jest', kompozycja wyraża relację 'ma' i jest często preferowanym podejściem w projektowaniu obiektowym. Graf obiektów w rzeczywistych systemach może być bardzo złożony, ale nawigacja po nim jest naturalna i czytelna dzięki notacji kropkowej. Kompozycja pozwala na elastyczne budowanie funkcjonalności poprzez łączenie prostych obiektów w bardziej złożone struktury. Wzorzec ten jest fundamentem takich koncepcji jak wstrzykiwanie zależności czy architektura warstwowa.

9 Kod: "Samochod ma-silnik"

Kompozycja - Samochód ma Silnik

class Silnik:
    def __init__(self, moc, typ="benzynowy"):
        self.moc = moc
        self.typ = typ
        self.wlaczony = False

    def uruchom(self):
        self.wlaczony = True
        return f"Silnik {self.typ} o mocy {self.moc} KM wlaczony."

    def wylacz(self):
        self.wlaczony = False
        return "Silnik wylaczony."

class Samochod:
    def __init__(self, marka, moc, typ_silnika="benzynowy"):
        self.marka = marka
        self.silnik = Silnik(moc, typ_silnika)  # kompozycja - silnik tworzony wewnatrz

    def jedz(self):
        return self.silnik.uruchom()  # delegacja do Silnik.uruchom()

    def zatrzymaj(self):
        return self.silnik.wylacz()

# Tworzymy samochod - silnik powstaje automatycznie w konstruktorze
auto = Samochod("Toyota", 150)
print(auto.jedz())       # delegacja: Samochod.jedz() -> Silnik.uruchom()
print(auto.zatrzymaj())  # delegacja: Samochod.zatrzymaj() -> Silnik.wylacz()

# Samochod "ma" silnik, ale NIE "jest" silnikiem
# auto.uruchom()  # to NIE zadziala - Samochod nie ma metody uruchom(), ma jedz()
# auto.wlaczony   # to NIE zadziala - wlaczony nalezy do Silnik, nie do Samochod

Slajd zatytułowany "Kod: "Samochod ma-silnik"" przedstawia istotne zagadnienie z zakresu programowania obiektowego w Pythonie. Zrozumienie przedstawionych tu koncepcji jest niezbędne do dalszej nauki i praktycznego stosowania OOP w codziennej pracy programisty. Zaleca się dokładne przeanalizowanie przykładów kodu i samodzielne ich przetestowanie w środowisku REPL lub edytorze. Warto również zwrócić uwagę na powiązania między tym tematem a innymi zagadnieniami omawianymi w kursie. Systematyczne budowanie wiedzy krok po kroku to klucz do opanowania programowania obiektowego w Pythonie. Nie pomijaj żadnego slajdu, nawet jeśli wydaje Ci się, że temat jest Ci już znany - powtórka utrwala wiedzę i pozwala dostrzec nowe szczegóły w znajomym materiale.

Zachęcamy do samodzielnego eksperymentowania z omawianymi mechanizmami i modyfikowania przykładowego kodu. Praktyczne ćwiczenia i własne projekty to najskuteczniejsza metoda nauki programowania, ponieważ wymagają aktywnego stosowania wiedzy. Pamiętaj, że błędy są naturalną częścią procesu uczenia się i każda pomyłka przybliża Cię do mistrzostwa. Warto prowadzić własny dziennik błędów, w którym zapisujesz napotkane problemy i ich rozwiązania. Taki zeszyt stanie się z czasem bezcennym źródłem wiedzy i referencją na przyszłość. Korzystaj z dokumentacji oficjalnej Pythona oraz społeczności programistycznych - to nieocenione źródła wiedzy i wsparcia w trudnych momentach.

10 Kod: "Czlowiek ma-adres"

Kompozycja - Człowiek ma Adres

class Adres:
    def __init__(self, miasto, ulica, kod_pocztowy=""):
        self.miasto = miasto
        self.ulica = ulica
        self.kod_pocztowy = kod_pocztowy

    def pobierz_adres(self):
        return f"{self.ulica}, {self.miasto}"

    def pobierz_adres_pelny(self):
        if self.kod_pocztowy:
            return f"{self.ulica}, {self.kod_pocztowy} {self.miasto}"
        return self.pobierz_adres()

class Czlowiek:
    def __init__(self, imie, miasto, ulica, kod=""):
        self.imie = imie
        self.adres = Adres(miasto, ulica, kod)  # kompozycja - Adres tworzony wewnatrz

    def przedstaw_sie(self):
        return f"Mam na imie {self.imie} i mieszkam pod adresem: {self.adres.pobierz_adres()}"

osoba1 = Czlowiek("Jan", "Warszawa", "Marszalkowska 1", "00-001")
print(osoba1.przedstaw_sie())

# Adres istnieje tylko jako czesc Czlowieka
# Nie ma zewnetrznej referencji do obiektu Adres
# Gdy osoba1 zostanie usunieta, adres rowniez

# Dostep do atrybutow komponentu - przez obiekt nadrzedny
print(osoba1.adres.pobierz_adres_pelny())  # delegacja

Slajd zatytułowany "Kod: "Czlowiek ma-adres"" przedstawia istotne zagadnienie z zakresu programowania obiektowego w Pythonie. Zrozumienie przedstawionych tu koncepcji jest niezbędne do dalszej nauki i praktycznego stosowania OOP w codziennej pracy programisty. Zaleca się dokładne przeanalizowanie przykładów kodu i samodzielne ich przetestowanie w środowisku REPL lub edytorze. Warto również zwrócić uwagę na powiązania między tym tematem a innymi zagadnieniami omawianymi w kursie. Systematyczne budowanie wiedzy krok po kroku to klucz do opanowania programowania obiektowego w Pythonie. Nie pomijaj żadnego slajdu, nawet jeśli wydaje Ci się, że temat jest Ci już znany - powtórka utrwala wiedzę i pozwala dostrzec nowe szczegóły w znajomym materiale.

Zachęcamy do samodzielnego eksperymentowania z omawianymi mechanizmami i modyfikowania przykładowego kodu. Praktyczne ćwiczenia i własne projekty to najskuteczniejsza metoda nauki programowania, ponieważ wymagają aktywnego stosowania wiedzy. Pamiętaj, że błędy są naturalną częścią procesu uczenia się i każda pomyłka przybliża Cię do mistrzostwa. Warto prowadzić własny dziennik błędów, w którym zapisujesz napotkane problemy i ich rozwiązania. Taki zeszyt stanie się z czasem bezcennym źródłem wiedzy i referencją na przyszłość. Korzystaj z dokumentacji oficjalnej Pythona oraz społeczności programistycznych - to nieocenione źródła wiedzy i wsparcia w trudnych momentach.

11 Kiedy kompozycja ma sens

Kompozycję stosuj, gdy:

  • Obiekt zawiera inny obiekt jako swoją część (np. Rower ma Koło)
  • Chcesz ukryć implementację składowych przed światem zewnętrznym
  • Potrzebujesz elastyczności - możesz wymieniać komponenty w trakcie działania
  • Relacja między klasami to "ma-a", a nie "jest-a"
  • Chcesz uniknąć sztywnych hierarchii dziedziczenia
  • Poszczególne komponenty mogą być testowane niezależnie

Kompozycja jest szczególnie użyteczna, gdy przewidujesz, że zachowanie obiektu może się zmieniać w czasie. Na przykład gracz w grze komputerowej może w różnych momentach posiadać różną broń - kompozycja pozwala dynamicznie zmieniać wyposażenie bez tworzenia rozbudowanej hierarchii klas.

Kolejną zaletą kompozycji jest testowalność. Możesz przetestować każdy komponent w izolacji, a następnie testować obiekt nadrzędny z atrapami (mockami) komponentów. To znacznie upraszcza pisanie testów jednostkowych i zwiększa zaufanie do kodu.

Kompozycja wspiera również zasadę pojedynczej odpowiedzialności (Single Responsibility Principle) - każda klasa odpowiada za jedną rzecz, a złożone zachowanie powstaje przez łączenie małych, wyspecjalizowanych komponentów.

Przechowywanie obiektów w kolekcjach, takich jak listy, słowniki czy zbiory, jest podstawowym wzorcem w programowaniu obiektowym. Listy obiektów pozwalają na zbiorcze operacje, takie jak iterowanie, filtrowanie, sortowanie czy agregowanie danych. Python oferuje bogaty zestaw narzędzi do pracy z listami, w tym wyrażenia listowe, funkcje sorted(), filter() oraz sortowanie z kluczem lambda. Ważne jest zrozumienie, że lista przechowuje referencje do obiektów, a nie ich kopie, co ma wpływ na modyfikację danych przez różne części programu. Iterowanie po liście obiektów i wywoływanie ich metod to jeden z najczęstszych wzorców w codziennej pracy.

Kompozycja, czyli umieszczanie jednych obiektów wewnątrz innych, jest podstawową techniką budowania złożonych systemów z prostszych komponentów. W przeciwieństwie do dziedziczenia, które wyraża relację 'jest', kompozycja wyraża relację 'ma' i jest często preferowanym podejściem w projektowaniu obiektowym. Graf obiektów w rzeczywistych systemach może być bardzo złożony, ale nawigacja po nim jest naturalna i czytelna dzięki notacji kropkowej. Kompozycja pozwala na elastyczne budowanie funkcjonalności poprzez łączenie prostych obiektów w bardziej złożone struktury. Wzorzec ten jest fundamentem takich koncepcji jak wstrzykiwanie zależności czy architektura warstwowa.

12Podsumowanie "ma-a"

Relacja "ma-a" - kluczowe punkty

  • Kompozycja modeluje relację "X ma Y" (np. Samochód ma Silnik)
  • Obiekt składowy jest atrybutem obiektu nadrzędnego
  • Cykl życia obiektu składowego jest zazwyczaj związany z cyklem życia obiektu nadrzędnego (kompozycja ścisła)
  • Możliwe jest delegowanie zachowania do obiektów składowych
  • Zaleta: elastyczność, luźne powiązania, łatwe testowanie
  • Wada: więcej klas, ręczne przekazywanie metod (delegacja)

W praktyce kompozycja jest często lepszym wyborem niż dziedziczenie, szczególnie gdy system ewoluuje w czasie. Relacje "ma-a" są bardziej elastyczne i łatwiejsze do modyfikacji niż sztywne hierarchie "jest-a". Jednak kompozycja wymaga więcej początkowego wysiłku - trzeba zaprojektować interfejsy komponentów i jawnie delegować metody. To inwestycja, która zwraca się w dłuższej perspektywie, gdy system wymaga rozbudowy lub modyfikacji.

Podsumowanie ma-a

Podsumowanie to dobry moment na refleksję nad przyswojonym materiałem i identyfikację obszarów wymagających dodatkowej pracy. Wymienione punkty stanowią esencję przerobionej części kursu - od definicji klasy, przez tworzenie obiektów, aż po kompozycję i wzorzec fabryki. Każdy z tych punktów będzie rozwijany w kolejnych modułach, dlatego warto upewnić się, że są dobrze zrozumiane. Zachęcamy do tworzenia własnych notatek i map myśli, które pomagają w usystematyzowaniu wiedzy. Regularne powtórki są kluczowe dla trwałego zapamiętania materiału.

Mapy myśli są skutecznym narzędziem wizualizacji złożonych koncepcji i relacji między nimi. Przedstawiona mapa obrazuje najważniejsze pojęcia omówione w tej części kursu oraz ich wzajemne powiązania. Tworzenie własnych map myśli podczas nauki programowania aktywuje inne obszary mózgu niż czytanie liniowego tekstu, co przekłada się na lepsze zapamiętywanie. Studenci, którzy regularnie tworzą mapy myśli, osiągają lepsze wyniki w testach koncepcyjnych i szybciej łączą nowe informacje z już posiadaną wiedzą. Zachęcamy do wykorzystania tej techniki.

13 Kompozycja: obiekt zawiera inny obiekt

Obiekt jako atrybut

W kompozycji jeden obiekt przechowuje referencję do innego obiektu jako swojego atrybutu. To najprostsza forma relacji "ma-a". W przeciwieństwie do dziedziczenia, gdzie klasa pochodna przejmuje cechy klasy bazowej, w kompozycji obiekt nadrzędny po prostu posiada obiekt składowy i deleguje do niego odpowiednie zadania.

Istnieją trzy kluczowe aspekty kompozycji:

  1. Tworzenie - obiekt składowy jest tworzony wewnątrz obiektu nadrzędnego (w __init__ )
  2. Delegacja - obiekt nadrzędny wywołuje metody obiektu składowego, udostępniając ich funkcjonalność na zewnątrz
  3. Zależność życia - gdy obiekt nadrzędny jest niszczony, obiekt składowy również przestaje istnieć (w przeciwieństwie do agregacji)

Te trzy aspekty odróżniają ścisłą kompozycję od luźniejszej agregacji. W kompozycji obiekt nadrzędny jest pełnym "właścicielem" obiektu składowego - decyduje o jego utworzeniu i kontroluje jego cykl życia. Dzięki temu enkapsulacja jest pełniejsza: świat zewnętrzny nie musi wiedzieć o istnieniu komponentów - komunikuje się tylko z obiektem nadrzędnym.

Tworzenie obiektu przez wywołanie klasy to jeden z najważniejszych wzorców w Pythonie. Składnia NazwaKlasy() może być myląca dla początkujących, ponieważ wygląda jak wywołanie funkcji, ale w rzeczywistości uruchamia złożony proces alokacji i inicjalizacji. Python, jako język dynamiczny, pozwala na tworzenie obiektów w dowolnym momencie działania programu, w tym wewnątrz pętli, warunków czy wyrażeń listowych. Każde wywołanie klasy tworzy nowy, niezależny obiekt w pamięci, nawet jeżeli klasa nie ma zdefiniowanego konstruktora. Zmienna przechowuje referencję do obiektu, a nie sam obiekt, co ma kluczowe znaczenie dla zrozumienia mechanizmu przypisań i kopiowania.

Proces tworzenia obiektu składa się z kilku etapów: najpierw wywoływana jest metoda __new__, która alokuje pamięć dla nowej instancji, następnie uruchamiany jest __init__ do inicjalizacji atrybutów, a na końcu zwracana jest referencja do gotowego obiektu. Większość programistów modyfikuje wyłącznie __init__, pozostawiając __new__ w domyślnej implementacji. Zrozumienie tej kolejności jest ważne przy tworzeniu bardziej zaawansowanych wzorców, takich jak Singleton czy Fabryka. Gdy tworzymy wiele obiektów w pętli, każdy z nich jest niezależną instancją z własnym stanem i tożsamością.

14 Kod: kompozycja w __init__

Tworzenie komponentu w konstruktorze - Rower ma Koła

class Kolo:
    def __init__(self, rozmiar):
        self.rozmiar = rozmiar
        self.przebita = False

    def przebij(self):
        self.przebita = True
        return "Kolo przebite!"

    def napompuj(self):
        self.przebita = False
        return "Kolo napompowane."

class Rower:
    def __init__(self, rozmiar_kol):
        # Kompozycja: Rower tworzy obiekty Kol w konstruktorze
        # Kola nie istnieja bez roweru - scisla zaleznosc zycia
        self.kolo_przednie = Kolo(rozmiar_kol)
        self.kolo_tylne = Kolo(rozmiar_kol)

    def opis(self):
        return (f"Rower z kolami rozmiaru "
                f"{self.kolo_przednie.rozmiar}")

    def przebij_przednie(self):
        return self.kolo_przednie.przebij()  # delegacja

    def napompuj_wszystkie(self):
        self.kolo_przednie.napompuj()
        self.kolo_tylne.napompuj()
        return "Oba kola napompowane."

r = Rower(29)
print(r.opis())
print(r.przebij_przednie())
print(r.napompuj_wszystkie())
Kompozycja init

Atrybuty są podstawowym mechanizmem przechowywania stanu obiektu i mogą zawierać dane dowolnego typu dostępnego w Pythonie. Każdy obiekt ma własną, niezależną kopię atrybutów zdefiniowanych w konstruktorze, co oznacza, że zmiana wartości w jednej instancji nie wpływa na pozostałe. W Pythonie atrybuty są przechowywane w słowniku __dict__ każdego obiektu, co umożliwia dynamiczne dodawanie i usuwanie atrybutów w czasie wykonania. Chociaż dynamiczne dodawanie atrybutów jest możliwe, w praktyce zaleca się definiowanie wszystkich atrybutów w konstruktorze __init__ dla zachowania przewidywalności kodu. Taki nawyk sprawia, że struktura obiektu jest jasna dla każdego programisty czytającego definicję klasy. Ponadto wiele narzędzi do statycznej analizy kodu wymaga jawnego deklarowania atrybutów.

Konstruktor __init__ jest wywoływany automatycznie po utworzeniu obiektu przez metodę __new__, która alokuje pamięć dla nowej instancji. Zadaniem __init__ jest przygotowanie obiektu do użycia poprzez ustawienie początkowych wartości atrybutów i wykonanie ewentualnych czynności inicjalizacyjnych. W przeciwieństwie do niektórych innych języków, Python nie wspiera przeciążania konstruktorów - zamiast tego stosuje się parametry opcjonalne z wartościami domyślnymi oraz wzorzec fabryki. Parametr self, obowiązkowy w każdej metodzie instancyjnej, jest referencją do konkretnego obiektu, na którym wywołano metodę. Dzięki self metoda ma dostęp do wszystkich atrybutów i innych metod danego obiektu.

15Kod: delegacja metod

Delegacja - obiekt nadrzędny wywołuje metody składowe

Delegacja to mechanizm, w którym obiekt nadrzędny przekazuje wykonanie zadania do obiektu składowego. Dzięki temu obiekt nadrzędny nie musi znać szczegółów implementacji - wystarczy, że wie, jaki interfejs udostępnia komponent.

class Drukarka:
    def drukuj(self, tekst):
        return f"Drukowanie: {tekst}"

    def skanuj(self):
        return "Skanowanie zakonczone."

class Komputer:
    def __init__(self):
        self.drukarka = Drukarka()  # kompozycja

    # Delegacja - Komputer deleguje drukowanie do Drukarki
    def drukuj_dokument(self, dokument):
        # Metoda Komputera tylko deleguje do Drukarki
        return self.drukarka.drukuj(dokument)

    def skanuj_dokument(self):
        return self.drukarka.skanuj()

    def drukuj_wielokrotnie(self, tekst, razy):
        # Delegacja z dodatkowa logika - wypisywanie wiele razy
        wyniki = []
        for i in range(razy):
            wyniki.append(self.drukarka.drukuj(f"{tekst} (kopia {i+1})"))
        return "\n".join(wyniki)

pc = Komputer()
print(pc.drukuj_dokument("Raport roczny"))
print(pc.skanuj_dokument())
print(pc.drukuj_wielokrotnie("Faktura", 3))
Delegacja metod

Metody są funkcjami zdefiniowanymi w przestrzeni nazw klasy, które otrzymują automatycznie pierwszy argument w postaci referencji do obiektu. Dzięki temu mogą odczytywać i modyfikować stan obiektu, na którym zostały wywołane. W Pythonie istnieje kilka rodzajów metod: instancyjne (z self), klasowe (z cls i dekoratorem @classmethod), statyczne (z @staticmethod) oraz abstrakcyjne (z @abstractmethod w klasach ABC). Metody instancyjne są najczęściej używane i to one będą głównym tematem tej części kursu. Każda metoda instancyjna przyjmuje self jako pierwszy parametr, a Python automatycznie przekazuje obiekt w momencie wywołania przez notację kropkową. Metody mogą przyjmować dodatkowe argumenty, mieć wartości domyślne i zwracać wartości za pomocą instrukcji return.

Dobrą praktyką projektowania metod jest zasada pojedynczej odpowiedzialności - każda metoda powinna wykonywać jedną, dobrze określoną operację. Metody tylko do odczytu stanu, nazywane getterami, nie powinny modyfikować atrybutów obiektu. Metody modyfikujące stan, nazywane setterami, powinny walidować dane wejściowe przed wprowadzeniem zmian. Przestrzeganie tych zasad prowadzi do kodu łatwiejszego w testowaniu i debugowaniu. Warto również stosować metody pomocnicze, aby uniknąć powielania kodu wewnątrz klasy. Dobrze zaprojektowane metody sprawiają, że klasa jest intuicyjna w użyciu i trudna do niepoprawnego wykorzystania.

16Kod: zależność życia

Zależność życia w kompozycji - Sesja jako część Aplikacji

Zależność życia oznacza, że obiekt składowy istnieje tylko tak długo, jak istnieje obiekt nadrzędny. W tym przykładzie Sesja jest tworzona przez Aplikacje przy logowaniu i niszczona przy wylogowaniu - nie może istnieć poza kontekstem aplikacji.

class Sesja:
    def __init__(self, uzytkownik):
        self.uzytkownik = uzytkownik
        self.aktywna = True
        self.dane = {}  # przechowuje dane sesji
        print(f"Sesja utworzona dla {uzytkownik}")

    def ustaw_dane(self, klucz, wartosc):
        self.dane[klucz] = wartosc

    def pobierz_dane(self, klucz):
        return self.dane.get(klucz)

    def zakoncz(self):
        self.aktywna = False
        self.dane.clear()
        print(f"Sesja zakonczona dla {self.uzytkownik}")

class Aplikacja:
    def __init__(self):
        self.sesja = None  # brak sesji na starcie

    def loguj(self, uzytkownik):
        # Tworzymy nowa sesje - zaleznosc zycia: sesja zalezy od aplikacji
        self.sesja = Sesja(uzytkownik)

    def wyloguj(self):
        if self.sesja:
            self.sesja.zakoncz()
        self.sesja = None  # Sesja przestaje istniec (brak referencji)

    def czy_zalogowany(self):
        return self.sesja is not None and self.sesja.aktywna

app = Aplikacja()
app.loguj("janek")
print(app.czy_zalogowany())  # True
app.wyloguj()
print(app.czy_zalogowany())  # False
Zależność życia

Slajd zatytułowany "Kod: zależność życia" przedstawia istotne zagadnienie z zakresu programowania obiektowego w Pythonie. Zrozumienie przedstawionych tu koncepcji jest niezbędne do dalszej nauki i praktycznego stosowania OOP w codziennej pracy programisty. Zaleca się dokładne przeanalizowanie przykładów kodu i samodzielne ich przetestowanie w środowisku REPL lub edytorze. Warto również zwrócić uwagę na powiązania między tym tematem a innymi zagadnieniami omawianymi w kursie. Systematyczne budowanie wiedzy krok po kroku to klucz do opanowania programowania obiektowego w Pythonie. Nie pomijaj żadnego slajdu, nawet jeśli wydaje Ci się, że temat jest Ci już znany - powtórka utrwala wiedzę i pozwala dostrzec nowe szczegóły w znajomym materiale.

Zachęcamy do samodzielnego eksperymentowania z omawianymi mechanizmami i modyfikowania przykładowego kodu. Praktyczne ćwiczenia i własne projekty to najskuteczniejsza metoda nauki programowania, ponieważ wymagają aktywnego stosowania wiedzy. Pamiętaj, że błędy są naturalną częścią procesu uczenia się i każda pomyłka przybliża Cię do mistrzostwa. Warto prowadzić własny dziennik błędów, w którym zapisujesz napotkane problemy i ich rozwiązania. Taki zeszyt stanie się z czasem bezcennym źródłem wiedzy i referencją na przyszłość. Korzystaj z dokumentacji oficjalnej Pythona oraz społeczności programistycznych - to nieocenione źródła wiedzy i wsparcia w trudnych momentach.

17 Podsumowanie kompozycji

Kompozycja - kluczowe cechy

  • Obiekt składowy jest tworzony wewnątrz obiektu nadrzędnego
  • Obiekt nadrzędny deleguje zadania do obiektu składowego
  • Cykl życia obiektu składowego jest zależny od cyklu życia obiektu nadrzędnego
  • Obiekt składowy nie istnieje samodzielnie - jest częścią całości
  • Umożliwia enkapsulację - szczegóły implementacji składowej są ukryte
  • Ułatwia testowanie - komponenty można testować w izolacji

Kompozycja jest fundamentem elastycznego, modułowego kodu. Jej główną siłą jest to, że pozwala budować złożone systemy z prostych, niezależnych komponentów. Każdy komponent ma jasno określoną odpowiedzialność i może być rozwijany, testowany i modyfikowany niezależnie od reszty systemu. To sprawia, że kompozycja jest preferowanym wyborem w nowoczesnym projektowaniu obiektowym, szczególnie w połączeniu z takimi wzorcami jak Strategia, Dekorator czy Fasada.

Przechowywanie obiektów w kolekcjach, takich jak listy, słowniki czy zbiory, jest podstawowym wzorcem w programowaniu obiektowym. Listy obiektów pozwalają na zbiorcze operacje, takie jak iterowanie, filtrowanie, sortowanie czy agregowanie danych. Python oferuje bogaty zestaw narzędzi do pracy z listami, w tym wyrażenia listowe, funkcje sorted(), filter() oraz sortowanie z kluczem lambda. Ważne jest zrozumienie, że lista przechowuje referencje do obiektów, a nie ich kopie, co ma wpływ na modyfikację danych przez różne części programu. Iterowanie po liście obiektów i wywoływanie ich metod to jeden z najczęstszych wzorców w codziennej pracy.

Kompozycja, czyli umieszczanie jednych obiektów wewnątrz innych, jest podstawową techniką budowania złożonych systemów z prostszych komponentów. W przeciwieństwie do dziedziczenia, które wyraża relację 'jest', kompozycja wyraża relację 'ma' i jest często preferowanym podejściem w projektowaniu obiektowym. Graf obiektów w rzeczywistych systemach może być bardzo złożony, ale nawigacja po nim jest naturalna i czytelna dzięki notacji kropkowej. Kompozycja pozwala na elastyczne budowanie funkcjonalności poprzez łączenie prostych obiektów w bardziej złożone struktury. Wzorzec ten jest fundamentem takich koncepcji jak wstrzykiwanie zależności czy architektura warstwowa.

18 Agregacja - słabsza forma

Agregacja - różnica od kompozycji

Agregacja to słabsza forma relacji "ma-a". W agregacji obiekt składowy może istnieć niezależnie od obiektu nadrzędnego. Obiekty są ze sobą powiązane, ale nie są nierozerwalnie związane. Agregacja modeluje sytuacje, w których części mogą być współdzielone między różnymi całościami lub mogą zmieniać przynależność.

Różnica kluczowa:

  • Kompozycja: część nie istnieje bez całości (np. Strona w Książce - strona bez książki nie ma sensu)
  • Agregacja: część może istnieć bez całości (np. Student w grupie zajęciowej - student istnieje również poza grupą)

W praktyce agregacja występuje, gdy obiekty są ze sobą luźno powiązane. Na przykład samochód może mieć kierowcę, ale kierowca nie jest częścią samochodu - może zmienić samochód lub istnieć bez niego. Podobnie pracownik może należeć do działu, ale dział istnieje niezależnie od pracownika.

W Pythonie agregacja technicznie wygląda tak samo jak kompozycja - to wciąż przechowywanie referencji. Różnica jest koncepcyjna i dotyczy tego, kto kontroluje cykl życia obiektu składowego.

Agregacja różnica

Slajd zatytułowany "Agregacja - słabsza forma" przedstawia istotne zagadnienie z zakresu programowania obiektowego w Pythonie. Zrozumienie przedstawionych tu koncepcji jest niezbędne do dalszej nauki i praktycznego stosowania OOP w codziennej pracy programisty. Zaleca się dokładne przeanalizowanie przykładów kodu i samodzielne ich przetestowanie w środowisku REPL lub edytorze. Warto również zwrócić uwagę na powiązania między tym tematem a innymi zagadnieniami omawianymi w kursie. Systematyczne budowanie wiedzy krok po kroku to klucz do opanowania programowania obiektowego w Pythonie. Nie pomijaj żadnego slajdu, nawet jeśli wydaje Ci się, że temat jest Ci już znany - powtórka utrwala wiedzę i pozwala dostrzec nowe szczegóły w znajomym materiale.

Zachęcamy do samodzielnego eksperymentowania z omawianymi mechanizmami i modyfikowania przykładowego kodu. Praktyczne ćwiczenia i własne projekty to najskuteczniejsza metoda nauki programowania, ponieważ wymagają aktywnego stosowania wiedzy. Pamiętaj, że błędy są naturalną częścią procesu uczenia się i każda pomyłka przybliża Cię do mistrzostwa. Warto prowadzić własny dziennik błędów, w którym zapisujesz napotkane problemy i ich rozwiązania. Taki zeszyt stanie się z czasem bezcennym źródłem wiedzy i referencją na przyszłość. Korzystaj z dokumentacji oficjalnej Pythona oraz społeczności programistycznych - to nieocenione źródła wiedzy i wsparcia w trudnych momentach.

19 Kod: agregacja - obiekt z zewnątrz

Agregacja - obiekt przekazany z zewnątrz

W agregacji obiekt składowy jest tworzony na zewnątrz i przekazywany do obiektu nadrzędnego. Oznacza to, że ten sam obiekt może być współdzielony przez wiele obiektów nadrzędnych.

class Student:
    def __init__(self, imie):
        self.imie = imie
        self.indeks = True

class Grupa:
    def __init__(self, nazwa):
        self.nazwa = nazwa
        self.studenci = []   # agregacja - lista referencji

    def dodaj_studenta(self, student):
        self.studenci.append(student)  # obiekt istniał wcześniej

    def lista_studentow(self):
        return [s.imie for s in self.studenci]

# Studenci istnieja niezaleznie od grupy
s1 = Student("Anna")
s2 = Student("Piotr")

# Ta sama grupa przyjmuje studentow
grupa = Grupa("Grupa A")
grupa.dodaj_studenta(s1)
grupa.dodaj_studenta(s2)
print(grupa.lista_studentow())  # ['Anna', 'Piotr']

# Student moze nalezec do wielu grup jednoczesnie
grupa_b = Grupa("Grupa B")
grupa_b.dodaj_studenta(s1)  # Anna nalezy do dwoch grup

# Usuniecie grupy nie niszczy studentow
del grupa
print(s1.imie)  # Anna - student istnieje dalej
Agregacja kod

Tworzenie obiektu przez wywołanie klasy to jeden z najważniejszych wzorców w Pythonie. Składnia NazwaKlasy() może być myląca dla początkujących, ponieważ wygląda jak wywołanie funkcji, ale w rzeczywistości uruchamia złożony proces alokacji i inicjalizacji. Python, jako język dynamiczny, pozwala na tworzenie obiektów w dowolnym momencie działania programu, w tym wewnątrz pętli, warunków czy wyrażeń listowych. Każde wywołanie klasy tworzy nowy, niezależny obiekt w pamięci, nawet jeżeli klasa nie ma zdefiniowanego konstruktora. Zmienna przechowuje referencję do obiektu, a nie sam obiekt, co ma kluczowe znaczenie dla zrozumienia mechanizmu przypisań i kopiowania.

Proces tworzenia obiektu składa się z kilku etapów: najpierw wywoływana jest metoda __new__, która alokuje pamięć dla nowej instancji, następnie uruchamiany jest __init__ do inicjalizacji atrybutów, a na końcu zwracana jest referencja do gotowego obiektu. Większość programistów modyfikuje wyłącznie __init__, pozostawiając __new__ w domyślnej implementacji. Zrozumienie tej kolejności jest ważne przy tworzeniu bardziej zaawansowanych wzorców, takich jak Singleton czy Fabryka. Gdy tworzymy wiele obiektów w pętli, każdy z nich jest niezależną instancją z własnym stanem i tożsamością.

20 Kod: niezależne życie obiektów

Agregacja - obiekty żyją niezależnie

Ten przykład pokazuje, że w agregacji obiekt składowy (kategoria) istnieje niezależnie od obiektów nadrzędnych (produktów). Produkty jedynie referencjonują kategorię - nie są jej właścicielami.

class Kategoria:
    def __init__(self, nazwa):
        self.nazwa = nazwa

    def opis(self):
        return f"Kategoria: {self.nazwa}"

class Produkt:
    def __init__(self, nazwa, kategoria):
        self.nazwa = nazwa
        self.kategoria = kategoria  # agregacja - Kategoria istniala wczesniej

    def szczegoly(self):
        return f"{self.nazwa} ({self.kategoria.nazwa})"

kat = Kategoria("Elektronika")

# Wiele produktow wspoldzieli te sama kategorie
p1 = Produkt("Laptop", kat)
p2 = Produkt("Mysz", kat)
p3 = Produkt("Monitor", kat)

# Kategoria istnieje niezależnie od Produktow
print(kat.opis())  # Kategoria: Elektronika

# Nawet gdy usuniemy produkty, kategoria pozostaje
del p1, p2, p3
print(kat.opis())  # Kategoria: Elektronika - kategoria zyje dalej

# Kategoria moze byc uzyta dla nowych produktow
p4 = Produkt("Klawiatura", kat)
print(p4.szczegoly())  # Klawiatura (Elektronika)
Niezależne życie

Tworzenie obiektu przez wywołanie klasy to jeden z najważniejszych wzorców w Pythonie. Składnia NazwaKlasy() może być myląca dla początkujących, ponieważ wygląda jak wywołanie funkcji, ale w rzeczywistości uruchamia złożony proces alokacji i inicjalizacji. Python, jako język dynamiczny, pozwala na tworzenie obiektów w dowolnym momencie działania programu, w tym wewnątrz pętli, warunków czy wyrażeń listowych. Każde wywołanie klasy tworzy nowy, niezależny obiekt w pamięci, nawet jeżeli klasa nie ma zdefiniowanego konstruktora. Zmienna przechowuje referencję do obiektu, a nie sam obiekt, co ma kluczowe znaczenie dla zrozumienia mechanizmu przypisań i kopiowania.

Proces tworzenia obiektu składa się z kilku etapów: najpierw wywoływana jest metoda __new__, która alokuje pamięć dla nowej instancji, następnie uruchamiany jest __init__ do inicjalizacji atrybutów, a na końcu zwracana jest referencja do gotowego obiektu. Większość programistów modyfikuje wyłącznie __init__, pozostawiając __new__ w domyślnej implementacji. Zrozumienie tej kolejności jest ważne przy tworzeniu bardziej zaawansowanych wzorców, takich jak Singleton czy Fabryka. Gdy tworzymy wiele obiektów w pętli, każdy z nich jest niezależną instancją z własnym stanem i tożsamością.

21 Porównanie kompozycja vs agregacja

Kompozycja vs Agregacja

Cecha Kompozycja Agregacja
Zależność życia Ścisła - część nie istnieje bez całości Słaba - część może istnieć samodzielnie
Tworzenie W konstruktorze (wewnątrz) Z zewnątrz (przekazany jako argument)
Własność Obiekt nadrzędny jest właścicielem Współdzielona referencja
Przykład Książka i Strona Grupa i Student

Rozróżnienie między kompozycją a agregacją ma praktyczne konsekwencje przy projektowaniu systemów. W kompozycji możesz być pewien, że zarządzanie pamięcią jest proste - usunięcie obiektu nadrzędnego automatycznie czyści wszystkie komponenty. W agregacji musisz uważać na efekty uboczne - ten sam obiekt może być współdzielony, więc usunięcie go z jednego miejsca nie powinno wpływać na inne.

W Pythonie różnica ta jest szczególnie subtelna, ponieważ język nie ma wbudowanych mechanizmów wymuszania kompozycji czy agregacji - wszystko opiera się na referencjach i konwencji programisty. Dobrą praktyką jest dokumentowanie, które relacje są ścisłą kompozycją, a które agregacją, np. przez komentarze w kodzie lub w dokumentacji API.

Porównanie programowania proceduralnego i obiektowego na konkretnych przykładach pokazuje fundamentalne różnice w organizacji kodu. Podejście proceduralne koncentruje się na sekwencji operacji i funkcjach przetwarzających dane, podczas gdy obiektowe grupuje powiązane dane i funkcje w spójne jednostki zwane klasami. W praktyce zawodowej rzadko spotyka się czyste implementacje jednego paradygmatu - nowoczesne aplikacje łączą różne podejścia w zależności od potrzeb konkretnego modułu. Python, jako język wieloparadygmatowy, doskonale wspiera taki hybrydowy styl programowania, pozwalając programiście na elastyczny wybór narzędzia. Wybór paradygmatu zależy przede wszystkim od charakteru problemu - proste skrypty często lepiej napisać proceduralnie, a złożone systemy obiektowo.

Tabelaryczne zestawienie cech obu podejść ułatwia zrozumienie, kiedy które z nich jest bardziej odpowiednie. Programowanie proceduralne sprawdza się w małych, liniowych skryptach, gdzie ważna jest prostota i szybkość wykonania. OOP dominuje w dużych projektach, gdzie kluczowe są organizacja kodu, wielokrotne użycie i łatwość utrzymania. Warto również zauważyć, że wiele języków nowej generacji, takich jak Rust czy Kotlin, łączy elementy obu podejść, oferując programistom najlepsze cechy każdego z nich. Świadomy wybór paradygmatu jest oznaką dojrzałości programistycznej i pozwala na podejmowanie optymalnych decyzji projektowych w codziennej pracy.

22Podsumowanie agregacji

Agregacja - kluczowe punkty

  • Agregacja to słabsza forma relacji "ma-a"
  • Obiekt składowy może istnieć bez obiektu nadrzędnego
  • Obiekt składowy jest przekazywany z zewnątrz (np. przez argument konstruktora lub metodę)
  • Ten sam obiekt może być współdzielony przez wiele obiektów nadrzędnych
  • Brak ścisłej zależności życia - usunięcie obiektu nadrzędnego nie niszczy składowego
  • W Pythonie różnica jest koncepcyjna - technicznie to wciąż przechowywanie referencji

Pamiętaj, że agregacja i kompozycja znajdują się na tym samym spektrum relacji "ma-a", ale na różnych jego krańcach. W praktyce wiele relacji mieści się gdzieś pośrodku - nie zawsze jesteśmy w stanie jednoznacznie stwierdzić, czy mamy do czynienia z agregacją, czy kompozycją. Kluczowe jest świadome projektowanie i konsekwentne stosowanie przyjętych założeń dotyczących cyklu życia obiektów.

Podsumowanie to dobry moment na refleksję nad przyswojonym materiałem i identyfikację obszarów wymagających dodatkowej pracy. Wymienione punkty stanowią esencję przerobionej części kursu - od definicji klasy, przez tworzenie obiektów, aż po kompozycję i wzorzec fabryki. Każdy z tych punktów będzie rozwijany w kolejnych modułach, dlatego warto upewnić się, że są dobrze zrozumiane. Zachęcamy do tworzenia własnych notatek i map myśli, które pomagają w usystematyzowaniu wiedzy. Regularne powtórki są kluczowe dla trwałego zapamiętania materiału.

Mapy myśli są skutecznym narzędziem wizualizacji złożonych koncepcji i relacji między nimi. Przedstawiona mapa obrazuje najważniejsze pojęcia omówione w tej części kursu oraz ich wzajemne powiązania. Tworzenie własnych map myśli podczas nauki programowania aktywuje inne obszary mózgu niż czytanie liniowego tekstu, co przekłada się na lepsze zapamiętywanie. Studenci, którzy regularnie tworzą mapy myśli, osiągają lepsze wyniki w testach koncepcyjnych i szybciej łączą nowe informacje z już posiadaną wiedzą. Zachęcamy do wykorzystania tej techniki.

23 Samochod "ma-a" Silnik - przykład kompozycji

Praktyczny przykład: Samochód i Silnik

Zaprojektujemy system, w którym Samochód zawiera Silnik . To klasyczny przykład kompozycji - silnik jest integralną częścią samochodu. Bez silnika samochód nie może istnieć jako funkcjonalny pojazd. W przeciwieństwie do relacji "jest-a" (np. Pojazd → Samochód), tutaj mamy relację "ma-a": Samochód ma Silnik, ale nie jest Silnikiem.

W tym przykładzie zobaczymy:

  • Jak tworzyć obiekt składowy w konstruktorze
  • Jak delegować zachowanie do obiektu składowego
  • Jak enkapsulować szczegóły implementacyjne silnika
  • Jak testować kompozycję w praktyce

Ten przykład jest szczególnie pouczający, ponieważ modeluje rzeczywistą relację między obiektami. W prawdziwym świecie samochód posiada silnik jako swoją część - nie ma sensu mówić, że samochód "jest" silnikiem. Projektowanie obiektowe powinno odzwierciedlać takie naturalne relacje.

Samochod ma Silnik

Przechowywanie obiektów w kolekcjach, takich jak listy, słowniki czy zbiory, jest podstawowym wzorcem w programowaniu obiektowym. Listy obiektów pozwalają na zbiorcze operacje, takie jak iterowanie, filtrowanie, sortowanie czy agregowanie danych. Python oferuje bogaty zestaw narzędzi do pracy z listami, w tym wyrażenia listowe, funkcje sorted(), filter() oraz sortowanie z kluczem lambda. Ważne jest zrozumienie, że lista przechowuje referencje do obiektów, a nie ich kopie, co ma wpływ na modyfikację danych przez różne części programu. Iterowanie po liście obiektów i wywoływanie ich metod to jeden z najczęstszych wzorców w codziennej pracy.

Kompozycja, czyli umieszczanie jednych obiektów wewnątrz innych, jest podstawową techniką budowania złożonych systemów z prostszych komponentów. W przeciwieństwie do dziedziczenia, które wyraża relację 'jest', kompozycja wyraża relację 'ma' i jest często preferowanym podejściem w projektowaniu obiektowym. Graf obiektów w rzeczywistych systemach może być bardzo złożony, ale nawigacja po nim jest naturalna i czytelna dzięki notacji kropkowej. Kompozycja pozwala na elastyczne budowanie funkcjonalności poprzez łączenie prostych obiektów w bardziej złożone struktury. Wzorzec ten jest fundamentem takich koncepcji jak wstrzykiwanie zależności czy architektura warstwowa.

24Kod: klasa Silnik

Klasa Silnik - komponent z enkapsulacją

Klasa Silnik jest samodzielnym komponentem, który enkapsuluje stan (moc, włączony) i zachowanie (uruchom, zatrzymaj). Nie wie o istnieniu Samochodu - to ważna cecha kompozycji: komponent nie zna obiektu nadrzędnego.

class Silnik:
    def __init__(self, moc_konna):
        self.moc_konna = moc_konna
        self.wlaczony = False
        self.obroty = 0

    def uruchom(self):
        if self.wlaczony:
            return "Silnik juz pracuje."
        self.wlaczony = True
        self.obroty = 800  # obroty biegu jalowego
        return f"Silnik {self.moc_konna} KM uruchomiony."

    def zatrzymaj(self):
        if self.wlaczony:
            self.wlaczony = False
            self.obroty = 0
            return "Silnik zatrzymany."
        return "Silnik juz jest wylaczony."

    def zwieksz_obroty(self, wartosc):
        if not self.wlaczony:
            return "Najpierw uruchom silnik!"
        self.obroty += wartosc
        return f"Obroty: {self.obroty} RPM"

    def status(self):
        stan = "wlaczony" if self.wlaczony else "wylaczony"
        return f"Silnik {self.moc_konna} KM - {stan}, obroty: {self.obroty} RPM"
Klasa Silnik

Slajd zatytułowany "Kod: klasa Silnik" przedstawia istotne zagadnienie z zakresu programowania obiektowego w Pythonie. Zrozumienie przedstawionych tu koncepcji jest niezbędne do dalszej nauki i praktycznego stosowania OOP w codziennej pracy programisty. Zaleca się dokładne przeanalizowanie przykładów kodu i samodzielne ich przetestowanie w środowisku REPL lub edytorze. Warto również zwrócić uwagę na powiązania między tym tematem a innymi zagadnieniami omawianymi w kursie. Systematyczne budowanie wiedzy krok po kroku to klucz do opanowania programowania obiektowego w Pythonie. Nie pomijaj żadnego slajdu, nawet jeśli wydaje Ci się, że temat jest Ci już znany - powtórka utrwala wiedzę i pozwala dostrzec nowe szczegóły w znajomym materiale.

Zachęcamy do samodzielnego eksperymentowania z omawianymi mechanizmami i modyfikowania przykładowego kodu. Praktyczne ćwiczenia i własne projekty to najskuteczniejsza metoda nauki programowania, ponieważ wymagają aktywnego stosowania wiedzy. Pamiętaj, że błędy są naturalną częścią procesu uczenia się i każda pomyłka przybliża Cię do mistrzostwa. Warto prowadzić własny dziennik błędów, w którym zapisujesz napotkane problemy i ich rozwiązania. Taki zeszyt stanie się z czasem bezcennym źródłem wiedzy i referencją na przyszłość. Korzystaj z dokumentacji oficjalnej Pythona oraz społeczności programistycznych - to nieocenione źródła wiedzy i wsparcia w trudnych momentach.

25 Kod: klasa Samochod z kompozycją

Klasa Samochod - używa kompozycji z delegacją

Klasa Samochod tworzy obiekt Silnik w swoim konstruktorze i deleguje do niego operacje związane z silnikiem. Samochod nie implementuje samodzielnie logiki silnika - korzysta z gotowego komponentu.

class Samochod:
    def __init__(self, marka, model, moc):
        self.marka = marka
        self.model = model
        self.silnik = Silnik(moc)  # kompozycja: tworzymy silnik wewnatrz
        self.predkosc = 0
        self.bieg = 0

    # Delegacja do obiektu silnik
    def uruchom_silnik(self):
        return self.silnik.uruchom()

    def zatrzymaj_silnik(self):
        wynik = self.silnik.zatrzymaj()
        self.predkosc = 0
        self.bieg = 0
        return wynik

    def status_silnika(self):
        return self.silnik.status()

    # Wlasna logika Samochodu korzystajaca z komponentu
    def przyspiesz(self):
        if self.silnik.wlaczony:
            self.predkosc += 10
            self.silnik.zwieksz_obroty(500)
            self.bieg = min(self.bieg + 1, 6)
            return f"Predkosc: {self.predkosc} km/h (bieg: {self.bieg})"
        return "Najpierw uruchom silnik!"

    def hamuj(self):
        self.predkosc = max(0, self.predkosc - 10)
        return f"Predkosc: {self.predkosc} km/h"
Klasa Samochod

Przechowywanie obiektów w kolekcjach, takich jak listy, słowniki czy zbiory, jest podstawowym wzorcem w programowaniu obiektowym. Listy obiektów pozwalają na zbiorcze operacje, takie jak iterowanie, filtrowanie, sortowanie czy agregowanie danych. Python oferuje bogaty zestaw narzędzi do pracy z listami, w tym wyrażenia listowe, funkcje sorted(), filter() oraz sortowanie z kluczem lambda. Ważne jest zrozumienie, że lista przechowuje referencje do obiektów, a nie ich kopie, co ma wpływ na modyfikację danych przez różne części programu. Iterowanie po liście obiektów i wywoływanie ich metod to jeden z najczęstszych wzorców w codziennej pracy.

Kompozycja, czyli umieszczanie jednych obiektów wewnątrz innych, jest podstawową techniką budowania złożonych systemów z prostszych komponentów. W przeciwieństwie do dziedziczenia, które wyraża relację 'jest', kompozycja wyraża relację 'ma' i jest często preferowanym podejściem w projektowaniu obiektowym. Graf obiektów w rzeczywistych systemach może być bardzo złożony, ale nawigacja po nim jest naturalna i czytelna dzięki notacji kropkowej. Kompozycja pozwala na elastyczne budowanie funkcjonalności poprzez łączenie prostych obiektów w bardziej złożone struktury. Wzorzec ten jest fundamentem takich koncepcji jak wstrzykiwanie zależności czy architektura warstwowa.

26 Kod: testowanie kompozycji

Testowanie kompozycji Samochod–Silnik

Testowanie pokazuje, jak Samochod deleguje operacje do Silnika. Zwróć uwagę, że stan silnika jest zarządzany pośrednio przez Samochod - użytkownik klasy Samochod nie musi znać szczegółów implementacji Silnika.

# Tworzymy samochod - silnik tworzony wewnatrz
auto = Samochod("Toyota", "Corolla", 140)

# Uruchamiamy silnik przez samochod
print(auto.uruchom_silnik())
# Silnik 140 KM uruchomiony.

print(auto.status_silnika())
# Silnik 140 KM - wlaczony, obroty: 800 RPM

print(auto.przyspiesz())
# Predkosc: 10 km/h (bieg: 1)

print(auto.przyspiesz())
# Predkosc: 20 km/h (bieg: 2)

print(auto.zatrzymaj_silnik())
# Silnik zatrzymany.

print(auto.status_silnika())
# Silnik 140 KM - wylaczony, obroty: 0 RPM

# Samochod posiada silnik - ale dostep do silnika
# jest kontrolowany przez metode status_silnika()
# To enkapsulacja w praktyce

Przechowywanie obiektów w kolekcjach, takich jak listy, słowniki czy zbiory, jest podstawowym wzorcem w programowaniu obiektowym. Listy obiektów pozwalają na zbiorcze operacje, takie jak iterowanie, filtrowanie, sortowanie czy agregowanie danych. Python oferuje bogaty zestaw narzędzi do pracy z listami, w tym wyrażenia listowe, funkcje sorted(), filter() oraz sortowanie z kluczem lambda. Ważne jest zrozumienie, że lista przechowuje referencje do obiektów, a nie ich kopie, co ma wpływ na modyfikację danych przez różne części programu. Iterowanie po liście obiektów i wywoływanie ich metod to jeden z najczęstszych wzorców w codziennej pracy.

Kompozycja, czyli umieszczanie jednych obiektów wewnątrz innych, jest podstawową techniką budowania złożonych systemów z prostszych komponentów. W przeciwieństwie do dziedziczenia, które wyraża relację 'jest', kompozycja wyraża relację 'ma' i jest często preferowanym podejściem w projektowaniu obiektowym. Graf obiektów w rzeczywistych systemach może być bardzo złożony, ale nawigacja po nim jest naturalna i czytelna dzięki notacji kropkowej. Kompozycja pozwala na elastyczne budowanie funkcjonalności poprzez łączenie prostych obiektów w bardziej złożone struktury. Wzorzec ten jest fundamentem takich koncepcji jak wstrzykiwanie zależności czy architektura warstwowa.

27 Podsumowanie przykładu Samochod–Silnik

Podsumowanie - Samochód ma Silnik

  • Silnik jest tworzony wewnątrz konstruktora Samochodu - to kompozycja ścisła
  • Samochód deleguje operacje związane z silnikiem do obiektu Silnik
  • Enkapsulacja: szczegóły implementacji silnika są ukryte przed użytkownikiem Samochodu
  • Zależność życia: gdy Samochód zostanie usunięty, Silnik również przestaje istnieć
  • Samochód nie jest Silnikiem - to relacja "ma-a", nie "jest-a"

Ten przykład pokazuje praktyczne zastosowanie kompozycji. Zauważ, że Samochod nie dziedziczy po Silniku - to byłoby nielogiczne (samochód nie jest silnikiem). Zamiast tego Samochod posiada Silnik jako swój atrybut i deleguje do niego odpowiednie operacje. To modelowanie bliższe rzeczywistości i bardziej elastyczne: gdybyśmy chcieli dodać inny typ silnika (np. elektryczny), wystarczy zmodyfikować klasę Silnik lub dostarczyć jej nową implementację - Samochod pozostaje bez zmian.

Podsumowanie to dobry moment na refleksję nad przyswojonym materiałem i identyfikację obszarów wymagających dodatkowej pracy. Wymienione punkty stanowią esencję przerobionej części kursu - od definicji klasy, przez tworzenie obiektów, aż po kompozycję i wzorzec fabryki. Każdy z tych punktów będzie rozwijany w kolejnych modułach, dlatego warto upewnić się, że są dobrze zrozumiane. Zachęcamy do tworzenia własnych notatek i map myśli, które pomagają w usystematyzowaniu wiedzy. Regularne powtórki są kluczowe dla trwałego zapamiętania materiału.

Mapy myśli są skutecznym narzędziem wizualizacji złożonych koncepcji i relacji między nimi. Przedstawiona mapa obrazuje najważniejsze pojęcia omówione w tej części kursu oraz ich wzajemne powiązania. Tworzenie własnych map myśli podczas nauki programowania aktywuje inne obszary mózgu niż czytanie liniowego tekstu, co przekłada się na lepsze zapamiętywanie. Studenci, którzy regularnie tworzą mapy myśli, osiągają lepsze wyniki w testach koncepcyjnych i szybciej łączą nowe informacje z już posiadaną wiedzą. Zachęcamy do wykorzystania tej techniki.

28 Pracownik "jest-a" Osoba - przykład dziedziczenia

Praktyczny przykład: Pracownik i Osoba

Teraz zobaczymy przykład dziedziczenia jako relacji "jest-a". Pracownik jest Osobą - dziedziczy wszystkie cechy osoby i dodaje własne, specyficzne dla pracownika. To przeciwieństwo poprzedniego przykładu: podczas gdy Samochód ma Silnik, Pracownik jest Osobą.

Porównaj to z poprzednim przykładem Samochód–Silnik: tam była relacja "ma-a", tutaj "jest-a". Różnica jest fundamentalna - w dziedziczeniu klasa pochodna przejmuje cały interfejs i implementację klasy bazowej, podczas gdy w kompozycji obiekt nadrzędny wybiórczo deleguje zadania do komponentów.

W tym przykładzie zobaczymy:

  • Jak dziedziczyć atrybuty i metody
  • Jak rozszerzać klasę bazową o nowe funkcjonalności
  • Jak używać super() do wywołania konstruktora klasy bazowej
  • Jak działa polimorfizm - Pracownik może być używany tam, gdzie oczekiwana jest Osoba
Pracownik jest Osoba

Slajd zatytułowany "Pracownik "jest-a" Osoba - przykład dziedziczenia" przedstawia istotne zagadnienie z zakresu programowania obiektowego w Pythonie. Zrozumienie przedstawionych tu koncepcji jest niezbędne do dalszej nauki i praktycznego stosowania OOP w codziennej pracy programisty. Zaleca się dokładne przeanalizowanie przykładów kodu i samodzielne ich przetestowanie w środowisku REPL lub edytorze. Warto również zwrócić uwagę na powiązania między tym tematem a innymi zagadnieniami omawianymi w kursie. Systematyczne budowanie wiedzy krok po kroku to klucz do opanowania programowania obiektowego w Pythonie. Nie pomijaj żadnego slajdu, nawet jeśli wydaje Ci się, że temat jest Ci już znany - powtórka utrwala wiedzę i pozwala dostrzec nowe szczegóły w znajomym materiale.

Zachęcamy do samodzielnego eksperymentowania z omawianymi mechanizmami i modyfikowania przykładowego kodu. Praktyczne ćwiczenia i własne projekty to najskuteczniejsza metoda nauki programowania, ponieważ wymagają aktywnego stosowania wiedzy. Pamiętaj, że błędy są naturalną częścią procesu uczenia się i każda pomyłka przybliża Cię do mistrzostwa. Warto prowadzić własny dziennik błędów, w którym zapisujesz napotkane problemy i ich rozwiązania. Taki zeszyt stanie się z czasem bezcennym źródłem wiedzy i referencją na przyszłość. Korzystaj z dokumentacji oficjalnej Pythona oraz społeczności programistycznych - to nieocenione źródła wiedzy i wsparcia w trudnych momentach.

29Kod: klasa Osoba

Klasa bazowa Osoba - ogólne cechy człowieka

Klasa Osoba definiuje podstawowe cechy każdego człowieka: imię, nazwisko, wiek. To klasa bazowa, która będzie rozszerzana przez bardziej wyspecjalizowane klasy.

class Osoba:
    def __init__(self, imie, nazwisko, wiek):
        self.imie = imie
        self.nazwisko = nazwisko
        self.wiek = wiek

    def przedstaw_sie(self):
        return f"Jestem {self.imie} {self.nazwisko}, mam {self.wiek} lat."

    def urodziny(self):
        self.wiek += 1
        return f"Wszystkiego najlepszego! Masz teraz {self.wiek} lat."

    def czy_pelnoletni(self):
        return self.wiek >= 18
Klasa Osoba

Slajd zatytułowany "Kod: klasa Osoba" przedstawia istotne zagadnienie z zakresu programowania obiektowego w Pythonie. Zrozumienie przedstawionych tu koncepcji jest niezbędne do dalszej nauki i praktycznego stosowania OOP w codziennej pracy programisty. Zaleca się dokładne przeanalizowanie przykładów kodu i samodzielne ich przetestowanie w środowisku REPL lub edytorze. Warto również zwrócić uwagę na powiązania między tym tematem a innymi zagadnieniami omawianymi w kursie. Systematyczne budowanie wiedzy krok po kroku to klucz do opanowania programowania obiektowego w Pythonie. Nie pomijaj żadnego slajdu, nawet jeśli wydaje Ci się, że temat jest Ci już znany - powtórka utrwala wiedzę i pozwala dostrzec nowe szczegóły w znajomym materiale.

Zachęcamy do samodzielnego eksperymentowania z omawianymi mechanizmami i modyfikowania przykładowego kodu. Praktyczne ćwiczenia i własne projekty to najskuteczniejsza metoda nauki programowania, ponieważ wymagają aktywnego stosowania wiedzy. Pamiętaj, że błędy są naturalną częścią procesu uczenia się i każda pomyłka przybliża Cię do mistrzostwa. Warto prowadzić własny dziennik błędów, w którym zapisujesz napotkane problemy i ich rozwiązania. Taki zeszyt stanie się z czasem bezcennym źródłem wiedzy i referencją na przyszłość. Korzystaj z dokumentacji oficjalnej Pythona oraz społeczności programistycznych - to nieocenione źródła wiedzy i wsparcia w trudnych momentach.

30 Kod: klasa Pracownik dziedziczy

Klasa Pracownik - dziedziczy po Osoba, dodaje własne cechy

Klasa Pracownik dziedziczy po Osoba , co oznacza, że automatycznie otrzymuje wszystkie atrybuty (imie, nazwisko, wiek) i metody (przedstaw_sie, urodziny, czy_pelnoletni). Dodaje własne atrybuty (stanowisko, pensja) i metody (pracuj, podwyzka).

class Pracownik(Osoba):  # Pracownik JEST Osoba
    def __init__(self, imie, nazwisko, wiek,
                 stanowisko, pensja):
        # super() wywoluje konstruktor klasy bazowej Osoba
        # Dzięki temu nie musimy ponownie inicjalizować imię/nazwisko/wiek
        super().__init__(imie, nazwisko, wiek)
        # Dodajemy atrybuty specyficzne dla Pracownika
        self.stanowisko = stanowisko
        self.pensja = pensja

    def pracuj(self):
        return f"{self.imie} pracuje jako {self.stanowisko}."

    def podwyzka(self, kwota):
        self.pensja += kwota
        return f"Nowa pensja: {self.pensja} PLN"

    def przedstaw_sie(self):  # przeslaniamy (override) metode z Osoba
        return (f"Jestem {self.imie} {self.nazwisko}, "
                f"pracuje jako {self.stanowisko} "
                f"i zarabiam {self.pensja} PLN.")

# Uzycie - Pracownik ma wszystkie metody Osoby + swoje
p = Pracownik("Jan", "Kowalski", 35,
              "Programista", 10000)
print(p.przedstaw_sie())  # przeslonieta wersja z Pracownik
print(p.pracuj())          # wlasne z Pracownik
print(p.urodziny())        # dziedziczone z Osoba
print(p.czy_pelnoletni())  # dziedziczone z Osoba
Klasa Pracownik

Slajd zatytułowany "Kod: klasa Pracownik dziedziczy" przedstawia istotne zagadnienie z zakresu programowania obiektowego w Pythonie. Zrozumienie przedstawionych tu koncepcji jest niezbędne do dalszej nauki i praktycznego stosowania OOP w codziennej pracy programisty. Zaleca się dokładne przeanalizowanie przykładów kodu i samodzielne ich przetestowanie w środowisku REPL lub edytorze. Warto również zwrócić uwagę na powiązania między tym tematem a innymi zagadnieniami omawianymi w kursie. Systematyczne budowanie wiedzy krok po kroku to klucz do opanowania programowania obiektowego w Pythonie. Nie pomijaj żadnego slajdu, nawet jeśli wydaje Ci się, że temat jest Ci już znany - powtórka utrwala wiedzę i pozwala dostrzec nowe szczegóły w znajomym materiale.

Zachęcamy do samodzielnego eksperymentowania z omawianymi mechanizmami i modyfikowania przykładowego kodu. Praktyczne ćwiczenia i własne projekty to najskuteczniejsza metoda nauki programowania, ponieważ wymagają aktywnego stosowania wiedzy. Pamiętaj, że błędy są naturalną częścią procesu uczenia się i każda pomyłka przybliża Cię do mistrzostwa. Warto prowadzić własny dziennik błędów, w którym zapisujesz napotkane problemy i ich rozwiązania. Taki zeszyt stanie się z czasem bezcennym źródłem wiedzy i referencją na przyszłość. Korzystaj z dokumentacji oficjalnej Pythona oraz społeczności programistycznych - to nieocenione źródła wiedzy i wsparcia w trudnych momentach.

31 Podsumowanie przykładu Pracownik–Osoba

Podsumowanie - Pracownik jest Osobą

  • Pracownik dziedziczy wszystkie atrybuty i metody klasy Osoba
  • Używa super().__init__() do wywołania konstruktora klasy bazowej
  • Rozszerza klasę bazową o nowe atrybuty (stanowisko, pensja) i metody (pracuj, podwyzka)
  • Obiekt Pracownik jest również obiektem Osoba - polimorfizm
  • Jeśli zmienimy Osoba, zmiana automatycznie dotyczy Pracownik
  • To relacja "jest-a" - Pracownik jest szczególnym przypadkiem Osoby

Ten przykład ilustruje siłę dziedziczenia: w kilku liniach kodu stworzyliśmy wyspecjalizowaną klasę, która ma wszystkie cechy klasy ogólnej. Gdybyśmy chcieli dodać kolejną specjalizację (np. Manager dziedziczący po Pracownik ), wystarczy dodać nową klasę bez modyfikacji istniejącego kodu. To jedna z głównych zalet dziedziczenia - łatwość rozszerzania hierarchii.

Należy jednak pamiętać, że to powiązanie jest sztywne. Jeśli zmienimy konstruktor Osoba (np. dodając obowiązkowy atrybut pesel ), będziemy musieli zaktualizować wszystkie klasy pochodne. W małych hierarchiach to nie problem, ale przy głębokim dziedziczeniu staje się uciążliwe.

Podsumowanie to dobry moment na refleksję nad przyswojonym materiałem i identyfikację obszarów wymagających dodatkowej pracy. Wymienione punkty stanowią esencję przerobionej części kursu - od definicji klasy, przez tworzenie obiektów, aż po kompozycję i wzorzec fabryki. Każdy z tych punktów będzie rozwijany w kolejnych modułach, dlatego warto upewnić się, że są dobrze zrozumiane. Zachęcamy do tworzenia własnych notatek i map myśli, które pomagają w usystematyzowaniu wiedzy. Regularne powtórki są kluczowe dla trwałego zapamiętania materiału.

Mapy myśli są skutecznym narzędziem wizualizacji złożonych koncepcji i relacji między nimi. Przedstawiona mapa obrazuje najważniejsze pojęcia omówione w tej części kursu oraz ich wzajemne powiązania. Tworzenie własnych map myśli podczas nauki programowania aktywuje inne obszary mózgu niż czytanie liniowego tekstu, co przekłada się na lepsze zapamiętywanie. Studenci, którzy regularnie tworzą mapy myśli, osiągają lepsze wyniki w testach koncepcyjnych i szybciej łączą nowe informacje z już posiadaną wiedzą. Zachęcamy do wykorzystania tej techniki.

32 Zasada "favor composition over inheritance"

Słynna zasada projektowa

Zasada "favor composition over inheritance" (przedkładaj kompozycję nad dziedziczenie) pochodzi z książki Design Patterns (Gang of Four) z 1994 roku. Jest to jedna z najczęściej cytowanych zasad projektowania obiektowego. Mówi ona, że kompozycja jest zwykle lepszym wyborem niż dziedziczenie, ponieważ:

  • Kompozycja daje większą elastyczność - łatwiej zmieniać zachowanie w czasie wykonania
  • Kompozycja nie tworzy sztywnych hierarchii - unika problemów głębokiego dziedziczenia
  • Kompozycja lepiej enkapsuluje - szczegóły implementacji są ukryte
  • Kompozycja ułatwia testowanie - komponenty można testować w izolacji
  • Kompozycja wspiera zasadę pojedynczej odpowiedzialności - każda klasa ma jedno zadanie

Ta zasada nie mówi, że dziedziczenie jest zawsze złe - mówi, że kompozycja powinna być pierwszym wyborem , a dziedziczenie stosujemy tylko wtedy, gdy relacja "jest-a" jest naturalna i uzasadniona. W praktyce doświadczeni programiści często zaczynają od kompozycji, a dziedziczenie wprowadzają dopiero wtedy, gdy widzą wyraźną korzyść z polimorfizmu lub współdzielenia interfejsu.

Favor composition

Slajd zatytułowany "Zasada "favor composition over inheritance"" przedstawia istotne zagadnienie z zakresu programowania obiektowego w Pythonie. Zrozumienie przedstawionych tu koncepcji jest niezbędne do dalszej nauki i praktycznego stosowania OOP w codziennej pracy programisty. Zaleca się dokładne przeanalizowanie przykładów kodu i samodzielne ich przetestowanie w środowisku REPL lub edytorze. Warto również zwrócić uwagę na powiązania między tym tematem a innymi zagadnieniami omawianymi w kursie. Systematyczne budowanie wiedzy krok po kroku to klucz do opanowania programowania obiektowego w Pythonie. Nie pomijaj żadnego slajdu, nawet jeśli wydaje Ci się, że temat jest Ci już znany - powtórka utrwala wiedzę i pozwala dostrzec nowe szczegóły w znajomym materiale.

Zachęcamy do samodzielnego eksperymentowania z omawianymi mechanizmami i modyfikowania przykładowego kodu. Praktyczne ćwiczenia i własne projekty to najskuteczniejsza metoda nauki programowania, ponieważ wymagają aktywnego stosowania wiedzy. Pamiętaj, że błędy są naturalną częścią procesu uczenia się i każda pomyłka przybliża Cię do mistrzostwa. Warto prowadzić własny dziennik błędów, w którym zapisujesz napotkane problemy i ich rozwiązania. Taki zeszyt stanie się z czasem bezcennym źródłem wiedzy i referencją na przyszłość. Korzystaj z dokumentacji oficjalnej Pythona oraz społeczności programistycznych - to nieocenione źródła wiedzy i wsparcia w trudnych momentach.

33 Kod: kompozycja zamiast głębokiego dziedziczenia

Kompozycja zamiast hierarchii dziedziczenia

Po lewej stronie widzisz głęboką hierarchię dziedziczenia, która jest sztywna i trudna do modyfikacji. Po prawej - elastyczną kompozycję, która łatwo adaptuje się do zmian.

# Zla praktyka: gleboka hierarchia dziedziczenia (5 poziomow!)
class Pojazd: pass
class PojazdKolowy(Pojazd): pass
class PojazdSilnikowy(PojazdKolowy): pass
class SamochodOsobowy(PojazdSilnikowy): pass
class Sedan(SamochodOsobowy): pass
# Problem: co jesli chcemy stworzyc ElektrycznySedan?
# Musimy dodac kolejny poziom hierarchii!

# Lepsze rozwiazanie: kompozycja zamiast hierarchii
class Silnik:
    def dzialaj(self): pass

class Kolo:
    def tocz_sie(self): pass

class Nadwozie:
    def otworz_drzwi(self): pass

class Samochod:
    def __init__(self):
        self.silnik = Silnik()          # kompozycja
        self.kola = [Kolo() for _ in range(4)]  # kompozycja
        self.nadwozie = Nadwozie()    # kompozycja

    def jedz(self):
        self.silnik.dzialaj()
        for kolo in self.kola:
            kolo.tocz_sie()
        return "Samochod jedzie!"

# Kompozycja pozwala latwo zmieniac komponenty
# np. wymienic silnik na elektryczny bez zmiany reszty

Przechowywanie obiektów w kolekcjach, takich jak listy, słowniki czy zbiory, jest podstawowym wzorcem w programowaniu obiektowym. Listy obiektów pozwalają na zbiorcze operacje, takie jak iterowanie, filtrowanie, sortowanie czy agregowanie danych. Python oferuje bogaty zestaw narzędzi do pracy z listami, w tym wyrażenia listowe, funkcje sorted(), filter() oraz sortowanie z kluczem lambda. Ważne jest zrozumienie, że lista przechowuje referencje do obiektów, a nie ich kopie, co ma wpływ na modyfikację danych przez różne części programu. Iterowanie po liście obiektów i wywoływanie ich metod to jeden z najczęstszych wzorców w codziennej pracy.

Kompozycja, czyli umieszczanie jednych obiektów wewnątrz innych, jest podstawową techniką budowania złożonych systemów z prostszych komponentów. W przeciwieństwie do dziedziczenia, które wyraża relację 'jest', kompozycja wyraża relację 'ma' i jest często preferowanym podejściem w projektowaniu obiektowym. Graf obiektów w rzeczywistych systemach może być bardzo złożony, ale nawigacja po nim jest naturalna i czytelna dzięki notacji kropkowej. Kompozycja pozwala na elastyczne budowanie funkcjonalności poprzez łączenie prostych obiektów w bardziej złożone struktury. Wzorzec ten jest fundamentem takich koncepcji jak wstrzykiwanie zależności czy architektura warstwowa.

34 Kod: elastyczność kompozycji

Elastyczność kompozycji - zmiana zachowania w locie

Kompozycja pozwala na dynamiczną zmianę zachowania obiektu w czasie wykonania. W tym przykładzie Auto może działać z różnymi typami silników - wystarczy przekazać odpowiedni obiekt. To realizacja wzorca Strategia (Strategy Pattern).

class SilnikSpalinowy:
    def uruchom(self):
        return "Wrrrr!"

    def typ(self):
        return "spalinowy"

class SilnikElektryczny:
    def uruchom(self):
        return "Zzzzz!"

    def typ(self):
        return "elektryczny"

class SilnikHybrydowy:
    def uruchom(self):
        return "Vrummm!"

    def typ(self):
        return "hybrydowy"

class Auto:
    def __init__(self, silnik):
        self.silnik = silnik  # wstrzykiwanie zależności (Dependency Injection)

    def uruchom(self):
        return f"{self.silnik.typ()}: {self.silnik.uruchom()}"

# Zmiana zachowania bez zmiany klas - to jest moc kompozycji!
auto1 = Auto(SilnikSpalinowy())
auto2 = Auto(SilnikElektryczny())
auto3 = Auto(SilnikHybrydowy())

print(auto1.uruchom())  # spalinowy: Wrrrr!
print(auto2.uruchom())  # elektryczny: Zzzzz!
print(auto3.uruchom())  # hybrydowy: Vrummm!

# Nawet zmiana silnika po utworzeniu obiektu
auto1.silnik = SilnikElektryczny()
print(auto1.uruchom())  # elektryczny: Zzzzz! - zachowanie zmienione w locie

Przechowywanie obiektów w kolekcjach, takich jak listy, słowniki czy zbiory, jest podstawowym wzorcem w programowaniu obiektowym. Listy obiektów pozwalają na zbiorcze operacje, takie jak iterowanie, filtrowanie, sortowanie czy agregowanie danych. Python oferuje bogaty zestaw narzędzi do pracy z listami, w tym wyrażenia listowe, funkcje sorted(), filter() oraz sortowanie z kluczem lambda. Ważne jest zrozumienie, że lista przechowuje referencje do obiektów, a nie ich kopie, co ma wpływ na modyfikację danych przez różne części programu. Iterowanie po liście obiektów i wywoływanie ich metod to jeden z najczęstszych wzorców w codziennej pracy.

Kompozycja, czyli umieszczanie jednych obiektów wewnątrz innych, jest podstawową techniką budowania złożonych systemów z prostszych komponentów. W przeciwieństwie do dziedziczenia, które wyraża relację 'jest', kompozycja wyraża relację 'ma' i jest często preferowanym podejściem w projektowaniu obiektowym. Graf obiektów w rzeczywistych systemach może być bardzo złożony, ale nawigacja po nim jest naturalna i czytelna dzięki notacji kropkowej. Kompozycja pozwala na elastyczne budowanie funkcjonalności poprzez łączenie prostych obiektów w bardziej złożone struktury. Wzorzec ten jest fundamentem takich koncepcji jak wstrzykiwanie zależności czy architektura warstwowa.

35 Kiedy jednak wybrać dziedziczenie

Kiedy dziedziczenie jest lepsze?

Mimo zasady "favor composition over inheritance", istnieją sytuacje, w których dziedziczenie jest właściwym wyborem. Kluczem jest umiejętność rozpoznania, kiedy relacja "jest-a" jest naturalna i stabilna.

  • Naturalna hierarchia: gdy relacja "jest-a" jest oczywista i stabilna (np. Kot jest Ssakiem, Prostokat jest Figura)
  • Polimorfizm: gdy potrzebujesz traktować obiekty różnych klas w jednolity sposób (np. lista obiektów Figura, z których każdy ma metodę rysuj())
  • Wielokrotne użycie interfejsu: gdy wiele klas dzieli ten sam interfejs (klasy abstrakcyjne lub ABC w Pythonie)
  • Frameworki: gdy framework wymaga dziedziczenia po klasach bazowych (np. dziedziczenie po QWidget w PyQt, TestCase w unittest)
  • Niewielka głębokość: hierarchia 1-2 poziomów jest zazwyczaj bezpieczna i łatwa w utrzymaniu

W tych przypadkach dziedziczenie jest nie tyle opcją, co koniecznością - rezygnacja z niego prowadziłaby do bardziej skomplikowanego kodu. Przykładowo, w frameworkach GUI dziedziczenie po klasach bazowych jest standardem, ponieważ framework zarządza cyklem życia widżetów i dostarcza gotowej implementacji obsługi zdarzeń.

Kiedy dziedziczenie

Slajd zatytułowany "Kiedy jednak wybrać dziedziczenie" przedstawia istotne zagadnienie z zakresu programowania obiektowego w Pythonie. Zrozumienie przedstawionych tu koncepcji jest niezbędne do dalszej nauki i praktycznego stosowania OOP w codziennej pracy programisty. Zaleca się dokładne przeanalizowanie przykładów kodu i samodzielne ich przetestowanie w środowisku REPL lub edytorze. Warto również zwrócić uwagę na powiązania między tym tematem a innymi zagadnieniami omawianymi w kursie. Systematyczne budowanie wiedzy krok po kroku to klucz do opanowania programowania obiektowego w Pythonie. Nie pomijaj żadnego slajdu, nawet jeśli wydaje Ci się, że temat jest Ci już znany - powtórka utrwala wiedzę i pozwala dostrzec nowe szczegóły w znajomym materiale.

Zachęcamy do samodzielnego eksperymentowania z omawianymi mechanizmami i modyfikowania przykładowego kodu. Praktyczne ćwiczenia i własne projekty to najskuteczniejsza metoda nauki programowania, ponieważ wymagają aktywnego stosowania wiedzy. Pamiętaj, że błędy są naturalną częścią procesu uczenia się i każda pomyłka przybliża Cię do mistrzostwa. Warto prowadzić własny dziennik błędów, w którym zapisujesz napotkane problemy i ich rozwiązania. Taki zeszyt stanie się z czasem bezcennym źródłem wiedzy i referencją na przyszłość. Korzystaj z dokumentacji oficjalnej Pythona oraz społeczności programistycznych - to nieocenione źródła wiedzy i wsparcia w trudnych momentach.

36Podsumowanie zasady

Favor composition over inheritance - podsumowanie

  • Zawsze rozważ kompozycję jako pierwszy wybór - zadaj sobie pytanie: "czy ta klasa ma, czy jest?"
  • Kompozycja daje większą elastyczność i luźniejsze powiązania
  • Dziedziczenie stosuj, gdy relacja "jest-a" jest naturalna i nie zmieni się w przyszłości
  • Unikaj głębokiego dziedziczenia (więcej niż 2-3 poziomy) - to znak ostrzegawczy
  • "Composition over inheritance" to wskazówka, nie sztywna reguła - są sytuacje, w których dziedziczenie jest lepsze
  • W praktyce często łączy się obie techniki w jednym projekcie - nie traktuj ich jako wzajemnie wykluczających się

Pamiętaj, że celem tych wszystkich zasad jest stworzenie kodu, który jest łatwy w utrzymaniu i rozwijaniu. Jeśli twoja hierarchia dziedziczenia działa dobrze i nie sprawia problemów, nie ma powodu, by ją zmieniać na siłę. Zasady projektowe są wskazówkami, a nie dogmatami - ważniejsze jest zrozumienie, dlaczego dana technika działa w konkretnym kontekście, niż ślepe stosowanie reguł.

Podsumowanie zasady

Podsumowanie to dobry moment na refleksję nad przyswojonym materiałem i identyfikację obszarów wymagających dodatkowej pracy. Wymienione punkty stanowią esencję przerobionej części kursu - od definicji klasy, przez tworzenie obiektów, aż po kompozycję i wzorzec fabryki. Każdy z tych punktów będzie rozwijany w kolejnych modułach, dlatego warto upewnić się, że są dobrze zrozumiane. Zachęcamy do tworzenia własnych notatek i map myśli, które pomagają w usystematyzowaniu wiedzy. Regularne powtórki są kluczowe dla trwałego zapamiętania materiału.

Mapy myśli są skutecznym narzędziem wizualizacji złożonych koncepcji i relacji między nimi. Przedstawiona mapa obrazuje najważniejsze pojęcia omówione w tej części kursu oraz ich wzajemne powiązania. Tworzenie własnych map myśli podczas nauki programowania aktywuje inne obszary mózgu niż czytanie liniowego tekstu, co przekłada się na lepsze zapamiętywanie. Studenci, którzy regularnie tworzą mapy myśli, osiągają lepsze wyniki w testach koncepcyjnych i szybciej łączą nowe informacje z już posiadaną wiedzą. Zachęcamy do wykorzystania tej techniki.

37 Problemy z nadmiernym dziedziczeniem

Głęboka hierarchia - problemy

Nadmierne dziedziczenie prowadzi do kilku poważnych problemów, które w literaturze określa się mianem "kruchej klasy bazowej" (fragile base class problem). Im głębsza hierarchia, tym bardziej prawdopodobne, że zmiana w klasie bazowej będzie miała nieprzewidziane skutki w klasach pochodnych.

  • Kruchość hierarchii - zmiana w klasie bazowej może zepsuć wszystkie klasy pochodne (efekt domina)
  • Sztywność - hierarchia jest sztywna, trudno zmienić strukturę w trakcie rozwoju
  • Problem diamentowy - przy wielodziedziczeniu, konflikt nazw metod (choć Python radzi sobie z tym przez MRO)
  • Nadmiarowy kod - klasy dziedziczą metody, których nie potrzebują (np. Ptak dziedziczący po Ssak dostałby metodę karm_mlekiem)
  • Trudność testowania - testowanie klasy pochodnej wymaga całej hierarchii, co komplikuje testy jednostkowe
  • Złamana enkapsulacja - klasy pochodne często polegają na szczegółach implementacji bazowej, co utrudnia modyfikacje

Te problemy nie pojawiają się od razu - zwykle ujawniają się w miarę rozwoju systemu. Dlatego tak ważne jest, aby od początku projektować hierarchie z myślą o przyszłych zmianach. Zasada "favor composition over inheritance" powstała właśnie w odpowiedzi na te problemy.

Błędy są naturalną i nieodłączną częścią procesu uczenia się programowania. Każdy, nawet najbardziej doświadczony programista, popełnia błędy na co dzień - kluczowa jest umiejętność ich szybkiego identyfikowania i poprawiania. Przedstawione na tym slajdzie typowe pomyłki zostały zebrane na podstawie wieloletnich doświadczeń nauczycieli programowania i występują u większości początkujących. Zapamiętanie ich i zrozumienie przyczyn pomoże Ci uniknąć frustracji i straconego czasu na debugowanie. Warto również zapoznać się z technikami debugowania, takimi jak użycie print(), logging czy debuggera wbudowanego w IDE. Świadomość typowych pułapek to pierwszy krok do ich unikania.

Nowoczesne edytory kodu i IDE oferują wiele narzędzi pomagających w wykrywaniu błędów jeszcze przed uruchomieniem programu. PyCharm, VS Code z wtyczką Python, a nawet zaawansowane edytory tekstu z obsługą lintingu potrafią ostrzegać przed wieloma typowymi pomyłkami w czasie rzeczywistym. Korzystanie z type hintów, narzędzi takich jak mypy do statycznej analizy typów oraz systemów CI/CD z automatycznym uruchamianiem testów znacząco redukuje liczbę błędów w kodzie produkcyjnym. Warto od początku nauki wyrobić sobie nawyk korzystania z tych narzędzi, ponieważ znacząco podnoszą one jakość i niezawodność tworzonego oprogramowania.

38 Kod: kruchość hierarchii

Kruchość hierarchii - przykład

class Baza:
    def __init__(self):
        self.dane = [1, 2, 3]

    def przetworz(self):
        self.dane.append(4)
        return sum(self.dane)

class Pochodna(Baza):
    def __init__(self):
        super().__init__()
        self.dane = [10, 20]  # nadpisuje! psuje logike Bazy

# Ktos zmienia metode Baza.przetworz():
# def przetworz(self):
#     self.dane = [x * 2 for x in self.dane]  # nowa implementacja
#     return max(self.dane)
# Pochodna nagle przestaje dzialac poprawnie

p = Pochodna()
print(p.przetworz())  # moze dac nieoczekiwany wynik!
Kruchość hierarchii

Slajd zatytułowany "Kod: kruchość hierarchii" przedstawia istotne zagadnienie z zakresu programowania obiektowego w Pythonie. Zrozumienie przedstawionych tu koncepcji jest niezbędne do dalszej nauki i praktycznego stosowania OOP w codziennej pracy programisty. Zaleca się dokładne przeanalizowanie przykładów kodu i samodzielne ich przetestowanie w środowisku REPL lub edytorze. Warto również zwrócić uwagę na powiązania między tym tematem a innymi zagadnieniami omawianymi w kursie. Systematyczne budowanie wiedzy krok po kroku to klucz do opanowania programowania obiektowego w Pythonie. Nie pomijaj żadnego slajdu, nawet jeśli wydaje Ci się, że temat jest Ci już znany - powtórka utrwala wiedzę i pozwala dostrzec nowe szczegóły w znajomym materiale.

Zachęcamy do samodzielnego eksperymentowania z omawianymi mechanizmami i modyfikowania przykładowego kodu. Praktyczne ćwiczenia i własne projekty to najskuteczniejsza metoda nauki programowania, ponieważ wymagają aktywnego stosowania wiedzy. Pamiętaj, że błędy są naturalną częścią procesu uczenia się i każda pomyłka przybliża Cię do mistrzostwa. Warto prowadzić własny dziennik błędów, w którym zapisujesz napotkane problemy i ich rozwiązania. Taki zeszyt stanie się z czasem bezcennym źródłem wiedzy i referencją na przyszłość. Korzystaj z dokumentacji oficjalnej Pythona oraz społeczności programistycznych - to nieocenione źródła wiedzy i wsparcia w trudnych momentach.

39 Kod: problem diamentowy

Problem diamentowy w wielodziedziczeniu

class A:
    def m(self):
        return "A"

class B(A):
    def m(self):
        return "B"

class C(A):
    def m(self):
        return "C"

class D(B, C):
    pass

# Ktora metoda m() zostanie wywolana?
d = D()
print(d.m())  # Python uzywa MRO (Method Resolution Order)

# Sprawdzenie kolejnosci rozdzielczosci metod
print(D.__mro__)
# (<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>)
Problem diamentowy

Błędy są naturalną i nieodłączną częścią procesu uczenia się programowania. Każdy, nawet najbardziej doświadczony programista, popełnia błędy na co dzień - kluczowa jest umiejętność ich szybkiego identyfikowania i poprawiania. Przedstawione na tym slajdzie typowe pomyłki zostały zebrane na podstawie wieloletnich doświadczeń nauczycieli programowania i występują u większości początkujących. Zapamiętanie ich i zrozumienie przyczyn pomoże Ci uniknąć frustracji i straconego czasu na debugowanie. Warto również zapoznać się z technikami debugowania, takimi jak użycie print(), logging czy debuggera wbudowanego w IDE. Świadomość typowych pułapek to pierwszy krok do ich unikania.

Nowoczesne edytory kodu i IDE oferują wiele narzędzi pomagających w wykrywaniu błędów jeszcze przed uruchomieniem programu. PyCharm, VS Code z wtyczką Python, a nawet zaawansowane edytory tekstu z obsługą lintingu potrafią ostrzegać przed wieloma typowymi pomyłkami w czasie rzeczywistym. Korzystanie z type hintów, narzędzi takich jak mypy do statycznej analizy typów oraz systemów CI/CD z automatycznym uruchamianiem testów znacząco redukuje liczbę błędów w kodzie produkcyjnym. Warto od początku nauki wyrobić sobie nawyk korzystania z tych narzędzi, ponieważ znacząco podnoszą one jakość i niezawodność tworzonego oprogramowania.

40 Podsumowanie wad dziedziczenia

Wady dziedziczenia - podsumowanie

  • Sztywna hierarchia - raz zaprojektowana hierarchia jest trudna do zmiany; wymaga refaktoryzacji wielu klas
  • Kruchość - zmiany w klasie bazowej mogą mieć nieprzewidziane skutki w klasach pochodnych
  • Brak enkapsulacji - klasy pochodne polegają na szczegółach implementacji bazowej
  • Problem diamentowy - konflikt nazw przy wielodziedziczeniu (Python rozwiązuje przez MRO)
  • Nadmiarowy bagaż - klasy dziedziczą metody, których nie potrzebują (np. klasa Prostokat dziedziczy metodę obliczObjetosc po klasie Figura3D)
  • Trudne testowanie - wymaga skonfigurowania całej hierarchii, co utrudnia izolowane testy
  • Złamanie zasady pojedynczej odpowiedzialności - klasa bazowa często robi zbyt wiele, by obsłużyć wszystkie klasy pochodne

Warto zapamiętać te wady, ponieważ pomagają one zrozumieć, dlaczego kompozycja jest często lepszym wyborem. Nie oznacza to jednak, że dziedziczenie jest zawsze złe - w odpowiednich sytuacjach (płytkie hierarchie, stabilne relacje "jest-a") jest to wciąż potężne i użyteczne narzędzie.

Wady dziedziczenia

Wymienione zalety programowania obiektowego mają bezpośrednie przełożenie na praktykę inżynierii oprogramowania i codzienną pracę programisty. Czytelność kodu przekłada się na niższe koszty utrzymania, ponieważ nowi członkowie zespołu szybciej rozumieją strukturę projektu. Wielokrotne użycie klas i dziedziczenie redukują ilość duplikacji kodu, co jest jednym z głównych celów dobrych praktyk programistycznych. Organizacja kodu w klasy ułatwia nawigację po projekcie i przyspiesza wprowadzanie zmian. Łatwość utrzymania oznacza, że modyfikacje w jednym miejscu nie powodują nieoczekiwanych skutków ubocznych w innych. Bezpieczeństwo danych jest zwiększone dzięki enkapsulacji chroniącej stan obiektu przed przypadkową modyfikacją z zewnątrz.

Należy jednak pamiętać, że OOP nie jest srebrną kulą rozwiązującą wszystkie problemy programistyczne. Nadużywanie dziedziczenia prowadzi do głębokich hierarchii trudnych w utrzymaniu i zrozumieniu. Przesadna enkapsulacja może utrudniać testowanie i debugowanie kodu. W przypadku prostych zadań, takich jak jednorazowe skrypty do przetwarzania danych, podejście proceduralne jest w pełni wystarczające i często szybsze w implementacji. Dlatego tak ważne jest zrozumienie nie tylko zalet, ale też potencjalnych pułapek OOP. Świadomy programista wybiera narzędzie odpowiednie do zadania, zamiast ślepo stosować jeden paradygmat.

41Wady kompozycji

Kompozycja nie jest idealna - jej wady

Kompozycja, choć elastyczna, również ma swoje wady. Ważne, aby być ich świadomym i umieć je zrównoważyć z zaletami:

  • Większa liczba klas - kompozycja często prowadzi do większej liczby małych klas, co może przytłoczyć nowych programistów
  • Ręczna delegacja - każda metoda komponentu, która ma być dostępna z zewnątrz, musi być jawnie delegowana (to dodatkowy kod)
  • Więcej kodu "klejowego" - w porównaniu do dziedziczenia, kompozycja wymaga napisania więcej kodu łączącego komponenty
  • Mniej przejrzysta struktura - w bardzo złożonych systemach może być trudniej śledzić przepływ wywołań przez wiele warstw komponentów
  • Brak automatycznego polimorfizmu - kompozycja nie daje automatycznego polimorfizmu (choć można go osiągnąć przez interfejsy/ABC)

Te wady są jednak zazwyczaj akceptowalne w zamian za elastyczność i łatwość utrzymania, jaką oferuje kompozycja. W praktyce dodatkowy kod "klejowy" to niewielka cena za możliwość łatwej wymiany komponentów i testowania ich w izolacji. Ponadto nowoczesne języki i frameworki oferują narzędzia (np. Dependency Injection containers), które automatyzują część tej pracy.

Wady kompozycji

Wymienione zalety programowania obiektowego mają bezpośrednie przełożenie na praktykę inżynierii oprogramowania i codzienną pracę programisty. Czytelność kodu przekłada się na niższe koszty utrzymania, ponieważ nowi członkowie zespołu szybciej rozumieją strukturę projektu. Wielokrotne użycie klas i dziedziczenie redukują ilość duplikacji kodu, co jest jednym z głównych celów dobrych praktyk programistycznych. Organizacja kodu w klasy ułatwia nawigację po projekcie i przyspiesza wprowadzanie zmian. Łatwość utrzymania oznacza, że modyfikacje w jednym miejscu nie powodują nieoczekiwanych skutków ubocznych w innych. Bezpieczeństwo danych jest zwiększone dzięki enkapsulacji chroniącej stan obiektu przed przypadkową modyfikacją z zewnątrz.

Należy jednak pamiętać, że OOP nie jest srebrną kulą rozwiązującą wszystkie problemy programistyczne. Nadużywanie dziedziczenia prowadzi do głębokich hierarchii trudnych w utrzymaniu i zrozumieniu. Przesadna enkapsulacja może utrudniać testowanie i debugowanie kodu. W przypadku prostych zadań, takich jak jednorazowe skrypty do przetwarzania danych, podejście proceduralne jest w pełni wystarczające i często szybsze w implementacji. Dlatego tak ważne jest zrozumienie nie tylko zalet, ale też potencjalnych pułapek OOP. Świadomy programista wybiera narzędzie odpowiednie do zadania, zamiast ślepo stosować jeden paradygmat.

42 Kod: ręczne przekazywanie

Ręczne przekazywanie - wada kompozycji

Wada kompozycji uwidacznia się, gdy komponent ma wiele metod. Każda z nich, jeśli ma być dostępna z poziomu obiektu nadrzędnego, musi być jawnie delegowana. Przy dziedziczeniu wszystkie metody są dostępne automatycznie.

class GPS:
    def pobierz_pozycje(self):
        return "52.2297 N, 21.0122 E"

    def pobierz_predkosc(self):
        return "60 km/h"

    def pobierz_czas(self):
        return "12:30:00"

    def pobierz_wysokosc(self):
        return "150 m n.p.m."

    def pobierz_kierunek(self):
        return "180 stopni (poludnie)"

class Smartfon:
    def __init__(self):
        self.gps = GPS()  # kompozycja

    # Kazda metoda GPS musi byc jawnie delegowana - to meczace!
    def pobierz_pozycje(self):
        return self.gps.pobierz_pozycje()

    def pobierz_predkosc(self):
        return self.gps.pobierz_predkosc()

    def pobierz_czas(self):
        return self.gps.pobierz_czas()

    def pobierz_wysokosc(self):
        return self.gps.pobierz_wysokosc()

    def pobierz_kierunek(self):
        return self.gps.pobierz_kierunek()

    # ... wiecej delegacji ...
    # Przy dziedziczeniu: class Smartfon(GPS): - wszystko za darmo
    # Ale wtedy Smartfon JEST GPS-em, a nie ma GPS - to nielogiczne!

# Rozwiazanie: mozna uzyc __getattr__ do automatycznej delegacji
class Smartfon2:
    def __init__(self):
        self.gps = GPS()

    def __getattr__(self, nazwa):
        # Automatyczna delegacja - oszczedza pisania kodu "klejowego"
        if hasattr(self.gps, nazwa):
            return getattr(self.gps, nazwa)
        raise AttributeError(nazwa)
Ręczne przekazywanie

Slajd zatytułowany "Kod: ręczne przekazywanie" przedstawia istotne zagadnienie z zakresu programowania obiektowego w Pythonie. Zrozumienie przedstawionych tu koncepcji jest niezbędne do dalszej nauki i praktycznego stosowania OOP w codziennej pracy programisty. Zaleca się dokładne przeanalizowanie przykładów kodu i samodzielne ich przetestowanie w środowisku REPL lub edytorze. Warto również zwrócić uwagę na powiązania między tym tematem a innymi zagadnieniami omawianymi w kursie. Systematyczne budowanie wiedzy krok po kroku to klucz do opanowania programowania obiektowego w Pythonie. Nie pomijaj żadnego slajdu, nawet jeśli wydaje Ci się, że temat jest Ci już znany - powtórka utrwala wiedzę i pozwala dostrzec nowe szczegóły w znajomym materiale.

Zachęcamy do samodzielnego eksperymentowania z omawianymi mechanizmami i modyfikowania przykładowego kodu. Praktyczne ćwiczenia i własne projekty to najskuteczniejsza metoda nauki programowania, ponieważ wymagają aktywnego stosowania wiedzy. Pamiętaj, że błędy są naturalną częścią procesu uczenia się i każda pomyłka przybliża Cię do mistrzostwa. Warto prowadzić własny dziennik błędów, w którym zapisujesz napotkane problemy i ich rozwiązania. Taki zeszyt stanie się z czasem bezcennym źródłem wiedzy i referencją na przyszłość. Korzystaj z dokumentacji oficjalnej Pythona oraz społeczności programistycznych - to nieocenione źródła wiedzy i wsparcia w trudnych momentach.

43 Ćwiczenie: system zamówień

Projektowanie klas dla systemu zamówień

Zaprojektuj system zamówień składający się z następujących klas. Zdecyduj, które relacje to kompozycja, a które dziedziczenie. To ćwiczenie podsumowuje całą część - musisz świadomie zastosować poznane techniki.

Wymagania:

  • Zamowienie - zawiera listę pozycji, klienta, adres dostawy
  • PozycjaZamowienia - zawiera produkt, ilość, cenę
  • Produkt - nazwa, cena, kategoria
  • Klient - imię, nazwisko, adres, email
  • Adres - ulica, miasto, kod pocztowy
  • KlientFirmowy - jak Klient + NIP, nazwa_firmy

Zastanów się: która relacja jest "ma-a", a która "jest-a"?

Wskazówki:

  • KlientFirmowy jest Klientem - to dziedziczenie
  • Zamowienie ma Adres, Klienta i Pozycje - to kompozycja/agregacja
  • Adres jest ściśle związany z Zamowieniem (przy kompozycji) lub może być współdzielony (przy agregacji)
  • Produkt może istnieć niezależnie od Zamowienia - to agregacja
System zamówień

Ćwiczenia praktyczne są niezbędnym elementem nauki programowania, ponieważ pozwalają na bezpośrednie zastosowanie poznanej teorii. Samodzielne pisanie kodu, nawet prostych klas i obiektów, utrwala wiedzę znacznie skuteczniej niż bierne czytanie przykładów. Każde ćwiczenie w tym kursie zostało zaprojektowane tak, aby sprawdzić konkretne umiejętności i przygotować do bardziej złożonych zadań. Zaleca się wykonanie ćwiczenia bez podglądania rozwiązania, a dopiero potem porównanie własnego kodu z przykładowym. W przypadku trudności warto wrócić do odpowiedniego slajdu i przeanalizować omawiane na nim koncepcje jeszcze raz.

Nauka programowania przypomina naukę języków obcych - teoria jest ważna, ale to praktyka decyduje o biegłości. Im więcej samodzielnie napiszesz kodu, tym szybciej nabierzesz wprawy w myśleniu obiektowym. Eksperymentuj z przykładami, dodawaj nowe atrybuty i metody, testuj różne scenariusze użycia. Nie bój się błędów - każdy komunikat błędu to cenna lekcja, która przybliża Cię do mistrzostwa. Systematyczna praca i regularne programowanie są kluczem do sukcesu w nauce Pythona. Wykorzystaj REPL do szybkiego testowania hipotez i pomysłów.

44 Kod: implementacja z kompozycją

Implementacja systemu zamówień z kompozycją i dziedziczeniem

Poniższa implementacja łączy obie techniki. Zobacz, jak naturalnie współgrają ze sobą w jednym systemie.

class Adres:
    def __init__(self, ulica, miasto, kod):
        self.ulica = ulica
        self.miasto = miasto
        self.kod = kod

    def pelny_adres(self):
        return f"{self.ulica}, {self.kod} {self.miasto}"

class Produkt:
    def __init__(self, nazwa, cena):
        self.nazwa = nazwa
        self.cena = cena

    def opis(self):
        return f"{self.nazwa} ({self.cena} PLN)"

class Klient:
    def __init__(self, imie, nazwisko, email):
        self.imie = imie
        self.nazwisko = nazwisko
        self.email = email

    def dane_klienta(self):
        return f"{self.imie} {self.nazwisko} ({self.email})"

class KlientFirmowy(Klient):  # DZIEDZICZENIE: KlientFirmowy JEST Klientem
    def __init__(self, imie, nazwisko, email,
                 nip, firma):
        super().__init__(imie, nazwisko, email)
        self.nip = nip
        self.firma = firma

    def dane_klienta(self):  # przeslonienie metody
        return f"{self.firma} ({self.imie} {self.nazwisko}, NIP: {self.nip})"

class PozycjaZamowienia:
    def __init__(self, produkt, ilosc):
        self.produkt = produkt  # AGREGACJA: Produkt istnieje niezaleznie
        self.ilosc = ilosc

    def suma(self):
        return self.produkt.cena * self.ilosc

    def opis(self):
        return f"{self.produkt.nazwa} x {self.ilosc} = {self.suma()} PLN"

class Zamowienie:
    def __init__(self, klient, adres):
        self.klient = klient     # AGREGACJA: Klient istnieje niezaleznie
        self.adres = adres       # AGREGACJA: Adres przekazany z zewnatrz
        self.pozycje = []        # AGREGACJA: lista pozycji
        self.status = "nowe"

    def dodaj(self, produkt, ilosc):
        self.pozycje.append(PozycjaZamowienia(produkt, ilosc))

    def calkowita_kwota(self):
        return sum(p.suma() for p in self.pozycje)

    def podsumowanie(self):
        linie = [f"Zamowienie dla: {self.klient.dane_klienta()}"]
        linie.append(f"Adres: {self.adres.pelny_adres()}")
        linie.append("Pozycje:")
        for p in self.pozycje:
            linie.append(f"  - {p.opis()}")
        linie.append(f"Razem: {self.calkowita_kwota()} PLN")
        linie.append(f"Status: {self.status}")
        return "\n".join(linie)

# Przyklad uzycia
produkt1 = Produkt("Laptop", 3500)
produkt2 = Produkt("Mysz", 120)
klient = Klient("Anna", "Nowak", "anna@example.com")
adres = Adres("Główna 1", "Poznań", "60-001")

zamowienie = Zamowienie(klient, adres)
zamowienie.dodaj(produkt1, 1)
zamowienie.dodaj(produkt2, 2)
print(zamowienie.podsumowanie())

Przechowywanie obiektów w kolekcjach, takich jak listy, słowniki czy zbiory, jest podstawowym wzorcem w programowaniu obiektowym. Listy obiektów pozwalają na zbiorcze operacje, takie jak iterowanie, filtrowanie, sortowanie czy agregowanie danych. Python oferuje bogaty zestaw narzędzi do pracy z listami, w tym wyrażenia listowe, funkcje sorted(), filter() oraz sortowanie z kluczem lambda. Ważne jest zrozumienie, że lista przechowuje referencje do obiektów, a nie ich kopie, co ma wpływ na modyfikację danych przez różne części programu. Iterowanie po liście obiektów i wywoływanie ich metod to jeden z najczęstszych wzorców w codziennej pracy.

Kompozycja, czyli umieszczanie jednych obiektów wewnątrz innych, jest podstawową techniką budowania złożonych systemów z prostszych komponentów. W przeciwieństwie do dziedziczenia, które wyraża relację 'jest', kompozycja wyraża relację 'ma' i jest często preferowanym podejściem w projektowaniu obiektowym. Graf obiektów w rzeczywistych systemach może być bardzo złożony, ale nawigacja po nim jest naturalna i czytelna dzięki notacji kropkowej. Kompozycja pozwala na elastyczne budowanie funkcjonalności poprzez łączenie prostych obiektów w bardziej złożone struktury. Wzorzec ten jest fundamentem takich koncepcji jak wstrzykiwanie zależności czy architektura warstwowa.

45 Podsumowanie - mapa myśli

Podsumowanie części 9: Kompozycja kontra dziedziczenie

Po przerobieniu tej części powinieneś rozumieć różnicę między relacją "jest-a" (dziedziczenie) a "ma-a" (kompozycja) oraz wiedzieć, kiedy świadomie stosować każdą z tych technik. Poniższa mapa myśli podsumowuje najważniejsze koncepcje:

# Mapa myśli: Kompozycja vs Dziedziczenie

1. RELACJA "JEST-A" (DZIEDZICZENIE)
├── Pies jest Zwierzeciem
├── Pracownik jest Osoba
├── Zalety: polimorfizm, wielokrotne uzycie
└── Wady: sztywnosc, krucha hierarchia

2. RELACJA "MA-A" (KOMPOZYCJA)
├── Samochod ma Silnik
├── Czlowiek ma Adres
├── Zalety: elastycznosc, enkapsulacja
└── Wady: wiecej kodu, reczna delegacja

3. ZASADA: favor composition over inheritance
├── Kompozycja czesciej lepszym wyborem
├── Dziedziczenie gdy naturalna hierarchia "jest-a"
└── Laczymy obie techniki w praktyce

4. AGREGACJA vs KOMPOZYCJA
├── Kompozycja: scisla zaleznosc zycia
└── Agregacja: niezalezne zycie obiektow

5. PRAKTYCZNE WSKAZOWKI
├── Zadaj pytanie: "czy to MA, czy JEST?"
├── Unikaj hierarchii > 2-3 poziomow
└── Projektuj z mysla o testowaniu

Pamiętaj, że nie ma jednej słusznej odpowiedzi we wszystkich sytuacjach. Doświadczony programista obiektowy wybiera technikę odpowiednią do konkretnego problemu, kierując się zasadami, ale też praktycznym doświadczeniem. Ćwicz rozpoznawanie relacji między klasami w swoich projektach - z czasem stanie się to intuicyjne.

Mapa myśli

Podsumowanie to dobry moment na refleksję nad przyswojonym materiałem i identyfikację obszarów wymagających dodatkowej pracy. Wymienione punkty stanowią esencję przerobionej części kursu - od definicji klasy, przez tworzenie obiektów, aż po kompozycję i wzorzec fabryki. Każdy z tych punktów będzie rozwijany w kolejnych modułach, dlatego warto upewnić się, że są dobrze zrozumiane. Zachęcamy do tworzenia własnych notatek i map myśli, które pomagają w usystematyzowaniu wiedzy. Regularne powtórki są kluczowe dla trwałego zapamiętania materiału.

Mapy myśli są skutecznym narzędziem wizualizacji złożonych koncepcji i relacji między nimi. Przedstawiona mapa obrazuje najważniejsze pojęcia omówione w tej części kursu oraz ich wzajemne powiązania. Tworzenie własnych map myśli podczas nauki programowania aktywuje inne obszary mózgu niż czytanie liniowego tekstu, co przekłada się na lepsze zapamiętywanie. Studenci, którzy regularnie tworzą mapy myśli, osiągają lepsze wyniki w testach koncepcyjnych i szybciej łączą nowe informacje z już posiadaną wiedzą. Zachęcamy do wykorzystania tej techniki.