Wzorce Projektowe - Fabryka abstrakcyjna

PL
Data dodania: 2011-09-18, Autor: ori, Dodał: Karol, Wyświetleń: 617

Wzorce Projektowe

Wzorce projektowe to zbiór prostych eleganckich rozwiązań konkretnych problemów projektowych i programistycznych. Sprawiają, że projekty stają się bardziej elastyczne i dają się łatwiej wykorzystać ponownie. Techniki stosowane w nich upraszczają tworzenie nawet złożonych systemów i sprawiają, że architekt systemu ma czas na dopracowanie szczegółów, przez co projekty stają się bardziej dojrzałe. Według mnie, wzorce projektowe to potężne narzędzie w rękach nawet mało doświadczonego programisty. Powszechnie sądzi się, iż do poznania wzorców projektowych, konieczne jest duże doświadczenie w programowaniu i projektowaniu obiektowym. Uważam, że to nie prawda. Sądzę, że zapoznanie się z tematyką wzorców pomaga początkującym programistom zrozumienie istoty technik obiektowych. Mam nadzieję, że przedstawiony dalej jeden ze wzorców, pokaże zalety ich stosowania i zachęci do dalszego poznawania tej ciekawej tematyki.

Fabryka abstrakcyjna

Najlepszym sposobem nauki czegokolwiek, w szczególności programowania i projektowania jest poznawanie przyswajanych zagadnień poprzez przykłady i praktykę. Zaczniemy, więc od przykładu, w którym poczyniono dużo uproszczeń, aby łatwiej zobrazować wykorzystanie tego wzorca. Następnie podamy jego definicję, oraz podsumujemy zalety i pokażemy także wady.

Przypuśćmy, że masz do napisania zestaw klas realizujących pewne specyficzne zadania. Niech to będzie wyszukiwanie pewnych informacji w dokumencie tekstowym i odpowiednia ich modyfikacja. Dla uproszczenia przyjmijmy, że jest to tylko wyszukiwanie wszystkich wystąpień zadanego fragmentu tekstu, usunięcie fragmentu, zmiana wszystkich wystąpień wzorca na podany zamiennik i wyróżnienie wzorca np. poprzez zamianę wszystkich liter na duże. Wszystko wygląda w miarę prosto, więc zabierasz się do pracy. Od razu widać, że stosując obiektowe podejście zaimplementujesz cztery klasy. Klasa wyszukująca KSzukacz, i klasy korzystające z wyników wyszukiwania KUsuwacz, KMarker i KZamieniacz. Implementujesz te klasy, testujesz i oddajesz je do użytku. Reszta zespołu, w którym pracujesz zaczyna wykorzystywać je przy tworzeniu aplikacji, nad którą pracujecie jakiś już czas.

Wszystko wygląda na to, że było to proste zadanie. Aplikacja, w której zostały użyte twoje klasy, jest praktycznie na ukończeniu i zaczyna przechodzić pierwsze testy.

Podczas wstępnych uruchomień okazuje się, że zwykły tekstowy format dokumentów jest nieodpowiedni do prezentacji treści większości dokumentów. Użycie HTML'a wydaje się odpowiednie w celu ożywienia prezentowanych treści, więc kierownictwo decyduje się na jego wprowadzenie. Natychmiast wychodzą na jaw niedoskonałości twoich klas, które przestały działać tak jak powinny. KUsuwacz, KZamieniacz, KMarker nie usuwają i nie zmieniają wszystkich wystąpień wzorców w obrabianym dokumencie. Dodatkowo zażyczono sobie, żeby KMarker w HTML'u nie zmieniał małych liter na duże, tylko pogrubiał je dodając w odpowiednich miejscach znaczniki , .

