Alternatywny wstęp do programowania obiektowego.

Programowanie zorientowane obiektowo (ang. Object oriented programming, OOP) jest dominującym obecnie paradygmatem w programowaniu, a jednocześnie dość niezrozumianym (to jest opinia samego twórcy pojęcia). W popularnych językach (C++, Java, Python), na ogół kładzie się nacisk na inne jego aspekty, co bardzo odczułem programując w Common Lispie, który reprezentuje inne podejście, bliższe oryginalnej idei.

Do napisania tego tekstu zainspirowało mnie odkrycie, że w Perlu można stosować ten drugi model (choć większość przykładów jakie można znaleźć w sieci próbuje naginać język by przypominał ten pierwszy model). Dlatego też użyję go w tym tekście, jako język względnie strawny dla większości programistów.

Spis treści:

  1. Dlaczego stosujemy OOP?
  2. Metody
  3. Konstruktory
  4. Polimorfizm
  5. Interfejsy
  6. Enkapsulacja
  7. Dziedziczenie
  8. Podsumowując

Dlaczego stosujemy OOP?

Jednym z najważniejszych problemów, z jakimi borykamy się w programowaniu jest złożoność, a konkretnie złożona sieć zależności między różnymi fragmentami kodu. Nasze umysły są tak złożone, że możemy jednocześnie pamiętać o co najwyżej kilku rzeczach na raz (podobno 7). Jeśli zmieniając linijkę w programie, zbyt wiele innych części programu od nich zależy, zaczynamy się gubić.

Dlatego, we współczesnym programowaniu (może z wyjątkiem krótkich programów i skryptów) dążymy do tego, żeby tworzyć możliwie małe kawałki kodu, których działanie można przewidzieć bez analizowania innych.

Zdecydowanie najbardziej restrykcyjnym w tej kwestii jest programowanie funkcyjne (ang. functional programming, FP), które zakłada, że takim kawałkiem będzie funkcja czysta (ang. pure function). Taka funkcja wyłącznie zwraca wartość wyłącznie na podstawie argumentów do niej przekazanych. Wówczas, wywołanie funkcji zawiera w sobie wszystkie informacje potrzebne do określenia wyniku.

OOP natomiast zakłada trochę większe kawałki, zwane klasami. Klasa jest typem, a zatem określa możliwe wartości, przeważnie są to struktury złożone z wartości zwanych polami (ang. fields, w Common Lispie slots). Jednocześnie, razem z klasą dostarczamy metody pozwalające traktować obiekt danej klasy jako jednostkę, bez potrzeby wnikania w jej wewnętrzny stan. Wynik jest zatem bardzo podobny, implementacja klasy jest odseparowana od reszty. Jednak na zewnątrz istotne jest nie tyle pojedyncze wywołanie metody, lecz cały ciąg wywołań metod opisujących pewien proces. Musimy być pewni, że taki proces dostał przewidziany przez implementację klasy.

Jest to nieco bardziej skomplikowane, ale często bardziej naturalne, bardziej zgodne z naszym językiem naturalnym. Lubimy nazywać i opisywać przedmioty, bo w odróżnieniu od działań i zdarzeń, są mniej ulotne. Możemy zrobić im zdjęcie, przeanalizować z części z jakich się składa, w jakie interakcje ze sobą wchodzą. Często wręcz traktujemy procesy jak przedmioty i mówimy jak o przedmiotach, zaniedbując ich nieuniknioną zmienność.

Historia, państwo, społeczeństwo, nawet człowiek, zdrowie i choroba są tak naprawdę procesami, bezustannie się zmieniają, są tak naprawdę zbiorami różnych elementów, wchodzących w określone, interakcje ze sobą. Z drugiej strony, w odpowiednio długiej i szerokiej perspektywie, różne warianty uśredniają się i tworzą wrażenie stałości i traktując je jako jedną całość zyskujemy zupełnie nową perspektywę. Zaniedbując wewnętrzne szczegóły zwalniamy nasze mentalne zasoby, mogąc uchwycić dodatkowe zależności.

