W tym wpisie zrobimy swój pierwszy minimalistyczny projekt w obiektowym świecie. Zabawimy się w architektów i stworzymy nasz model samochodu.
Zakładam, że posiadasz już pewną wiedzę z tej dziedziny.
Co powinieneś już wiedzieć:
- co to jest klasa
- co to jest obiekt
- co to jest konstruktor
Jeśli jeszcze masz problem z którymś z tych zagadnień, to zachęcam do zapoznania się z poprzednim wpisem Obiektowy Tarpan – podstawy obiektowości.
Historyjka użycia
Jest to nic innego jak krótki opis funkcjonalności z perspektywy użytkownika. Warto pamiętać ,że poprzez użytkownika nie zawsze mówimy o użytkowniku końcowym.
Czasem naszymi klientami są inni programiści i tak właśnie będzie w tym przypadku. Naszym zadaniem będzie zaprojektowanie obiektowej struktury na podstawie krótkiej historyjki użycia.
Nasza historyjka użycia
Jako programista chcę mieć możliwość stworzenia dwóch pojazdów. Każdy z nich ma mieć daną markę i model. Musi posiadać ślinki o zadanej pojemności i mocy oraz skrzynię biegów o różnej ilości przeleżeń. Samochód powinien potrafić jeździć, hamować oraz zmieniać biegi zarówno w górę jak i w dół. Chcę mieć również możliwość sprawdzenia obecnie wybranego biegu.
Jak się pewnie spodziewasz kolorki jakie tu użyłem nie są bez znaczenia. Pomogą nam w zaprojektowaniu właściwej struktury klas. Zauważ ,że rzeczowniki zaznaczone są na zielono, a czasowniki na czerwono. Pozostałe słowa zaznaczone kolorem niebieskim opisują dany przedmiot. Niewątpliwie już się domyślasz co zrobimy z poszczególnymi wyrazami.
- zielone => zostaną naszymi klasami
- czerwone => ewoluują w metody tych klas
- niebieskie => to są parametry czyli pola naszych klas
Budujemy auto
Tak samo jak w fabryce tak i tutaj nie zbudujemy całego auta za jednym razem. Najpierw powstaną podzespoły naszej maszyny później zamontujemy je w docelowym samochodzie. Zaczniemy od budowy silnika i skrzyni biegów. Mimo wszytko dobrze by było mieć plan naszych prac aby na koniec nie okazało się, że miało być Porsche, a znowu wyszedł Passat. Schemat klas można by z powodzeniem przedstawiać jako diagram UML. Tutaj jednak, żeby nie stracić celu z oczu, posłużymy się czymś innym.
Schemat klas
Na tym obrazku doskonale widać gdzie będą się znajdować nasze klasy. Klasa „Auto” będzie naszą klasą główną natomiast klasy Sinik oraz „SkrzyniaBiegow” będą składowymi klasy „Auto” (będą w jej środku). Przyjrzyjmy się nazwom naszych klas:
- Auto
- Silnik
- SkrzyniaBiegow
Konwencja nazywania klas w Pythonie jest dość prosta i zbliżona do innych języków programowania.
Nazwy klas powinny zaczynać się z Wielkiej litery. Jeśli nazwa składa się z więcej niż jednego wyrazu należy zastosować notację wielbłądzią (camel case). Brzmi podejrzanie, ale jest banalnie proste. Oznacza to, że kolejne wyrazy piszemy razem z tym, że z wielkiej litery. Dobrym przykładem jest nasza „SkrzyniaBiegow”.
Zaczynamy budowę
Silnik
Konstruowanie naszego pojazdu zaczniemy od sinika.
Jak widać jest to klasa trzymająca dwa parametry. Po zdefiniowaniu typu „Silnik” będziemy mogli konstruować obiekty z ustawionymi parametrami „pojemnosc” i „moc”. Zobaczmy zatem jak to wygląda w kodzie:
class Silnik: def __int__(self, pojemnosc, moc): # konstruktor przyjmujący dwie wartości self.pojemnosc = pojemnosc # ustawienie pola klasy na wartość przekazaną w konstruktorze self.moc = moc # ustawienie pola klasy na wartość przekazaną w konstruktorze
Jak widać nasza klasa to głównie konstruktor. O konstruktorach mówiliśmy już w poprzednim wpisie, ale zróbmy małe powtórzenie. Konstruktor to taka magiczna metoda naszej klasy, która wywoływana jest automatycznie podczas tworzenia obiektu. Czyli jeśli zrobimy tak Auto() # konstruowanie obiektu odpalenie konstruktora
Przykład tworzenia obiektu typu „Silnik” – zachęcam do uruchomienia tego kodu w edytorze:
class Silnik: def __init__(self, pojemnosc, moc): print('konstruktor działa') self.pojemnosc = pojemnosc self.moc = moc Silnik(660, 48) # tu tworzymy obiekt i przekazujemy mu parametry do konstruktora
Temat sinika mamy już załatwiony.
Skrzynia biegów
Teraz pora na zaimplementowanie skrzyni biegów czyli klasy o nazwie „SkrzyniaBiegow”. Przypomnijmy co powinna zawierać nasza klasa.
Mamy tu dwa atrybuty klasy oraz dwie metody do zmiany biegów. Zastanówmy się co wydarzy się gdy wywołamy metodę bieg_wyzej(). Powinniśmy zmodyfikować wartość zapisaną w atrybucie obecny_bieg. Należałby by też upewnić się, że zamieniając bieg nie przekroczymy maksymalnej dozwolonej wartości zapisanej w zmiennej ilosc_przelozen. Analogicznie będzie to wyglądało podczas zmiany biegu w dół. Czas na kod:
class SkrzyniaBiegow: def __init__(self, ilosc_przelozen): # konstruktor self.ilosc_przelozen = ilosc_przelozen # ustawiamy ilość przełożeń self.obecny_bieg = 0 # obecny numer biegu ustawiamy na zero def bieg_wyzej(self): # jeśli możemy to zmieniamy na bieg wyżej if self.obecny_bieg + 1 <= self.ilosc_przelozen: self.obecny_bieg += 1 def bieg_nizej(self): # jeśli możemy to zmieniamy na bieg niżej if(self.obecny_bieg - 1 >= -1): self.obecny_bieg =- 1 skrzynia_biegow = SkrzyniaBiegow(2) print(skrzynia_biegow.obecny_bieg) skrzynia_biegow.bieg_wyzej() print(skrzynia_biegow.obecny_bieg) skrzynia_biegow.bieg_nizej() print(skrzynia_biegow.obecny_bieg)
Zauważ, że dzięki przekazywaniu „self” do metod jesteśmy w stanie operować na polach obiektu. Z wnętrza metod możemy modyfikować wartości atrybutów danego obiektu. Każdy stworzony obiekt ma swój własny komplet atrybutów. Daje nam to możliwość stworzenia wielu obiektów (instancji klas) i każdy z nich będzie miał własny zestaw atrybutów.
Auto
Teraz najtrudniejsze. Tak jak uzgodniliśmy wcześniej obiekt „Auto” musi zawierać w sobie obiekty „Silnik” i „SkrzyniaBiegow”. Znaczy to tylko tyle, że tworząc obiekt na podstawie klasy „Auto” powołamy do życia dodatkowo jeden obiekt typu „Silnik” oraz jeden obiekt typu „Skrzynia biegów”. Wydaje się to dość logiczne bo parzcież każdy nowy samochód ma już zamontowane swoje podzespoły. Dodatkowo obiekty niejako należą do danego auta. W obiektowym świecie takie połączenie modeluje się bardzo prosto. Wystarczy, że klasa agregująca („Auto”) będzie zawierała dwa pola (atrybuty) u nas „Silnik” i „SkrzyniaBiegow”. Tak tylko dla ciekawych dodam, że takie połączenie nazywamy agregacją. Zaimplementujmy więc klasę „Auto”:
class Auto: def __init__(self, pojemnosc, moc, ilosc_przelozen, marka, model): self.marka = marka self.model = model self.silnik = Silnik(pojemnosc, moc) # do pola klasy przypisujemy obiekt typu silnik self.skrzynia_biegow = SkrzyniaBiegow(ilosc_przelozen)#tworzenie obiektu SkrzyniaBiegow
Jak widać nasza klasa to głównie konstruktor i to w nim dzieją się najciekawsze rzeczy. Pamiętajmy, że konstruktor wywołuje się automatycznie podczas tworzenia obiektu. Z uwagi na to, że w konstruktorze klasy „Auto” wywołujemy dwa inne konstruktory, czyli mamy swego rodzaju reakcję łańcuchową. Stworzenie obiektu klasy „Auto” powoduje wywołanie konstruktora klasy „Auto”, który to powoduje stworzenie obiektów dla klasy „Silnik” i „SkrzyniaBiegow”, czyli wywołaniu konstruktorów tych klas. Naszym zadaniem jest rozpropagowanie po naszej strukturze wartości które zostaną ustawione w poszczególnych obiektach. Dlatego właśnie klasa „Auto” przyjmuje w konstruktorze dane potrzebne podczas tworzenia obiektów „Silnik” i „SkrzyniaBiegow” i przekazuje je do odpowiednich konstruktorów. Zobaczmy jak będzie wyglądać cały kod naszego programu:
class SkrzyniaBiegow: def __init__(self, ilosc_przelozen): # konstruktor self.ilosc_przelozen = ilosc_przelozen # ustawiamy ilość przełożeń self.obecny_bieg = 0 # obecny numer biegu ustawiamy na zero def bieg_wyzej(self): # ieślic możemy zmieniamy na bieg wyżej if self.obecny_bieg + 1 <= self.ilosc_przelozen: self.obecny_bieg += 1 def bieg_nizej(self): # ieślic możemy zmieniamy na bieg niżej if(self.obecny_bieg - 1 >= -1): self.obecny_bieg =- 1 class Silnik: def __init__(self, pojemnosc, moc): print('konstruktor działa') self.pojemnosc = pojemnosc self.moc = moc class Auto: def __init__(self, pojemnosc, moc, ilosc_przelozen, marka, model): self.marka = marka self.model = model self.silnik = Silnik(pojemnosc, moc) self.skrzynia_biegow = SkrzyniaBiegow(ilosc_przelozen) porsche = Auto(2000,200,6,'porsche','cayenne') print(porsche.model)# wyświetlamy model z obiektu Auto print(porsche.silnik.moc)# wyświtlamy moc z obiektu Sinki porsche.skrzynia_biegow.bieg_wyzej() # uruchamiamy metode print(porsche.skrzynia_biegow.obecny_bieg)# wyświetl obecny bieg
Jak widać odwołanie się do atrybutów i metod jest banalnie proste wystarczy posłużyć się kopką. Zastanówmy się nad takim przykładem:
porsche = Auto(2000,200,6,'porsche','cayenne') print(porsche.model)# wyświetlamy model z obiektu Auto
Obiekt porsche posiada pole model i aby się do niego dostać robimy tak porsche.model
porsche = Auto(2000,200,6,'porsche','cayenne') print(porsche.silnik.moc)# wyświtlamy moc z obiektu Sinki
Obiekt porsche posiada pole silnik, które jest obiektem, a ten obiekt posiada pole moc. Dobieramy się do tego tak porsche.silnik.moc
Analogicznie można dostać się do metod obiektów składowych np: porsche.skrzynia_biegow.bieg_wyzej()
Podsumowanie
W tym wpisie udało nam się zaimplementować prostą strukturę klas. Najbardziej zależy mi żebyś zrozumiał kolejność wywoływania konstruktorów w naszej implementacji klasy „Auto”. Tworząc jeden obiekt klasy „Auto” automatycznie tworzone są pola tej klasy u umieszczane w nich obiekty typu „Silnik” i „SkrzyniaBiegow”. Zrozumienie tej kaskadowości wywoływania się poszczególnych konstruktorów jest kluczowe dla zrozumienia programowania obiektowego. Cała reszta to już tylko kolejne bajery bazujące jednak na mechanice tworzenia się obiektów. Podsumowując po przeczytaniu tego wpisu umiesz już tworzyć obiekty, a nawet obiekty którymi składowymi są inne obiekty. Pewnie potrafił byś też zrobić obiekt, który zawiera obiekt, który to zawiera jeszcze inny obiekt heh, ale może nie brnijmy w to aż tak daleko. Powodzenia w zgłębianiu obiektowego świata programowania.
Dobry blog, dobry kanał na yt, ale na tej skrzyni biegów daleko się nie zajedzie. Redukcja biegu zawsze skutkuje włączeniem biegu -1. Jeśli by przyjąć, że -1 to wsteczny to redukcja przykładowo z 6 na -1 jest niewykonalna, no chyba że na postoju i/lub redukcja będzie wykonywana na niskich obrotach. Przy założeniach że -1 to wsteczny i że redukujemy bieg zawsze tylko o jedno przełożenie to metoda bieg_nizej powinna wyglądać co najmniej tak:
def bieg_nizej(self):
if(self.obecny_bieg – 1 >= -1):
self.obecny_bieg -= 1
Chyba, że bez wstecznego:
def bieg_nizej(self):
if(self.obecny_bieg – 1 >= 0):
self.obecny_bieg -= 1
Z taką skrzynią biegów daleko się nie zajedzie. Każda redukcja powoduje zmianę biegu na -1. Odpowiada za to przypisanie wartości -1 do obecny_bieg zamiast deinkrementacja o wartość 1. Nie ta kolejność znaków w kodzie zamiast =- powinno być -=.
Jeśli -1 to bieg wsteczny to metoda bieg_nizej powinna wyglądać co najmniej tak:
def bieg_nizej(self):
if(self.obecny_bieg – 1 >= -1):
self.obecny_bieg -= 1
a jeśli nie ma biegu wstecznego to tak:
def bieg_nizej(self): # jeśli możemy to zmieniamy na bieg niżej
if(self.obecny_bieg – 1 >= 0):
self.obecny_bieg -= 1