The
road to better programming: rozdział 5
Teodor Zlatanov
Programowanie zorientowane obiektowo w Perl'u
(tłum. Michał 'Podles' Podlewski , gniewko_syn_rybaka@wp.pl)
Czym jest
programowanie zorientowane obiektowo (w skrócie OOP, od Object Oriented
Programming)?
OOP jest rodzajem programowania lub ogólniej podejściem do rozwiązywania
problemów. Algorytmy, z drugiej strony, są konkretnymi podejściami do
rozwiązywania konkretnych problemów. OOP jest z natury silnym sposobem,
który dąży do stworzenia opartych na funkcjach i procedurach metod programowania
mniej powiązanych z konkretnym problemem.
Ten artykuł obejmuje podstawy OOP w Perl'u, porównuje je z rozwiązaniami
proceduralnymi i funkcjonalnymi i pokazuje, jak z niego korzystać w
programach i modułach. Proszę jednak pamiętać, że jest to raczej streszczenie
niż szczegółowe wyjaśnienie wszystkich aspektów OOP, ponieważ dogłębne
przedstawienie tego zagadnienia zajęłoby raczej nie pojedyncze strony,
ale całe książki, któ?e w dodatku ostały juz napisane. Po więcej informacji
zajrzyj do bibliografii.
Co
to dokładnie jest OOP?
Jest
to technika rozwiązywania problemów przy użyciu obiektów. Obiekty w
języku programowania to takie twory, których zachowanie i właściwości
są niezbędne aby od ręki rozwiązać problem. Definicja powinna być bardziej
konkretna, jednak nie może ze wzgledu na występującą obecnie niesłychaną
różnorodność podejść do OOP w branży komputerowej.
W kontekście
programowania w Perlu OOP nie jest niezbędne do używania języka. Perl
w wersji 5 i wyższych zachęca do OOP, ale go nie wymaga. Wszystkie biblioteki
Perla są modułami, co oznacza, że używają przynajmniej podstaw OOP.
Co więcej, większość bibliotek Perla jest implementowana jako obiekty,
co oznacza, że wykorzystując je uzytkownik musi stosować sie do ich
związanego z OOP zachowania, własciwości i interfejsu.
Najważniejsze
cechy języka programowania obiektowego.
Geneeralnie
trzy cechy języka programowania są najważniejsze dla programowania obiektowego.
Sa to dziedziczenie, polimorfizm i 'obudowanie' (encapsulation).
Perl
obsułguje dziedziczenie. Występuje ono kiedy jeden obiekt (potomek)
używa drugiego (rodzica) jako punktu startowego, a potem modyfikuje
jego właściwośi i zachowania w razie potrzeby. Ta relacja potomek-rodzic
jest najistotniejsza w OOP, jako że umożliwia tworzenie jednych obiektów
z drugich. Właśnie to ponowne użycie obiektów czyni z OOP ulubioną metodę
programistów.
Istnieją
dwa rodzaje dziedziczenia: pojedyncze i wielokrotne. Pojedyncze dziedziczenie
wymaga od obiektu-potomka posiadania tylko jednego rodzica, natomiast
dziedziczenie wielokrotne jest bardziej liberalne (w programowaniu,
tak jak w życiu posiadanie więcej niż dwojaga rodziców może uczynić
dzieciństwo trudnym, więc nie należy nadużywać wielokrotnego dziedziczenia).
Perl obsługuje dziedziczenie wielokrotne, jednak w praktyce rzadko spotyka
się korzystanie z dwojga lub więcej rodziców.
Polimorfizm
(z greckiego - wielokształtność) jest techniką polegającą na umożliwianiu
obiektowi bycia widzianym jako inny obiekt. Jest to troche złożone,
więc posłużę się przykładem. Powiedzmy, że masz farmę z czterema owcami
(Ovis aries), ale kupiłeś właśnie dwie kozy (Capra hircus) i jednego
owczarka niemieckiego (Canis lupis familiaris). Jak wiele zwierząt posiadasz?
Dodajesz wszystkie owce, kozy i psa - wynik wynosi siedem. Własnie użyłeś
polimorfizmu traktując trzy różne gatunki zwierząt jako jeden typ -
'zwierze' dla potrzeb liczenia.
W Perlu
polimorfizm jest w pełni obsługiwany. Nie jest on jednak używany bardzo
często, ponieważ programiści Perla z reguły wolą zmieniać odzedziczone
zachowania raczej poprzez zmiane właściwości niż poprzez zmiane odziedziczonego
zachowania. To oznacza, że częściej widzi się kod tworzący trzy obiekty
'IO::Socket::INET', jeden dla odbierania i transmisji pakietów UDP na
porcie 234, jeden dla odbierania pakietów TCP na porcie 80 i jeden dla
transmisji pakietów na porcie 1024, niż kod któ?y używa oddzielnie 'IO::Socket::INET::UDPTransceiver'
dla pierwszego przypadku, 'IO::Socket::INET::TCPReceiver' dla drugiego
i 'IO::Socket::TCPTransmitter' dla trzeciego.
Puryści
OOP uważają, że wszystko powinno być właściwie sklasyfikowane, jednak
programisci Perla nie są pod żadnym względem purystami. Traktują zasady
OOP z dystansem, dzięki czemu sa na przyjęciach żródłem lepszej zabawy
niż puryści OOP.
Obudowanie
(encapsulation) odnosi się do takiego definiowania zachowania i własciwości
obiektu aby uczynić go niedostępnym dla użytkowników, dopóki autor obiektu
nie dopuści do takiego dostępu. W ten sposób użytkownicy obiektów nie
mogą robić tego, czego robić nie powinni i nie mają dostępu do danych,
do których nie powinni mieć dostępu. Perl pozwala na obudowanie (encapsulation)
w zwykły, luźny sposób.
Dlaczego
OOP jest silną techniką?
Wracając
do głównego tematu - dlaczego OOP jest silną techniką możemy zobaczyć,
że OOP zawiera kilka kluczowych koncepcji trudnych do połączenia z programowanie
funkcjonalnym (PF) i programowaniem proceduralnym (PP). Po pierwsze,
PP ani PF nie znają koncepcji dziedziczenia ani polimorfizmu klas, ponieważ
nie znają samych klas. Obudowanie (encapsulation) występuje w PP i PF,
ale tylko na poziomie procedury a nigdy jako atrybut klasy czy obiektu.
Dlatego własnie programiści wolą przejść całkiem na OOP niż łączyć ze
sobą niekompatybilne elementy.
Niektórzy
udowadniają, że wszystkie programy są ostatecznie redukowane do proceduralnego
wykonywania instrukcji, więc każdy program OOP, niezależnie jak 'czysto'
jest ono zaimplementowane, zawiera 'proceduralny' kod w swoich funkcjach
(zwanych metodami) i w kodzie tworzącym pierwszy obiekt, wykonujący
resztę zadań. Nawet w języku tak bliskim czystemu OOP jak Java nie można
uniknąć użycia funkcji "main()". Dlatego wydawałoby się, że
OOP jest tylko podzbiorem PP. Jednak taka redukcja OOP nie powinna obchodzić
programisty czy inżyniera oprogramowania bardziej niż instrukcje assemblera
wykonywane dla każdej operacji. Pamiętaj, że programowanie obiektowe
to tylko sposób tworzenia, a nie końcowy efekt pracy.
OOP nie
działa dobrze z programowaniem proceduralnym ponieważ koncentruje się
na obiektach natomiast PP bazuje na procedurach (będziemy w tym artykule
luźno definowali 'procedury' jako funkcje dostępne bez użycia technik
obiektowych natomiast 'metody' jako funkcje, które można wywołać jedynie
ze środka obiektu). Procedury, tak jak metody, są jedynie funkcjami,
do któ?ych odwołuje się użytkownik, jednak są między nimi pewne różnice.
Procedury
nie mają danych obiektowych, na których mogłyby pracować. Dane muszą
być im dostarczane w postaci listy argumentów, lub muszą one zawierać
dane w obrębie siebie. Procedury mogą używać danych przekazanych jako
argument lub ogólnych danych programu. Metody mogą używać jedynie danych
w obrębie swojego obiektu. W efekcie, zasięg działania metody to zwykle
obiekt w obrębie którego jest dana metoda.
Procedury
często używają danych globalnych, mimo, że powinno się to dziać tylko
w wypadku absolutnej konieczności. Metody używające danych globalnych
powinny zostać napisane od nowa najszybciej, jak to tylko możliwe. Procedury
często odwołują się do innych procedur z kilkoma parametrami, natomiast
metody powinny mieć mało parametrów, za to częściej odwoływać się do
innych metod.
Programowanie
funkcjonalne (FP) trudno stosować razem z programowaniem obiektowym
z kilku powodów. Najważniejszym jest to, że FP bazuje na konkretnych
podejściach do rozwiązania problemu, natomiast OOP używa obiektów jedynie
do wyrażenia koncepcji. Poza tym procedury FP mogą być używane wszędzie,
w przeciwieństwie do metod, których można używać tylko w obrębie obiektu,
do którego należą.
Wiedząc
to wszystko możemy wyjaśnić, dlaczego Perl jest jednym z najlepszych
języków do łączenia metod OOP, PP i FP.
Jak
Perl łączy OOP, FP i PP?
Perl
jest luźnym językiem. Robi bardzo wiele, aby programista mógł robić
co tylko chce, niezależnie od tego, czy mu to wyjdzie na zdrowie. Kontrastuje
to z jezykami takim jak C++ czy Java. Na przykład - Perl pozwala programistom
tworzyć i używać zmiennych bez wcześniejszej deklaracji (chociaż nie
jest to zalecane, a często jest wręcz niedozwolone na skutek stosowania
zalecenia 'use strict'). Pozwala też na dostęp do wewnętrznych danych
obiektu, zmianę klas i redefiniowanie metod 'w locie'. Perlowe podejście
do programowania to pozwalanie człowiekowi na wszystko w imie lepszego
kodowania i debugowania. Pomaga tym samym wykonać zadanie, jest więc
najlepszym przyjacielem lub najzagorzalszym wrogiem programisty.
Dlaczego
ktoś chciałby mieszać OOP, PP i FP, skoro jest to łamanie reguł? Posuńmy
się krok w tył i przemyślmy pytanie. Czym są OOP, PP i FP? Są tylko
metodami, zbiorami koncepcji i reguł stworzonych po to, aby służyły
programistom. OOP, PP i FP są narzędziami, a pierwszym zadaniem każdego
programisty jest poznanie swoich narzędzi. Programista, który tworzy
narzędzia na skutek niewiedzy o ich istnieniu marnuje czas, pieniądze
i wysiłek.
Programista
może zadowolić się narzędziami, które zna najlepiej i to jest chyba
najgorsze, co może mu się przydarzyć. Używanie jedynie podzbioru narzędzi
dostępnych w przedsiębiorstwie to gwarancja, że programista stanie się
za kilka lat bezużyteczny. Programista powinien móc mieszać metody o
ile pozwala mu to na bycie bardziej efektywnym, ulepsza jego kod a jego
grupe czyni bardziej innowacyjną. Perl rozumie i wspiera takie podejście.
Korzyści
z OOP
Jest
ich za dużo, aby wymienić wszystkie w tym artykule a mały zbiór najważniejszych
to: łatwość powtórnego użycia kodu, ulepszania jakości kodu, spójny
interfejs i adaptowalność.
Jako
że OOP bazuje na zbiorze klas i obiektów, powtórne użycie kodu to po
prostu importowanie tych klas w razie potrzeby. Jest to jak dotąd najważniejsza
pojedyncza przyczyna używania OOP oraz powód jego popularności.
Są w tym jednak pewne wady - na przykład rozwiązania dawnych problemów
mogą nie być idealne do problemów obecnych a zrozumienie biblioteki
ze złą dokumentacją może zająć tyle czasu co napisanie jej od nowa.
Unikanie takich wpadek jest zadaniem inżyniera oprogramowania.
Jakość
kodu polepsza się ponieważ 'stopień obudowania' (encapsulation) ogranicza
psucie danych, a dziedziczenie i polimorfizm zmniejszają ilość i złożoność
nowego kodu, który trzeba napisać. Należy delikatnie balansować pomiędzy
jakośćią kodu a innowacyjnością programowania - najlepiej zostawić to
do odkrycia grupie, ponieważ w całości zależy to od jej przygotowania
i celu.
Adaptowalność
jewt nieco mglistą koncepcją w programowaniu. Zdefiniowałbym ją jako
przewidywanie i akceptacjia zmian w użytkowaniu i środowisku działania
programu. Adaptowalność jest ważna dla dobrze napisanego software'u
ponieważ każde oprogramowanie musi współdziałać z otaczającym światem,
a dobrze napisane oprogramowanie musi robić to z gracją. OOP ułatwia
to za pomocą modułowej budowy, dzięki której zmiany fragmentów kodu
spowodowane zmianami środowiska nie muszą wpływać na zmiane rdzenia
architektury programu.
Jak
używać OOP w Perlu
Czy w
to wierzysz czy nie, OOP w Perlu nie jest trudne na poziomie podstawowym
czy średnio zaawansowanym, a nawet użycie zaawansowane nie jest bardzo
skomplikowane.
Perl
stara się nakładać na programiste najmniej ograniczeń jak to możliwe.
OOP w Perlu jest (wybaczcie porównanie) jak grillowanie. Każdy przynosi
własne jedzenie i przyrządza je jak chce. Jest tu nawet zbiorowa atmosfera
grillowania - niepowiązane obiekty mogą dzielić się danymi.
Pierwszym
krokiem jest zrozumienie pakietów (packages) Perla. Pakiety są jak przestrzenie
nazw w C++ i biblioteki w Javie: są ogrodzeniami dzielącymi dane na
poszczególne obszary. Jednakże pakiety w Perlu są jedynie 'doradcami'
dla programisty. Domyślnie Perl nie zabrania wymiany danych pomiędzy
pakietami (chociaż programista może to zrobić poprzez zmienne leksykalne.
Listing
1. Nazwy pakietów, zmienianie pakietów, dzielenie danych pomiędzy pakietami,
zmienne pakietowe.
#!/usr/bin/perl
# uwaga: poniższy kod wygeneruje ostrzeżenia podczas użycia z przełącznikiem
-w,
# a nawet nie skompiluje się przy "use strict". Ma on na celu
zademonstrowanie
# zmiennych pakietowych i leksykalnych. Powinieneś zawsze używać "use
strict".
# Zwróć uwagę na każdą linijke!
# To jest globalna zmienna pakietowa; Nie powinieneś ich używać z "use
strict"
# Wszystko mieści się w pakiecie "main"
$global_sound = "
";
package Krowa; # Tu zaczyna się pakiet krowa
# To jest zmienna pakietowa, dostępna ze wszystkich innych pakietów
jako $Krowa::sound
$sound = "moo";
# To jest zmienna leksykalna, dostępna wszędzie w tym pliku
my $extra_sound = "stampede";
package Swinia; # Tu zaczyna się pakiet Swinia, a pakiet Krowa się kończy
# To jest zmienna pakietowa dostępna ze wszystkich innych pakietów jako
$Pig::sound
$Pig::sound = "oink";
$::global_sound = "pigs do it better"; # inna zmienna pakietu
main
# Wracamy do pakietu main
package main;
print "Krowy robia: ", $Krowa::sound; # wyswietla "moo"
print "\nSwinie robia: ", $Swinia::sound; # wyswietla "oink"
print "\nExtra sound: ", $extra_sound; # wyswietla "stampede"
print "\nWhat's this I hear: ", $sound; # $main::sound jest
niezdefiniowane!
print "\nEveryone says: ", $global_sound; # wyswietla "pigs
do it better"
#koniec
Listingu 1.
Zauważ,
że ogólno-plikowa zmienna leksykalna $extra_sound jest dostępna we wszystkich
trzech pakietach, bo są one zdefiniowane w obrębie tego samego pliku
(w tym przykładzie). Normalnie każdy pakiet zdefiniowany jest we własnym
pliku co daje pewność, że zmienne leksykalne są prywatne dla każdego
pakietu. Tak więc da się osiągnąć 'obudowanie' (encapsulation) pakietów
w Perlu (po więcej informacji sprawdź 'perldoc perlmod').
Następnie
musimy znaleść jakąś zależność między pakietami a klasami. W wypadku
Perla klasy to po prostu rodzaj pakietów (w przeciwieństwie do obiektów,
które są swoiśćie tworzone funkcją 'bless()'). Tu po raz kolejny Perl
rozluźnia zasady OOP, tak aby nie krępowały programisty.
Metoda
'new()' jest zwyczajowym konstruktorem klasy (chociaż, jak to zwykle
w Perlu, możesz nazwać ją jak chcesz). Jest ona wywoływana zawsze wtedy,
kiedy klasa staje się obiektem.
Listing
2. - Klasa Barebones
#!/usr/bin/perl
-w
package Barebones;
use strict;
# konstruktor tej klasy nie przyjmuje żadnych parametrów
sub new
{ my $classname = shift; # znamy nazwę naszej klasy
bless {}, $classname; # i bless'ujemy niezdefiniowany hash
}
1;
Możesz
sprawdzić ten kod samodzielnie umieszczając powyższy kod w oddzielnym
pliku o nazwie Barebones.pm w jakimkolwiek katalogu i uruchamiając w
tym katalogu następującą komendę (tworzącą obiekt Barebones):
perl
-I. -MBarebones -e 'my $b = Barebones->new()'
Możesz
też na przykłąd umieścić w new() rozkaz print żeby zobaczyć co przechowyuje
zmienna $classname
Jeśli zamiast Barebones->new() użyjesz Barebones::new(), nazwa klasy
nie zostanie przekazana do new(). Inaczej mówiąc new() nie zadziała
jak konstruktor, ale jak prosta funkcja.
Pewnie
zastanawiasz się, do czego potrzebne jest przekazanie $classname i dlaczego
zamiast tego nie napisać po prostu:
Bless {}, "Barebones";
Odpowiedzią
jest dziedziczenie - konstruktor ten może być wywołany przez klasę,
która dziedziczy coś od Barebones, ale sama się tak nie nazywa. Blessowałbyś
złą funkcję ze złą nazwą, a to zły pomysł w OOP.
Każda
funkcja potrzebuje danych i metod innych niż new(). Uzyskanie tego to
nic innego jak napisanie kilku prostych procedur.
Listing
3. Klasa z własnymi danymi i metodami.
#!/usr/bin/perl
-w
package Barebones;
use strict;
my $count = 0;
# ta klasa nie przyjmuje żadnych parametrów konstruktora
sub new
{ my $classname = shift; # znamy nazwe klasy
$count++; # zapamiętaj ilość obiektów
bless {}, $classname; # I bless'uj anonimowy hash
}sub count
{ my $self = shift; # A to sam obiekt
return $count;
}1;
#koniec listingu 3.
Możesz
sprawdzić ten obiekt pisząc:
perl
-I. -MBarebones -e 'my $b = Barebones->new(); Barebones->new();
print $b->count'
W rezultacie
powineneś otrzymać 2. Konstruktor jest wywoływany dwa razy modyfikując
zmienną leksykalną $count , która jest zamknięta w _pakiecie_ Barebones
a nie w każdym obiekcie tego pakietu. Lokalne dane obiektu powinny być
przechowywane w samym obiekcie. W wypadku Barebones jest to anonimowy
hash bless'owany jako obiekt. Zauważ, że możemy uzyskać dostęp do obiektu
zawsze kiedy jego metody są przywoływane, poniewać odwołanie do obiektu
jest pierwszym parametrem przekazywanym do tych metod.
Istnieje
kilka specjalnych metod takich jak DESTROY() i AUTOLOAD(), do których
odwołuje się automatycznie sam Perl w pewnych warunkach. AUTOLOAD()
jest przechwytującą wszystko metodą pozwalającą na używanie dynamicznych
nazw metod. DESTRUCT() jest destruktorem obiektu, ale nie powinno się
go używać jeśli nie jest to naprawde konieczne. Używanie go dowodzi,
że ciągle myślisz jak programista C/C++.
Teraz
zajmijmy się dziedziczeniem. W Perlu odbywa się ono poprzez modyfikacje
zmiennej @ISA. Po prostu przypisujesz do zmiennej liste nazw klas i
tyle. Do @ISA możesz wstawić cokolwiek. Jako rodzica swojej klasy możesz
wpisać nawet Szatana i Perl się nie obrazi (chociaż Twój ksiądz, pastor,
rabin czy imam może).
Listing
4. - Dziedziczenie
#!/usr/bin/perl
-w
package Barebones;
# Dodaj ten kod na początku swojego modułu, przed całą resztą i deklaracjami
zmiennych
require Animal; # klasa - rodzic
@ISA = qw(Animal); # ogłasza, że klasa jest dzieckiem 'zwierzęcia'
# zauważ, że @ISA z założenia jest zmienną globalną, i "use
# strict" znajduje się za jej deklaracją.
use strict;
use Carp;
# zrób swoją metodę new() wyglądającą mniej więcej tak:
sub new
{ my $proto = shift;
my $class = ref($proto) || $proto;
my $self = $class->SUPER::new(); # używaj metody new() należącej
do rodzica
bless ($self, $class); # ale bless'uj $self (Animal) jako Barebones
}1;
#koniec
listingu 4.
To najbardziej
Podstawowa wiedza o OOP w Perlu. Jest w nim jeszcze wiele rzeczy, które
powinieneś zgłębić. Napisano w tym temacie wiele dobrych książek - zajrzyj
do bibliografii.
h2xs:
Twój nowy najlepszy przyjaciel
Chciałbyś,
aby ktos napisał za Ciebie klasy w Perlu, spisał szkielet dokumentacji
(POD) i ogólnie uczynił Twoje życieprostszym? Perl daje Ci takie narzędzie:
h2xs
Jego
najważniejsze opcje to "-A -n Module". Kiedy ich użyjesz,
h2xs stworzy szkielet katalogu nazwanego Module pełnego przydatnych
plików:
- Module.pm
- moduł jako taki, z wpisanym szkieletem dokumentacji
- Module.xs
- dla łączenia z kodem C (odpal 'perldoc perlxs' po więcej informacji)
- MANIFEST
- lista plików do tworzenia pakietu (do dystrybucji)
- test.pl
- szkielet skryptu testującego
- Changes
- dziennik zmian w module
- Makefile.PL
- generator makefile'i (do użycia z 'perl Makefile.PL')
Nie musisz
ich wszystkich używać, ale miło jest wiedzieć, że są, jeślibyś ich potrzebował.
Ćwiczenia:
- Jakie
są różnice między OOP, PP i FP?
- Jakie
są najważniejsze cechy języka zorientowanego obiektowo?
- Kiedy
unikałbyś OOP?
- Napisz
klase z metodą new() przechowującą aktualną ilość obiektów w samym
tworzonym obiekcie, jako rodzaj jego identyfikatora. Dlaczego to jest/nie
jest dobry pomysł?
- Jeśli
wszystkie obiekty dziedziczyłyby z jednego źródła, bazowego obiektu
wszystkich innych obiektów, to jakie cechy i metody umieściłbyś w
tym obiekcie? Dlaczego jest to niekoniecznie dobry pomysł?
- przypatrz
się dokłądnie plikom generowanym przez h2xs i Makefile'owi tworzonemu
przez Perl z Makefile.PL
Bibliografia:
-4 poprzedzające rozdziały serii Teodora Zlatanova, dostępne pod adresami:
Rozdział 1 i wprowadzenie -
http://www-106.ibm.com/developerworks/library/l-road1.html
Rozdział 2 (ogónie o kodzie) -
http://www-106.ibm.com/developerworks/library/l-road2.html
Rozdział 3 (o pętlach) - http://www-106.ibm.com/developerworks/library/l-road3.html
Rozdział 4 (o programowaniu funkcjonalnym) - http://www-106.ibm.com/developerworks/library/l-road4.html
-Perl
zorientowany obiektowo Damiana Conwaya - http://www.manning.com/Conway/index.html
-Dokumentacja Perla (perldoc) - rozdziały: 'perltool', 'perlmod', 'perlref',
'perlobj', 'perlbot', 'perltie'
-FAQ grupy comp.object - http://www.cyberdyne-object-sys.com/oofaq2/index.htm
-Perl Programowanie - Larry'ego Walla i Toma Christiansena (wydawnictwa
O'Reilly - http://www.oreilly.com)
-Inne Publikacje
Teodora Zlatanova na DeveloperWorks (seria Cultured Perl)