Posts tagged ‘classes’

Niemal standardowe nazwy klas

2008-01-02 13:48
Model-View-Controller
Model-View-Controller (MVC),
bardzo modny ostatnio ‘wzorzec’

Wzorce projektowe (design patterns) to w założeniu ogólne modele związków pomiędzy klasami (i samych klas), jakie mogą pojawiać się w projektach. Każdy taki wzorzec jest przeznaczony do dość ściśle określonych okoliczności, dokładnie opisany i, przede wszystkim, nazwany. Na pewno każdy średnio zaawansowany programista miał okazję spotkać się z takimi terminami jak Iterator, Fabryka czy Singleton. Są one już na tyle długo używane, że w większości przypadków nie ma problemów ze zrozumieniem tego, co w danej sytuacji oznaczają.

Wzorce nie są oczywiście doskonałe. Ze względu na względnie dużą ścisłość nie mogą opisywać wszystkich rozwiązań, jakie mogą być konieczne, jeśli myślimy o zaprojektowaniu dowolnego programu. Nie jest więc tak, że każda klasa, jaka przyjdzie nam głowy, może być od razu wpasowana w jakiś gotowy szablon.
Przeglądając gotowe kody i różnego rodzaju dokumentacje stwierdziłem jednak, że często powtarzają się w nich różnego rodzaju “pseudowzorce”. Objawiają się one głównie używaniem pewnych słów w nazwach klas, dzięki którym można mniej więcej domyślić się, jaka jest rola poszczególnych typów, do czego służą, jak – z grubsza – działają oraz jakie wykazują zależności z innymi klasami. Naturalnie mogą występować spore różnice pomiędzy poszczególnymi bibliotekami i językami, ale przynajmniej dla dwóch największych zbiorów klas, jakie są obecnie w powszechnym użyciu (czyli .NET Framework i JDK), rozbieżności nie są zbyt duże. Co więcej, ponieważ wielu programistów używa któregoś z tych dwóch narzędzi, często przejmują oni te wzorce nazewnictwa (świadomie lub nie) i stosują je we własnych kodach. Kto wie, może dzięki temu przeciętna czytelność kodu produkowanego przez statystycznego programistę (jeśli w ogóle istnieje ktoś taki :]) ma szansę choć odrobinę wzrosnąć?…

Jakie są więc te nieformalne “wzorce”? Otóż znalazłem kilka następujących:

  • Manager to egzemplarz występujący bardzo często i niestety chyba najmniej jednoznaczny z nich wszystkich. W dosłownym tłumaczeniu jest to ‘zarządca’ czegoś i właściwie niewiele więcej można o nim powiedzieć. Nazywanie wszystkiego ‘menedżerem’ jest popularne zwłaszcza wśród programistów gier: mamy więc menedżery zasobów, tekstur, stanów, czcionek – i tak dalej. Zastanawiam się tylko, skąd to upodobanie to tego akurat terminu. Czyżby programiści z sentymentem wspominali w ten sposób Menedżera Programów, czyli sławetną “powłokę” systemu Windows w archaicznych wersjach 3.x? :)
  • Handler (ew. listener) jest już znacznie precyzyjniejszym pojęciem. Zazwyczaj jest to bowiem obiekt, który ma wykonywać jakieś działanie w reakcji na określenie zdarzenie – czyli ma je ‘obsługiwać’. Najczęściej oznacza to, że typ handlera jest tylko abstrakcyjną klasą bazową lub interfejsem, po którym powinniśmy dziedziczyć, a następnie zaimplementować jego metody i wreszcie “podpiąć” powstały obiekt pod mechanizm obsługi zdarzeń. Typowym zastosowaniem takiego rozwiązania jest reakcja na zdarzenia interfejsu użytkownika (klikanie w przyciski, wciśnięcia klawiszy, itd.) albo powiadamianie o przebiegu operacji asynchronicznych (np. komunikacji sieciowej).
  • Provider jest z kolei obiektem, który ma ‘dostarczać'(lub ‘zapewniać’) jakąś funkcjonalność. Od strony technicznej często działa on tak samo jak handler (czyli w oparciu o polimorfizm metod wirtualnych), tyle że jego przeznaczenie i sposób użycia są nieco inne. Takiego providera podajemy w funkcji lub w obiekcie, dzięki czemu może ona wykonać przy jego pomocy jakieś czynności. Prostym przykładem jest chociażby sortowanie, któremu podajemy obiekt komparatora, odpowiedzialny za dokonywanie porównań. Tak naprawdę jest to więc po prostu wzorzec Strategia (Polityka – policy), któremu ktoś z niewiadomych powodów nadał bardziej “profesjonalną” nazwę.
  • Context oznacza najczęściej taki obiekt, który definiuje jakieś ‘środowisko’, w którym działają inne obiekty. Są one zależne od obiektu reprezentującego kontekst, ale nie można powiedzieć, aby ten się z nich składał (może on nawet “nie wiedzieć”, że jest przez nie wykorzystywany). Po prostu dzięki niemu obiekty “potomne” mogą wykonywać swoje czynności.

