Klasy w PHP - pierwszy krok do programowania obiektowego

Ziomal

Wstęp

Większość programistów, zaczynając swoją przygodę, tworzy swoje programy jako wielką drabinkę. Najczęściej jeden plik, zawierający instrukcje wykonujące się od góry do dołu, często z duplikacjami. Zauważamy wtedy duplikację pewnych oczywistych powtórzeń - irytuje nas to że musimy pisać pewien kod dwa razy. Odkrywamy wtedy funkcje/procedury, i część naszego kodu umieszczamy w nich. Nie umiemy jednak korzystać z nich odpowiednio, i tworzymy potwory które nadal są proceduralne, nadal mają bardzo skomplikowany flow, a na dodatek, na potrzeby naszych funkcji deklarujemy zmienne globalne, by w jakiś sposób kontrolować stan. Poprzez użycie zmiennych globalnych nasze funkcje stają się mniej podatne na zmianę i ich powtórne użycie, przez co aplikacje potem ciężej zmienić i łatwiej o błąd.

Słyszymy wtedy, żeby dzielić programy na mniejsze kawałki - na mniejsze funkcje. Funkcje wykonują inne funkcje, z kolei tamte wykonują kolejne. Dzielimy cały nasz program na listę małych funkcji. Początkowi programiści nie lubią takiego modelu, ze względu na to że wcześniej mieli "widok na cały program", teraz widzą jego maleńki kawałek, i ciężej im dostrzec pełen obraz. Zapominają jednak o fakcie, że jeśli program ma ponad 3000 linijek (powiedzmy), to już zobaczego jego pełnego obraz ustaje się nie możliwe, więć w gruncie rzeczy nic nie tracimy.

Pojawia się wtedy jednak kolejny problem. Dane. Programy to nic innego jak zamieniajki danych, z jednej formy w inną. Tam gdzie nie ma danych, programy nie mają racji bytu. Każdy program, funkcja, procedura, bierze dane z jednego/kilku miejsc, i wypluwa te dane w innej formie w innych miejscach. Z reguły mamy wielkie pliki i wielkie programy z masą małch funkcji - to dobrze - dużo małych funkcji lepiej komponuje program niż kilka dużych. Funkcje te przerzucają dane pomiędzy sobą - niektóre funkcje modyfikują dane w miejscu, niektóre zwracają inne. Różne funkcje chcą otrzymać różne dane, w różnych formatach. W miare jak aplikacje rosną - w jaki sposób chcemy pamiętać które dane mają wejść do której funkcji w jakim formacie?

Pierwszym, co przychodzi do głowy początkującym programistom to struktury (lub tablice, rekordy, listy, słowniki - zależnie od języka. w PHP byłyby to tablice). Po co przekazywać kilka parametrów int, string lub bool skoro można przekazać tablicę, strukturę rekord która ma te dane w jednym parametrze. Po co przekazywać np zmienne string $username, string $password, int $age. Stwórzymy tablicę, nazwiemy strukturę/tablicę user i już! Jest to owszem pewne usprawnienie, jednak nie do końca wystarczające, dlatego że dane nie mówią za siebie. Jeśli zobaczymy liczbę 1956 - czy wiemy co to jest? year (rok)? mountainHeightInMeters (wysokość góry w metrach)? laptopPrice? Nie wiemy tego. Umieszczamy dane w strukturach, tak by inne funkcje operowały na nich, ale niestety takie podejście to jest jedynie mały krok w stronę dobrego oprogramowania.

Jeśli stworzymy strukturę, która posiada pewne dane, i przekażemy je do kolejnych funkcji - to te funkcje będą musiały przystosować dane które dostały do formatu którego one oczekują. Jeśli mamy zmienną int heightInMeters, i przekażemy ją do funkcji, która operuje na centymetrach, ta funkcja będzie musiała przeprowadzić konwersję metrów na centymatry (czyli po prostu pomnożyć razy 100). Przykład z metrami jest oczywiście prosty, ale istnieją bardziej skomplikowane konwersje, np XML na JSON, lub ASCII do Utf-8.

Mamy wtedy kod, który wygląda bardzo na kod proceduralny. Dostajemy "gołe" dane, i przeprowadzamy na nich operacje.

$metry = ['meters' => 10];

function oblicz(array $m) {
  $cm = $m['meters'] * 100; // zamiana formatu na potrzeby funkcji
}

