Wprowadzenie do programowania obiektowego
Termin “OOP” oznacza programowanie obiektowe (Object-Oriented Programming), które jest sposobem organizacji i strukturyzacji kodu. OOP pozwala nam postrzegać program jako zbiór obiektów, które komunikują się ze sobą, a nie jako sekwencję poleceń i funkcji.
W OOP “obiekt” to jednostka zawierająca dane i funkcje, które działają na tych danych. Obiekty są tworzone w oparciu o “klasy”, które można rozumieć jako plany lub szablony obiektów. Gdy mamy już klasę, możemy utworzyć jej “instancję”, która jest konkretnym obiektem utworzonym z tej klasy.
Przyjrzyjmy się jak możemy stworzyć prostą klasę w PHP. Podczas definiowania klasy używamy słowa kluczowego “class”, po którym następuje nazwa klasy, a następnie nawiasy klamrowe, które otaczają funkcje klasy (zwane “metodami”) i zmienne klasy (zwane “właściwościami” lub “atrybutami”):
W tym przykładzie utworzyliśmy klasę o nazwie Car
z jedną funkcją (lub “metodą”) o nazwie
honk
.
Każda klasa powinna rozwiązywać tylko jedno główne zadanie. Jeśli klasa wykonuje zbyt wiele zadań, może być wskazane podzielenie jej na mniejsze, wyspecjalizowane klasy.
Klasy są zwykle przechowywane w oddzielnych plikach, aby kod był uporządkowany i łatwy w nawigacji. Nazwa pliku powinna
być zgodna z nazwą klasy, więc dla klasy Car
nazwa pliku brzmiałaby Car.php
.
Podczas nazywania klas dobrze jest przestrzegać konwencji “PascalCase”, co oznacza, że każde słowo w nazwie zaczyna się wielką literą i nie ma podkreśleń ani innych separatorów. Metody i właściwości są zgodne z konwencją “camelCase”, co oznacza, że zaczynają się od małej litery.
Niektóre metody w PHP mają specjalne role i są poprzedzone __
(dwa podkreślenia). Jedną z najważniejszych
metod specjalnych jest “konstruktor”, oznaczony jako __construct
. Konstruktor jest metodą, która jest
automatycznie wywoływana podczas tworzenia nowej instancji klasy.
Często używamy konstruktora do ustawienia początkowego stanu obiektu. Na przykład, podczas tworzenia obiektu reprezentującego osobę, można użyć konstruktora, aby ustawić jej wiek, imię lub inne atrybuty.
Zobaczmy jak używać konstruktora w PHP:
W tym przykładzie klasa Person
ma właściwość (zmienną) $age
i konstruktor, który ustawia tę
właściwość. Metoda howOldAreYou()
zapewnia następnie dostęp do wieku osoby.
Pseudo-zmienna $this
jest używana wewnątrz klasy w celu uzyskania dostępu do właściwości i metod
obiektu.
Słowo kluczowe new
służy do tworzenia nowej instancji klasy. W powyższym przykładzie utworzyliśmy nową
osobę w wieku 25 lat.
Można również ustawić wartości domyślne dla parametrów konstruktora, jeśli nie zostały one określone podczas tworzenia obiektu. Na przykład:
W tym przykładzie, jeśli nie określisz wieku podczas tworzenia obiektu Person
, zostanie użyta domyślna
wartość 20.
Fajną rzeczą jest to, że definicję właściwości z jej inicjalizacją za pomocą konstruktora można skrócić i uprościć w następujący sposób:
Dla kompletności, oprócz konstruktorów, obiekty mogą mieć destruktory (metoda __destruct
), które są
wywoływane przed zwolnieniem obiektu z pamięci.
Przestrzenie nazw
Przestrzenie nazw pozwalają nam organizować i grupować powiązane klasy, funkcje i stałe, unikając jednocześnie konfliktów nazewnictwa. Można o nich myśleć jak o folderach na komputerze, gdzie każdy folder zawiera pliki związane z konkretnym projektem lub tematem.
Przestrzenie nazw są szczególnie przydatne w większych projektach lub podczas korzystania z bibliotek innych firm, gdzie mogą wystąpić konflikty nazewnictwa klas.
Wyobraź sobie, że masz w projekcie klasę o nazwie Car
i chcesz umieścić ją w przestrzeni nazw o nazwie
Transport
. Można to zrobić w następujący sposób:
Jeśli chcesz użyć klasy Car
w innym pliku, musisz określić, z której przestrzeni nazw pochodzi
ta klasa:
Dla uproszczenia można określić na początku pliku, której klasy z danej przestrzeni nazw chcemy użyć, co pozwala na tworzenie instancji bez podawania pełnej ścieżki:
Dziedziczenie
Dziedziczenie jest narzędziem programowania obiektowego, które umożliwia tworzenie nowych klas na podstawie istniejących, dziedziczenie ich właściwości i metod oraz rozszerzanie lub redefiniowanie ich w razie potrzeby. Dziedziczenie zapewnia możliwość ponownego wykorzystania kodu i hierarchię klas.
Mówiąc prościej, jeśli mamy jedną klasę i chcemy utworzyć inną, pochodną od niej, ale z pewnymi modyfikacjami, możemy “odziedziczyć” nową klasę z oryginalnej.
W PHP dziedziczenie jest implementowane za pomocą słowa kluczowego extends
.
Nasza klasa Person
przechowuje informacje o wieku. Możemy mieć inną klasę, Student
, która
rozszerza Person
i dodaje informacje o kierunku studiów.
Spójrzmy na przykład:
Jak działa ten kod?
- Użyliśmy słowa kluczowego
extends
, aby rozszerzyć klasęPerson
, co oznacza, że klasaStudent
dziedziczy wszystkie metody i właściwości zPerson
. - Słowo kluczowe
parent::
pozwala nam wywoływać metody z klasy nadrzędnej. W tym przypadku wywołaliśmy konstruktor z klasyPerson
przed dodaniem naszej własnej funkcjonalności do klasyStudent
. I podobnie, metodę przodkaprintInformation()
przed wyświetleniem informacji o uczniu.
Dziedziczenie jest przeznaczone dla sytuacji, w których istnieje relacja “is a” między klasami. Na przykład, klasa
Student
jest klasą Person
. Kot jest zwierzęciem. Pozwala nam to w przypadkach, w których oczekujemy
jednego obiektu (np. “Osoba”) w kodzie na użycie obiektu pochodnego (np. “Student”).
Ważne jest, aby zdać sobie sprawę, że głównym celem dziedziczenia nie jest zapobieganie powielaniu kodu. Wręcz przeciwnie, niewłaściwe wykorzystanie dziedziczenia może prowadzić do skomplikowanego i trudnego w utrzymaniu kodu. Jeśli między klasami nie ma relacji “is a”, powinniśmy rozważyć kompozycję zamiast dziedziczenia.
Należy zauważyć, że metody printInformation()
w klasach Person
i Student
wyświetlają nieco inne informacje. Możemy też dodać inne klasy (takie jak Employee
), które zapewnią inne
implementacje tej metody. Zdolność obiektów różnych klas do reagowania na tę samą metodę na różne sposoby nazywana jest
polimorfizmem:
Kompozycja
Kompozycja to technika, w której zamiast dziedziczyć właściwości i metody z innej klasy, po prostu używamy jej instancji w naszej klasie. Pozwala nam to łączyć funkcjonalności i właściwości wielu klas bez tworzenia złożonych struktur dziedziczenia.
Na przykład, mamy klasę Engine
i klasę Car
. Zamiast mówić “Samochód jest silnikiem”,
mówimy “Samochód ma silnik”, co jest typową relacją kompozycji.
W tym przypadku klasa Car
nie ma wszystkich właściwości i metod klasy Engine
, ale ma do nich
dostęp poprzez właściwość $engine
.
Zaletą kompozycji jest większa elastyczność projektu i lepsza zdolność adaptacji do przyszłych zmian.
Widoczność
W PHP można zdefiniować “widoczność” dla właściwości, metod i stałych klasy. Widoczność określa, gdzie można uzyskać dostęp do tych elementów.
- Jeśli element jest oznaczony jako
public
, oznacza to, że można uzyskać do niego dostęp z dowolnego miejsca, nawet spoza klasy. - Element oznaczony jako
protected
jest dostępny tylko w obrębie klasy i wszystkich jej klas potomnych (klas, które po niej dziedziczą). - Jeśli element jest
private
, można uzyskać do niego dostęp tylko w obrębie klasy, w której został zdefiniowany.
Jeśli nie określisz widoczności, PHP automatycznie ustawi ją na public
.
Spójrzmy na przykładowy kod:
Kontynuacja dziedziczenia klas:
W tym przypadku metoda printProperties()
w klasie ChildClass
może uzyskać dostęp do właściwości
publicznych i chronionych, ale nie może uzyskać dostępu do właściwości prywatnych klasy nadrzędnej.
Dane i metody powinny być jak najbardziej ukryte i dostępne tylko poprzez zdefiniowany interfejs. Pozwala to na zmianę wewnętrznej implementacji klasy bez wpływu na resztę kodu.
Końcowe słowo kluczowe
W PHP możemy użyć słowa kluczowego final
, jeśli chcemy zapobiec dziedziczeniu lub nadpisywaniu klasy, metody
lub stałej. Gdy klasa jest oznaczona jako final
, nie może zostać rozszerzona. Gdy metoda jest oznaczona jako
final
, nie może zostać nadpisana w podklasie.
Świadomość, że dana klasa lub metoda nie będzie już modyfikowana, pozwala nam łatwiej wprowadzać zmiany bez obawy o potencjalne konflikty. Na przykład, możemy dodać nową metodę bez obawy, że potomek może już mieć metodę o tej samej nazwie, co doprowadzi do kolizji. Możemy też zmienić parametry metody, ponownie bez ryzyka spowodowania niespójności z nadpisaną metodą w metodzie potomnej.
W tym przykładzie próba dziedziczenia z klasy finalnej FinalClass
spowoduje błąd.
Statyczne właściwości i metody
Kiedy mówimy o “statycznych” elementach klasy w PHP, mamy na myśli metody i właściwości, które należą do samej klasy, a nie do konkretnej instancji klasy. Oznacza to, że nie musisz tworzyć instancji klasy, aby uzyskać do nich dostęp. Zamiast tego, wywołujesz je lub uzyskujesz do nich dostęp bezpośrednio poprzez nazwę klasy.
Należy pamiętać, że ponieważ elementy statyczne należą do klasy, a nie do jej instancji, nie można używać
pseudo-zmiennej $this
wewnątrz metod statycznych.
Używanie właściwości statycznych prowadzi do zaciemniania kodu pełnego pułapek, więc nigdy nie powinieneś ich używać, a my nie pokażemy tutaj przykładu. Z drugiej strony, metody statyczne są użyteczne. Oto przykład:
W tym przykładzie utworzyliśmy klasę Calculator
z dwiema metodami statycznymi. Możemy wywołać te metody
bezpośrednio bez tworzenia instancji klasy za pomocą operatora ::
. Metody statyczne są szczególnie przydatne w
przypadku operacji, które nie zależą od stanu konkretnej instancji klasy.
Stałe klasowe
W ramach klas mamy możliwość definiowania stałych. Stałe to wartości, które nigdy nie zmieniają się podczas wykonywania programu. W przeciwieństwie do zmiennych, wartość stałej pozostaje taka sama.
W tym przykładzie mamy klasę Car
ze stałą NumberOfWheels
. Podczas uzyskiwania dostępu do stałej
wewnątrz klasy możemy użyć słowa kluczowego self
zamiast nazwy klasy.
Interfejsy obiektów
Interfejsy obiektowe działają jak “kontrakty” dla klas. Jeśli klasa ma zaimplementować interfejs obiektowy, musi zawierać wszystkie metody zdefiniowane przez interfejs. Jest to świetny sposób na zapewnienie, że niektóre klasy przestrzegają tego samego “kontraktu” lub struktury.
W PHP interfejsy definiowane są za pomocą słowa kluczowego interface
. Wszystkie metody zdefiniowane w
interfejsie są publiczne (public
). Gdy klasa implementuje interfejs, używa słowa kluczowego
implements
.
Jeśli klasa implementuje interfejs, ale nie wszystkie oczekiwane metody są zdefiniowane, PHP zgłosi błąd.
Klasa może implementować wiele interfejsów jednocześnie, co różni się od dziedziczenia, gdzie klasa może dziedziczyć tylko z jednej klasy:
Klasy abstrakcyjne
Klasy abstrakcyjne służą jako szablony bazowe dla innych klas, ale nie można bezpośrednio tworzyć ich instancji. Zawierają one mieszankę kompletnych metod i metod abstrakcyjnych, które nie mają zdefiniowanej zawartości. Klasy dziedziczące po klasach abstrakcyjnych muszą zawierać definicje wszystkich metod abstrakcyjnych z klasy nadrzędnej.
Do zdefiniowania klasy abstrakcyjnej używamy słowa kluczowego abstract
.
W tym przykładzie mamy klasę abstrakcyjną z jedną zwykłą i jedną abstrakcyjną metodą. Następnie mamy klasę
Child
, która dziedziczy po AbstractClass
i zapewnia implementację metody abstrakcyjnej.
Czym różnią się interfejsy od klas abstrakcyjnych? Klasy abstrakcyjne mogą zawierać zarówno metody abstrakcyjne, jak i konkretne, podczas gdy interfejsy definiują tylko metody, które klasa musi zaimplementować, ale nie zapewniają implementacji. Klasa może dziedziczyć tylko z jednej klasy abstrakcyjnej, ale może implementować dowolną liczbę interfejsów.
Sprawdzanie typu
W programowaniu kluczowe jest upewnienie się, że dane, z którymi pracujemy, są poprawnego typu. W PHP mamy narzędzia, które to zapewniają. Weryfikacja poprawności typu danych nazywana jest “sprawdzaniem typu”.
Typy, które możemy napotkać w PHP:
- Podstawowe typy: Należą do nich
int
(liczby całkowite),float
(liczby zmiennoprzecinkowe),bool
(wartości logiczne),string
(ciągi znaków),array
(tablice) inull
. - Klasy: Gdy chcemy, aby wartość była instancją określonej klasy.
- Interfaces: Definiuje zestaw metod, które klasa musi zaimplementować. Wartość, która spełnia interfejs, musi mieć te metody.
- Typ mieszany: Możemy określić, że zmienna może mieć wiele dozwolonych typów.
- Void: Ten specjalny typ wskazuje, że funkcja lub metoda nie zwraca żadnej wartości.
Zobaczmy, jak zmodyfikować kod, aby uwzględnić typy:
W ten sposób upewniamy się, że nasz kod oczekuje i działa z danymi odpowiedniego typu, pomagając nam zapobiegać potencjalnym błędom.
Niektóre typy nie mogą być zapisane bezpośrednio w PHP. W takim przypadku są one wymienione w komentarzu phpDoc, który
jest standardowym formatem dokumentowania kodu PHP, zaczynając od /**
i kończąc na */
. Pozwala on na
dodawanie opisów klas, metod itp. A także wylistować złożone typy za pomocą tak zwanych adnotacji @var
,
@param
i @return
. Typy te są następnie wykorzystywane przez narzędzia do statycznej analizy kodu, ale
nie są sprawdzane przez sam PHP.
Porównanie i tożsamość
W PHP można porównywać obiekty na dwa sposoby:
- Porównanie wartości
==
: Sprawdza, czy obiekty są tej samej klasy i mają te same wartości we właściwościach. - Tożsamość
===
: Sprawdza, czy jest to ta sama instancja obiektu.
Operator instanceof
Operator instanceof
pozwala określić, czy dany obiekt jest instancją określonej klasy, potomkiem tej klasy lub
czy implementuje określony interfejs.
Wyobraźmy sobie, że mamy klasę Person
i inną klasę Student
, która jest potomkiem klasy
Person
:
Z danych wyjściowych wynika, że obiekt $student
jest uważany za instancję zarówno klasy
Student
, jak i Person
.
Płynne interfejsy
“Fluent Interface” to technika w OOP, która pozwala na łączenie metod w jednym wywołaniu. Często upraszcza to i wyjaśnia kod.
Kluczowym elementem płynnego interfejsu jest to, że każda metoda w łańcuchu zwraca odniesienie do bieżącego obiektu.
Osiąga się to za pomocą return $this;
na końcu metody. Ten styl programowania jest często kojarzony z metodami
zwanymi “setterami”, które ustawiają wartości właściwości obiektu.
Zobaczmy, jak mógłby wyglądać płynny interfejs do wysyłania wiadomości e-mail:
W tym przykładzie metody setFrom()
, setRecipient()
i setMessage()
są używane do
ustawiania odpowiednich wartości (nadawca, odbiorca, treść wiadomości). Po ustawieniu każdej z tych wartości, metody
zwracają bieżący obiekt ($email
), pozwalając nam na łańcuchowanie kolejnej metody po nim. Na koniec wywołujemy
metodę send()
, która faktycznie wysyła wiadomość e-mail.
Dzięki płynnym interfejsom możemy pisać kod, który jest intuicyjny i czytelny.
Kopiowanie z clone
W PHP możemy utworzyć kopię obiektu za pomocą operatora clone
. W ten sposób otrzymujemy nową instancję
o identycznej zawartości.
Jeśli podczas kopiowania obiektu musimy zmodyfikować niektóre z jego właściwości, możemy zdefiniować specjalną
metodę __clone()
w klasie. Metoda ta jest automatycznie wywoływana, gdy obiekt jest klonowany.
W tym przykładzie mamy klasę Sheep
z jedną właściwością $name
. Kiedy klonujemy instancję tej
klasy, metoda __clone()
zapewnia, że nazwa sklonowanej owcy otrzyma przedrostek “Clone of”.
Cechy
Traity w PHP są narzędziem, które pozwala na współdzielenie metod, właściwości i stałych pomiędzy klasami i zapobiega duplikacji kodu. Można o nich myśleć jak o mechanizmie “kopiuj-wklej” (Ctrl-C i Ctrl-V), w którym zawartość cechy jest “wklejana” do klas. Pozwala to na ponowne wykorzystanie kodu bez konieczności tworzenia skomplikowanych hierarchii klas.
Przyjrzyjmy się prostemu przykładowi wykorzystania cech w PHP:
W tym przykładzie mamy cechę o nazwie Honking
, która zawiera jedną metodę honk()
. Następnie
mamy dwie klasy: Car
i Truck
, z których obie używają cechy Honking
. W rezultacie obie
klasy “mają” metodę honk()
i możemy ją wywołać na obiektach obu klas.
Cechy pozwalają na łatwe i efektywne współdzielenie kodu pomiędzy klasami. Nie wchodzą one w hierarchię dziedziczenia,
tzn. $car instanceof Honking
zwróci false
.
Wyjątki
Wyjątki w OOP pozwalają nam z wdziękiem obsługiwać błędy i nieoczekiwane sytuacje w naszym kodzie. Są to obiekty, które przenoszą informacje o błędzie lub nietypowej sytuacji.
W PHP mamy wbudowaną klasę Exception
, która służy jako podstawa dla wszystkich wyjątków. Ma ona kilka
metod, które pozwalają nam uzyskać więcej informacji o wyjątku, takich jak komunikat o błędzie, plik i linia, w której
wystąpił błąd itp.
Gdy w kodzie wystąpi błąd, możemy “rzucić” wyjątek za pomocą słowa kluczowego throw
.
Gdy funkcja division()
otrzyma null jako drugi argument, rzuca wyjątek z komunikatem o błędzie
'Division by zero!'
. Aby zapobiec awarii programu, gdy wyjątek zostanie rzucony, uwięzimy go w bloku
try/catch
:
Kod, który może rzucić wyjątek, jest zawijany w blok try
. Jeśli wyjątek zostanie rzucony, wykonanie kodu
przenosi się do bloku catch
, gdzie możemy obsłużyć wyjątek (np. napisać komunikat o błędzie).
Po blokach try
i catch
możemy dodać opcjonalny blok finally
, który jest zawsze
wykonywany niezależnie od tego, czy wyjątek został rzucony, czy nie (nawet jeśli użyjemy return
,
break
lub continue
w bloku try
lub catch
):
Możemy również tworzyć własne klasy wyjątków (hierarchie), które dziedziczą po klasie Exception. Jako przykład rozważmy prostą aplikację bankową, która umożliwia wpłaty i wypłaty:
Można określić wiele bloków catch
dla pojedynczego bloku try
, jeśli spodziewane są różne typy
wyjątków.
W tym przykładzie ważne jest, aby zwrócić uwagę na kolejność bloków catch
. Ponieważ wszystkie wyjątki
dziedziczą z BankingException
, gdybyśmy mieli ten blok jako pierwszy, wszystkie wyjątki zostałyby w nim
przechwycone, a kod nie dotarłby do kolejnych bloków catch
. Dlatego ważne jest, aby bardziej specyficzne wyjątki
(tj. te, które dziedziczą po innych) znajdowały się wyżej w kolejności bloków catch
niż ich wyjątki
nadrzędne.
Iteracje
W PHP można przechodzić przez obiekty za pomocą pętli foreach
, podobnie jak przez tablice. Aby to działało,
obiekt musi implementować specjalny interfejs.
Pierwszą opcją jest zaimplementowanie interfejsu Iterator
, który ma metody current()
zwracające
bieżącą wartość, key()
zwracające klucz, next()
przechodzące do następnej wartości,
rewind()
przechodzące do początku i valid()
sprawdzające, czy jesteśmy już na końcu.
Inną opcją jest zaimplementowanie interfejsu IteratorAggregate
, który ma tylko jedną metodę
getIterator()
. Zwraca ona albo obiekt zastępczy, który zapewni przechodzenie, albo może być generatorem, który
jest specjalną funkcją, która używa yield
do sekwencyjnego zwracania kluczy i wartości:
Najlepsze praktyki
Po opanowaniu podstawowych zasad programowania obiektowego ważne jest, aby skupić się na najlepszych praktykach OOP. Pomogą ci one pisać kod, który jest nie tylko funkcjonalny, ale także czytelny, zrozumiały i łatwy w utrzymaniu.
- Separation of Concerns: Każda klasa powinna mieć jasno określoną odpowiedzialność i powinna zajmować się tylko jednym podstawowym zadaniem. Jeśli klasa robi zbyt wiele rzeczy, może być właściwe podzielenie jej na mniejsze, wyspecjalizowane klasy.
- Ekapsułkowanie: Dane i metody powinny być jak najbardziej ukryte i dostępne tylko poprzez zdefiniowany interfejs. Pozwala to na zmianę wewnętrznej implementacji klasy bez wpływu na resztę kodu.
- Wstrzykiwanie zależności: Zamiast tworzyć zależności bezpośrednio w klasie, należy “wstrzykiwać” je z zewnątrz. Aby lepiej zrozumieć tę zasadę, zalecamy zapoznanie się z rozdziałami dotyczącymi wstrzykiwania zależności.