Prawdopodobnie dałoby się wyróżnić jeszcze kilka pozycji (chociaż część byłaby dokładnym odpowiednikiem klasycznych wzorców projektowych), ale, jak widać, w sumie chyba nie jest ich zbyt dużo. To w gruncie rzeczy całkiem dobra wiadomość, gdyż nietrudno zauważyć, że wszystkie te nazwy są raczej “magiczne” i na pierwszy rzut nie przywołują jakichś natychmiastowych skojarzeń – zwłaszcza, jeśli nie jesteśmy do nich przyzwyczajeni. Ale taki już urok projektowania zorientowanego obiektowo, polegającego na tworzeniu dziwnych bytów i jeszcze dziwniejszych zależności między nimi :)

Tags: ,
Author: Xion, posted under Programming » 4 comments

Słowo kluczowe ‘base’

2007-09-24 17:51

Z dobrodziejstwa metod wirtualnych po prostu nie można nie korzystać. Dzięki nim kod jest bardziej elegancki, krótki, często (wbrew powszechnej opinii) efektywniejszy i naturalnie bardziej obiektowy :) Wszystkie te zalety opierają się oczywiście na tym, że tak naprawdę nie musimy wiedzieć, jaką wersję metody wirtualnej – oryginalną czy nadpisaną w klasie pochodnej – wywołujemy w danym przypadku.
Sama metoda aczkolwiek ‘wie’ to doskonale. Czasami zdarza się jednak, że chcielibyśmy wywołać jej odziedziczoną wersję, pochodzącą z klasy bazowej. Podobnie jak większość języków, C++ nie czyni tego automatycznie (z wyjątkiem konstruktorów i destruktorów), jako że nie zawsze jest to potrzebne. Ale nierzadko się przydaje i jest wygodne.

W wielu językach, jak choćby Delphi czy C#, mamy pomocnicze słowa kluczowe, służące do takich właśnie wywołań. W przeciwieństwie do nich C++ oferuje jednak dziedziczenie wielokrotne, wobec tego czasami klasa bazowa nie jest określona jednoznacznie. Dlatego też chcąc wywołać odziedziczoną wersję metody, musimy jawnie użyć nazwy tej klasy., np.:

  1. class CFoo { public: virtual void Do() { /* ... */ } };
  2.  
  3. class CBar : public CFoo
  4. {
  5.    public:
  6.       void Do()
  7.       {
  8.          // ...
  9.          CFoo::Do();   // wywołanie odziedziczonej wersji metody
  10.       }
  11. };

Wielodziedziczenia używamy jednak rzadko i w zdecydowanej większości sytuacji klasa bazowa będzie tylko jedna. Na takie okazje Visual C++ przewidział własne słowo kluczowe __super. Możemy też pokusić się o bardziej przenośne rozwiązanie, definiując taki oto szablon:

  1. template <class T> class Inherits : public T
  2. {
  3.    protected:
  4.       typedef T base;
  5. };

a wówczas zyskamy swoje własne “słowo kluczowe” base o możliwościach podobnych do tych z C#:

  1. class CBar : public Inherits<CFoo>
  2. {
  3.    public:
  4.       void Do()   { /* ... */   base::Do(); }
  5. };