Jeśli mamy takich funkcji wiele, to informacja o tym że metr to 100 cm jest rozsiana po całym programie. Wtedy zmiana zmiennej $tablica na inną jednostke staje się praktycznie nie możliwa.

Z pomocą mogą nam przyjść klasy. Dzięki klasom, możemy zrobić wiele niesamowitych rzeczy, jedną z nich jest reprezentacja danych w innej formie, np

$metry = new Meters(10); // zamiast $metry = ['meters' => 10];

function oblicz(Meters $m) {
  $cm = $m->cm(): //  zamiana formatu na potrzeby funkcji
}

Zauważ, że funkcja oblicz() "nie wie" jak zamienić metry na centymetry. Dostaje jedynie obiekt, i prosi go $->cm() żeby dał jej już liczbę w centymetrach.

Jeśli klasa miałaby same publiczne pola, np $m->meters, to i tak musielibyśmy napisać $m->meters * 100, więc taka klasa nic by nam nie dała. Funkcja oblicz() nadal musiałby umieć zamienić metry na centymetry. Nie różniłaby się od struktury. Niesamowitą siła klas jest jednak to, dzięki klasą możemy porozmawiać z nią jej językiem, zamiast chamsko wyciągać dane z jej środka i mnożyć liczby.

Tworzenie pierwszego programu

Aby utworzyć jakiś obiekt musimy stworzyć najpierw jego klasę. Klasa jest to plan, mogący zawierać pola i zmienne, które będzie miał nasz obiekt. Każda zmienna w PHP ma typy, tak samo każdy obiekt ma swoja klasę. Definicja klasy wygląda w następujący sposób:

<?php
class nazwaklasy
{
    var $zmienna = "jakas wartosc";

    function nazwafunkcji()
    {
        print $this->zmienna;
    }
}
?>

Definicja klasy zaczyna się od słowa kluczowego class i nazwy klasy. Następnie występują pola i metody. Pole (zwane tez atrybutem) poprzedzona jest słowem kluczowym public, protected, private lub var.

Dostęp do pól i metod wymaga operatora ->. Używa się do tego zapisu $obiekt->pole lub $obiekt->metoda(). Wewnątrz klasy, na aktualny obiekt wskazuje $this.

<?php
class nazwaklasy
{
    var $pole= "jakas wartosc";

    function metoda()
    {
        print $this->pole;
    }
}

$obiekt = new nazwaklasy;
$obiekt->pole = "inna wartosc";
$obiekt->metoda();
?>

Jak widzimy na przykładzie, przy odniesieniu się do pola klasy (zmiennej w klasie) korzystamy z $ tylko raz, na początku.

Konstruktory

Konstruktor to meta-metoda, która jest uruchamiana podczas tworzenia objektu. Deklaruje się go podobnie jak każdą inną meta-metodę, z dwoma podkreślnikami: __construct(). Może posłużyć do nadawania zmiennym początkowej wartości podczas tworzenia objektu lub zapisania parametrów konstruktora do pól.

<?php
class JakasKlasa
{
    var $zmienna = "jakas wartosc";

    function __construct($wartoscpoczatkowa)
    {
        $this->zmienna = $wartoscpoczatkowa;
    }

    function jakasfunkcja()
    {
        print $this->zmienna;
    }
}

Modyfikatory dostępu

Zarówno pola jak i metody w obiekcie mogą mieć różne poziomy dostępu: private, protected, public. Dodatkowo, jako naleciałość z bardzo starych wersji PHP, pola mogą być dodatkowo zadeklarowane z var (co jest tożsame ze słowem public), a metody mogą nie podawać modyfikatora dostępu, co również jest tożsame z public.

<?php
class JakasKlasa
{
    public $publicznePole = "jakas wartosc";
    private $prywatnePole;

    public function __construct($wartoscpoczatkowa)
    {
        $this->zmienna = $wartoscpoczatkowa;
    }

    public function metodaPubliczna() {}

    private function metodaPrywatna() {}
}

Konstruktory również mogą mieć modyfikator, public, private, protected. Najczęściej konstruktory są deklarowane jako public. Konstruktory protected mają zastosowanie w rzadkich przypadkach, kiedy korzystamy z dziedziczenia. Konstruktory private to jeszcze rzadszy przypadek, kiedy chcemy kontrolować interfejs klasy, i tworzyć obiekt jedynie z wewnętrz klasy, np poprzed funkcje statyczne.

