Programowanie obiektowe: podstawy

avatar icon
Tomasz Hanc
Programowanie obiektowe znamy już od wielu lat. Wikipedia stwierdza, że od połowy lat 80. programowanie obiektowe uzyskało status techniki dominującej w świecie wytwarzania programowania. Praktycznie każde ogłoszenie o pracę, dotyczące technologii backendowych, zawiera znajomość OOP jako niezbędne wymaganie do ubiegania się o dane stanowisko. Czy programowanie obiektowe to po prostu zamykanie wszystkiego w obiekt? Co w takim obiekcie powinno się znaleźć?

Zacznijmy od podstaw

Zakładam, że każdy wie jaka jest różnicą między obiektem a klasą. Aż takich podstaw to raczej tłumaczyć nie trzeba, jednak drobne przypomnienie może się przydać. Na rozmowach kwalifikacyjnych dość często pada pytanie o podstawowe założenia programowania obiektowego. Mamy cztery takie założenia: abstrakcja, hermetyzacja, polimorfizm i dziedziczenie. Na pierwszy rzut oka wszystkie te założenia są bardzo potrzebne i dzięki nim programowanie obiektowe ma sens. Kiedyś podczas rozmowy rekrutacyjnej usłyszałem pytanie: "Jeżeli miałbyś zrezygnować z jednego z założeń programowania obiektowego to które by to było i dlaczego?" Oczywiście pytanie było czysto hipotetyczne, bo nie będziemy z niczego rezygnować, ale warto się nad tym zastanowić.

Abstrakcja

Abstrakcja to pewnego rodzaju uogólnienie, przedstawiane najczęściej jako klasa abstrakcyjna lub interfejs. Praktycznie wszystkie wzorce projektowe i zasady programowania bazują na abstrakcji. Jeżeli tworzymy zależności w kodzie to chcemy się uzależniać od abstrakcji, a nie od konkretnej implementacji. Usuwając to założenie bardzo ograniczamy sobie możliwości. Abstrakcja jest ważna, dzięki niej możemy zapewnić rozszerzalność naszej aplikacji czy możemy zredukować niepotrzebne zależności.

Hermetyzacja

Znana też jako enkapsulacja, czyli innymi słowy ukrywanie implementacji. Najprostsze zastosowanie tego założenia to deklaracja zmiennych jako prywatnych. Nie chcemy dawać dostępu do szczegółów na zewnątrz. Chcemy mieć obiekty hermetycznie zamknięte, takie, które nie zostaną uszkodzone przez przypadek. Czy da się bez tego żyć? Pewnie tak, trzeba przy tym uważać, ale jest to do zrobienia. Czy jest to rozważne? Z tym bym się już kłócił, chociaż różne biblioteki i tak nas do tego "zmuszają" poprzez wymaganie getterów i setterów do wszystkich pól obiektu.

Polimorfizm

Mechanizm silnie powiązany z abstrakcją. Dzięki niemu interesujemy się tym co ma zostać wykonane, a nie jak. To, jak coś zostanie wykonane zostanie określone dopiero podczas wykonywania kodu. Dzięki polimorfizmowi możemy odwracać zależności. Bez polimorfizmu nie ma miejsca dla abstrakcji. Obecnie bardzo łatwo możemy korzystać z polimorfizmu, kiedyś (język C) podobny efekt uzyskiwało się poprzez wskaźniki na funkcje.

Dziedziczenie

Gdy uczyłem się, czym jest programowanie obiektowe, to dziedziczenie było dla mnie czymś najważniejszym. Kod, który się powtarzał można było upchnąć w klasie nadrzędnej i nie było "duplikacji" kodu. Jednak teraz uważam troszkę inaczej. Dziedziczenie służy nam głównie do rozszerzania zachowania obiektu. Czy istnieje inny sposób, aby tego dokonać? Tak, istnieje, a jest nim kompozycja. Dlatego jeżeli miałbym wskazać jedno z założeń do usunięcia to wybrałbym właśnie dziedziczenie. Szczerze mówiąc to dziedziczenie źle wykorzystane bardziej przeszkadza niż pomaga. Może to być troszkę kontrowersyjne, ale bez dziedziczenia wciąż jesteśmy wstanie programować w pełni obiektowo.

Tutaj chodzi o komunikację