Na nieszczęście jest tu mnóstwo różnych “ale”. Największym problemem jest to, że właściwej klasy bazowej (tutaj CFoo) nie ma jak zainicjalizować w klasie pochodnej, wobec czego musi ona dysponować domyślnym konstruktorem, który na dodatek będzie używany zawsze. To poważny feler, którego nie ma za bardzo jak naprawić. Dlatego jeśli bardzo doskwiera nam brak słowa base, to chyba jedynym sposobem jest… ręczne dodawanie typedefa podobnego do tego w szablonie Inherits.
Dopiero C++0x wprowadzi możliwość dziedziczenia konstruktorów (na zasadzie przekierowywania ich parametrów do klasy bazowej), która pozwoli wyeliminować wspomniane ograniczenie. Wówczas taka wersja szablonu:

  1. template <class T> class Inherits : public T
  2. {
  3.    protected:
  4.       typedef T base;
  5.    public:
  6.       // polecenie odziedziczenia konstruktorów z klasy T
  7.       using T::T;
  8. };

powinna zdać egzamin dla dowolnej klasy T.

Tags: , ,
Author: Xion, posted under Programming » Comments Off on Słowo kluczowe ‘base’

Referencje do klas w Delphi

2007-08-20 11:30

W ramach kontynuacji przeglądu nietypowych konstrukcji językowych – który to nieopatrznie rozpocząłem, zajmując się pętlami w Pythonie – obejrzymy sobie jeden z elementów języka Object Pascal. Są to referencje do klas, zwane też czasem metaklasami.

Ten dziwny twór działa jak odwołanie wskazujące na klasę jako typ, a nie na jej konkretny obiekt. Deklaruje się go mniej więcej w taki sposób:
[delphi]type TClass = class of TObject;[/delphi]
Zmienne należące do tak zdefiniowanego typu TClass mogą pokazywać na wszystkie klasy dziedziczące po TObject. Innymi słowy, takie zmienne są swego rodzaju dynamicznymi aliasami na nazwy klas; używając ich, nie musimy nawet wiedzieć, z jakiego typu klasą konkretną mamy do czynienia. Przypomina to oczywiście normalny dynamiczny polimorfizm obiektów, osiągany przy pomocy funkcji wirtualnych. Tutaj jest to niejako dynamiczny polimorfizm samych klas.

Użycie takiego typu referencyjnego może wyglądać choćby tak:
[delphi]type TFoo = class(TObject) // klasa dziedzicząca po TObject
// …
end;

var
AnyClass : TClass; // zmienna będąca referencją do klasy
AnyObject : TObject; // zwykłe odwołanie do obiektu
begin
AnyClass := TFoo; // referencja pokazuje na klasę TFoo
AnyObject := AnyClass.Create; // tworzy obiekt klas TFoo przy pomocy referencji
end;[/delphi]
Ten przykład pokazuje, że w Delphi przy pomocy referencji do klas możliwe jest łatwe zrealizowanie wzorca wirtualnego konstruktora. Nie musimy bowiem wiedzieć, na jaką klasę wskazuje referencja, a utworzony obiekt możemy “odebrać” posługując się zmienną typu bazowego (tutaj TObject).

Co na to C++? Nie ma tam naturalnie podobnej konstrukcji. Zbliżone do niej – w sensie możliwości korzystania z jakiegoś typu bez wiedzy, czym on naprawdę jest – są parametry szablonów. Podstawowa różnica polega jednak na tym, że szablony są rozwijane w trakcie kompilacji i “wartości” tych parametrów są niezmienne.
Nie wiem, czy można w jakiś sposób zaimplementować w C++ metaklasy o funkcjonalności zbliżonej do powyższej. Znając możliwości C++, to całkiem prawdopodobne :) Ich ewentualny brak nie były jednak jakoś szczególnie dotkliwy, gdyż większość ich zastosowań z powodzeniem daje się zastąpić szablonami lub zwykłymi funkcjami wirtualnymi.

Tags: , ,
Author: Xion, posted under Programming » 4 comments

Dalsze badania nad relacją zawierania

2007-08-03 20:09

