Od kilku miesięcy nic tu nie pisałem (oczywiście poza poprzednim nieplanowanym wpisem). Jak łatwo
się domyślić czas mi na to nie pozwalał. Z jednej strony natłok zadań w pracy
(stabilizacja finalnej wersji – Comarch
ALTUM ujrzał niedawno światło dzienne na tegorocznym CeBicie)
a z drugiej strony projekt, który chciałbym opisać w tym tekście.
Pierwsze odcinki z serii Build
your own CAB pojawiły się już dość dawno, a ponieważ Jeremy jest moim
ulubionym bloggerem, to śledzę je od początku z zainteresowaniem i nie mogę się
doczekać następnych (ciekaw jestem ilu z Was także czyta regularnie jego teksty).
Osobiście jako zawodowy programista mam jeszcze niewielką praktykę w budowaniu
złożonych aplikacji, ale czytając ciągle różne teksty, pojawiające się na
blogach, moja głowa wypełnia się wiadomościami teoretycznymi, a po przeczytaniu
wszystkich dostępnych dotąd artykułów z Serii zapragnąłem tę wiedzę wykorzystać
w praktyce.
Pracę jako programista zacząłem właśnie od
aplikacji budowanej w oparciu o Composite
Application Block, pracuję z nią już ponad rok, a Seria Jeremy’ego
pozwoliła mi z dystansu spojrzeć na rozwiązania zastosowane w CAB, lepiej je
zrozumieć i wykorzystać. Ponadto chęć poszerzania swoich doświadczeń i
umiejętności pchnęła mnie dalej. W Sieci można znaleźć pomysł, by Serię
opatrzyć przykładami.
Ten pomysł zachęcił mnie by wykorzystać zdobytą wiedzę w praktyce i wykonać
jakiś projekt open source. Tak na marginesie trzeba zauważyć, że i tak najlepszym
przykładem jest projekt Story Teler
Jeremy’ego, z którego to pochodzi większość przykładów, które możemy zobaczyć w
Serii.
Pomysł ten pojawił się w momencie, gdy szukałem
trochę wolnego czasu by zająć się projektem dotyczącym Team Foundation Server
Workitem Tracking, czyli po prostu zarządzaniem workitemami. Na co dzień
pracuję w środowisku TFS i do tego celu używam programu Fissum. Program ten
jest naprawdę sprytny i pozwala mi lepiej wykorzystać mój czas. Gdy pobieram
wersję z repozytorium, bądź też kompiluję projekt, Team Explorer nie nadaje się
do użytku, tak jak i całe Visual Studio.
Są po prostu zablokowane. Inną sprawą jest to, że TE w ogóle jest toporny i
wolny - czytałem, że w VS2008 ma być o niebo lepiej. Pożyjemy, zobaczymy.
Program zewnętrzny, taki jak Fissum, powala mi w tym czasie zająć się moimi workitemami.
W szczególności najczęściej korzystam z niego w fazie stabilizacji, w której
zwykle mam do czynienia z błędami, które muszę poprawić. Dzięki niemu w czasie,
gdy Visual Studio jest zajęte, mogę na boku analizować przychodzące błędy jak i
weryfikować te już istniejące. To jest mój sposób na optymalizację czasu, a
czas przecież trzeba szanować (szczególnie, gdy harmonogram ciśnie).
Można zapytać: skoro tak dobrze Ci się pracuje z Fissum,
to czemu chcesz pisać swoje oprogramowanie? Przede wszystkim chodzi o naukę.
Generalnie aplikacja ma być prosta, jednak z drugiej strony będzie na tyle
skomplikowana, że większość tematów poruszanych w Serii znajdzie w niej
zastosowanie - chociażby implementacja UI, komend, stanu menu, zakładek itd. Drugim
celem było zapoznanie się z narzędziami, które od dawna czekają na półce - mam
tu na myśli jakiś kontener IoC (w moim wypadku StructureMap) oraz bibliotekę
do logowania (Logging
Application Block już widziałem w akcji
u Arka, więc wypadałoby z niego skorzystać). Trzecim powodem jest to, że
projekt Fissum mimo że jest otwarty (open source), to jednak jest zamknięty. Program
ten zawiera kilka rzeczy, które mi przeszkadzają. O jednej już pisałem.
Ponadto mam kilka pomysłów jak można by było go usprawnić (generalnie chodzi o
użyteczność a nie o funkcjonalność). Proponowałem Miiitchowi swoją pomoc jednak
powiedział mi, że jest to jego prywatny projekt, na którym realizuje swoje
pomysły dotyczące TFS. Trzeba to uszanować. Na początku modyfikowałem Fissum
do swoich potrzeb, ale jest to uciążliwe do utrzymania, gdy Miiitch wydaje nowe
wersje.
W niniejszym tekście chciałbym opisać kilka problemów,
które napotkałem budując TfsSpotlight oraz znalezionych dzięki Serii
rozwiązań. Jakoś tak wyszło, że zacząłem trochę od końca. W swojej Serii Jeremy
nie poruszył jeszcze tematu Application Shell, a w momencie gdy pisałem
pierwszą wersję aplikacji nie było też tekstu o Command
Executor, a aby zbudować podstawę, trzeba było zacząć właśnie od tych
elementów. Dlatego właśnie rozwiązania zawarte w TfsSpotlight dotyczące tych
zagadnień są tylko i wyłącznie mojego pomysłu.
Oddzielenie logiki aplikacji od formatek
Po prezentacji
Wojtka o MVC chyba każdy się ze mną zgodzi, że budując chociażby kalkulator
należy oddzielić logikę dziedziny od sposobu prezentacji. Pierwszą i dla mnie
najważniejszą zaletą takiego podejścia jest możliwość łatwego przetestowania
logiki bez konieczności angażowania w ten proces komponentów graficznych. Nie
napisałem tego wcześniej, ale w tym projekcie jednym z większych dla mnie
wyzwań jest zadanie pisania testów jednostkowych dla jak największych części
kodu. Co z tego wyjdzie – zobaczymy.
Od dłuższego czasu przy budowaniu formatek
korzystam z pewnego wariantu wzorca Model-View-Presenter. Mówię tu wariantu,
gdyż, tak jak już wspominał Wojtek na swojej prezentacji, nie ma jednej
najlepszej implementacji wzorca MVP. W moim wykonaniu widok odpowiada wzorcowi
PasiveView (Fowler,
Jeremy),
gdyż jest to postać najbardziej przyjazna testowaniu, bo zawiera minimalną
ilość logiki. Prezenter jest główną jednostką dowodzącą, która zawiera wszelką
logikę prezentacji i obsługi danego widoku - SupervisingController (Fowler,
Jeremy).
Z modelem natomiast bywa różnie. W prostych przypadkach są to bezpośrednio
encje reprezentujące dane, na których dany widok pracuje. W przypadkach
bardziej skomplikowanych preferuję oddzielną klasę modelu, która w
szczególności zawiera logikę obsługi danych tj. wczytywanie, zapisywanie. Ma to
szczególne znaczenie, gdy widok ma wiele źródeł danych (np. listy wyboru, które
również trzeba zasilić danymi z bazy). W takich przypadkach model zajmuje się przygotowaniem
wszystkich potrzebnych zestawów danych i odciąża tym prezentera. W niniejszym
projekcie taka skomplikowana sytuacja jeszcze nie zaszła, gdyż biblioteki TFS
dostarczają nam gotowych kontrolek do reprezentacji całych elementów, toteż
zostałem zwolniony z konieczności implementowania ich własnoręcznie.
Wykorzystując MVP preferuję podejście bazujące na
bezpośrednim odwoływaniu się widoku do prezentera. W porównaniu z podejściem
opartym na zdarzeniach jest to podejście znacznie prostsze. Przede wszystkim
dlatego, że prostsza jest nawigacja po kodzie. Mając bezpośrednie odwołania do
metod możemy wykorzystać narzędzia nawigacyjne, jakie daje nam Visual Studio, i
przemieszczać się z widoku do prezentera dwoma kliknięciami myszy. Sprawa się
trochę komplikuje, jeżeli prezenter jest opisany interfejsem. Wtedy niestety VS
sobie nie radzi i pokazuje nam implementację interfejsu, a nie kod konkretnego
prezentera, czyli nie do końca tego, czego byśmy chcieli. Problemu tego nie
mają użytkownicy ReSharepera,
którego genialna funkcja Go to inheritor
pozwala natychmiast przemieścić się do klasy implementującej dany interfejs.
Poza tym implementacja zdarzeń wymaga od nas o wiele większego nakładu pracy.
Należy przecież w widoku zaimplementować zdarzenia dla każdej możliwej do
wykonania operacji. Następnie w prezenterze trzeba do tych wszystkich zdarzeń
podpiąć odpowiednie metody. Jak dla mnie za dużo roboty.
Zarządzanie komendami
Pytanie jest proste: jak zarządzać komendami,
które użytkownik może wykonywać? Wymagania zwykle są następujące:
-
Komenda powinna być dostępna w wielu miejscach aplikacji: menu główne,
menu kontekstowe, ikona na pasku narzędzi, skrót klawiszowy, wywołanie
z kodu programu nawet w innym module.
-
Zarządzanie stanem komendy – czy jest aktywna czy nie.
-
Sposób uruchomienia komendy – synchroniczne czy asynchroniczne – w samej implementacji
komendy chcielibyśmy abstrahować od sposobu jej uruchomienia.
Jak nie trudno się domyśleć bez spójnego
mechanizmu definiowania i obsługi komend szybko zabrniemy w ślepy zaułek i
rozwijanie aplikacji stanie się nieprzyjemne. Z resztą bez odpowiedniego
mechanizmu trudno będzie w prosty sposób zarządzać choćby stanem poszczególnych
komend. Z pomocą przychodzi nam bardzo prosty wzorzec – Komenda (Jeremy).
Wzorzec ten wprowadza interfejs komendy – ICommand – dzięki któremu możemy wykonanie
każdej komendy zunifikować do postaci wykonania jednej metody
ICommand.Execute(). Osobiście nigdy nie wykorzystywałem jeszcze tego podejścia,
znałem je jedynie z definicji. Pierwsze pytanie jakie mi się nasunęło to jak
dana komenda ma poznać swój kontekst (czyli dane na których ma pracować)?
Przecież metoda Execute nie przyjmuje żadnego parametru! Tutaj zrozumiałem, że
aby dobrze zaimplementować ten wzorzec trzeba do tego odpowiednio nasz system
przygotować.
Na przykład rozważmy interfejs programu, który
opiera się na zakładkach. Z reguły będziemy mieli jeden pasek z przyciskami i
jedno menu główne, gdzie umieścimy komendy dotyczące aktywnej zakładki. Aby móc
dostarczyć tym komendom kontekstu należy wprowadzić jakiś sposób pobierania
aktywnej zakładki, aby komenda mogła oddelegować do niej akcję. Podobnie należy
pomyśleć o innych elementach powłoki, do których będziemy chcieli mieć dostęp.
Wydzielenie odpowiednich serwisów pozwala szybko odpowiedzieć na pytanie „jak
dana komenda ma uzyskać interesujące ją dane”.
Kontynuując zadanie, potrzebny jest nam teraz
spójny sposób obsługi komend. Po pierwsze potrzebujemy jednolitego sposobu przypisywania
komend do kontrolek, a po drugie jakiegoś mechanizmu pozwalającego te komendy
uruchamiać. Zajmijmy się teraz pierwszym zadaniem, a drugie omówimy sobie
trochę później.
Od jakiegoś już czasu obserwuję w eterze coraz
częściej pojawiające się odwołania do tekstu Martina Fowlera o fluent interfaces.
Co więcej, zauważyć można pojawiające się implementacje wykorzystujące ten
sposób budowania interfejsów klas. Prawdę mówiąc, gdy po raz pierwszy
przeczytałem ten artykuł, idea bardzo mi się spodobała, ale nie widziałem
jeszcze konkretnego jej zastosowania. Dopiero później, używając Rhino Mocks
zauważyłem, że przecież używam właśnie fluent interface! I rzeczywiście, do
zadań konfiguracyjnych podejście to jest niezastąpione, a wynikowy kod jest
niesamowicie czytelny. Idąc za przykładem
Jeremy’ego konfigurację komend zaimplementowałem wykorzystując tę technikę. Z
wyniku jestem bardzo zadowolony, gdyż powstał naprawdę bardzo elastyczny, a
zarazem spójny mechanizm przypinania komend do interfejsu użytkownika. Sposób
implementacji możecie zobaczyć w kodzie (plik ConfigureMenuExpression.cs).
Poniżej przedstawię tylko wynik użycia tej klasy konfiguracyjnej.
Przykład [C#] 1. Przykład wykorzystania klasy
konfiguracyjnej wykorzystującej fluent interface.
ConfigureMenuExpression
.Execute(CommandsNames.ExitApplication)
.Synchronous()
.For(this.miExit)
.For(this.tsbExit)
.WithShortcut(Keys.F10)
.Enable();
ConfigureMenuExpression
.Execute(CommandsNames.SaveAllWorkItems)
.For(this.tsbSaveAll)
.WithShortcut(Keys.Control | Keys.Shift | Keys.S)
.Disable();
Dzięki zastosowaniu takiego mechanizmu udało mi się
zebrać całą logikę dotyczącą konfiguracji komend w jednym miejscu. Jak łatwo
zauważyć spełniłem większość wymagań postawionych wcześniej. Po pierwsze daną
komendę możemy przypisać do wielu elementów, możemy nadać skrót klawiszowy,
możemy oznaczyć jako operację synchroniczną (domyślnie komendy uruchamiane są w
trybie asynchronicznym) oraz możemy nadać komendzie początkowy stan. Wszystko w
jednym miejscu, wszystko czytelne do granic możliwości. Kontrastując to z
koniecznością odpalenia formatki w trybie projektowania, nawigowaniu po
elementach menu i sprawdzaniu w panelu właściwości czy podpięta jest
odpowiednia metoda i czy został dobrze zdefiniowany skrót klawiszowy widać jak
wiele zalet ma przedstawione tu podejście.
Powłoka – application shell
Chciałem, aby moja aplikacja składała się z okna
głównego (powłoki, ang. shell), które zawierać będzie podstawowe elementy takie
jak menu, pasek narzędzi z ikonami, pasek statusu oraz kontener na zakładki. W
odróżnieniu od
Fissum chciałem, aby
TfsSpotlight pracował cały czas w
jednym oknie, a za pomocą zakładek pozwalał na otwieranie wielu elementów
jednocześnie. Ponadto chciałem dać użytkownikowi bezpośredni dostęp do zapytań
zdefiniowanych dla danego projektu, co zaowocowało powstaniem panelu bocznego.
W moim odczuciu zadaniem powłoki jest
dostarczenie API pozwalającego na manipulację jej elementami. Dlatego właśnie
wydzieliłem z niej kilka serwisów (zarządzanie menu – komendami, zarządzanie
zakładkami, zarządzanie paskiem statusu oraz zarządzenie zakładkami).
Zdefiniowałem spójne interfejsy opisujące te usługi i dzięki temu dowolny
element systemu może mieć dostęp do elementów wspólnych.
Jak to wszystko ze sobą powiązać?
Każdy, kto choć trochę liznął wzorców
projektowych GoF, do tego problemu podszedłby z Singletonem pod pachą. Implementując
każdy z serwisów powłoki w postaci singletonu umożliwiamy innym elementom
systemu łatwy dostęp do instancji tych serwisów. Niestety singleton ma jedną
poważną wadę – bardzo, ale to bardzo mocno wiąże ze sobą klasy. Klasa, która
odwołuje się do elementów statycznych innej klasy jest z nią bardzo mocno
związana. Dlaczego nie chcemy takiego mocnego powiązania? Przecież miliony
programistów na całym świecie używają singletonów z powodzeniem. Otóż tak mocne
powiązania nie pozwalają efektywnie testować klas w izolacji, a przecież o to
właśnie chodzi w pisaniu testów jednostkowych – żeby poszczególne klasy
testować w izolacji. Zatem z mojego punktu widzenia i z punktu widzenia testów
jednostkowych silngleton wypada blado. Na marginesie należy dodać, że istnieją
narzędzia pozwalające testować takie sytuacje. Narzędziem takim jest TypeMock, które w środowisku praktyków TDD
jest dość kontrowersyjne.
Z pomocą przychodzi nam zasada odwracania
zależności (Dependency
Inversion Principle) i narzędzia pozwalające tę zasadę wprowadzać w życie,
czyli kontenery IoC (Inversion of Control). Osobiście w żadnym z moich
prywatnych projektów nie korzystałem jeszcze z tego typu narzędzi. Naturalnym
moim wyborem oczywiście jest StructureMap Jeremy’ego. W momencie, gdy piszę
ten tekst, na horyzoncie jest już Unity
ze stajni Microsoftu. David Hayden zrobił screencast
wprowadzający w jego użycie.
Zasada odwracania zależności mówi, że:
-
Moduły
wysokiego poziomu nie powinny być zależne od modułów niższego poziomu. I
jedne i drugie powinny być zależne od abstrakcji.
-
Abstrakcje
nie powinny być zależne od szczegółów. To szczegóły powinny być zależne od
abstrakcji.
Klasa A zależy od klasy B, gdy klasa A wymaga
obecności klasy B podczas kompilacji. Klasa A jest nazywana klientem natomiast
klasa B – usługą. O zależnościach między klasami pisał już
Stefan Jungmayr. Jungmayr
wyróżnił dwa rodzaje zależności:
- Zależność
od typu (ang.
dependen