Podczas projektowania nowych aplikacji czy funkcjonalności bardzo często znaczną część czasu poświęcamy na zaplanowanie struktury. Myślimy o tym jakie pola powinna mieć tabela w bazie danych, co potem odzwierciedlamy w kodzie klasy. Przy pomocy nowoczesnych IDE automatycznie generujemy gettery czy settery i kod klasy gotowy. Natomiast bardzo mało czasu poświęcamy na zaplanowanie komunikacji pomiędzy obiektami. A w OOP to nie struktura jest najważniejsza, a właśnie komunikacja. Ważne jest to jak obiekty się ze sobą komunikują. Obiekty mogą wykorzystywać struktury, ale te informacje powinny być ukryte (hermetyzacja). Obiekt powinien posiadać zachowanie, a nie tylko dane. Czym taka anemiczna encja tak naprawdę różni się od tablicy w PHPie czy hashmapy w Javie?

Modyfikatory dostępu a odpowiedzialność

Wszyscy wiemy do czego służą modyfikatory dostępu. A czy zastanawialiśmy się jaką nakładamy na siebie odpowiedzialność wykorzystując np. modyfikator protected zamiast private? Ogólnie rzecz biorąc, modyfikator private, pod względem bezpieczeństwa jest najlepszy. Żadna klasa która dziedziczyłaby po naszej nie ma dostępu do rzeczy prywatnych. Dzięki temu nie musimy się kompletnie o nic martwić, gdy będziemy chcieli zmienić nazwę tej metody, usunąć ją czy podzielić na kilka mniejszych metod. Nie ma praktycznie szans by popsuć inny kod, który korzysta z naszego.

Gdy zejdziemy stopień niżej i nadamy naszej metodzie modyfikator protected to w momencie zmiany czegokolwiek musimy pomyśleć o klasach, które dziedziczą po naszej. Wciąż jest to raczej bezpieczne, ale już trzeba się nad tym bardziej zastanowić. Szczególnie wtedy, gdy udostępniamy nasz kod do powszechnego użytku.

Natomiast nadając modyfikator public, oprócz klas potomnych musimy pamiętać o każdym wywołaniu danego kodu. Dlatego im niższy modyfikator dostępu tym większa odpowiedzialność. I tym bardziej pewni musimy być swojego kodu, bo więcej bytów staje się od niego zależnych. To właśnie wtedy przy zmianach w jednym miejscu psujemy coś w niby kompletnie niezależnym innym miejscu.

DRY, czyli Don't Repeat Yourself

Pamiętam, jak na studiach wiele razy prowadzący zajęcia powtarzali, żeby unikać duplikacji kodu. Bardzo często było to rozwiązywane przez dziedziczenie lub tworzenie klas typu Utils. Ale o co tak naprawdę chodzi z tą duplikacją?

Duplikacja struktury

Załóżmy, że mamy dwa obiekty, które posiadają podobne pola. Czy to oznacza, że powinniśmy stworzyć klasę nadrzędną, która będzie częścią wspólną dla tych obiektów? Czasem może być to wskazane, ale najczęściej będzie to błędne:

class Human
{
    private $name;
    private $dateOfBirth;
}

class Dog
{
    private $name;
    private $dateOfBirth;
}

Przykład przerysowany, ale pamiętajmy, że podobna struktura nie zawsze jest duplikacją.

Duplikacja metody

class Basket
{
    public function addProduct($product)
    {
        if (3 == count($this->products)) {
            throw new Exception("Max 3 products allowed");
        }
        $this->products[] = $product;
    }
}

class Shipment
{
    public function addProduct($product)
    {
        if (3 == count($this->products)) {
            throw new Exception("Max 3 products allowed");
        }
        $this->products[] = $product;
    }
}

A jak z metodami? Niestety, to zależy. Co mówią reguły biznesowe? Czy maksymalna ilość produktów w koszyku i w wysyłce jest od siebie zależna? Czy będzie zmieniana razem? Jeżeli nie, to też nie jest duplikacja (powyższy przykład pochodzi z bloga Mathiasa).

Duplikacja występuje wtedy, kiedy wynika to z reguł biznesowych, kiedy reguły te będą zmieniały się razem. Dotyczy takich sytuacji, w których łatwo byłoby zapomnieć, by dokonać zmiany w dwóch, trzech czy większej ilość miejsc. Taką duplikację musimy usuwać. Najprostszym przykładem takiej duplikacji może być np. sprawdzanie daty w różnych miejscach systemu:

if ($date->diff(new DateTime())->y < 18) {
    throw new NotAdultException();
}

Usunięcie tej duplikacji możemy dokonać poprzez utworzenie obiektu Adult, który będzie walidował wiek użytkownika lub wrapper na obiekt DateTime, który będzie to kontrolował.
Unikałbym rozwiązania, które polega na utworzenie klasy Utils. Tak naprawdę rzadko kiedy tego typu klasy mają sens istnienia. Jednak to jest już materiał na oddzielny artykuł.