Nie tak dawno temu w notce Trzy rozwiązania dla relacji zawierania zastanawiałem się nad tym, jak elegancko i odpornie (na błędy) zrealizować interfejs klasy-kontenera zawierającego różne elementy. Po jakimś czasie jednak zapomniałem o całej sprawie, gdyż zająłem się menedżerem fontów, gdzie kwestia ta nie była mi potrzebna.
W końcu przyszła pora na zastanowienie się nad systemem GUI i wtedy przypomniałem sobie o tym problemie. Rezultatem tego jest wątek na forum Warsztatu.

Wywiązała się w nim ciekawa dyskusja, ale ostatecznie tylko odrobinę przybliżyła mnie do dokonania jakichś decyzji. Bardziej interesujące są może nieco inne wnioski, które przy tej okazji wyciągnąłem… Tak, chciałbym sobie teraz trochę pofilozofować :)
Zaskoczyło mnie trochę to, że na proste i jednoznaczne pytanie (“Które rozwiązanie jest lepsze?”) w dziedzinie tak zdawałoby się ścisłej jak programowanie nie ma dobrze uzasadnionej i obiektywnej odpowiedzi. Właściwie wszystkie argumenty, jakie tam padały były wielce subiektywne, na zasadzie: “wydaje mi się”, “bardziej naturalne będzie”, “najbardziej logiczne jest”, itd.
Okazało się, że programowanie – a zwłaszcza faza projektowania – wcale nie musi być taką ścisłą dziedziną, a każdy jego wytwór może mieć indywidualny charakter. Oczywiście, spora część podejmowanych przy okazji decyzji ma inny charakter niż dylemat, czy ładniejszy jest kolor zielony czy czerwony. Zdarzają się jednak i takie, gdzie kryteria są niejasne, nieostre i subiektywne.

Prostota, przejrzystość, elegancja… estetyka… piękno? Czy to dowód, że programowanie można w pewnym stopniu uważać za dziedzinę sztuki?

Tags: ,
Author: Xion, posted under Programming, Thoughts » Comments Off on Dalsze badania nad relacją zawierania

Trzy rozwiązania dla relacji zawierania

2007-07-25 13:47

Chcę dzisiaj zająć się pewną sprawą natury projektowej. Mam tu mianowicie na myśli sytuację, gdy mamy do czynienia ze zbiorem różnych elementów, wywodzących się z tej samej klasy bazowej. Najlepiej widać to będzie na odpowiednim diagramie:

Relacja zawierania

 

Jest tu klasa kontenera (Container), która posiada m.in. zestaw różnych Elementów. Ich dokładne typy nie są i nie muszą być znane – ważne, że wywodzą się od klasy bazowej Element. W szczególności klasy Container i Element mogą być jednym i tym samym – wtedy będziemy mieli do czynienia ze strukturą hierarchiczną, reprezentującą np. drzewo węzłów dokumentu XML czy kontrolki systemu GUI. Zauważmy jeszcze, że każdy element wie o swoim właścicielu (pole Owner), czyli o pojemniku, który go zawiera (albo o elemencie nadrzędnym w przypadku drzewa).
Z obsługą już dodanych elementów nie ma zbytniego problemu, jako że wyjątkowo naturalne jest tu wykorzystanie metod wirtualnych i polimorfizmu. Kłopot polega właśnie na tworzeniu i dodawaniu nowych elementów – a ściślej na interfejsie (funkcjach i parametrach), które mają do tego służyć. I tutaj właśnie pojawiają się te trzy rozwiązania.