Mało doświadczony programista postąpi zapewne w ten sposób, że zmodyfikuje utworzone klasy poprawiając kod metod oraz dodając nowe metody tak, aby uwzględniały specyfikę HTML. Dokona tego zapewne poprzez sparametryzowanie konstruktorów, aby wiadomo było, do jakich dokumentów są stosowane, a dodatkowe instrukcje warunkowe będą prawidłowo przełączały pomiędzy trybami obrabiania dokumentów. Okazuje się, że takie rozwiązanie ma kilka poważnych wad. Po pierwsze, kod dotychczas eleganckich klas znacznie rozrasta się, a co za tym idzie, czytanie i rozumienie go staje się trudniejsze. Lokalizacja i poprawianie ujawniających się błędów staje się znacznie bardziej pracochłonne. Co więcej, poprawienie ich spowoduje wzrost prawdopodobieństwa wystąpienia innych. Zaimplementowanie dodatkowego obsługiwanego formatu w ten sam sposób będzie już o niebo trudniejsze. Kod takich klas stanie się nieprzyjemny w pielęgnacji i utrzymaniu. Doświadczeni programiści, pracujący przy dużych projektach wiedzą, że o ile czasami lokalizacja i wprowadzenie poprawki może zająć stosunkowo mało czasu, to usunięcie skutków wprowadzenia tej poprawki zajmuje całe wieki. Dlatego też, tak istotne jest, aby kodu, który został przetestowany i zatwierdzony, nie trzeba było więcej modyfikować. Innym mankamentem jest to, że klasy te używała już reszta zespołu w wielu miejscach tworzonego programu. W konsekwencji trzeba znaleźć i poprawiać kod, w którym zostały wykorzystane, co nie musi być wcale banalnym zadaniem.

Rozwiązaniem eliminującym część wad poprzedniego, będzie zaimplementowanie odpowiedników klas KSzukacz, KMarker, KUsuwacz, KZamieniacz dla HTML, które będą uwzględniać specyficzne cechy dokumentu zapisanego w tym formacie. Powstaną w ten sposób grupy klas KSzukaczTxt. KUsuwaczTxt itp. KSzukaczHtml, KUsuwaczHtml itp. Dzięki temu nie będziemy modyfikować już kodu metod, które były już przetestowane dla formatu czystego tekstu. Zyskujemy przez to pewność, że przy usuwaniu usterek w działaniu klas dla formatu HTML, nie wprowadzimy niekorzystnych zmian dla klas formatu tekstowego. Pozostaje teraz wykorzystanie nowych klas w tworzonej aplikacji. Nie możemy prostym "znajdź i zamień" zmienić wszystkich wystąpień KSzukacz i innych na ich HTML'owe odpowiedniki, gdyż w ten sposób będziemy wstanie obsłużyć tylko format HTML, a my chcemy, aby były obsługiwane oba typy dokumentów. Trzeba więc, odnaleźć wszystkie miejsca, w których były używane stare klasy, i tak zmodyfikować te fragmenty, żeby możliwe było przełączanie pomiędzy typami dokumentów. Zauważmy, że klasy wykonujące te same operacje dla zwykłego tekstu i html mają identyczny interfejs. Możemy, więc zdefiniować abstrakcyjne klasy bazowe i skorzystać z mechanizmów dziedziczenia. Kod po zmodyfikowaniu mógłby wyglądać mniej więcej w ten sposób.

/**
* fragment kodu wykorzystujacy nasze klasy
*/
KSzukacz *szukacz;
if(typ_dok == "txt")
{
   szukacz = new KSzukaczTxt();
}
else //if(typ_dok == "html")
{
  szukacz = new KSzukaczHtml();
}
/**
* dalej jest ten sam kod co wcześniej,  bez żadnych modyfikacji,
* w którym występuje np. takie wywołanie
*/
szukacz->szukaj();