Takie postrzeganie ma jeszcze jedną konsekwencję, obserwujemy podobieństwa między różnymi procesami i relacjami, które zachodzą na różnych przedmiotach, nieraz na bardzo różnych poziomach abstrakcji. Następne słowo, następny w kolejce, następny krok; analizujemy program, partię szachów, dane statystyczne, wyniki badań. Jednym słowem wskazujemy na podobieństwo różnych procesów, zachodzących między zupełnie innymi elementami, przy użyciu zupełnie innych środków. I to ma swoje odzwierciedlenie w OOP, w postaci metod, które nie operują na obiektach jednej klasy, lecz na obiektach klas implementujących podobny proces (ten sam interfejs, mówiąc językiem OOP).

Metody

W popularnych językach, klasy są w centrum uwagi — klasy mają metody, metody są zapisane wewnątrz klas i nigdzie indziej. Jeśli jednak pomyślimy, to przecież wypisanie listy i wypisanie nagłówków pliku jest dla nas tym tym samym, bo służą temu samemu, a przecież to właśnie cel nas w tym wszystkim interesuje. Dlatego też, w drugim modelu OOP, metody istnieją obok klas, raczej wszystkie metody o tej samej nazwie i argumentach tworzą całość (w Common Lispie nazywane funkcjami uogólnionymi, ang. generic functions). Precyzują jak dane działanie wykonać na danym typie danych. Często jednak nawet nie przynależą klasie i nie mają do niej dostępu na specjalnych prawach (Perl, Common Lisp, w odróżnieniu od Javy, C++, ale też SmallTalk).

W Perlu, uproszczenie idzie jeszcze dalej, bo nawet nie musimy definiować wzorca klasy, na podstawie którego tworzone są obiekty. Wystarczy powiedzieć, że referencja należy do danej klasy:

sub Product::str {
    my ($self) = @_;

    return "$self->{qty} $self->{what} za $self->{price}";
}

my $apples = bless { qty => "2kg", what => "jabłek", price => "6zł" },
                   'Product';

print $apples->str();

# 2kg jabłek za 6zł

Dla nieobeznanych z Perlem kilka objaśnień. sub Product::str — definicja funkcji str() w przestrzeni nazw Product. Bardziej powszechnym sposobem jest oznaczenie przestrzeni nazw dyrektywą package. Jednak preferuję taki zapis, bo sprawia, że kawałek kodu jest mniej kontekstowy.

Funkcje w Perlu nie mają list argumentów przy nazwie, wszystkie argumenty znajdują się w zmiennej @_ (@ przed nazwą oznacza tablicę). Zapis ($self) = @_ zapisuje pierwszy element tablicy w zmiennej $self ($ oznacza pojedynczą wartość — liczbę, napis, referencję — perlowym żargonem mówimy skalar). Słowo kluczowe my oznacza, że zmienna $self jest lokalna.

Jako, że jest to metoda, pierwszy parametr to referencja do obiektu na którym wykonujemy metodę. Ten obiekt, jest, jak to zwykle bywa w Perlu, referencją na tablicę asocjacyjną (ang. hash, będę nazywał je dalej słownikami), jednak teoretycznie, może to być dowolna referencja, nawet referencja na funkcję.

Jak widzimy, wewnątrz napisu umieszczamy kolejne pola. Operator -> oznacza tu dereferencję, a napis w klamrach, pobranie klucza ze słownika. Połączone razem zwracamy.

Dalej tworzymy właściwy obiekt, poprzez stworzenie rzeczonego słownika: jako, że użyta jest klamra zamiast zwykłego nawiasu, jako parametr dla funkcji bless będzie już referencją. Funkcja ta zmienia typ referencji. Gdy mamy taką poświęconą (ang. blessed) referencję możemy użyć konstrukcji $apples->str(), wówczas będzie użyta funkcja o danej nazwie z przestrzeni nazw odpowiadającej nazwie jaką referencja została poświęcona.

Konstruktory

Jednak bardziej typowym rozwiązaniem dla tworzenie obiektów są typowe dla OOP konstruktory:

sub Product::new {
    my ($class, $qty, $what, $price) = @_;

    my $self = { qty => $qty,
                 what => $what,
                 price => $price };

    bless $self => $class;
    return $self;
}