Rozwiązanie pierwsze polega na wyposażeniu klasy Container w garnitur metod zajmujących się tworzeniem każdego typu elementów i dodawaniem ich do zbioru. W naszym przypadku byłyby to więc funkcje AddElementA, AddElementB i AddElementC:

  1. void Container::AddElementA(parametry)
  2. {
  3. Elements.insert (new ElementA(this /*właściciel nowego elementu */, parametry);
  4. }

Jak widać w tym przypadku dobrze jest wyposażyć konstruktory klasy Element i pochodnych w parametr, przez który będziemy podawali wskaźnik do nadrzędnego kontenera lub elementu.

Drugie rozwiązanie zakłada, że pojemnik dysponuje tylko jedną ogólną metodą AddElement. Przekazujemy jej już utworzone obiekty, a jej zadaniem jest tylko dodać je do już posiadanych:

  1. Container->AddElement (new ElementA(parametry));

Zauważmy, że tutaj konstruktory elementów już nie przyjmują wskaźników na swoich właścicieli. Dzięki temu możemy uchronić się przed omyłkowym dodaniem elementu do innego pojemnika niż ten, który podaliśmy podczas jego tworzenia.
Skąd jednak element wie, do kogo należy?… Za to odpowiada już metoda dodająca, która modyfikuje pole określające właściciela:

  1. void Container::AddElement (Element* pElem)
  2. {
  3. Elements.insert (pElem);
  4. pElem->m_pOwner = this;
  5. }

Oczywiście w tym celu klasa Container musi być zaprzyjaźniona z klasą bazową Element, co na szczęście w C++ nie stanowi problemu :) (W innych językach, jak np. C# czy Java, można osiągnąć bardzo podobny efekt przy pomocy pakietów.)

W końcu trzecie wyjście przenosi cały ciężar pracy na same elementy. W tej koncepcji to elementy same się dodają do podanego pojemnika:

  1. // konstruktor
  2. Element::Element(Container* pCont, parametry) : m_pCont(pCont)
  3. {
  4. if (pCont) pCont->AddElement (this);
  5. }

Żeby miało to sens, muszą być one aczkolwiek tworzone dynamicznie poprzez operator new.

Czas na nieodparcie nasuwające się pytanie, czyli… które rozwiązanie jest najlepsze? No cóż, sprawa nie jest taka prosta (inaczej bym o niej nie pisał ;D), więc może przyjrzyjmy się kilku argumentom:

  • Rozwiązanie nr 1 jest dość powszechnie stosowane zwłaszcza tam, gdy elementy mają ustalone i raczej niezmienne typy. Przykładem jest wspomniane drzewko węzłów XML: tam wachlarz typów ogranicza się do tekstu, elementów XML, atrybutów, sekcji CDATA, instrukcji przetwarzania i jeszcze paru innych. Napisanie dla każdego z nich odpowiedniej metody Add* nie jest specjalnie czasochłonne.
  • Trzecie rozwiązanie jest najwygodniejsze i daje najmniejsze pole manewru, jeżeli chodzi o popełnienie błędów – np. stworzenie obiektu i niedodanie go do pojemnika. Z drugiej strony może ono produkować nieintuicyjny kod. Jeżeli bowiem chcemy tylko dodać jakiś element i nie jest on nam za chwilę potrzebny, to użyjemy poniższego kodu:
    1. new ElementA(pContainer, parametry);

    Jest więc new, którego rezultat ignorujemy i tylko po dłuższym przyjrzeniu się można zauważyć, że to jednak nie jest wyciek pamięci.

  • Zarówno trzecie, jak i (zwłaszcza) drugie rozwiązanie dopuszcza tworzenie elementów “wolnych”, nieprzypisanych do żadnego pojemnika. Wbrew temu co napisałem przed chwilą, taka czynność nie musi być wcale błędem. W takim przypadku odwołanie do właściciela w elemencie ustawiamy po prostu na NULL.

Cóż więc wybrać? Ostatnio skłaniam się ku drugiemu rozwiązaniu. Według mnie jest ono z tych trzech najbardziej przejrzyste – tworzenie obiektu to jedno (operator new), a jego dodawanie drugie (metoda Add). Ponadto dzieli ono pracę w miarę równo między element, jak i pojemnik – w przeciwieństwie do pozostałych modeli. I w końcu, nic w tym rozwiązaniu nie jest robione za plecami programisty, co oczywiście może być zaletą, ale i wadą oznaczającą, że przecież wszystko musimy zrobić samemu.

A zatem… kwadratura koła? W takich sytuacjach warto chyba iść na kompromis, czyli poprzeć prostokąt z zaokrąglonymi rogami ;)

Tags: , ,
Author: Xion, posted under Programming » 2 comments
 


© 2023 Karol Kuczmarski "Xion". Layout by Urszulka. Powered by WordPress with QuickLaTeX.com.