Teraz przy wywoływaniu metody na rzecz obiektu szukacz np. szukacz->szukaj(), nie zostanie wywołana metoda klasy KSzukacz, gdyż takiego obiektu nie stworzyliśmy i stworzyć nie możemy, ponieważ klasa KSzukacz jest abstrakcyjnym typem. Przy wykonaniu polecenia szukacz->szukaj(), wykorzystany zostaje mechanizm późnego wiązania, polegający na tym, iż podczas takiego rzutowania szukacz = new KSzukaczHtml() (szukacz został zadeklarowany jako KSzukacz *szukacz;) nie traci się informacji o typie, co w konsekwencji powoduje, że zostaje wywołana metoda KSzukaczHtml::szukaj. Rozwiązanie takie ma spore wady. Każdorazowe dodanie do aplikacji dodatkowo obsługiwanego formatu spowoduje, iż trzeba znowu ingerować w fragmenty kodu aplikacji, w których następuje przełączanie pomiędzy typami obrabianych dokumentów. Jak już wiemy każda zmiana, może wprowadzić dodatkowe błędy, więc najlepszym wyjściem byłoby napisanie takiego kodu, którego nie trzeba później zmieniać. Lepiej więc kod z poprzedniego listingu zastąpić następującym:

/**
* dodatkowa funkcja(metoda) w module(klasie) który(a) wykorzystuje nasze klasy
*/
KSzukacz *UtworzObjSzukacz(string typDok)
{
  if(typDok == "txt")
  {
    return new KSzukaczHtml();
  }
  else if(typDok == "html")
  {
    return new KSzukaczTxt();
  }
  else 
  {
    return NULL;
  }
}

/**
* fragment kodu wykorzystujacy nasze klasy
*/
KSzukacz *szukacz;
szukacz = UtworzObjSzukacz();
/**
* dalej jest ten sam kod co wcześniej,  bez żadnych modyfikacji,
* w którym występuje np. takie wywołanie
*/
szukacz->szukaj();

Teraz, jeżeli zajdzie potrzeba wprowadzenia nowego formatu dokumentów, modyfikacje wprowadzimy tylko w funkcjach UtworzObjSzukacz, UtworzObjUsuwacz, UtworzObjZamieniacz, UtworzObjMarker. Funkcje te w zależności od parametru - typu dokumentu - tworzą obiekt odpowiedniego typu, więc jeżeli dodamy nowy typ dokument, wystarczy zmienić tylko kod w tych funkcjach. Przy takim podejściu liczba wprowadzanych modyfikacji zmniejszyła się do kilku, kilkunastu, czyli tyle ile jest miejsc, w których sprawdzamy, z jakim typem dokumentu mamy do czynienia. Zastanówmy się jednak, czy nie dałoby się tak tego zrobić, aby sprawdzenie rodzaju dokumentu było wykonane tylko raz. Z pomocą mogą przyjść znowu mechanizmy dziedziczenia i polimorfizmu. Spróbujmy utworzyć klasy, których jednym zadaniem byłoby tworzenie na żądanie obiektów z określonej grupy. Jednymi metodami, które będą posiadać niech są UtworzSzukacz, UtworzUsuwacz, UtworzZamieniacz, UtworzMarker. Funkcje te będą działać podobnie jak wcześniejsze z tym, że nie przyjmują parametru i zwracają tylko jeden typ obiektu. Utwórzmy abstrakcyjną klasę KFabrykaObj, która zdefiniuje interfejs dla klas dziedziczących po niej - KFabrykaObjHtml i KFabrykaObjTxt.

class KFabrykaObj
{
  public:
     KFabrykaObj();
    virtual ~KFabrykaObj();
    virtual KSzukacz* UtworzSzukacz() = 0;
    virtual KUsuwacz* UtworzUsuwacz() = 0;
    virtual KZamieniacz* UtworzZamieniacz() = 0;
    virtual KMarker* UtworzMarker() = 0;
}
class KFabrykaObjHtml: public KFabrykaObj
{
    public:
      KFabrykaObjHtml(){};
      ~KFabrykaObjHtml(){};
     virtual KSzukacz* UtworzSzukacz(){return new KSzukaczHtml();};
     virtual KUsuwacz* UtworzUsuwacz(){return new KUsuwaczHtml();};
     virtual KZamieniacz* UtworzZamieniacz(){return new KZamieniaczHtml();};
     virtual KMarker* UtworzMarker(){return new KMarkerHtml();};
}
class KFabrykaObjTxt: public KFabrykaObj
{
  public:
    KFabrykaObjTxt();
    ~KFabrykaObjTxt();
    virtual KSzukacz* UtworzSzukacz(){return new KSzukaczTxt();};
    virtual KUsuwacz* UtworzUsuwacz(){return new KUsuwaczTxt();};
    virtual KZamieniacz* UtworzZamieniacz(){return new KZamieniaczTxt();};
    virtual KMarker* UtworzMarker(){return new KMarkerTxt();};
}