Wówczas tworzymy obiekt tak:

my $apples = new Product("2kg", "jabłek", "6zł");

Zamiast new może się pojawić tutaj dowolna inna nazwa, też powinno zadziałać, ale może być mniej zrozumiałe.

Konstruktory zwykle wyglądają bardzo podobnie i definiują podstawową strukturę obiektów. Jest to dość niecodzienne, bo w większości języków istnieje na to specjalna składnia (ze znanych mi języków, chyba tylko javascript stosuje podobne rozwiązanie (przynajmniej kiedyś tak było, w nowszych wersjach jest składnia ze słowem kluczowym class; w przypadku Perla, dopiero wersja 6 coś takiego wprowadza, ale Perl6 to zasadniczo inny język).

Na przykład w Javie, taka klasa wyglądałaby tak:

public class Product {
    public Product (String qty, String what, String price) {
        this.qty = qty;
        this.what = what;
        this.price = price;
    }

    public String str() {
        return this.qty + " " + this.what + " za " + this.qty;
    }


    private String qty;
    private String what;
    private String price;
}

Definicja musi zawierać od razu wszystkie metody, i pola, wszystkie typy muszą być podane explicite. Trochę inne programowanie. Sztywniejsze, pozwalające kompilatorowi wyłapać więcej błędów, ale choćby do prototypowania składnia Perla jest znacznie przyjaźniejsza. Osobiście niezbyt lubię płacić ekspresywnością i zwięzłością za pewność kodu.

Swoją drogą, Perl jest na tyle ekspresyjnym językiem, że gdybyśmy chcieli, moglibyśmy napisać w nim moduł tworzący klasy w taki bardzo uporządkowany sposób i sprawdzać wszystkie typy automatycznie.

Polimorfizm

Jak już wspomniałem, metody różnią się od funkcji tym, że określają ogólny logiczny cel, który możemy chcieć osiągać na różnych typach obiektów. Fakt, że możemy napisać kod, który nie polega na konkretnym typie danych, tylko na tym, że wspiera pewne określone metody, nazywamy w żargonie OOP, polimorfizmem. Ilustruje to poniższy przykład:

sub Product::new {
    my ($class, $qty, $what, $price) = @_;

    my $self = { qty => $qty,
                 what => $what,
                 price => $price };

    bless $self => $class;
    return $self;
}

sub Income::new {
    my ($class, $qty, $forWhat) = @_;

    my $self = {
        qty => $qty,
        forWhat => $forWhat
    };

    bless $self => $class;
    return $self;
}

sub Product::str {
    my ($self) = @_;

    return "$self->{qty} $self->{what} za $self->{price}";
}

sub Income::str {
    my ($self) = @_;

    return "zarabiam $self->{qty} $self->{forWhat}";
}

sub nominal {
    my ($wartosc) = @_;

    $wartosc =~ m/^(\d+)/;

    return $1;
}

sub Product::balans {
    my ($self) = @_;

    return -nominal($self->{price});
}

sub Income::balans {
    my ($self) = @_;

    return nominal($self->{qty});
}

sub Budget::str {
    my ($self) = @_;

    my $bilans = 0;
    return join("\n", map { $bilans += $_->balans();
                            $_->str() }
                          @$self)
              . "\n  w sumie: ${bilans}zł\n";
}

my $budget = bless [ (new Product qw(2kg jabłek 6zł)),
                     (new Product qw(1l wody 2zł)),
                     new Income qw(20000zł pensji) ],
                   'Budget';

print $budget->str();

# 2kg jabłek za 6zł
# 1l wody za 2zł
# zarabiam 20000zł pensji
#   w sumie: 19992zł

Do typu Product, dochodzi Dochód. Dla obu implementujemy metodę str() i bilans() podający liczbę, która, będzie dodana do salda (odpowiednio ujemna i dodatnia). Oba typy mogą wymiennie pojawić się w tablicy poświęconej nazwą Budget. Ona nie ma pojęcia jakie typy będzie przechowywać, interesują ją tylko metody str() i bilans(), odpowiednia implementacja zostanie dobrana automatycznie na podstawie typu referencji.

Warto zauważyć ciche założenie, że ceny są zawsze w złotówkach. Oczywiście nie musi tak być. Gdyby okazało się, że tak, należałby stworzyć nowy typ dla ceny, która uwzględniałaby waluty, to jak są zapisywane, itp. Z drugiej strony, zależało mi na prostocie przykładu, który nie wymaga tego. To bardzo istotne, lepiej być raczej leniwym, jeśli chodzi o implementację szczególnych przypadków. Raczej nauczyć się pisać łatwo rozszerzalny kod i dodawać nowe przypadki później. Tak jest po prostu łatwiej, ze względów już wspomnianych — jako ludzie słabo sobie radzimy ze zbyt dużą ilością współistniejących zależności.

Zwracam też uwagę na to, że raczej grupuję implementacje jednej metody, nie zaś implementacje metod jednej klasy, co wymusza większość popularnych języków. Odkąd miałem przyjemność programować obiektowo w Common Lispie, robię tak, jeśli pozwala mi na to język. Tak jest intuicyjniej i łatwiej przeglądać kod, bo często różne implementacje tej samej metody są częścią tego samego procesu, a zatem chcę je edytować razem.

Pojawia się tu kilka perlowych smaczków, które powinienem objaśnić. Tym razem, oddzieliłem parametry funkcji bless operatorem => (używany również w definicjach słowników). Działa on tak samo, poza tym, że pozwala, żeby poprzedzający element był gołym słowem (ang. bareword), który zostanie automatycznie przełożony na napis. W tym wypadku, użyłem go ze względu na to, że wyraża przynależność $self do typu $class, a nie, że $self i $class tworzą zbiór.

Funkcja nominal używa wyrażenia regularnego dopasowującego liczbę na początku ciągu. Nawiasy oznaczają grupowanie, czyli fragment, który może być pobrany przez specjalną zmienną $1 (kolejne $2, $3, itd.). Tę wartość więc zwracamy.

W metodzie Budget::str() używamy funkcji map, popularnej w programowaniu funkcyjnym. Warto zaznaczyć, że bieżąca wartość w podanym jej bloku jest w zmiennej $_ (tzw. wartość domyślna, pojawiająca się tu i ówdzie. @$self oznacza pobranie tablicy przez referencję $self.

W definicji obiektu $budget pojawia się operator qw(), który działa jak cudzysłów, tylko że wykonuje jeszcze dzieli wnętrze w miejscach odstępów i zwraca tablicę z poszczególnych elementów. To pozwala oszczędzić sporo cudzysłowów w kodzie.

Również zastanawiać mogą nawiasy dookoła wywołań konstruktorów. Są one z tego względu, że jak mogliście zauważyć, nawiasy dookoła listy argumentów są opcjonalne w Perlu (patrz funkcje print i bless. Bez tego nawiasu, dalszy ciąg byłby zinterpretowany jako kolejny argument. Z tego względu, przy tworzeniu obiektu Income nawias już nie jest potrzebny.

Bardzo łatwo tu o błąd i cóż mogę rzec, luźne traktowanie list argumentów w Perlu bywa użyteczne, czasem jest bardzo wygodne, a czasem przysparza problemów. Oczywiście mógłbym napisać die "Wrong argument list: @_" unless @_ == 4; w konstruktorze, ale komu by się chciało. Z czasem człowiek się przyzwyczaja do takich pułapek i szybko dochodzi o co chodzi. Swoją drogą, w tej linijce są jeszcze dwa perlowe smaczki. Umieszczenie if/unless po wyrażeniu oraz użycie tablicy w kontekście skalara, wówczas oznacza on rozmiar tablicy.

Interfejsy

Wspominałem już o podziale na języki akcentujące swobodę (jak Perl czy Lisp) i akcentujące spójność i porządek (jak Java czy C++). Związane jest to z rodzajem typowania. W tych pierwszych mamy typowanie dynamiczne (typy są sprawdzane w czasie działania programu), w drugim jest typowanie statyczne (czyli typy są sprawdzane już w czasie kompilacji).

W językach statycznie typowanych, metoda oznacza konkretną implementację, która jest związana z klasą. Z tego względu już na etapie kompilacji musi być jasne, że obiekt ma daną metodę. Z tego względu muszą istnieć czysto abstrakcyjne typy, określające wymagania co do istnienia konkretnych metod. Takie typy nazywamy interfejsami. Zatem, gdybyśmy pisali ten kod w Javie, musielibyśmy zacząć od zdefiniowania interfejsu:

public interface Financial {
    public String str();
    public int bilans();
}

Definicje klas Product i Income musiałyby informować, że go implementują:

public class Product implements Financial {
    // ...
}

public class Income implements Financial {
    // ...
}

I wówczas to Financial jest nazwą typu na którym operuje Budget:

public class Budget {
    public Budget (List<Financial> elements) {
        this.elements = elements;
    }

    public String str() {
        // ...
    }

    private List<Financial> elements;
}

Więcej pisania, każda publiczna klasa i interfejs musi być w oddzielnym pliku. Duża cena za spójność. Jak już chcecie statycznego typowania, lepiej użyć Scali, bo znacznie redukuje programistyczną "biurokrację", którą kompilator może sam sobie dopowiedzieć. Używam jednak Javy ze względu na jej powszechność.

Czasem jednak możemy chcieć użyć tego typu konstrukcji, np. żeby upewnić się, że nie zaszła zbieżność nazw, albo żeby kod był bardziej zrozumiały. Można to łatwo zrobić przy pomocy funkcji UNIVERSAL::isa, sprawdzającej typ referencji:

print UNIVERSAL::isa(sub { print 42; }, "CODE") . "\n";            # 1
print UNIVERSAL::isa({32 => 42}, "HASH") . "\n";                   # 1
print UNIVERSAL::isa([32], 'ARRAY') . "\n";                        # 1
print UNIVERSAL::isa((bless {foo => 42} => 'Moo'), 'HASH') . "\n"; # 1
print UNIVERSAL::isa(4, 'HASH');                                   # ''

Funkcja wspiera dziedziczenie, o którym w dalszej części.

@Foo::ISA = qw(Bar); # Foo dziedziczy po Bar
@Bar::ISA = qw(Baz); # Bar dziedziczy po Baz

my $zz = bless {} => 'Foo';
print UNIVERSAL::isa($zz, 'Baz'); #1

Przestrzeń nazw UNIVERSAL oznacza, że funkcja jest metodą dla każdej klasy, więc jesteśmy pewni, że posługujemy się poświęconą referencją, możemy zrobić tak:

my $x = bless {} => "Fooo";
print $x->isa("HASH"); # 1

Niestety, jeśli spróbujemy użyć niepoświęconej referencji, spowoduje to błąd, więc prawdopodobnie wygodnie skrócić tę nazwę:

*isa = \&UNIVERSAL::isa;
print isa($zz, 'Bar');

Możemy więc po prostu deklarować implementację interfejsu poprzez mechanizm ISA. Jeśli zależy nam na tym, żeby się upewnić zawczasu, że interfejs naprawdę jest zaimplementowany (np. spodziewamy się, że ewentualny błąd wyszedłby po długim czasie obliczeń), możemy zrobić np. tak:

{
    my %ifs = ();

    sub interface {
        my($name, @methods) = @_;

        $ifs{$name} = \@methods;
    }

    sub implements {
        my ($class, $interface) = @_;
        die "Unknown interface '$interface'"
            unless (defined($ifs{$interface}));

        for my $method (@{$ifs{$interface}}) {
            die "class '$class' should implement method '$method'"
                unless defined &{"${class}::$method"};
        }

        push @{"${class}::ISA"}, $interface;
    }
}

interface("Foo", "doo");

sub Bar::new {
    my ($class) = @_;
    return bless {} => $class;
};

sub Bar::doo {
    print "yay";
}

implements("Bar", "Foo");

print new Bar()->isa("Foo"); # 1

implements("zoo", "Foo");

Jak widać, Perl jest bardzo elastyczny, pozwala zaimplementować wiele ciekawych koncepcji. Jest w tym bardzo podobny do Lispa. A przecież nie dzieją się tu właściwie żadne szczególne czary, poza tym, że odnosimy się do tablicy lub funkcji za pomocą stringa z jego nazwą. Tak więc, jeśli jest ochota, możemy zaimplementować bardziej restrykcyjne typowanie, nawet bardziej wyrafinowane niż te, które są zaimplementowane na sztywno w popularnych językach.

Enkapsulacja

Wspomniałem już o tym, że OOP zakłada odrębność klas od siebie, brak ingerencji w wewnętrzny stan obiektu (jego pól) z zewnątrz. W programowaniu obiektowym typu pierwszego nalega się, że powinno się wymuszać taką separację poprzez zakresy widzialności pól i metod, odpowiadają za nie słowa kluczowe private i public w powyższych przykładach. Puryści nalegają, że pola zawsze powinny być private, a metody mogą być public jeśli są przeznaczone do wywoływania z zewnątrz. Jeśli pole ma być dostępne z zewnątrz, należy tworzyć metody dostępu (tzw. gettery i settery). Taką cechę kodu nazywamy enkapsulacją.

Takie działanie ma swoje mocne uzasadnienie, gdy zmiana wartości pola wymaga dalszych działań, żeby utrzymać spójność obiektu (np. gdybyśmy mieli klasę obudowującą uchwyt pliku, zmiana wartości pozycji, powinna iść w parze z wykonaniem operacji seek() na uchwycie), albo przy ustawianiu wartości pola wymagana jest dalsza walidacja.

Nie zawsze tak jest i dlatego nie wszyscy programiści traktują to bardzo poważnie. Również architekci języków takich jak Perl czy Common Lisp nie uznali tego za priorytet, żeby w języku była specjalna składnia na wyrażenie pełnej enkapsulacji, raczej zakładając, że świadomy programista po prostu nie będzie używał pól, jeśli nie wie co robi.

Tym niemniej, jeśli nam zależy, możemy zaimplementować enkapsulację w Perlu w następujący sposób:

{
    my %fieldVal = ();

    sub Class::new {
        my ($self, $secret) = @_;

        $fieldVal{$self} = $secret;

        # ...
    }

    sub Class::useSecret {
        my ($self) = @_;

        my $secret = $fieldVal{$self};

        #  ...
    }
}

W takim wypadku, tylko metody new i useSecret mają dostęp do takiego pola, bo tablica %fieldVal jest lokalna dla bloku, w którym znajdują się tylko te metody.

Co ciekawe, Perl pozwala rozwiązać problem w inny sposób. Mianowicie, pozwala stworzyć zmienną, która jest jedynie interfejsem posiadającym głębszą logikę, a zatem również sprawić, że zmiana jednego pola w klasie automatycznie wyzwala inne działania.

sub Multiplication::new {
    my ($class, $base, $multiplier) = @_;

    my %dane = (
        base => $base,
        multiplier => $multiplier,
        product => $base * $multiplier
    );

    tie %self, MultiplicationIF, %dane;
    return bless \%self => Multiplication;
}

sub MultiplicationIF::TIEHASH {
    my ($pkg, %vals) = @_;

    my $self = \%vals;
    bless $self => $pkg;
    return $self;
}

sub MultiplicationIF::FETCH {
    my ($self, $klucz) = @_;

    return $self->{$klucz};
}

sub MultiplicationIF::STORE {
    my ($self, $klucz, $wartosc) = @_;

    die "Nie można zmienić iloczynu!"
        if $klucz eq "product";

    $self->{$klucz} = $wartosc;
    $self->{product} = $self->{base} * $self->{multiplier};
}

my $m = new Multiplication(3,4);
print "$m->{product}\n";
# 12

$m->{base} = 4;
print "$m->{product}\n";
# 16

$m->{product} = 16;
# Nie można zmienić iloczynu! at - line 31.

Dziedziczenie

Dochodzimy wreszcie do pojęcia, które, zwłaszcza w starszych tekstach, uznawane jest za kluczową cechę OOP — dziedziczenie. Otóż, czasem chcemy stworzyć klasę na podstawie innej, rozszerzyć ją nowe.

Gdybyśmy chcieli korzystać z klasy Income również w kontekście podatków i innych danin na rzecz państwa, moglibyśmy chcieć stworzyć klasę pochodną, która by to uwzględniała:

@Taxed::ISA = qw(Income);

sub Taxed::new {
    my ($class, $base, $nazwa, $tax, $zus) = @_;

    my $self = new Income($base, $nazwa);
    $self->{tax} = $tax;
    $self->{zus} = $zus;
    $self->{qty} = ($self->{qty} - $self->{zus}) * (1.0-$tax);

    bless $self => $class;
    return $self;
}

my $income = new Taxed qw(20000zł pensję 0.18 2200);
print $income->str() . ", brutto ($income->{tax} podatku, $income->{zus} zus
u)\n";

# zarabiam 14596 za pensję, brutto (0.18 podatku, 2200 zusu)

Jest to trochę naciągany przykład. Gdybym pisał program, który już uwzględnia podatki, raczej zaktualizowałbym funkcję bazową, bo skoro już podatki są w dziedzinie zainteresowań to są one wszędzie. W szczególności, gdybym chciał z obiektu Budget zsumować ilość zapłaconych danin, dziwne byłoby pisać coś w stylu:

grep { ref($_) eq 'Taxed' } @$self

już chyba wygodniej umieścić podatek wartości 0%. W większości przypadków dziedziczenie nie jest najlepszym pomysłem. Znacznie lepiej jest napisać klasę, która zawiera w sobie sposób na rozszerzenie bez potrzeby dziedziczenia.

Tym bardziej w takim języku, jak Perl, w którym nic nie stoi na przeszkodzie, żeby dopisać nowe pola i metody do istniejącej klasy, mało jest powodów, żeby z tego mechanizmu korzystać.

Podsumowując

OOP jest jednym ze sposobów wydzielania złożoności programu, zarządzania ją i opakowywania tak, żeby rozwiązania były łatwe do użycia ponownie gdy zajdzie taka potrzeba. Jest w tym zupełnie podobna do programowania funkcyjnego, jednak nakłada znacznie mniejsze ograniczenia. Jednak jedno drugiego nie wyklucza, można programować obiektowo z czystymi metodami, popularne ostatnio są niezmienne obiekty (ang. immutable objects), co oznacza, że żadne zmiany wartości nie zachodzą w miejscu, musi być utworzony nowy obiekt ze zmienioną wartością. Możemy również używać programowania obiektowego żeby odseparować kod zawierający skutki uboczne i móc go używać w sposób czysto funkcyjny na zewnątrz.

OOP odzwierciedla nasze ludzkie postrzeganie (podczas gdy FP odzwierciedla bardziej matematyczną dyscyplinę myślenia). Może się okazać łatwiejsze i bardziej naturalne. Z drugiej strony, trzeba zaznaczyć, że samo używanie programowania obiektowego nie rozwiązuje niczego, wręcz może przysporzyć problemów. Bardzo ważne jest, żeby prawidłowo wydzielać klasy — tak żeby nie były za duże (wówczas mamy zbyt wiele współzależności do ogarnięcia), ale jednocześnie mogły być niezależne. To jednak już jest zupełnie odrębny temat, który z resztą jest dobrze opisany w literaturze.

W tym tekście chciałem wyłącznie zaproponować nieco inne patrzenie na OOP, z trochę inaczej rozłożonymi akcentami. Z dużym naciskiem na osiąganie celu, definiowanie abstrakcyjnych procesów zbudowanych metod, które mogą być różnie implementowane. Natomiast z mniejszym naciskiem na zbyt dosłownie rozumianą obiektowość, w której klasa jest centrum uwagi — projektowanie programu zaczyna się od wyszczególnienia klas, ich powiązań i potem dla każdej z osobna określać metody, często w oderwaniu od rzeczywistego procesu, który chcemy zaimplementować.

Spodobało ci się? Pozostańmy w kontakcie:

Email:
Imię:
Wiadomość:

Subskrybując nie tylko będziesz dostawać powiadomienia o nowych odcinkach, ale też krótkie nowości z świata programowania i wpływ na tematykę przyszłych treści. Twój adres i imię będą przechowywane wyłącznie w tym celu.

więcej artykułów…