Dziedziczenie

Dziedziczenie proces "doklejania" ciała klasy z jednej, do drugiej. Klasa dziecko może dziedziczyć z klasy baza, i ma to taki efekt, że wszystkie pola, metody, jej klasy bazowe, i wszystko co było zadeklarowane w baza, jest też zadeklarowane w baza. Nie należy myśleć o dziedziczeniu jako o "korzystaniu z pól innej klasy", lub "dawanie dostępu jednej klasie do drugiej". Żeby osiągnąć taki efekt, należałoby korzystać z kompozycji. Dziedziczenie to w gruncie rzeczy prymitywny sposób współdzielenia kodu. Nie należy go stosować, chyba że w bardzo konkretnych, uzasadnionych przypadkach.

Jeśli chcesz, możesz znaleźć wiele przykładów o tym jak stosować dziedziczenie, ale pamiętaj że korzystanie z niego, z wyjątkiem nielicznych przykładów, raczej nie pomoże Ci w pisaniu dobrych aplikacji.

Programowanie obiektowe

Ze wzlędu na swoją sugestywną nazwę, wielu programistów mylnie utożsamia korzystanie z klas w swojej aplikacji jako "programowanie obiektowe". Programowanie obiektowe jednak, to bardzo bogaty paradygmat który operuje na wielu innych metodologiach i założeniach (podobnie jak np programowanie funkcyjne). Nikt nie powie, że po prostu deklarując funkcję w naszym kodzie, mamy programowanie funkcyjne. Dlaczego więc dodanie klas do naszego kodu jest mylnie interpretowane jako programowanie obiektowe?

Żeby faktycznie zmienić paradygmat na programowanie obiektowe, należałoby ze swojej aplikacji usunąć wszelkie zmienne globalne, wszelkie metody statyczne, funkcje globalne, posługiwać się interfejsami, separować obiekty na bardzo małe klasy, stosować kompozycję (czyli składać jedne klasy w inne), zasadę pojedynczej odpowiedzialności, nie stosować meta-programmingu, korzystać z enkapsulacji tak by obiekt nie wystawiał swojej implementacji na świat; korzytać z polimorfizmu do kontroli przepływu danych. To są takie podstawowe elementy, które musiałby zawierać program, by można go było nazwać programem obiektowym.

Wielu ludzi stosuje jednak zmienne globalne, funkcje globalne, funkcje statyczne i masę innych elementów ze świata proceduralnego, tłumacząc sobie ich użycie. Proszę bardzo, droga wolna - ale nie będzie to kod obiektowy. To nadal będzie kod proceduralny, tylko że z klasami.

Samo korzystanie z klas w aplikacji to nadal niestety jest programowanie proceduralne.

Jedynym z genialnych przykładów, jest choćby nasz kod z metrami i centymetrami. Wielu programistów chętnie zrobiłoby klasę:

class Size {
  private $meters;

  public function meters() {
    return $meters;
  }
}

Taka klasa jest jednak bezsensowna. Po co mielibyśmy używać $size = new Size(10), i potem $size->meters(), skoro to są te same dane? Wchodzą metry, wychodzą metry. Nie różni się to niczym od struktury/tablicy/zmiennej. Taki kod jest bardzo daleko od programowania obiektowego. Klasa Size jest jedynie pojemnikiem na dane, i nie wnosi nic dobrego do naszej aplikajci.

Natomiast taka klasa:

class Size {
  private $meters;

  public function cm() {
    return $meters * 100;
  }
}

Taka klasa wprowadza już dużą wartość. Funkcje mogą teraz przyjmować klasę Size jako parametr, i mogą z niej odczytać centrymetry.

Niektórzy powiedzieliby wtedy: mam pisać taką klasę, tylko po to żeby przemnożyć liczbę * 100? lub czy mam tworzyć nowe klasy, jak tylko chcę wykonać mnożenie?. I odpowiedź na oba te pytania brzmi: "Nie".

Ta klasa nie jest po to, żeby przemnożyć liczbę razy 100, i nie musisz tworzyć nowych, żeby wykonać więcej mnożeń. Ta klasa jest po to, żeby jedna część Twojej aplikacji mogła "wypluć dane" w metrach (klasę Meters), a druga mogła się posłużyć centymetrami $meters->cm(). Innymi słowy, ta klasa jest po to, żeby konwersja danych z jednego formatu (metry) na inny (centymetry) była w klasie która jest za to odpowiedzialna.

