Last modified: May 25, 2025
This article is written in: 🇵🇱
Dziedziczenie oraz kompozycja to dwa filary programowania obiektowego, dzięki którym możemy pisać elastyczny, modułowy i łatwy w utrzymaniu kod. Każde z tych rozwiązań niesie ze sobą unikalne zalety i wiąże się z określonymi ograniczeniami, dlatego decyzja o ich zastosowaniu powinna wynikać z charakteru projektu i stawianych przed nim wymagań.
Dziedziczenie to mechanizm, który pozwala jednej klasie (klasie potomnej) przejąć właściwości i zachowania innej klasy (klasy bazowej). Dzięki dziedziczeniu, programiści mogą tworzyć nowe klasy na podstawie istniejących, co pozwala na ponowne wykorzystanie kodu i redukcję jego złożoności.
Dziedziczenie (relacja "jest")
Przedstawione jako drzewo hierarchii:
[Pojazd]
|
----------------
| |
[Samochód] [Motocykl]
|
[Samochód Sportowy]
W tym diagramie Samochód
i Motocykl
dziedziczą po Pojazd
, a Samochód Sportowy
dziedziczy po Samochód
. Każda strzałka reprezentuje relację dziedziczenia, gdzie klasa pochodna jest rodzajem klasy bazowej.
Chociaż dziedziczenie jest potężnym mechanizmem w programowaniu obiektowym, ma swoje ograniczenia i wady, które mogą prowadzić do problemów w bardziej złożonych projektach:
Dziedziczenie znajduje zastosowanie w wielu dziedzinach programowania obiektowego, gdzie organizowanie kodu w postaci hierarchii klas prowadzi do większej czytelności, elastyczności i ponownego użycia kodu. Oto kilka przykładowych scenariuszy:
Content
. Na przykład, klasy Artykul
, Strona
czy Obraz
mogą dziedziczyć wspólne atrybuty, takie jak tytul
, autor
, czy data_publikacji
, dzięki czemu można je wspólnie zarządzać i prezentować.Widget
. Dzięki dziedziczeniu wspólnych cech (np. możliwość rysowania na ekranie, obsługi zdarzeń), każda specyficzna klasa widżetu może dodawać unikalne zachowania, jednocześnie korzystając z podstawowych funkcji klasy bazowej.Postac
może być klasą bazową dla innych klas, takich jak Bohater
i Przeciwnik
. Klasy potomne mogą dziedziczyć wspólne atrybuty, takie jak punkty_zycia
, atak
, i obrona
, a jednocześnie dodawać specyficzne funkcje, np. umiejetnosc_specjalna
.Uzytkownik
jest klasą bazową, a klasy takie jak Administrator
, Redaktor
, czy UzytkownikZwykly
dziedziczą po niej. Każda z klas potomnych może mieć dodatkowe uprawnienia lub funkcjonalności specyficzne dla swojej roli.Transakcja
. Na przykład, Przelew
, Wplata
, i Wyplata
mogą dziedziczyć wspólne cechy takie jak kwota
, data
, czy konto
, a jednocześnie dodawać specyficzne zachowania, np. obsługę opłat bankowych lub walidację danych.Resource
może implementować wspólne metody dla operacji HTTP (GET, POST, PUT, DELETE), a poszczególne zasoby, takie jak UzytkownikResource
, ProduktResource
i ZamowienieResource
, mogą dziedziczyć tę funkcjonalność, dostosowując ją do konkretnych zasobów.Produkt
. Na przykład, Ksiazka
, Odziez
, i Elektronika
mogą dziedziczyć podstawowe atrybuty, takie jak nazwa
, cena
, i opis
, ale dodawać specyficzne pola, np. autor
dla książki lub rozmiar
dla odzieży.W podstawowym przypadku dziedziczenie pozwala klasie podrzędnej (ang. subclass) na przejęcie atrybutów i metod klasy bazowej (ang. superclass). W poniższym przykładzie klasa Student
dziedziczy po klasie Czlowiek
, co pozwala na użycie wszystkich atrybutów i metod klasy bazowej, a jednocześnie umożliwia dodanie własnych, specyficznych dla klasy Student
.
class Czlowiek:
def __init__(self, imie: str, nazwisko: str, miejsce_urodzenia: str, zawod: str):
self.imie = imie
self.nazwisko = nazwisko
self.miejsce_urodzenia = miejsce_urodzenia
self.zawod = zawod
def __str__(self):
return f"{self.imie} {self.nazwisko}, Urodzony w: {self.miejsce_urodzenia}, Zawód: {self.zawod}"
class Student(Czlowiek):
def __init__(self, imie: str, nazwisko: str, miejsce_urodzenia: str, numer_albumu: int, kierunek_studiow: str):
super().__init__(imie, nazwisko, miejsce_urodzenia, 'student')
self.numer_albumu = numer_albumu
self.kierunek_studiow = kierunek_studiow
def __str__(self):
return f"{super().__str__()}, Numer albumu: {self.numer_albumu}, Kierunek: {self.kierunek_studiow}"
W tym przykładzie klasa Student
dziedziczy konstruktor i metodę __str__
z klasy Czlowiek
. Za pomocą super()
odwołujemy się do klasy bazowej, by zainicjalizować jej atrybuty, a następnie dodajemy dodatkowe atrybuty specyficzne dla klasy Student
. Dzięki temu, obiekt Student
posiada pełną funkcjonalność klasy Czlowiek
, jednocześnie rozszerzoną o własne cechy.
W Pythonie możemy tworzyć klasy, które dziedziczą po więcej niż jednej klasie bazowej. Taka elastyczność pozwala na łączenie różnych funkcjonalności w jednej klasie podrzędnej. Poniżej przedstawiamy przykład wielokrotnego dziedziczenia:
class Sportowiec:
def __init__(self, dyscyplina: str):
self.dyscyplina = dyscyplina
def __str__(self):
return f"Dyscyplina: {self.dyscyplina}"
class StudentSportowiec(Student, Sportowiec):
def __init__(self, imie: str, nazwisko: str, miejsce_urodzenia: str, numer_albumu: int, kierunek_studiow: str, dyscyplina: str):
Student.__init__(self, imie, nazwisko, miejsce_urodzenia, numer_albumu, kierunek_studiow)
Sportowiec.__init__(self, dyscyplina)
def __str__(self):
return f"{Student.__str__(self)}, {Sportowiec.__str__(self)}"
W tym przypadku klasa StudentSportowiec
dziedziczy po dwóch klasach bazowych: Student
i Sportowiec
. W konstruktorze klasy podrzędnej wywołujemy osobno konstruktory obu klas bazowych, by zainicjalizować ich atrybuty. Dzięki temu, obiekt StudentSportowiec
łączy cechy zarówno studenta, jak i sportowca.
Kompozycja to technika, w której klasy są tworzone z instancji innych klas, co pozwala na budowanie złożonych obiektów poprzez łączenie mniejszych, bardziej wyspecjalizowanych obiektów. Kompozycja promuje luźne powiązanie i lepszą modularność kodu.
Kompozycja (relacja "ma")
Przedstawione jako klasa z elementami składowymi w środku:
+-----------------+
| Samochód |
|-----------------|
| - Silnik |
| - Koła |
| - Nadwozie |
| - Wnętrze |
+-----------------+
Tutaj Samochód
ma różne komponenty, takie jak Silnik
, Koła
, Nadwozie
i Wnętrze
. Elementy te są zawarte wewnątrz klasy Samochód
, co ilustruje relację kompozycji.
Chociaż kompozycja ma wiele zalet, istnieją pewne wyzwania, które mogą się pojawić podczas jej stosowania:
Kompozycja jest szeroko stosowana w różnych obszarach programowania, szczególnie tam, gdzie elastyczność, modularność i ponowne użycie komponentów są kluczowe. Oto przykłady zastosowań kompozycji:
class Pensja:
def __init__(self, pensja: int, stopa_podwyzki: float):
self.pensja = pensja
self.stopa_podwyzki = stopa_podwyzki
def roczna_pensja(self):
return self.pensja * (1 + self.stopa_podwyzki)
def __str__(self):
return f"Pensja: {self.pensja}, Stopa podwyżki: {self.stopa_podwyzki*100}%"
class Pracownik:
def __init__(self, imie: str, nazwisko: str, pensja: Pensja):
self.imie = imie
self.nazwisko = nazwisko
self.pensja = pensja # kompozycja: obiekt Pensja jest częścią obiektu Pracownik
def __str__(self):
return f"Pracownik: {self.imie} {self.nazwisko}, Zarabia rocznie: {self.pensja.roczna_pensja()} PLN"
W powyższym przykładzie klasa Pracownik
korzysta z kompozycji, umieszczając obiekt klasy Pensja
jako jej atrybut. Dzięki temu Pracownik
może korzystać z metod zdefiniowanych w klasie Pensja
poprzez instancję tej klasy.
Kompozycja może być używana do tworzenia bardziej złożonych struktur, które są trudne do osiągnięcia za pomocą samego dziedziczenia.
class Adres:
def __init__(self, ulica: str, miasto: str, kod_pocztowy: str):
self.ulica = ulica
self.miasto = miasto
self.kod_pocztowy = kod_pocztowy
def __str__(self):
return f"{self.ulica}, {self.miasto}, {self.kod_pocztowy}"
class Osoba:
def __init__(self, imie: str, nazwisko: str, adres: Adres):
self.imie = imie
self.nazwisko = nazwisko
self.adres = adres
def __str__(self):
return f"{self.imie} {self.nazwisko}, Adres: {self.adres}"
adres = Adres("ul. Kwiatowa 15", "Warszawa", "00-001")
osoba = Osoba("Jan", "Kowalski", adres)
print(osoba) # Jan Kowalski, Adres: ul. Kwiatowa 15, Warszawa, 00-001
W tym przykładzie Osoba
ma obiekt Adres
jako jeden ze swoich atrybutów. Umożliwia to bardziej elastyczne i modularne projektowanie kodu, ponieważ możemy łatwo zmieniać adresy, nie zmieniając samej klasy Osoba
.
Cecha | Dziedziczenie | Kompozycja |
Relacja | "Jest rodzajem" (is-a relationship) | "Ma" lub "Składa się z" (has-a relationship) |
Elastyczność | Mniej elastyczne (hierarchia klas jest sztywna) | Bardziej elastyczne (łatwo dodawać/usuwać komponenty) |
Reużywalność | Może prowadzić do problemów z ponownym użyciem kodu | Wysoka reużywalność komponentów |
Dostęp do atrybutów | Bezpośredni dostęp do atrybutów klasy nadrzędnej | Dostęp przez instancje składników |
Zmienność | Zmiany w klasie bazowej mogą wpłynąć na klasy pochodne | Zmiany w jednej klasie nie wpłyną na inne |
Kiedy używać? | Gdy istnieje wyraźna relacja hierarchiczna | Gdy chcemy modelować relacje pomiędzy częściami |
Skomplikowanie kodu | Może prowadzić do nadmiernego rozproszenia kodu | Może prowadzić do większej ilości kodu |
Polimorfizm | Wspierany | Wymaga dodatkowego kodu |