Jednym z innych wzorców projektowych jest Singleton. Dziś poznasz kolejny, niewiele bardziej skomplikowany wzorzec – dekorator.
Wzorzec ten należy do grupy wzorców
strukturalnych. Idea działania dekoratora
jest bardzo prosta i doskonale oddaje
ją już sama jego nazwa. Załóżmy, że masz prostą
klasę do operowania na strumieniach danych
(np. pochodzących z pliku) – poniżej znajdują się
przykładowe klasy dla PHP oraz Javy:
class FileInputStream {
private $resFP = null;
private $blnEOF = false;
public function __construct($str-
FileName) {
$this->resFP = fopen($strFile-
Name, \'r\');
}
public function read() {
$strC = fgetc($this->resFP);
if ($strC === false) {
$this->blnEOF = true;
$strC = \'\';
}
return $strC;
}
public function isEOF() {
return $this->blnEOF;
}
}
public class FileInputStream {
private java.io.FileInputStream
fileStream;
private boolean EOF = false;
public FileInputStream(String fileName)
{
try {
fileStream = new java.io.FileInputStream(
fileName);
} catch(java.io.FileNotFoundException
e) {
}
}
public int read() {
int b = 0;
try {
b = fileStream.read();
} catch(java.io.IOException e)
{
}
if (b == -1) {
EOF = true;
}
return b;
}
public boolean isEOF() {
return EOF;
}
}
Obsługa błędów w powyższych przykładach
nie jest zaimplementowana, gdyż komplikowałaby
tylko niepotrzebnie nasz kod. Jeśli jednak
chcesz, w każdej chwili możesz sam zadbać o jej
implementację.
Co byś zrobił, gdyby okazało się, że w pewnych
miejscach projektu wymagane jest nieco inne działanie
powyższej klasy niż zwykle – np. wszystkie
małe litery mają zostać zamienione na wielkie?
Istnieje kilka sposobów, mniej lub bardziej poprawnych,
na rozwiązanie tego problemu.
Można dodać do powyższych klas metodę
{stala}readUpper(){/stala}, zamieniającą podczas odczytu
pliku małe litery na wielkie. Można w miejscach,
gdzie jest to konieczne, obudować metodę {stala}read(){/stala}
odpowiednią konstrukcją, konwertującą znaki
przez nią zwracane do wymaganego formatu.
Można wreszcie zbudować klasę dziedziczącą po
FileInputReader, implementując w niej odpowiednie
zachowanie metody {stala}read(){/stala}.
Niestety
żadna z tych metod nie jest idealna i pociąga za
sobą wiele problemów:
- dodawanie kolejnych metod do klasy FileInputStream
powoduje ciągłe jej modyfikacje i wyposażanie
w coraz to nowsze metody: {stala}readUpper(){/stala},
{stala}readLower(){/stala}, {stala}readBuffered(){/stala}, {stala}readBB2HTML(){/stala} i tak
dalej, co powoduje bałagan w klasie. Ponadto, jeśli
przyjdzie konieczność użycia jednej z tych metod
w miejscu, gdzie kiedyś używana była metoda
{stala}read(){/stala}, będziesz musiał przeszukać i zmodyfikować
kod ręcznie (nierzadko w wielu miejscach); - obudowywanie klasy {stala}read(){/stala} w miejscu jej wywołania
innymi metodami z biblioteki standardowej
(takimi jak {stala}strtoupper(){/stala} z PHP czy {stala}Character.toUpperCase(){/stala} w Javie) może doprowadzić do
wprowadzenia błędów do kodu. Dodatkowo
w sytuacji gdyby okazało się, że metoda, jaką
wymyśliłeś do osiągnięcia planowanej funkcjonalności,
nie jest idealna (np. zamienia małe litery
na wielkie, ale pod warunkiem że nie napotka po drodze znaków diakrytycznych), wprowadzenie
poprawki może być kłopotliwe, gdyż będzie
wymagało odnalezienia wszystkich miejsc w programie,
w których zastosowałeś daną modyfikację,
a wówczas łatwo coś przegapić;
Ostatnia metoda wydaje się najlepsza, następuje
w niej jednak ten sam problem, co w poprzednich
– co jeśli będziesz chciał zastosować jednocześnie
dwie lub więcej modyfikacji na strumieniu?
W drugiej metodzie jest to do wykonania, ale kod
stanie się jeszcze bardziej nieczytelny, w dwóch pozostałych
przypadkach nie będziesz w stanie tego
łatwo osiągnąć – pozostanie ci albo dodawanie
kolejnych metod do klasy albo budowa coraz to
nowych klas dziedziczących po klasie głównej.
Na szczęście istnieje stosunkowo proste rozwiązanie
problemu. Wystarczy zastosować wzorzec dekorator.
Aby z niego korzystać, należy z dekorowanej
klasy wyodrębnić interfejs – przy okazji pozwoli ci to
na budowę innych podobnych klas (w naszym przypadku
strumieni) implementujących ten interfejs,
które również będą mogły być dekorowane tymi
samymi dekoratorami (np. poza FileInputStream
będziesz mógł utworzyć ByteInputStream).
Interfejs
powinien być zbiorem metod wspólnych dla wszystkich
klas danej rodziny. W przypadku naszej klasy
interfejs może wyglądać następująco:
interface InputStream {
public function read();
public function isEOF();
}
interface InputStream {
public int read();
public boolean isEOF();
}
Dodatkowo musisz zmodyfikować napisaną
wcześniej klasę tak, aby implementowała powyższy
interfejs:
class FileInputStream implements InputStream
{
...
}
public class FileInputStream implements
InputStream{
...
}
Klasa FileInputStream (oraz inne klasy strumieni dziedziczące po InputStream)
będzie mogła być dekorowana za pomocą specjalnie w tym celu utworzonych
klas dekoratorów.
Ważne jest, aby każdy z dekoratorów rozszerzał specjalną,
zbudowaną do tego celu klasę abstrakcyjną – w naszym przypadku nosi ona
nazwę InputStreamDecorator:
abstract class InputStreamDecorator implements Input-
Stream {
protected $objInputStream = null;
public function __construct(InputStream $objInput-
Stream) {
$this->objInputStream = $objInputStream;
}
}
public abstract class InputStreamDecorator implements
InputStream {
protected InputStream inputStream;
public InputStreamDecorator(InputStream inputStream)
{
this.inputStream = inputStream;
}
}
Przyjrzyj się teraz bliżej tej klasie. Posiada ona konstruktor przyjmujący
jako parametr obiekt klasy implementującej interfejs InputStream (na przykład
obiekt naszej klasy strumienia – FileInputStream). Obiekt ten jest przypisywany
do pola chronionego, dzięki czemu klasa dziedzicząca po InputStreamDecorator
będzie zawsze miała dostęp do obiektu klasy, którą dekoruje. Zauważ również,
że klasa ta nie implementuje metod interfejsu InputStream, pozostawiając to
klasą dekoratorów.
Gdy masz już wszystko przygotowane, nie pozostało nic innego jak zbudować
klasę dekorującą:
class UpperInputStream extends InputStreamDecorator {
public function read() {
$strC = $this->objInputStream->read();
return strtoupper($strC);
}
public function isEOF() {
return $this->objInputStream->isEOF();
}
}
public class UpperInputStream extends InputStreamDecorator
{
public UpperInputStream(InputStream inputStream) {
super(inputStream);
}
public int read() {
int c = inputStream.read();
return Character.isLetter((char)c) ? (int)Character.
toUpperCase((char)c) : c;
}
public boolean isEOF() {
return inputStream.isEOF();
}
}
I co takiego robią klasy dekorujące? Pobierają dane zwrócone przez klasę
dekorowaną i odpowiednio dane te modyfikują. W powyższym przykładzie sprowadza się to do modyfikacji każdego znaku odczytanego przez metodę
read() klasy dekorowanej.
Teraz już prawdopodobnie wszystko zaczyna ci się rozjaśniać. Udekorowanie
klasy strumienia jest bardzo proste:
$objUIS = new UpperInputStream(
new FileInputStream(\'plik.txt\')
);
InputStream uis = new UpperInputStream(
new FileInputStream(\"plik.txt\")
);
Przyjrzyj się jednak jeszcze raz dokładniej klasie abstrakcyjnej, po której
dziedziczą nasze dekoratory – InputStreamDecorator. Jak widzisz, implementuje
ona interfejs InputStream, jednocześnie pobierając w konstruktorze obiekt
klasy, która ma zostać udekorowana, a która implementuje interfejs Input-
Stream. Inaczej mówiąc, dekorować możesz także obiekty dekoratorów. Co to
daje?
Możliwość udekorowania klasy wieloma dekoratorami naraz:
$objUIS = new UpperInputStream(
new BB2HTMLImputStream(
new FileInputStream(\'plik.txt\')
)
);
InputStream uis = new UpperInputStream(
new BB2HTMLImputStream(
new FileInputStream(\"plik.txt\")
);
Jak widać, dekorator niewielkim nakładem sił pozwolił na rozwiązanie problemu
z początku artykułu. Teraz dodatkowe udekorowanie klasy implementującej
interfejs InputStream sprowadzało się będzie do zbudowania kolejnej
klasy dekorującej i odpowiedniego obudowania konstrukcji tworzącej obiekt
klasy dekorowanej. Proste, łatwe i przyjemne.
Dekorator na Javie
Jeśli dobrze znasz Javę, zapewne już zauważyłeś, że wzorzec dekorator
został w niej wykorzystany między innymi w klasach… strumieni. Java
dostarcza własną klasę FileInputStream z podobną (lecz bardziej rozbudowaną)
funkcjonalnością (zauważ, że do implementacji naszej klasy FileInputStream
użyliśmy klasy o takiej samej nazwie z pakietu java.io). Oto przykład udekorowania
klasy FileInputStream z pakietu java.io:
InputStream is = new BufferedInputStream(
new FileInputStream(\"plik.txt\");
);
Biblioteka standardowa Javy używa w wielu miejscach różnych wzorców
projektowych, ułatwiając tym samym budowę kodu czytelnego, przyjemnego
w pielęgnacji i nad którym można łatwo zapanować.
Dekorator w pigułce
1. Wyodrębniamy interfejs z klasy (bądź rodziny klas, choć w tym przypadku
powinno to być już zrobione), którą chcemy dekorować.
2. Implementujemy wyodrębniony interfejs w dekorowanej klasie.
3. Budujemy abstrakcyjną klasę implementującą wyodrębniony interfejs
oraz posiadającą konstruktor przyjmujący jako parametr obiekt klasy,
która ma zostać poddana procesowi dekoracji – obiekt ten zapamiętujemy,
najlepiej w polu chronionym klasy.
4. Budujemy dekoratory rozszerzające ową klasę abstrakcyjną, implementując
dodatkowo wszystkie metody wyodrębnionego interfejsu.
5. dekorujemy!