Czytelnik mógłby wtedy zapytać: ale czy nie będę w ten sposób miał miliona małych obiektów? po co mi tyle klas, skoro mogę mieć do tego małe funkcje. Bardzo dobre pytanie! Odpowiedź na nie brzmi "Tak!". Jest to cały sens programowania obiektowego! W programowaniu procedralnym, chowamy w kod w małych procedurach. W programowaniu obiektowym, chowamy kod w małych obiektach.

Podsumowanie

Drogi czytelniku, ten rozdział był wyjątkowo trudny. Nie ze względu na korzystanie z elementu języka (class w PHP), ale ze względu na zmianę podejścia do pewnych elemtnów. Pamiętaj, młody programisto, że to że jakiegoś elementu języka programowania da się do czegoś użyć, to nie znaczy zaraz że się powinno. Klasy wyglądają jak struktury danych do któych dodano metody, nie zapominajmy jednak po co to zrobiliśmy. Po to, żeby mówić do danych ich językiem, a nie po to żeby wyciągać brutalnie dane z ich środka.

Programowanie obiektowe to bardzo złożona dziedzina, ponieważ nie ma jasno określonych granic lub zasad. W programowaniu obiektowym, chodzi głównie o to żeby kontrolę programu (tym czym program jest) budować klasami i obiektami, zamiast ifami i pętlami. A żeby to zrobić, to klasy muszą być odpowiednio "dojrzałe", np zwracać cm() zamiat metrów. Muszą mieć swoje odpowiedzialności, nie mogą być tylko pustymi pojemnikami na zmienne i funkcje.

Przed Tobą jeszcze długa droga, od nauki tego jak stworzyć klasę w PHP, do momentu w którym faktycznie Twoje klasy będą dla Ciebie pomocne. Umiejętność uruchomienia samochodu, to jedynie pierwszy krok do zdania prawa jazdy.

11 komentarzy

Przeciazac konstruktorow nie mozna w PHP, tak samo jak dziedziczyc z wielu klas (ograniczenie jezyka); destruktor to metoda __destruct()

Fajnie, tylko za krótko. Co z przeciążaniem konstruktora, dziedziczeniem z wielu klas, destruktorem??

Ślicznie, nie ma się co czepić. Do Bru2usa: OOP (taka krótka nazwa na programowanie obiektowe) jest ZAWSZE lepsze od proceduralnego, a jeśli już filozofujemy, to jest lepsze wtedy, gdy język programowania wspiera OOP :)

Dorzuciłbym jeszcze przypis: http://pl.php.net/manual/en/language.oop5.php

BTW, właśnie przerabiam pewien portal na OOP / PHP5. Poezja. Wszystko na modułach, a co najlepsze - moduły dadzą się bez przeróbek wykorzystać w innych projektach. Oszczędność "czasu i atłasu" jest nie do zignorowania.

Do Krolika: vide wyżej, dyskusja mogłaby się co najwyżej zrobić, dlaczego php nie jest do końca obiektowe, ale imo to dyskusja nie na temat, bo w zasadzie sprowadza się do kwestii pisać w php czy w czymś innym. Ja tam piszę w php i jego obiektowość mi wystarcza, przynajmniej jeśli brać pod uwagę php5.

Hmm... chyba troszke skromny ten tekst??

Ziomal: są minimalne niedopatrzenia, jednak nie czepiajmy się szczegółów.

  • niepotrzebna spacja (funkcja nazwa funkcji to nie to samo co nazwafunkcji)
  • znak > przy rozpoczynaniu skryptu.

Artykuł oceniam na b. dobry.

Dobrze tłumaczysz :) Aż miło, że tak młody człowiek, potrafi tak dobrze zaprezentować materiał :)

Dobrze, ze nie napisal, dlaczego obiektowe jest lepsze, bo od razu by sie zrobila taka glupia dyskusja jak ostatnio na forum C++ (WinAPI kontra klasy).

kiepska ortografia i fatalna gramatyka, ale może być.

Bardzo fajnie daje 5 :D tylko mogłeś jeszcze napisać czemu pisanie obiektowe jest lepsze od proceduralnego i w jakich przypadkach jest go lepiej stosować :)

Nie zauważyłem żadnych błędów

Cały kod wrzuć w tagi <php>.