Spróbujmy teraz wykorzystać nowe klasy.

/**
* W funkcji DajFabryke wybierana jest fabryka obiektów i tworzony jest jej egzemplarz,
* 
*/
KFabrykaObj* DajFabryke(string typDok)
{
   if(typDok == "txt")
   {
      return new KFabrykaObjTxt();
   }
   else if(typDok == "html")
   { 
       return new KFabrykaObjHtml();
   }
}

/**
* fragment kodu wykorzystujacy nasze klasy
*/
KFabrykaObj* fabryka;
fabryka = DajFabryke(string typDok);
KSzukacz *szukacz;
szukacz = fabryka->UtworzObjSzukacz();
/**
* dalej jest ten sam kod co wcześniej,  bez żadnych modyfikacji,
* w którym występuje np. takie wywołanie
*/
szukacz->szukaj();

Myślę, że dla uważnego czytelnika wszystko jest jasne i nie trzeba wyjaśniać tego, co się dzieje w powyższym kodzie.

Aby podkreślić korzyści wynikające z takiego rozwiązania, zastanówmy się, co zrobić, gdy zajdzie potrzeba wprowadzenia dodatkowo obsługiwanego formatu, dajmy na to RTF-u.

  • tworzymy klasy dziedziczące po klasach KSzukacz, KUsuwacz, KZamieniacz, KMarker, w których implementujemy obsługę tego formatu: KSzukaczRTF, KUsuwaczRFT, KZamieniaczRTF, KMarkerRTF.
  • tworzymy fabrykę obiektów KFabrykaObjRTF
  • zmieniamy kod
KFabrykaObj* DajFabryke(string typDok)
{
   if(typDok == "txt")
   {
      return new KFabrykaObjTxt();
   }
   else if(typDok == "html")
   { 
       return new KFabrykaObjHtml();
   }
}

na

KFabrykaObj* DajFabryke(string typDok)
{
   if(typDok == "txt")
   {
      return new KFabrykaObjTxt();
   }
   else if(typDok == "html")
   { 
      return new KFabrykaObjHtml();
   }
   <b>else if(typDok=="rtf")
   {
      return new KFabrykaObjRtf(); 
   }</b>
}

Definicja:

FABRYKA ABSTRAKCYJNA To obiektowy wzorzec kreacyjny, który dostarcza interfejs do tworzenia całych rodzin spokrewnionych ze sobą, lub zależnych od siebie okiektów bez konieczności definiowania ich klas konkretnych.

Uczestnicy:

  • Fabryka abstrakcyjna
  • Fabryki konkretne
  • Produkty abstrakcyjne
  • Produkty konkretne
  • Klient

Zalety:

  • Odseparowanie klas konkretnych - klienci nie wiedzą jakich typów konkretnych używają, posługują się interfejsami abstrakcyjnymi
  • Łatwa wymiana rodziny produktów
  • Spójność produktów - w sytuacji, gdy pożądane jest, aby klasy produkty były z określonej rodziny, fabryka bardzo dobrze to zapewnia

Wady:

  • Trudność w dołączaniu nowego produktu do rodzin produktów, spowodowana koniecznością rozszerzania interfejsów fabryki

 


Aby dodawać komentarze musisz być zalogowany!


Kontakt

Jeśli chcesz się z nami skontaktować napisz na adres: info(at)binboy.org lub odwiedź nasz profil na Facebooku!

O Nas

Serwis binboy.org to kopalnia wiedzy dla wszystkich z branży IT, w szczególności dla programistów i webmasterów. To duży zbiór kursów programowania, tutoriali, darmowych ebooków, setki kodów źródłowych itp.

Bądź w kontakcie

Panel użytkownika

Zaloguj się do panelu użytkownika.
Nie masz konta? Zarejestruj się!
Zapomniałeś hasła?