Wtyczki do programów w .NETRozszerzalność od dawna jest w modzie: praktycznie żadna poważniejsza aplikacja nie obywa się bez jakiegoś systemu pluginów, czyli "wtyczek" zwiększających jej funkcjonalność. Niektóre robią to przy okazji (acz ze słusznych powodów), inne czynią z elastyczności i rozszerzalności swój główny oręż (patrz np. uniwersalne komunikatory w typie Mirandy). Wszystko to kojarzy się trochę linuksiarsko, jednakże obecnie także wiele programów stricte pod Windows zachęca (a przynajmniej umożliwia) swoich użytkownika do zakasania rękawów i zakodowania im nowych funkcji.
Dotyczy to także aplikacji na platformę .NET. Co więcej, sama jej natura ułatwia budowanie programów wspierających koncepcję rozszerzalności. Rzecz opiera się na pojęciu assembly, czyli czegoś w rodzaju pakietu (javowe skojarzenia wskazane) zawierającego kod, a więc klasy. Ze względu na to, że .NET Framework zawiera wbudowany kompilator, jest zupełnie możliwe, by nasz program udostępniał całe środowisko do pisania do niego pluginów, a następnie przerabiania ich na kod wykonywalny i podłączania ich do aplikacji. Nie twierdzę, że znam program, który rzeczywiście tak robi, ale przynajmniej w teorii jest to możliwe :)
Powszechniejsze wydaje mi się prostsze podejście, w którym assembly dostarcza się w postaci skompilowanej. Zazwyczaj jest to biblioteka .NET-owych, zapisana jako plik .dll, niemający przy tym zbyt wiele wspólnego z natywnymi czy COM-owymi DLL-ami. Wczytanie go jest bardzo proste, gdy wykorzystujemy do tego klasę System.Reflection.Assembly:
Wskazana jest tutaj ostrożność np. w postaci zapewnienia, że każde assembly ładujemy tylko raz. Najlepiej jest wyznaczyć osobny podkatalog na wtyczki do naszego programu i ładować je jednokrotnie, zapewne podczas uruchamiania samej aplikacji.
Gdy mamy już gotowy obiekt klasy Assembly (niezależnie od tego, czy skompilowaliśmy go sami czy wczytaliśmy go z gotowej binarki tak, jak powyżej), chcielibyśmy pewnie jakoś wykorzystać zawarty w nim kod. Są nim oczywiście jakieś klasy, które możemy pobrać metodą GetExportedTypes. Zwróci nam ona metody tablicę obiektów Type, czyli znanej zapewne metaklasy należącej do .NET-owego systemu refleksji. Z nimi zaś zrobić możemy... no, prawie wszystko :) Do interesujących w kontekście pluginów czynności należy przede wszystkim sprawdzenie, czy dana klasa implementuje jakiś ustalony przez nas interfejs "wtyczkowy", a następnie utworzenie jej obiektu:
Takie beztroskie, dynamiczne tworzenie obiektów z binarnego kodu wczytanego już w trakcie działania programu jest jak najbardziej możliwe i, jak widać wyżej, całkiem proste - stosujemy do tego metodę CreateInstance klasy o wdzięcznej nazwie Activator. Wymogiem jest obecność w klasie wtyczki odpowiedniego konstruktora. W powyższym kodzie zakładamy na przykład najprostszy przypadek, iż dostępna jest jego wersja bezparametrowa.
To w sumie wszystko, jeśli chodzi o wczytywanie. To, w jaki sposób plugin może rozszerzać możliwości naszej aplikacji, zależy głównie od zawartości interfejsu, który nazwałem tutaj umownie IPlugin. Projektując go, powinno się zadbać o jego elastyczność i prostotę, a także wziąć pod uwagę chociażby kwestie bezpieczeństwa.
Co może udawać obiekt w C++Możliwości przeciążania operatorów w C++ dla własnych typów obiektów sprawiają, że mogą one (tj. te obiekty) zachowywać się w bardzo różny sposób. Mogą na przykład "udawać" pewne wbudowane konstrukcje językowe, nierzadko wykonując ich zadania lepiej i wygodniej. Przykładów na to można podać co najmniej kilka - oto one:
(). Jest on na tyle elastyczny, że może przyjmować dowolne parametry i zwracać dowolne rezultaty, co pozwala nawet na stworzenie więcej niż jednego sposobu "wywoływania" danego obiektu. Jednym z bardziej interesujących zastosowań dla tego operatora jest implementacja w C++ brakującego mechanizmu delegatów, czyli wskaźników na metody obiektów.vector. Wymagane jest tu przeciążenie operatora indeksowania []. Daje ono wtedy dostęp do elementów obiektu-pojemnika, który zresztą nie musi być wcale indeksowany liczbami, jak w przypadku tablic wbudowanych (dowodem jest choćby kontener map). Ograniczeniem jest jedynie to, że indeks może być (naraz) tylko jeden, bo chociaż konstrukcja typu:
jest składniowo najzupełniej poprawna, to działa zupełnie inaczej niż można by było się spodziewać :)
* (w wersji jednoargumentowej) i ->. Normalnie te operatory nie mają zastosowania wobec obiektów, ale można nadać im znaczenie. Wtedy też mamy obiekt, który zachowuje się jak wskaźnik, czego przykładem jest choćby auto_ptr z STL-a czy shared_ptr z Boosta.ifów i pętli. Sprytne wskaźniki zwykle to robią, a innymi wartymi wspomnienia obiektami, które też takie zachowanie wykazują, są strumienie z biblioteki standardowej. Jest to spowodowane przeciążeniem operatora logicznej negacji ! oraz konwersji na bool lub void*.Reasumując, w C++ dzięki przeciążaniu operatorów możemy nie tylko implementować takie "oczywiste" typy danych jak struktury matematyczne (wektory, macierze, itp.), ale też tworzyć własne, nowe (i lepsze) wersje konstrukcji obecnych w języku. Szkoda tylko, że często jest to wręcz niezbędne, by w sensowny sposób w nim programować ;)
Pola i akcesory wewnątrz klasyZgodnie z zasadami programowania obiektowego pola klas nie powinny być bezpośrednio dostępne na zewnątrz. Należy jest zawsze opakowywać w akcesory: właściwości lub krótkie metody typu get i set. Z nich właśnie korzysta potem kod zewnętrzny, dzięki czemu nie może on (w dobrze napisanej klasie) niczego zepsuć poprzez - chociażby - ustawienie jakiegoś pola na nieprzewidzianą wartość.
Taka praktyka jest powszechnie przyjęta i raczej nie budzi wątpliwości. Inaczej jest z używaniem tychże pól lub akcesorów wewnątrz klasy, a więc w jej własnych metodach. Tutaj często mamy wybór: czy odwołać się do "gołego" pola, czy też poprzez odpowiednią właściwość/metodę.
Które podejście jest właściwsze? C#/.NET od wersji 3.0 zdaje się to rozstrzygać, umożliwiając półautomatyczne tworzenie właściwości:
Nie ma tutaj nie tylko bloków get i set, ale i ukrytą pod tą właściwością pola. Przy korzystaniu z tego feature'a żadnego dylematu więc nie ma.
Wydaje mi się jednak, że wybór nie jest taki oczywisty i że niekoniecznie należy używać akcesorów wewnątrz klasy. Argumentem przeciw, który od razu przychodzi do głowy, jest troska o wydajność - zwykle jednak przesadzona, bo proste gettery i settery są bez problemu rozwijane w miejscu użycia. Drugim 'ale' jest wygląd kodu w językach bez właściwości; zwłaszcza dotyczy to Javy, w której odpowiednik C#-owego:
roiłby się od getów. W końcu można by się jeszcze pokusić o uzasadnienie na wpół merytoryczne: skoro bądź co bądź prywatne pole jest składnikiem klasy do jej wyłącznej dyspozycji, to dlaczego metody miałyby obchodzić je dokoła zamiast odwoływać się doń bezpośrednio? A może jednak lepiej jest skorzystać z tej dodatkowej warstwy pośredniczącej (mogącej np. wykrywać jakieś błędy)?...
Na razie - mimo całkiem przyzwoitego doświadczenia w programowaniu w językach wszelakich - trudno jest mi na te pytania odpowiedzieć. Ostatnio aczkolwiek skłaniam się ku bezpośredniemu dostępowi do pól w metodach klas. Chętnie poznałbym jednak opinie innych koderów na ten temat.
Obiektowy szowinizmOd jakiegoś czasu najpopularniejszymi językami programowania są albo te stricte obiektowe, albo chociaż oferujące przynajmniej najważniejsze cechy tego modelu programowania (czyli dziedziczenie, polimorfizm i metody wirtualne). Prawdopodobnie większość powstającego kodu realizuje więc założenia OOP-u co najmniej w takim stopniu, że można go uznać za spełniający paradygmat obiektowy - albo chociaż podpadający pod obiektowość bardziej niż pod cokolwiek innego.
Ano właśnie: warto od czasu do czasu zauważyć, że oprócz OOP-u istnieje też "coś innego", i że obiektowy model programowania wcale nie jest jedynym. Nie musi być on też ani ostatecznym, ani najbardziej uniwersalnym. A już tym bardziej nie jest on jedynie słusznym.
Jako koderzy podążający za wytycznymi programowania obiektowego często jednak tak uważamy - mniej lub bardziej świadomie. Zwłaszcza, że obecnie można zajmować się programowaniem całkiem długo i nawet nie słyszeć o innych paradygmatach niż obiektowy. A jeśli nawet ktoś na podobne "ciekawostki" lub na "dziwaków" opowiadających się na innym podejściem do kodowania, to najpewniej nie będzie to miało żadnego wpływu na jego dobre samopoczucie i przekonanie o wyższości OOP-u nad czymkolwiek innym.
Bo przecież łatwo znaleźć programistów myślących bardzo podobnie i bez wysiłku wyciągnąć całe mnóstwo silnych argumentów na poparcie swoich racji. Bodaj najczęściej wykorzystywanym jest ten mówiący o odpowiednim poziomie abstrakcji, który jest jakoby immanentną cechą programowania obiektowego. Z jednej strony jest on bowiem wyższy od brzydkiego programowania strukturalnego, dzięki czemu kod obiektowy łatwiej jest napisać i zrozumieć. Jednocześnie nie jest to też poziom zbyt wysoki, przez co nadal wiadomo, jak nasz program działa i jak z grubsza sprawuje się pod względem wydajnościowym. Tego samego nie można rzecz jasna powiedzieć wtedy, gdy kodujemy funkcyjnie czy deklaratywnie.
Wszystko to brzmi rozsądnie i wydaje się słuszne. W rzeczywistości jednak posługiwanie się takimi kryteriami jest zwykłym nadużyciem. Na takiej zasadzie można by narzekać na to, że nie da się przygotować omletu przy pomocy samego widelca (że pozwolę sobie posłużyć się analogią kulinarną ;]). Należy bowiem zawsze mieć na uwadze to, o jakim zastosowaniu mówimy. Programowanie nie jest przecież dziedziną abstrakcyjną, ale jak najbardziej praktyczną, niezależnie od tego, jak bardzo usiłowalibyśmy ją steoretyzować.
Dlatego też absurdem jest twierdzenie o wyższości programowania obiektowego nad innymi paradygmatami. Bo czy bawilibyśmy się w tworzenie klas w przypadku takiego oprogramowania jak firmware procesora czy karty graficznej, działającego w ścisłym powiązaniu ze sprzętem? I czy odpowiadałoby nam, gdyby zamiast określania za pomocą znaczników wyglądu fragmentów strony WWW bylibyśmy zmuszeni tworzyć dla każdego z nich osobny obiekt i ustawiać jego właściwości?...
Nie zamykajmy się więc w swoim obiektowym światku, nawet jeśli czujemy się w nim nadzwyczaj dobrze. Znajomość innych sposobów kodowania może nam bowiem tylko pomóc.
Niemal standardowe nazwy klas
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:
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 :)
Słowo kluczowe ‘base’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.:
class CBar : public CFoo
{
public:
void Do()
{
// ...
CFoo::Do(); // wywołanie odziedziczonej wersji metody
}
};
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:
a wówczas zyskamy swoje własne "słowo kluczowe" base o możliwościach podobnych do tych z C#:
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:
powinna zdać egzamin dla dowolnej klasy T.
Referencje do klas w DelphiW 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:
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:
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;
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.