3660


Rozdział 11.

Analiza i projektowanie zorientowane obiektowo

Gdy skoncentrujesz się wyłącznie na składni języka C++, łatwo zapomnisz, dlaczego techniki te są używane do tworzenia programów.

Z tego rozdziału dowiesz się, jak:

- używać analizy zorientowanej obiektowo w celu zrozumienia problemów, które próbujesz rozwiązać,

- używać modelowania zorientowanego obiektowo do tworzenia stabilnych, pewnych i możliwych do rozbudowania rozwiązań,

- używać zunifikowanego języka modelowania (UML, Unified Modeling Language) do dokumentowania analizy i projektu.

Budowanie modeli

Jeśli chcemy ogarnąć złożony problem, musimy stworzyć „model świata”. Zadaniem tego modelu jest symboliczne przedstawienie świata rzeczywistego. Taki abstrakcyjny model powinien być prostszy niż świat rzeczywisty, ale powinien poprawnie go odzwierciedlać, tak, aby na podstawie modelu można było przewidzieć zachowanie przedmiotów istniejących w realnym świecie.

Klasycznym modelem świata jest dziecięcy globus. Model ten nie jest tylko rzeczą; choć nigdy nie mylimy go z Ziemią, odwzorowuje on Ziemię na tyle dobrze, że możemy poznać jej budowę oglądając powierzchnię globusa.

W modelu występują oczywiście znaczne uproszczenia. Na globusie mojej córki nigdy nie pada deszcz, nie ma powodzi, trzęsień ziemi, itd., ale mogę go użyć, aby przewidzieć, ile czasu zajmie mi podróż z domu do Indianapolis, gdybym musiał osobiście stawić się w wydawnictwie i usprawiedliwić się, dlaczego rękopis się opóźnia („Wiesz, wszystko szło dobrze, ale nagle pogubiłem się w metaforach i przez kilka godzin nie mogłem się z nich wydostać”).

Metoda, która nie jest prostsza od modelowanej rzeczy, nie jest przydatna. Komik Steve Wright zażartował kiedyś: „Mam mapę, na której jeden cal równa się jednemu calowi. Mieszkam na E5.”

Projektowanie oprogramowania zorientowane obiektowo zajmuje się budowaniem dobrych modeli. Składa się z dwóch ważnych elementów: języka modelowania oraz procesu.

Projektowanie oprogramowania: język modelowania

Język modelowania jest najmniej znaczącym aspektem obiektowo zorientowanej analizy i projektowania; niestety, przyciąga on najwięcej uwagi. Język modelowania nie jest tylko niż konwencją, określającą sposób rysowania modelu na papierze. Możemy zdecydować, że trójkąty będą reprezentować klasy, a przerywane linie będą symbolizować dziedziczenie. Przy takich założeniach możemy stworzyć model geranium tak, jak pokazano na rysunku 11.1.

Rys. 11.1. Generalizacja - specjalizacja

0x01 graphic

Na tym rysunku widać, że Geranium jest szczególnym rodzajem Kwiatu. Jeśli zarówno ty, jak i ja zgodzimy się na rysowanie diagramów dziedziczenia (generalizacji - specjalizacji) w ten sposób, wtedy wzajemnie się zrozumiemy. Prawdopodobnie wkrótce zechcemy stworzyć model mnóstwa złożonych zależności, w tym celu opracujemy nasz złożony zestaw konwencji i reguł rysowania.

Oczywiście, musimy przedstawić nasze konwencje wszystkim osobom, z którymi pracujemy; będzie je musiał poznać każdy nowy pracownik lub współpracownik. Możemy współpracować z innymi firmami, posiadającymi własne konwencje, w związku z czym będziemy potrzebować czasu na wynegocjowanie wspólnej konwencji i wyeliminowanie ewentualnych nieporozumień.

Dużo wygodniej byłoby, gdyby wszyscy zgodzili się na wspólny język modelowania. (Wygodnie byłoby, gdyby wszyscy mieszkańcy Ziemi zgodzili się na używanie wspólnego języka, ale to już inne zagadnienie.) Takim lingua franca w projektowaniu oprogramowania jest UML - Unified Modeling Language (zunifikowany język modelowania). Zadaniem UML jest udzielenie odpowiedzi na pytania w rodzaju: „Jak rysować relację dziedziczenia?” Model geranium z rysunku 11.1 w UML mógłby zostać przedstawiony tak, jak na rysunku 11.2.

Rys. 11.2. Specjalizacja narysowana w UML

0x01 graphic

W UML klasy są rysowane w postaci prostokątów, zaś dziedziczenie jest przedstawiane jako linia zakończona strzałką. Strzałka przebiega w kierunku od klasy bardziej wyspecjalizowanej do klasy bardziej ogólnej. Dla większości osób taki kierunek strzałki jest niezgodny ze zdrowym rozsądkiem, ale nie ma to większego znaczenia; gdy wszyscy się na to zgodzimy, cały system zadziała poprawnie.

Szczegóły działania UML są raczej proste. Diagramy nie są trudne w użyciu i zrozumieniu; zostaną opisane w trakcie ich wykorzystywania. Choć na temat UML można napisać całą książkę, jednak w 90 procentach przypadków będziesz korzystał jedynie z małego podzbioru tego języka; podzbiór ten jest bardzo łatwy do zrozumienia.

Projektowanie oprogramowania: proces

Proces obiektowo zorientowanej analizy i projektowania jest dużo bardziej złożony i ważniejszy niż język modelowania. Oczywiście, słyszy się o nim dużo mniej. Dzieje się tak dlatego, że niezgodności dotyczące języka modelowania zostały już w dużym stopniu wyeliminowane; przemysł informatyczny zdecydował się na używanie UML. Debata na temat procesu wciąż trwa.

Metodolog jest osobą, która opracowuje lub studiuje jedną lub więcej metod. Zwykle metodolodzy opracowują i publikują własne metody. Metoda jest językiem modelowania i procesem. Trzech wiodących w branży metodologów to: Grady Booch, który opracował metodę Boocha, Ivar Jacobson, który opracował obiektowo zorientowaną inżynierię oprogramowania oraz James Rumbaugh, który opracował technologię OMT (Object Modeling Technology). Ci trzej mężczyźni stworzyli wspólnie tzw. Rational Unified Process (dawniej znany jako Objectory), metodę oraz komercyjny produkt firmy Rational Software Inc. Wszyscy trzej są zatrudnieni we wspomnianej wyżej firmie, gdzie są znani jako trzej przyjaciele (Three Amigos).

Ten rozdział przedstawia w ogólnym zarysie stworzony przez nich procesem. Nie będę szczegółowo go przedstawiał, gdyż nie wierzę w niewolnicze przywiązanie do akademickiej teorii - dużo bardziej niż postępowanie zgodne z metodą interesuje mnie sprzedanie produktu. Inne metody również dostarczają ciekawych rozwiązań, więc będę starał się wybierać z nich to, co wydaje mi się najlepsze i łączyć to użyteczną całość.

Proces projektowania oprogramowania jest iteracyjny. Oznacza to, że opracowując program „przechodzimy” przez cały proces wielokrotnie, coraz lepiej rozumiejąc jego wymagania. Projekt ukierunkowuje implementację, ale szczegóły, na które zwracamy uwagę podczas implementacji, wpływają z kolei na projekt. Nie próbujemy opracować jakiegokolwiek niebanalnego projektu w pojedynczym, uporządkowanym procesie liniowym; zamiast tego rozwijamy fragmenty projektu, wciąż poprawiając jego założenia oraz ulepszając szczegóły implementacji.

Opracowywanie iteracyjne można odróżnić od opracowywania kaskadowego. W opracowywaniu kaskadowym wynik jednego etapu staje się wejściem dla następnego, przy czym nie istnieje możliwość powrotu (patrz rysunek 11.3). W procesie opracowywania kaskadowego wymagania są szczegółowo przedstawione klientowi i podpisane przez niego („Tak, właśnie tego potrzebuję”); następnie wymagania te są przekazywane projektantowi. Projektant tworzy projekt, po czym przekazuje go programiście, w celu implementacji. Z kolei programista wręcza kod osobie zajmującej się kontrolą jakości, która sprawdza jego działanie i przekazuje go klientowi. Wspaniałe w teorii, katastrofalne w praktyce.

Rys. 11.3. Model kaskadowy

0x01 graphic

Przy opracowywaniu iteracyjnym zaczynamy od koncepcji; pomysłu, jak moglibyśmy to zbudować. W miarę poznawania szczegółów nasza wizja może rozrastać się i ewoluować.

Gdy już dobrze znamy wymagania, możemy rozpocząć projektowanie, doskonale zdając sobie sprawę, że pytania, które się wtedy pojawią, mogą wprowadzić zmiany w wymaganiach. Pracując nad projektem, zaczynamy tworzyć prototyp, a następnie implementację produktu. Zagadnienia pojawiające się podczas opracowywania programu wpływają na zmiany w projekcie i mogą nawet wpłynąć na zrozumienie wymagań. Projektujemy i implementujemy tylko części produktu, powtarzając za każdym razem fazy projektowania i implementacji.

Choć poszczególne etapy tego procesu są powtarzane, jednak opisanie ich w sposób cykliczny jest prawie niemożliwe. Dlatego opiszę je w następującej kolejności: koncepcja początkowa, analiza, projekt, implementacja, testowanie, prezentacja. Nie zrozum mnie źle — w rzeczywistości, podczas tworzenia pojedynczego produktu przechodzimy przez każdy z tych kroków wielokrotnie. Po prostu proces iteracyjny byłby trudny do przedstawienia, gdybyśmy chcieli pokazać cykliczne wykonywanie każdego z kroków.

Oto kolejne kroki iteracyjnego procesu projektowania:

1. Konceptualizacja.

2. Analiza.

3. Projektowanie.

4. Implementacja.

5. Testowanie

6. Prezentacja.

Konceptualizacja to „tworzenie wizji”. Jest pojedynczym zdaniem, opisującym dany pomysł.

Analiza jest procesem zrozumienia wymagań.

Projektowanie jest procesem tworzenia modelu klas, na podstawie którego wygenerujemy kod.

Implementacja jest pisaniem kodu (na przykład w C++).

Testowanie jest upewnianiem się, czy wykonaliśmy wszystko poprawnie.

Prezentacja to pokazanie produktu klientom.

Bułka z masłem. Cała reszta to detale.

Kontrowersje

Pojawia się mnóstwo kontrowersji na temat tego, co dzieje się na każdym etapie procesu projektowania iteracyjnego, a nawet na temat nazw poszczególnych etapów. Zdradzę ci sekret: to nie ma znaczenia. Podstawowe kroki są w każdym obiektowo zorientowanym procesie takie same: dowiedz się, co chcesz zbudować, zaprojektuj rozwiązanie i zaimplementuj projekt.

Choć w grupach dyskusyjnych i listach mailingowych dyskutujących o technologii obiektowej dzieli się włos na czworo, podstawowa analiza i projektowanie obiektowe są niezmienne. W tym rozdziale przedstawię pewien punkt widzenia na ten temat, mając nadzieję, że utworzę w ten sposób fundament, na którym będziesz mógł stworzyć architekturę swojej aplikacji.

Celem tej pracy jest stworzenie kodu, który spełnia założone wymagania i który jest stabilny, możliwy do rozbudowania i łatwy do modyfikacji. Najważniejsze jest stworzenie kodu o wysokiej jakości (w określonym czasie i przy założonym funduszu).

Programowanie ekstremalne

Ostatnio pojawiła się nowa koncepcja analizy i projektowania, zwana programowaniem ekstremalnym. Programowanie to zostało omówione przez Kena Becka w książce Extreme Programming Expanded: Embrace Change (Addison-Wesley, 1999 ISBN 0201616416).

W tej książce Beck przedstawia kilka radykalnych i cudownych pomysłów, np. by nie kodować niczego, dopóki nie będzie można sprawdzić, czy to działa, a także programowanie w parach (dwóch programistów przy jednym komputerze). Jednak z naszego punktu widzenia, najważniejszym jego stwierdzeniem jest to, że zmianom ulegają wymagania. Należy więc sprawić, by program działał i utrzymywać to działanie; należy projektować dla wymagań, które się zna i nie tworzyć projektów „na wyrost”.

Jest to, oczywiście, ogromne uproszczenie wypowiedzi Becka i sądzę, że mógłby uznać je za przeinaczenie, jednak w każdym razie wierzę w jego sedno: spraw by program działał, tworząc go dla wymagań, które rozumiesz i staraj się nie doprowadzić do sytuacji, w której, programu nie można już zmienić.

Bez zrozumienia wymagań (analiz) i planowania (projekt), trudno jest stworzyć stabilny i łatwy do modyfikacji program, jednak staraj się nie kontrolować zbyt wielu czynności.

Pomysł

Wszystkie wspaniałe programy powstają z jakiegoś pomysłu. Ktoś ma wizję produktu, który uważa za warty wdrożenia. Pożyteczne pomysły rzadko kiedy powstają w wyniku pracy zbiorowej. Pierwszą fazą obiektowo zorientowanej analizy i projektowania jest zapisanie takiego pomysłu w pojedynczym zdaniu (a przynajmniej krótkim akapicie). Pomysł ten staje się myślą przewodnią tworzonego programu, zaś zespół, który zbiera się w celu zaimplementowania tego pomysłu, powinien podczas pracy odwoływać się do tej myśli przewodniej — w razie potrzeby nawet ją modyfikując.

Nawet jeśli pomysł narodził się w wyniku zespołowej pracy działu marketingu, „wizjonerem” powinna zostać jedna osoba. Jej zadaniem jest utrzymywanie „czystości idei”. W miarę rozwoju projektu wymagania będą ewoluować. Harmonogram pracy może (i powinien) modyfikować to, co próbujesz osiągnąć w pierwszej iteracji programowania, jednak wizjoner musi zapewnić, że wszystko to, co zostanie stworzone, odzwierciedli pierwotny pomysł. Właśnie jego bezwzględne poświęcenie i żarliwe zaangażowanie doprowadza do ukończenia projektu. Gdy stracisz z oczu pierwotny zamysł, twój produkt jest skazany na niepowodzenie.

Analiza wymagań

Faza konceptualizacji, w której precyzowany jest pomysł, jest bardzo krótka. Może trwać krócej niż błysk olśnienia połączony z czasem wymaganym do zapisania pomysłu na kartce. Często zdarza się, że dołączasz do projektu jako ekspert zorientowany obiektowo wtedy, gdy wizja została już sprecyzowana.

Niektóre firmy mylą pomysł z wymaganiami. Wyraźna wizja jest potrzebna, lecz sama w sobie nie jest wystarczająca. Aby przejść do analizy, musisz zrozumieć, w jaki sposób produkt będzie używany i jak musi działać. Celem fazy analizy jest sprecyzowanie tych wymagań. Efektem końcowym tej fazy jest stworzenie dokumentu zawierającego opracowane wymagania. Pierwszą częścią tego dokumentu jest analiza przypadków użycia produktu.

Przypadki użycia

Istotną częścią analizy, projektowania i implementacji są przypadki użycia. Przypadek użycia jest ogólnym opisem sposobu, w jaki produkt będzie używany. Przypadki użycia nie tylko ukierunkowują analizę, ale także pomagają w określeniu klas i są szczególnie ważne podczas testowania produktu.

Tworzenie stabilnego i wyczerpującego zestawu przypadków użycia może być najważniejszym zadaniem całej analizy. Właśnie wtedy jesteś najbardziej uzależniony od ekspertów w danej dziedzinie, to oni wiedzą najwięcej o dziedzinie pracy, której wymagania próbujesz określić.

Przypadki użycia są w niewielkim stopniu związane z interfejsem użytkownika, nie są natomiast związane z wnętrzem budowanego systemu. Każda osoba (lub system) współpracująca z projektowanym systemem jest nazywana aktorem.

Dokonajmy krótkiego podsumowania:

- przypadek użycia - opis sposobu, w jaki używane będzie oprogramowanie,

- eksperci - osoby znające się na dziedzinie, dla której tworzysz produkt,

- aktor - każda osoba (lub system) współpracująca z projektowanym systemem.

Przypadek użycia jest opisem interakcji zachodzących pomiędzy aktorem a samym systemem. W trakcie analizy przypadku użycia system jest traktowany jako „czarna skrzynka.” Aktor „wysyła komunikat” do systemu, po czym zwracana jest informacja, zmienia się stan systemu, statek kosmiczny zmienia kierunek, itd.

Identyfikacja aktorów

Należy pamiętać, że nie wszyscy aktorzy są ludźmi. Systemy współpracujące z budowanym systemem także są aktorami. Gdy budujesz na przykład bankomat, aktorami mogą być urzędnik bankowy i klient - a także inny system współpracujący z aktualnie tworzonym systemem, na przykład system śledzenia pożyczek czy udzielania kredytów studenckich. Oto podstawowa charakterystyki aktorów:

- są oni na zewnątrz dla systemu,

- współpracują z systemem.

Często najtrudniejszą częścią analizy przypadków użycia jest jej początek. Zwykle najlepszą metodą „ruszenia z miejsca” jest sesja burzy mózgów. Po prostu spisz listę osób i systemów, które będą pracować z nowym systemem. Pamiętaj, że mówiąc o ludziach, w rzeczywistości mamy na myśli role - urzędnika bankowego, kasjera, klienta, itd. Jedna osoba może pełnić więcej niż jedną rolę.

We wspomnianym przykładzie z bankomatem, na naszej liście mogą wystąpić następujące role:

- klient

- personel banku

- system bankowy

- osoba wypełniająca bankomat pieniędzmi i materiałami

Na początku nie ma potrzeby wychodzenia poza tę listę. Wygenerowanie trzech czy czterech aktorów może wystarczyć do rozpoczęcia generowania przypadków użycia. Każdy z tych aktorów pracuje z systemem w inny sposób; chcemy wykryć te interakcje w naszych sposobach użycia.

Wyznaczanie pierwszych przypadków użycia

Zacznijmy od roli klienta. Podczas burzy mózgów możemy określić następujące przypadki użycia dla klienta:

- klient sprawdza stan swojego rachunku,

- klient wpłaca pieniądze na swój rachunek,

- klient wypłaca pieniądze ze swojego rachunku,

- klient przelewa pieniądze z rachunku na rachunek,

- klient otwiera rachunek,

- klient zamyka rachunek.

Czy powinniśmy dokonać rozróżnienia pomiędzy „klient wpłaca pieniądze na swój rachunek bieżący” a „klient wpłaca pieniądze na lokatę”, czy też powinniśmy te działania połączyć (tak jak na powyższej liście) w „klient wpłaca pieniądze na swój rachunek?” Odpowiedź na to pytanie zależy od tego, czy takie rozróżnienie ma znaczenie dla danej dziedziny (dziedzina jest rzeczywistym środowiskiem, które modelujemy - w tym przypadku jest nią bankowość).

Aby sprawdzić, czy te działania są jednym przypadkiem użycia, czy też dwoma, musisz zapytać, czy ich mechanizmy są różne (czy klient w każdym z przypadków robi coś innego) i czy różne są wyniki (czy system odpowiada na różne sposoby). W naszym przykładzie, w obu przypadkach odpowiedź brzmi „nie”: klient składa pieniądze na każdy z rachunków w ten sam sposób, przy czym wynik także jest podobny, gdyż bankomat odpowiada, zwiększając stan odpowiedniego rachunku.

Zakładając, że aktor i system działają i odpowiadają mniej więcej identycznie, bez względu na to, na jaki rachunek dokonuje wpłaty, te dwa przypadki użycia są w rzeczywistości jednym sposobem. Później, gdy opracujemy scenariusze przypadków użycia, możemy wypróbować obie wariacje i sprawdzić, czy ich rezultatem są jakiekolwiek różnice.

Odpowiadając na poniższe pytania, możesz odkryć dodatkowe przypadki użycia:

1. Dlaczego aktor używa tego systemu?

Klient używa tego systemu, aby zdobyć gotówkę, złożyć depozyt lub sprawdzić bieżący stan rachunku.

2. Jakiego wyniku oczekuje aktor po każdym żądaniu?

Zwiększenia stanu rachunku lub uzyskania gotówki na zakupy.

3. Co spowodowało, że aktor używa w tym momencie systemu?

Być może ostatnio otrzymał wypłatę lub jest na zakupach.

4. Co aktor musi zrobić, aby użyć systemu?

Włożyć kartę do szczeliny w bankomacie.

Aha! Potrzebujemy przypadku użycia dla logowania się klienta do systemu.

5. Jakie informacje aktor musi dostarczyć systemowi?

Musi wprowadzić kod PIN.

Aha! Potrzebujemy przypadków użycia dla uzyskania i edycji kodu PIN.

6. Jakich informacji aktor oczekuje od systemu?

Stanu rachunku itd.

Często dodatkowe przypadki użycia możemy znaleźć, skupiając się na atrybutach obiektów w danej dziedzinie. Klient posiada nazwisko, kod PIN oraz numer rachunku; czy występują przypadki użycia dla zarządzania tymi obiektami? Rachunek posiada swój numer, stan oraz historię transakcji; czy wykryliśmy te elementy w przypadkach użycia?

Po szczegółowym przeanalizowaniu przypadków użycia dla klienta, następnym krokiem w opracowywaniu listy przypadków użycia jest opracowanie przypadków użycia dla wszystkich pozostałych aktorów. Poniższa lista przedstawia pierwszy zestaw przypadków użycia dla naszego przykładu z bankomatem:

- klient sprawdza stan swojego rachunku,

- klient wpłaca pieniądze na swój rachunek,

- klient wypłaca pieniądze ze swojego rachunku,

- klient przekazuje pieniądze z rachunku na rachunek,

- klient otwiera rachunek,

- klient zamyka rachunek,

- klient loguje się do swojego rachunku,

- klient sprawdza ostatnie transakcje,

- urzędnik bankowy loguje się do specjalnego konta przeznaczonego do zarządzania,

- urzędnik bankowy dokonuje zmian w rachunku klienta,

- system bankowy aktualizuje stan rachunku klienta na podstawie działań zewnętrznych,

- zmiany rachunku użytkownika są odzwierciedlane w systemie bankowym,

- bankomat sygnalizuje niedobór pieniędzy,

- technik uzupełnia w bankomacie gotówkę i materiały.

Tworzenie modelu dziedziny

Gdy masz już pierwszą wersję przypadków użycia, możesz zacząć wypełniać dokument wymagań szczegółowym modelem dziedziny. Model dziedziny jest dokumentem zawierającym wszystko to, co wiesz o danej dziedzinie (zagadnieniu, nad którym pracujesz). Jako część modelu dziedziny tworzysz obiekty dziedziny, opisujące wszystkie obiekty wymienione w przypadkach użycia. Przykład z bankomatem zawiera następujące obiekty: klient, personel banku, system bankowy, rachunek bieżący, lokata, itd.

Dla każdego z tych obiektów dziedziny chcemy uzyskać tak ważne dane, jak nazwa obiektu (na przykład klient, rachunek, itd.), czy obiekt jest aktorem, podstawowe atrybuty i zachowanie obiektu, itd. Wiele narzędzi do modelowania wspiera zbieranie tych informacji w opisach „klas.” Na przykład, rysunek 11.4 przedstawia sposób, w jaki te informacje są zbierane w systemie Rational Rose.

Rys. 11.4. Rational Rose

0x01 graphic

Należy zdawać sobie sprawę, że to, co opisujemy, nie jest obiektem projektu, ale obiektem dziedziny. Odzwierciedla sposób funkcjonowania świata, a nie sposób działania naszego systemu.

Możemy określić relacje pomiędzy obiektami dziedziny pojawiającymi się w przykładzie z bankomatem, używając UML - korzystając z takich samych konwencji rysowania, jakich użyjemy później do opisania relacji pomiędzy klasami w dziedzinie. Jest to jedna z ważniejszych zalet UML: możemy używać tych samych narzędzi na każdym etapie projektu.

Na przykład, używając konwencji UML dla klas i powiązań generalizacji, możemy przedstawić rachunki bieżące i rachunki lokat jako specjalizacje bardziej ogólnej koncepcji rachunku bankowego, tak jak pokazano na rysunku 11.5.

Rys. 11.5. Specjalizacje

0x01 graphic

Na diagramie z rysunku 11.5 prostokąty reprezentują różne obiekty dziedziny; zaś strzałki wskazują generalizację. UML zakłada, że linie są rysowane w kierunku od klasy wyspecjalizowanej do bardziej ogólnej klasy „bazowej.” Dlatego, zarówno Rachunek bieżący, jak i Rachunek lokaty, wskazują na Rachunek bankowy, informując że każdy z nich jest wyspecjalizowaną formą Rachunku bankowego.

UWAGA Pamiętajmy, że w tym momencie widzimy tylko zależności pomiędzy obiektami w dziedzinie. Później być może zdecydujesz się na zastosowanie w projekcie obiektów o nazwach RachunekBiezacy oraz RachunekBankowy i może odwzorujesz te zależności, używając dziedziczenia, ale będą to decyzje podjęte w czasie projektowania. W czasie analizy dokumentujemy jedynie obiekty istniejące w danej dziedzinie.

UML jest bogatym językiem modelowania i można w nim umieścić dowolną ilość relacji. Jednak podstawowe relacje wykrywane podczas analizy to: generalizacja (lub specjalizacja), zawieranie i powiązanie.

Generalizacja

Generalizacja jest często porównywana z „dziedziczeniem,” lecz istnieje pomiędzy nimi wyraźna, istotna różnica. Generalizacja opisuje relację; dziedziczenie jest programową implementacją generalizacji - jest sposobem przedstawienia generalizacji w kodzie. Odwrotnością generalizacji jest specjalizacja. Kot jest wyspecjalizowaną formą zwierzęcia, zaś zwierzę jest generalną formą kota lub psa.

Specjalizacja określa, że obiekt wyprowadzony jest podtypem obiektu bazowego. Zatem rachunek bieżący jest rachunkiem bankowym. Ta relacja jest symetryczna: rachunek bankowy generalizuje ogólne zachowanie i atrybuty rachunku bieżącego i rachunku lokaty.

Podczas analizowania dziedziny chcemy przedstawić te zależności dokładnie tak, jak występują w realnym świecie.

Zawieranie

Często obiekt składa się z wielu podobiektów. Na przykład samochód składa się z kierownicy, kół, drzwi, radia, itd. Rachunek bieżący składa się ze stanu, historii transakcji, identyfikatora klienta, itd. Mówimy, że rachunek bieżący posiada te elementy; zawieranie modeluje właśnie takie relacje posiadania. UML ilustruje relację zawierania za pomocą strzałki z rombem, wskazującej obiekt zawierany (patrz rysunek 11.6).

Rys. 11.6. Zawieranie

0x01 graphic

Diagram z rysunku 11.6 sugeruje, że rachunek osobisty posiada stan. Można połączyć oba diagramy, przedstawiając w ten sposób dość złożony zestaw relacji (patrz rysunek 11.7).

Rys. 11.7. Relacje pomiędzy obiektami

0x01 graphic

Diagram z rysunku 11.7 informuje, że rachunek bieżący i rachunek lokaty są rachunkami bankowymi oraz, że rachunki bankowe posiadają zarówno stan, jak i historię transakcji.

Powiązania

Trzecią relacją, wykrywaną zwykle podczas analizowania dziedziny, jest proste powiązanie. Powiązanie sugeruje, że dwa obiekty w jakiś sposób ze sobą współpracują. Ta definicja staje się dużo bardziej precyzyjna w fazie projektowania, ale w fazie analizy sugerujemy jedynie, że obiekt A współpracuje z obiektem B, i że jeden obiekt nie zawiera drugiego; a także, że żaden z nich nie jest specjalizacją drugiego. W UML powiązania między obiektami są przedstawiane jako zwykła prosta linia pomiędzy obiektami, co pokazuje rysunek 11.8.

Diagram z rysunku 11.8 wskazuje, że obiekt A w jakiś sposób współpracuje z obiektem B.

Rys. 11.8.

0x01 graphic

Tworzenie scenariuszy

Gdy mamy już gotowy wstępny zestaw przypadków użycia oraz narzędzi, dzięki którym możemy przedstawić relacje pomiędzy obiektami w dziedzinie, jesteśmy gotowi do uporządkowania przypadków użycia i zdefiniowania ich przeznaczenia.

Każdy przypadek użycia można „rozbić” na serie scenariuszy. Scenariusz jest opisem określonego zestawu okoliczności towarzyszących danemu przypadkowi użycia. Na przykład, przypadek użycia „klient wypłaca pieniądze ze swojego rachunku” może posiadać następujące scenariusze:

- klient żąda trzystu dolarów z rachunku bieżącego, otrzymuje gotówkę, po czym system drukuje kwit,

- klient żąda trzystu dolarów z rachunku bieżącego, lecz na rachunku znajduje się tylko dwieście dolarów. Klient jest informowany, że na koncie znajduje się zbyt mało środków, aby spełnić jego żądanie,

- klient żąda trzystu dolarów z rachunku bieżącego, ale tego dnia pobrał już sto dolarów, a limit dzienny wynosi trzysta dolarów. Klient jest informowany o problemie i może się zdecydować na pobranie jedynie dwustu dolarów,

- klient żąda trzystu dolarów z rachunku bieżącego, ale skończył się papier w drukarce kwitów. Klient jest informowany o problemie i może się zdecydować na pobranie pieniędzy bez potwierdzenia w postaci kwitu.

I tak dalej. Każdy scenariusz przedstawia wariant tego samego przypadku użycia. Często te sytuacje są sytuacjami wyjątkowymi (zbyt mało środków na rachunku, zbyt mało gotówki w bankomacie, itd.). Czasem warianty dotyczą niuansów w podejmowaniu decyzji w samym sposobie użycia (na przykład, czy przed podjęciem gotówki klient chce dokonać transferu środków).

Nie musimy analizować każdego ewentualnego scenariusza. Szukamy tych scenariuszy, które prezentują wymagania systemu lub szczegóły interakcji z aktorem.

Tworzenie wytycznych

Teraz, jako część metodologii, będziemy tworzyć wytyczne dla udokumentowania każdego ze scenariuszy. Te wytyczne znajdą się w dokumentacji wymagań. Zwykle chcemy, by każdy scenariusz zawierał:

- warunki wstępne - jakie warunki muszą być spełnione, aby scenariusz się rozpoczął,

- włączniki - co powoduje, że scenariusz się rozpoczyna,

- akcje, jakie podejmuje aktor,

- wyniki lub zmiany powodowane przez system,

- informację zwrotną otrzymywaną przez aktora,

- informacje o występowaniu cyklicznych operacji i o przyczynach ich wykonywania,

- schematyczny opis przebiegu scenariusza,

- okoliczności powodujące zakończenie scenariusza,

- warunki końcowe - jakie warunki muszą być spełnione w momencie zakończenia scenariusza.

Ponadto, każdemu sposobowi użycia i każdemu scenariuszowi powinno się nadać nazwę. Możesz spotkać się z następującą sytuacją:

Przypadek użycia:

Klient wypłaca pieniądze.

Scenariusz:

Pomyślne pobranie gotówki z rachunku bieżącego.

Warunki wstępne:

Klient jest już zalogowany do systemu.

Włącznik:

Klient żąda gotówki.

Opis:

Klient decyduje się na wypłacenie gotówki z rachunku bieżącego. Na rachunku znajduje się wystarczająca ilość środków, w bankomacie jest wystarczająco dużo pieniędzy i papieru na kwity, a sieć działa. Bankomat prosi klienta o podanie wysokości wypłaty, klient prosi o trzysta dolarów, co w tym momencie jest kwotą dozwoloną. Maszyna wydaje trzysta dolarów i wypisuje kwit; klient odbiera pieniądze i kwit.

Warunki końcowe:

Rachunek klienta jest obciążany kwotą trzystu dolarów, zaś klient otrzymuje trzysta dolarów w gotówce.

Ten przypadek użycia może zostać przedstawiony za pomocą prostego diagramu, pokazanego na rysunku 11.9.

Rys. 11.9. Diagram przypadku użycia

0x01 graphic

Ten diagram nie dostarcza zbyt wielu informacji, poza wysokopoziomową abstrakcją interakcji pomiędzy aktorem (klientem) a systemem. Diagram stanie się nieco bardziej użyteczny, gdy przedstawimy interakcję pomiędzy sposobami użycia. Tylko nieco bardziej użyteczny, gdyż możliwe są tylko dwie interakcje: <<korzysta z>> (<<uses>>) i <<rozszerza>> (<<extends>>). Stereotyp <<korzysta z>> wskazuje, że jeden przypadek użycia jest nadzestawem innego. Na przykład, nie jest możliwa wypłata gotówki bez wcześniejszego zalogowania się. Tę relację przedstawiamy za pomocą diagramu, pokazanego na rysunku 11.10.

Rys. 11.10. Stereotyp <<korzysta z>>

0x01 graphic

Rysunek 11.10 pokazuje, że przypadek użycia Wypłata Gotówki „korzysta z” przypadku użycia Logowanie i w pełni implementuje Logowanie jako część Wypłaty Gotówki.

Przypadek użycia <<rozszerza>> został opracowany w celu wskazania relacji warunkowych i częściowo odnosił się do dziedziczenia, ale wywoływał tyle nieporozumień wśród projektantów obiektowych (związanych z odróżnieniem go od <<korzysta z>>), że wielu z nich odrzuca go, uważając że nie jest wystarczająco dobrze zrozumiany. Ja używam <<korzysta z>> aby uniknąć kopiowania i wklejania całego przypadku użycia, a <<rozszerza>> używam wtedy, gdy korzystam z przypadku użycia tylko w określonych warunkach.

Diagramy interakcji

Choć diagram przypadku użycia może mieć ograniczoną wartość, można powiązać go z przypadkiem użycia, który może znacznie wzbogacić dokumentację i ułatwić zrozumienie interakcji. Na przykład wiemy, że scenariusz Wypłata Gotówki reprezentuje interakcję pomiędzy następującymi obiektami dziedziny: klientem, rachunkiem bieżącym oraz interfejsem użytkownika. Możemy przedstawić tę interakcję na diagramie interakcji, widocznym na rysunku 11.11.

Rys. 11.11. Diagram interakcji w języku UML

0x01 graphic

Diagram interakcji z rysunku 11.11 przedstawia te szczegóły scenariusza, które mogą nie zostać zauważane podczas czytania tekstu. Współdziałające ze sobą obiekty są obiektami dziedziny, a cały bankomat (ATM) wraz z interfejsem użytkownika traktowany jest jako pojedynczy obiekt, wywoływany szczegółowo jest tylko określony rachunek bankowy.

Ten prosty przykład bankomatu pokazuje jedynie ograniczony zestaw interakcji, ale szczegółowe ich przeanalizowanie może okazać się bardzo pomocne w zrozumieniu zarówno dziedziny problemu, jak i wymagań nowego systemu.

Tworzenie pakietów

Ponieważ dla każdego problemu o znacznej złożoności generuje się wiele przypadków użycia, UML umożliwia grupowanie ich w pakiety.

Pakiet przypomina kartotekę lub folder - jest zbiorem obiektów modelowania (klas, aktorów, itd.). Aby opanować złożoność przypadków użycia, możemy tworzyć pakiety pogrupowane według charakterystyk, mających znaczenie dla danego projektu. Możesz więc pogrupować swoje przypadki użycia według rodzaju rachunku (wszystko, co odnosi się do rachunku bieżącego albo do lokaty), według wpływów albo obciążeń, według rodzaju klienta czy według jakiejkolwiek innej charakterystyki, która ma sens w danym przypadku. Pojedynczy przypadek użycia może występować w kilku różnych pakietach, ułatwiając w ten sposób projektowanie.

Analiza aplikacji

Oprócz tworzenia przypadków użycia, dokument wymagań powinien zawierać założenia i ograniczenia twojego klienta, a także wymagania wobec sprzętu i systemu operacyjnego. Wymagania aplikacji są założeniami pochodzącymi od konkretnego klienta - zwykle określiłbyś je podczas projektowania i implementacji, ale klient zadecydował o nich za ciebie.

Wymagania aplikacji są często narzucane przez konieczność współpracy z istniejącymi systemami. W takim przypadku kluczowym elementem analizy jest zrozumienie sposobów działania istniejących systemów.

W idealnych warunkach analizujesz problem, projektujesz rozwiązanie, po czym decydujesz, jaka platforma i system operacyjny najlepiej odpowiadają potrzebom twojego projektu. Taka sytuacja jest nie tylko idealna, ale i rzadka. Dużo częściej zdarza się, że klient zainwestował już w określony sprzęt lub system operacyjny. Plany jego firmy opierają się na działaniu twojego oprogramowania w istniejącym już systemie, więc musisz poznać te wymagania jak najwcześniej i odpowiednio się do nich dostosować.

Analiza systemów

Czasem oprogramowanie jest zaprojektowane jako samodzielne; współpracuje ono jedynie z końcowym użytkownikiem. Często jednak twoim zadaniem będzie współpraca z istniejącym systemem. Analiza systemów to proces zbierania wszystkich informacji na temat systemów, z którymi będziesz współpracował. Czy twój nowy system będzie serwerem, dostarczającym usługi istniejącym systemom, czy też będzie ich klientem? Czy będziesz mógł negocjować interfejs pomiędzy systemami, czy też musisz się dostosować do istniejącego standardu? Czy inne systemy pozostaną niezmienne, czy też przez cały czas będziesz śledził zachodzące w nich zmiany?

Na te i inne pytania należy odpowiedzieć podczas fazy analizowania, jeszcze przed przystąpieniem do projektowania nowego systemu. Oprócz tego, powinieneś poznać ograniczenia wynikające ze współpracy z innymi systemami. Czy spowolnią one szybkość odpowiedzi twojego systemu? Czy nakładają one na twój system wysokie wymagania, zajmując zasoby i czas procesora?

Tworzenie dokumentacji

Gdy już określisz zadania systemu i sposób jego działania, nadchodzi czas, aby podjąć pierwszą próbę stworzenia dokumentu, określającego czas i budżet produkcji. Często termin jest narzucony przez klienta z góry: „Masz na to osiemnaście miesięcy.” Byłoby wspaniale, gdybyś mógł przeanalizować wymagania i oszacować czas, jaki zajmie ci zaprojektowanie i zaimplementowanie rozwiązania. W praktyce większość systemów powstaje w bardzo krótkim terminie i przy niskich kosztach, zaś prawdziwa sztuka polega na określeniu, jak duża część założeń może zostać spełniona w zadanym czasie — oraz przy założonym budżecie.

Oto wytyczne, o których powinieneś pamiętać, określając harmonogram i budżet projektu:

- jeśli musisz zmieścić się w pewnym przedziale, wtedy założeniem optymistycznym jest najprawdopodobniej jego ograniczenie zewnętrzne,

- zgodnie z prawem Liberty'ego, wszystko będzie trwać dłużej niż tego oczekujesz - nawet jeśli uwzględnisz to prawo.

Konieczne będzie też określenie priorytetów. Nie skończysz w wyznaczonym terminie - po prostu. Zadbaj, by system działał w momencie, gdy kończy się czas ukończenia prac i by był wystarczający sprawny dla pierwszego wydania. Gdy budujesz most zbliża się termin ukończenia prac, a nie została jeszcze wykonana ścieżka rowerowa, to niedobrze; możesz jednak otworzyć już most i zacząć pobierać myto. Jeśli jednak most sięga dopiero połowy rzeki, to już bardzo źle.

Dokumentów planowania przeważnie są błędne. Na tak wczesnym etapie projektu praktycznie nie jest możliwe właściwe oszacowanie czasu jego trwania. Gdy już znasz wymagania, możesz w przybliżeniu określić ilość czasu, jaką zajmie projektowanie systemu, jego implementacja i testowanie. Do tego musisz zaplanować dodatkowo od dwudziestu do dwudziestu pięciu procent „zapasu”, który możesz zmniejszać w trakcie wykonywania zlecenia (gdy dowiadujesz się coraz więcej).

UWAGA Uwzględnienie „zapasu” czasu nie może być wymówką dla uniknięcia tworzenia planu. Jest jedynie ostrzeżeniem, że nie można na nim do końca polegać. W trakcie prac nad projektem lepiej poznasz działanie systemu, a obliczenia staną się bardziej dokładne.

Wizualizacje

Ostatnim elementem dokumentu wymagań jest wizualizacja. Jest to nazwa wszystkich diagramów, rysunków, zrzutów ekranu, prototypów i wszelkich innych wizualnych reprezentacji, przeznaczonych do wsparcia analizy i projektu graficznego interfejsu użytkownika dla produktu.

W przypadku dużych projektów możesz opracować pełny prototyp, który pomoże tobie (i twoim klientom) zrozumieć jak będzie działał system. W niektórych przypadkach prototyp staje się odzwierciedleniem wymagań; prawdziwy system jest projektowany tak, by implementował funkcje zademonstrowane w prototypie.

Dokumentacja produktu

Na koniec każdej fazy analizy i projektowania stworzysz serię dokumentów produktu. Tabela 11.1 pokazuje kilka z takich dokumentów dla fazy analizy. Są one używane przez klienta w celu upewnienia się, czy rozumiesz jego potrzeby, przez końcowego użytkownika jako wsparcie i wytyczne dla projektu, zaś przez zespół projektowy do zaprojektowania i zaimplementowania kodu. Wiele z tych dokumentów dostarcza także materiału istotnego zarówno dla zespołu zajmującego się dokumentacją, jak i zespołu kontroli jakości, informując ,w jaki sposób powinien zachowywać się system.

Tabela 11.1. Dokumenty produktu tworzone podczas fazy analizy

Dokument

Opis

Raport przypadków użycia

Dokument opisujący szczegółowo przypadki użycia, scenariusze, stereotypy, warunki wstępne, warunki końcowe oraz wizualizacje.

Analiza dziedziny

Dokument i diagramy, opisujące powiązania pomiędzy obiektami dziedziny.

Diagramy analizy współpracy

Diagramy współpracy, opisujące interakcje pomiędzy obiektami dziedziny.

Diagramy analizy działań

Diagramy działań, opisujące interakcje pomiędzy obiektami dziedziny.

Analiza systemu

Raport i diagramy, opisujące na niższym poziomie system i sprzęt, dla którego będzie tworzony projekt.

Dokument analizy zastosowań

Raport i diagramy, opisujące wymagania klienta wobec konkretnego produktu.

Raport ograniczeń działania

Raport charakteryzujący wydajność oraz ograniczenia narzucone przez klienta.

Dokument kosztów i harmonogramu

Raport z wykresami Ganta i Perta, opisującymi zakładany harmonogram, etapy i koszty.

Projektowanie

Analiza skupia się na dziedzinie problemu, natomiast projektowanie zajmuje się stworzeniem rozwiązania. Projektowanie jest procesem przekształcenia wymagań w model, który może być zaimplementowany w postaci oprogramowania. Rezultatem tego procesu jest stworzenie dokumentu projektowego.

Dokument projektowy jest podzielony na dwie części: projekt klas oraz mechanizmy architektury. Część projektu klas dzieli się z kolei na projekt statyczny (szczegółowo określający poszczególne klasy, ich powiązania i charakterystyki) oraz projekt dynamiczny (określający, jak te klasy ze sobą współpracują).

Część mechanizmów architektury zawiera informacje na temat implementacji przechowywania obiektów, rozproszonego systemu obiektów, konkurencji pomiędzy elementami, itd. W następnej części rozdziału skupimy się na aspekcie projektowania klas; zaś do projektowania mechanizmów architektury wykorzystamy wiadomości zawarte w następnych rozdziałach tej książki.

Czym są klasy?

Jako programista C++, przywykłeś do tworzenia klas. Metodologia projektowania wymaga operowania klasami C++ poprzez klasy projektu, mimo, iż są one dość ściśle powiązane. Klasa C++ zapisana w kodzie programu stanowi implementację klasy zaprojektowanej. Każda klasa stworzona w kodzie będzie stanowić odzwierciedlenie klasy w projekcie, ale nie należy mylić jednej z drugą. Oczywiście, klasy projektu można zaimplementować także w innym języku, jednak składnia definicji klasy może być inna.

Z tego powodu przez większość czasu będziemy mówić o klasach bez dokonywania takiego rozróżnienia, gdyż różnice między nimi są zbyt abstrakcyjne. Gdy mówimy, że w naszym modelu klasa Cat posiada metodę Meow(), naszym zdaniem oznacza to, że metodę Meow() umieścimy także w naszej klasie C++.

Klasy modelu przedstawia się w postaci diagramów UML, zaś klasy C++ jako kod, który może zostać skompilowany. Rozróżnienie, choć subtelne, jest jednak istotne.

Największym wyzwaniem dla wielu nowicjuszy jest określenie początkowego zestawu klas i zrozumienie, z czego składa się dobrze zaprojektowana klasa. Jedną z technik jest wypisanie scenariuszy przypadków użycia, a następnie stworzenie osobnej klasy dla każdego rzeczownika. Spójrzmy na poniższy scenariusz przypadku użycia:

Klient decyduje się na wypłatę gotówki z rachunku osobistego. Na rachunku znajduje się wystarczająca ilość środków, w bankomacie jest wystarczająca ilość gotówki i papieru, działa także sieć. Bankomat prosi klienta o podanie kwoty wypłaty, zaś klient prosi o wypłatę trzystu dolarów, co w tym momencie jest możliwe. Maszyna wydaje trzysta dolarów i drukuje kwit, po czym klient bierze pieniądze i odbiera kwit.

Z tego scenariusza możesz wybrać następujące klasy:

- klient

- gotówka

- rachunek bieżący

- rachunek

- kwity

- bankomat

- sieć

- kwota

- wypłata

- maszyna

- pieniądze

Możesz następnie usunąć z listy synonimy, po czym stworzyć klasy dla każdego z następujących rzeczowników:

- klient

- gotówka (pieniądze, kwota, wypłata)

- rachunek bieżący

- rachunek

- kwity

- bankomat (maszyna)

- sieć

Jak na razie, to niezły początek. Możesz następnie przedstawić na diagramie relacje pomiędzy niektórymi z tych klas (patrz rysunek 11.12).

Rysunek 11.12. Wstępnie zdefiniowane klasy

0x01 graphic

Przekształcenia

Proces, który zaczął się w poprzednim podrozdziale, jest nie tyle wybieraniem rzeczowników ze scenariusza, ile początkiem przekształcania obiektów z analizy dziedziny w obiekty projektowe. To ważny, pierwszy krok. Wiele obiektów dziedziny będzie posiadało w projekcie reprezentacje. Obiekt jest nazywany reprezentacją w celu odróżnienia, na przykład, rzeczywistego papierowego kwitu wydawanego przez bankomat od obiektu w projekcie, który jest jedynie zaimplementowaną w kodzie abstrakcją.

Najprawdopodobniej odkryjesz, że większość obiektów dziedziny posiada izomorficzną reprezentację w projekcie - tj. pomiędzy obiektem dziedziny a obiektem projektu istnieje relacja „jeden do jednego”. Zdarza się jednak, że pojedynczy obiekt dziedziny jest reprezentowany w projekcie przez całą serię obiektów. Kiedy indziej seria obiektów dziedziny może być reprezentowana przez pojedynczy obiekt projektowy.

Zwróć uwagę, że w rysunku 11.12 już zauważyliśmy fakt, iż RachunekBieżący jest specjalizacją Rachunku. Nie przygotowywaliśmy się do wyszukiwania relacji generalizacji, ale ta była tak oczywista, że od razu ją zauważyliśmy. Z analizy dziedziny wiemy, że Bankomat wydaje Gotówkę i Kwity, więc natychmiast wyszukaliśmy tę informację w projekcie.

Relacja pomiędzy Klientem a RachunkiemBieżacym jest już mniej oczywista. Wiemy, że taka relacja istnieje, ale ponieważ jej szczegóły nie są oczywiste, na razie nie będziemy się nią zajmować.

Inne przekształcenia

Gdy przekształcimy już obiekty dziedziny, możemy zacząć szukać innych użytecznych obiektów projektowych. Mogą być nimi np. interfejsy. Każdy interfejs pomiędzy nowym systemem a systemami już istniejącymi, powinien zostać ujęty w klasie interfejsu. Jeśli współpracujesz z bazą danych (obojętne, jakiego rodzaju), baza ta także jest dobrym kandydatem na klasę interfejsu.

Klasy interfejsów umożliwiają ukrycie szczegółów interfejsu i w ten sposób chronią nas przed zmianami w innych systemach. Klasy interfejsów pozwalają na zmianę własnego projektu lub dostosowywanie się do zmian w projekcie innych systemów, bez zmian w pozostałej części kodu. Dopóki dwa systemy współpracują ze sobą poprzez uzgodniony interfejs, mogą się zmieniać niezależnie od siebie.

Manipulowanie danymi

Gdy stworzysz klasy dla manipulowania danymi, a musisz przekształcać dane z formatu do formatu (na przykład ze skali Celsjusza do Fahrenheita lub z systemu angielskiego na metryczny), możesz ukryć szczegóły takiej transformacji w klasie. Możesz użyć tej techniki, przekazując dane w żądanym formacie do innego systemu lub transmitując je poprzez Internet. Gdy musisz manipulować danymi w określonym formacie, możesz ukryć szczegóły protokołu w klasie manipulowania danymi.

Widoki

Każdy „widok” lub „raport” generowany przez system (lub w przypadku, gdy generujesz wiele raportów, każdy zestaw raportów) jest kandydatem na klasę. Reguły tworzenia raportu - sposób gromadzenia informacji i ich przedstawiania - można ukryć wewnątrz klasy.

Urządzenia

Gdy twój system współpracuje z urządzeniami (takimi jak drukarki, modemy, skanery, itd.) lub operuje nimi, specyfika protokołu komunikacji z urządzeniem także powinna zostać ukryta w klasie. Także w tym przypadku, przez stworzenie klas dla interfejsu urządzenia, możesz podłączać nowe urządzenia z nowymi protokołami, nie naruszając przy tym żadnych pozostałych części swojego kodu; po prostu tworzysz nową klasę interfejsu, obsługującą ten sam (lub wyprowadzony) interfejs. I gotowe!

Model statyczny

Gdy określisz już wstępny zestaw klas, pora rozpocząć modelowanie powiązań i interakcji pomiędzy nimi. W tym rozdziale najpierw opiszemy model statyczny, a dopiero potem model dynamiczny. W rzeczywistym procesie projektowania będziesz swobodnie przechodził pomiędzy tymi modelami, dodając nowe klasy i, szkicując je w miarę postępu prac.

Model statyczny skupia się na trzech obszarach: odpowiedzialności, atrybutach i powiązaniach. Najważniejszy z nich - i na nim skupisz się najpierw - jest zestaw odpowiedzialności dla każdej z klas. Najważniejszą wytyczną będzie teraz: Każda klasa powinna być odpowiedzialna za jedną rzecz.

Nie chcę przez to powiedzieć, że każda klasa ma tylko jedną metodę; wiele klas będzie miało tuziny metod. Jednak wszystkie te metody muszą być spójne i wzajemnie do siebie przystające; tj. wszystkie muszą być ze sobą powiązane i zapewniać klasie zdolność osiągnięcia określonego obszaru odpowiedzialności.

W dobrze zaprojektowanym systemie, każdy obiekt jest egzemplarzem dobrze zdefiniowanej i dobrze zrozumianej klasy, odpowiedzialnej za określony obszar. Klasy zwykle delegują zewnętrzne odpowiedzialności na inne, powiązane z nimi klasy. Dzięki stworzeniu klas, które zajmują się tylko jednym obszarem, umożliwiasz tworzenie kodu łatwego do konserwacji i rozbudowy.

Aby określić obszar odpowiedzialności swoich klas, możesz zacząć projektowanie od użycia kart CRC.

Karty CRC

CRC oznacza Class, Responsibility i Collaboration (klasa, odpowiedzialność, współpraca). Karta CRC jest tylko zwykłą kartką z notatnika. To proste urządzenie umożliwia nawiązanie współpracy z innymi osobami w celu określenia podstawowych odpowiedzialności dla początkowego zestawu klas. W tym celu ułóż na stole stos pustych kart CRC, a przy stole zorganizuj serię sesji CRC.

W jaki sposób przeprowadzać sesję CRC

Każda sesja CRC powinna odbywać się w grupie od trzech do sześciu osób; przy większej ich ilości staje się nieefektywna. Powinieneś wyznaczyć koordynatora, którego zadaniem będzie zapewnienie właściwego przebiegu sesji i pomaganie jej uczestnikom w zidentyfikowaniu najważniejszych zagadnień. Powinien być obecny co najmniej jeden doświadczony architekt oprogramowania, najlepiej ktoś z dużym doświadczeniem w obiektowo zorientowanej analizie i projektowaniu. Oprócz tego, w sesji powinien wziąć udział co najmniej jeden ekspert w danej dziedzinie, rozumiejący wymagania systemu i mogący udzielić fachowej porady na temat działania systemu.

Najważniejszym elementem sesji CRC jest nieobecność menedżerów. Sprawia ona, że sesja jest kreatywnym, swobodnie toczącym się spotkaniem, na przebieg którego nie może mieć wpływu chęć zrobienia wrażenia na czyimś szefie. Celem jej jest eksperyment, podjęcie ryzyka, odkrycie wymagań klas oraz zrozumienie, w jaki sposób mogą one ze sobą współpracować.

Sesję CRC rozpoczyna się od zebrania grupy przy stole, na którym znajduje się niewielki stos kartek. Na górze każdej karty CRC wypisuje się nazwę pojedynczej klasy. Poniżej narysuj pionową linię biegnącą w poprzez kartki, następnie opisz rubrykę po lewej stronie jako Odpowiedzialności, zaś po prawej stronie jako Współpraca.

Zacznij od wypełnienia kart dla najważniejszych zidentyfikowanych dotąd klas. Na odwrocie każdej karty zapisz jedno lub dwuzdaniową definicję. Możesz także wskazać, jaką klasę specjalizuje dana klasa (o ile jest to wiadome w czasie posługiwania się kartą CRC). Poniżej nazwy klasy napisz po prostu Superklasa: oraz nazwę klasy, od której ta klasa pochodzi.

Skup się na odpowiedzialnościach

Celem sesji CRC jest zidentyfikowanie odpowiedzialności każdej z klas. Nie zwracaj większej uwagi na atrybuty, wychwytuj tylko te najważniejsze. Najważniejszym zadaniem jest zidentyfikowanie odpowiedzialności. Jeśli w celu wypełnienia odpowiedzialności klasa musi delegować pracę na inną klasę, zapisz tę informację w rubryce Współpraca.

W miarę postępu prac zwracaj uwagę na listę odpowiedzialności. Gdy na karcie CRC zabraknie miejsca, zadaj sobie pytanie, czy nie żądasz od klasy zbyt wiele. Pamiętaj, każda klasa powinna być odpowiedzialna za jeden ogólny obszar pracy, zaś wymienione na karcie odpowiedzialności powinny być spójne i przystające - tj. powinny współgrać ze sobą w celu zapewnienia ogólnej odpowiedzialności klasy.

Nie powinieneś teraz skupiać się na powiązaniach ani na interfejsie klasy lub na tym, która metoda będzie publiczna, a która prywatna. Postaraj się jedynie zrozumieć, co robi każda z klas.

Antropomorfizacja i ukierunkowanie na przypadki użycia

Kluczową cechą kart CRC jest ich antropomorfizacja - tj. przypisywanie każdej z klas ludzkich atrybutów. Oto sposób jej działania: gdy masz już wstępny zestaw klas, wróć do scenariuszy użycia. Rozdziel karty wśród uczestników sesji i razem prześledźcie scenariusz. Na przykład, zastanówmy się nad następującym scenariuszem:

Klient decyduje się na wypłatę gotówki z rachunku osobistego. Na rachunku znajduje się wystarczająca ilość środków, w bankomacie jest wystarczająca ilość gotówki i papieru, działa także sieć. Bankomat prosi klienta o podanie kwoty wypłaty, zaś klient prosi o wypłatę trzystu dolarów, co jest w tym momencie możliwe. Maszyna wydaje trzysta dolarów i drukuje kwit, klient odbiera pieniądze i kwit.

Załóżmy, że w sesji uczestniczy pięć osób: Amy, koordynator i projektant obiektowo zorientowanego oprogramowania; Barry, główny programista; Charlie, klient; Dorris, ekspert w danej dziedzinie oraz Ed, programista.

Amy trzyma kartę CRC reprezentującą RachunekBieżący i mówi: „Mówię klientowi, ile pieniędzy jest dostępnych. Klient prosi mnie o wypłacenie trzystu dolarów. Wysyłam do dystrybutora polecenie wypłacenia trzystu dolarów w gotówce.” Barry podnosi swoją kartę i mówi: „Jestem dystrybutorem; wydaję trzysta dolarów i wysyłam do Amy komunikat nakazujący jej zmniejszenie stanu rachunku o trzysta dolarów. Komu mam powiedzieć, że maszyna zawiera teraz o trzysta dolarów mniej? Czy też ja to śledzę?” Charlie odpowiada: „Myślę, że potrzebujemy obiektu do śledzenia ilości gotówki w maszynie.” Ed mówi: „Nie, dystrybutor powinien wiedzieć, ile ma gotówki; to należy do jego zadań.” Amy nie zgadza się z tym i mówi: „Nie, ktoś powinien koordynować wydawanie pieniędzy. Dystrybutor musi wiedzieć, czy gotówka jest dostępna i czy klient posiada wystarczającą ilość środków na koncie, powinien wypłacić pieniądze i w odpowiednim momencie zamknąć szufladę. Powinien delegować na kogoś innego odpowiedzialność za śledzenie ilości dostępnej gotówki - na pewien rodzaj wewnętrznego rachunku. osoba, która zna ilość dostępnej gotówki, może także poinformować biuro o tym, że zasoby bankomatu powinny zostać uzupełnione. W przeciwnym razie dystrybutor miałby zbyt wiele zadań.”

Dyskusja trwa dalej. Trzymając karty i współpracując z innymi, odkrywa się wymagania i możliwości delegacji; każda klasa „ożywa” i „odkrywa” swoje odpowiedzialności. Gdy grupa zbytnio zagłębi się w projekt, koordynator może podjąć decyzję o przejściu do następnego zagadnienia.

Ograniczenia kart CRC

Choć karty CRC mogą stanowić dobre narzędzie dla rozpoczęcia projektowania, posiadają one duże ograniczenia. Podstawowym problemem jest to, że nie zapewniają dobrego skalowania. W bardzo skomplikowanym projekcie posługiwanie się kartami CRC może być trudne.

Karty CRC nie odzwierciedlają także wzajemnych relacji pomiędzy klasami. Choć można zapisać na nich zakres współpracy, jednak nie da się nimi wymodelować tej współpracy. Patrząc na kartę CRC, nie jesteś w stanie powiedzieć, czy klasa agreguje inną klasę, kto kogo tworzy itd. Karty CRC nie wychwytują także atrybutów, więc trudno jest z nich przejść bezpośrednio do kodu. Karty CRC są statyczne; choć możesz za ich pomocą ustalić interakcje między klasami, same karty CRC nie wychwytują tej informacji.

Karty CRC są dobre na początek, ale jeśli chcesz stworzyć stabilny i kompletny model swojego projektu, powinieneś przedstawić klasy w języku UML. Choć przejście do UML nie jest zbyt trudne, jest jednak operacją jednokierunkową. Gdy przeniesiesz swoje klasy do diagramów UML, nie będzie już odwrotu; nie wrócisz do kart CRC. Po prostu synchronizacji obu modeli jest zbyt trudna.

Przekształcanie kart CRC na UML

Każda karta CRC może zostać przekształcona bezpośrednio w klasę wymodelowaną w UML. Odpowiedzialności są przekształcane na metody klasy, ewentualnie dodawane są także wychwycone atrybuty. Definicja klasy z odwrotnej strony karty jest umieszczana w dokumentacji klasy. Rysunek 11.13 przedstawia relację pomiędzy kartą CRC RachunekBieżący, a stworzoną na podstawie tej karty klasą UML.

Rys. 11.13. Karta CRC

0x01 graphic

Klasa: RachunekBieżący

Superklasa: Rachunek

Odpowiedzialności:

śledzenie stanu bieżącego

przyjmowanie depozytów i transfery na rachunek

wypisywanie czeków

transfery z rachunku

śledzenie dziennego limitu wypłaty gotówki z bankomatu

Współpraca:

inne rachunki

system bankowy

dystrybutor gotówki

Relacje pomiędzy klasami

Gdy klasy zostaną już przedstawione w UML, możesz zwrócić uwagę na relacje pomiędzy różnymi klasami. Podstawowe modelowane relacje to:

- generalizacja

- powiązania

- agregacja

- kompozycja

Relacja generalizacji jest w C++ implementowana poprzez publiczne dziedziczenie. Pamiętając jednak że najważniejszy jest projekt, nie skupimy się mechanizmie działania relacji, ale na semantyce: co z takiej relacji wynika.

Relacje sprawdziliśmy już w fazie analizy, teraz skupimy się nie tylko na obiektach dziedziny, ale także na obiektach w naszym projekcie. Naszym zadaniem jest określenie „wspólnej funkcjonalności” w powiązanych ze sobą klasach i wydzielenie z nich klas bazowych, obejmujących te wspólne właściwości.

Gdy określisz „wspólną funkcjonalność”, powinieneś przenieść ją z klas specjalizowanych do klasy bardziej ogólnej. Gdy zauważymy, że oba rachunki, bieżący i lokaty, potrzebują metod do transferu pieniędzy do i z rachunku, metodę TransferŚrodków() przeniesiemy do klasy bazowej Rachunek. Im więcej funkcji przeniesiemy z klas potomnych, tym bardziej polimorficzny stanie się projekt.

Jedną z możliwości dostępnych w C++, lecz niedostępnych w Javie, jest wielokrotne dziedziczenie (Java ma podobną, choć ograniczoną, możliwość posiadania wielu interfejsów). Wielokrotne dziedziczenie pozwala klasie na dziedziczenie po więcej niż jednej klasie bazowej, wprowadzając składowe i metody z dwóch lub więcej klas.

Doświadczenie wykazało, że wielokrotne dziedziczenie powinno być używane rozważnie, gdyż może skomplikować zarówno projekt, jak i implementację. Wiele problemów rozwiązywanych dawniej poprzez wielokrotne dziedziczenie obecnie rozwiązuje się poprzez agregację. Należy jednak pamiętać, że wielokrotne dziedziczenie jest użytecznym narzędziem, zaś projekt może wymagać, by pojedyncza klasa specjalizowała zachowanie dwóch lub więcej innych klas.

Wielokrotne dziedziczenie a zawieranie

Czy obiekt jest sumą swoich części? Czy ma sens modelowanie obiektu Samochód jako specjalizacji Kierownicy, Drzwi i Kół, tak jak pokazano na rysunku 11.14?

Rys. 11.14. Fałszywe dziedziczenie

0x01 graphic

Trzeba powrócić do źródeł: publiczne dziedziczenie powinno zawsze modelować generalizację. Ogólnie przyjętym wyrażeniem tej reguły jest stwierdzenie, że dziedziczenie powinno modelować relację jest czymś. Jeśli chcesz wymodelować relację posiada (na przykład samochód posiada kierownicę), powinieneś użyć agregacji, jak pokazano na rysunku 11.15.

Rys. 11.15. Agregacja

0x01 graphic

Diagram z rysunku 11.15 wskazuje, że samochód posiada kierownicę, cztery koła oraz od dwóch do pięciu drzwi. Jest to właściwy model relacji pomiędzy samochodem a jego elementami. Zwróć uwagę, że romby na rysunku nie są wypełnione; rysujemy je w ten sposób, aby wskazać, że modelujemy agregację, a nie kompozycję. Kompozycja implikuje kontrolę czasu życia obiektu. Choć samochód posiada koła i drzwi, mogą one istnieć jako elementy samochodu, a także jako samodzielne obiekty.

Rysunek 11.16 modeluje kompozycję. Ten model pokazuje, że ciało jest nie tylko agregacją głowy, dwóch rąk i dwóch nóg, ale także, że obiekty te (głowa, ręce, nogi) są tworzone w momencie tworzenia ciała i znikają w chwili, gdy znika ciało. Nie mogą istnieć niezależnie; ciało jest złożone z tych rzeczy, a czas ich istnienia jest powiązany.

Rys. 11.16. Kompozycja

0x01 graphic

Cechy i główne typy

Jak zaprojektować klasy potrzebne do zaprezentowania różnych linii modelowych typowego producenta samochodów? Przypuśćmy, że zostałeś wynajęty do zaprojektowania systemu dla Acme Motors, który aktualnie produkuje pięć modeli: Pluto (powolny, kompaktowy samochód z niewielkim silnikiem), Venus (czterodrzwiowy sedan ze średnim silnikiem), Mars (sportowe coupe z największym silnikiem, opracowanym w celu uzyskiwania rekordowych szybkości), Jupiter (minwan z takim samym silnikiem, jak w sportowym coupe, lecz z możliwością zmiany biegów przy niższych obrotach oraz dostosowaniemm do napędzania większej masy) oraz Earth (furgonetka z niewielkim silnikiem o wysokich obrotach).

Możesz zacząć od stworzenia podtypów samochodów, odzwierciedlających różne modele, po czym tworzyć egzemplarze każdego z modelu w miarę ich schodzenia z linii montażowej, tak jak pokazano na rysunku 11.17.

Rys. 11.17. Modelowanie podtypów

0x01 graphic

Czym różnią się te modele? Typem nadwozia oraz rozmiarem i charakterystykami silnika. Te elementy mogą być łączone i dopasowywane w celu stworzenia różnych modeli. Możemy to wymodelować w UML za pomocą stereotypu cecha, tak jak pokazano na rysunku 11.18.

Rys. 11.18. Modelowanie cech

0x01 graphic

Diagram z rysunku 11.18 wskazuje, że klasy mogą być wyprowadzone z klasy „samochód” dzięki mieszaniu i dopasowaniu trzech atrybutów cech. Rozmiar silnika określa siłę pojazdu, zaś charakterystyka wydajności określa, czy samochód jest pojazdem sportowym, czy rodzinnym. Dzięki temu możemy stworzyć silną, sportową furgonetkę, słaby rodzinny sedan, itd.

Każdy atrybut może być zaimplementowany za pomocą zwykłego wyliczenia. Typ nadwozia można zaimplementować w kodzie za pomocą poniższego wyliczenia:

enum TypNadwozia = { sedan, coupe, minivan, furgonetka };

Może się jednak okazać, że pojedyncza wartość nie wystarcza do wymodelowania określonej cechy. Na przykład, charakterystyka wydajności może być raczej złożona. W takim przypadku cecha może zostać wymodelowana jako klasa, zaś określona cecha obiektu może istnieć jako konkretny egzemplarz tej klasy.

Model samochodu może modelować charakterystykę wydajności, np. typ wydajność, zawierający informacje o tym, w którym momencie silnik zmienia bieg i jak wysokie obroty może osiągnąć. Stereotyp UML dla klasy obejmującej cechę — klasa ta może służyć do tworzenia egzemplarzy klasy (Samochód) należącej logicznie do innego typu (np. SamochódSportowy i SamochódLuksusowy) — to <<typ główny>>. W tym przypadku, klasa Wydajność jest typem głównym dla klasy Samochód. Gdy tworzymy egzemplarz klasy Samochód, jednocześnie tworzymy obiekt Wydajność, wiążąc go z danym obiektem Samochód, jak pokazano na rysunku 11.19.

Rys. 11.19. Cecha jako typ główny

0x01 graphic

Typy główne umożliwiają tworzenie różnorodnych typów logicznych, bez potrzeby używania dziedziczenia. Dzięki temu można obsłużyć duży i złożony zestaw typów, bez gwałtownego wzrostu złożoności klas, jaki mógłby nastąpić przy używaniu samego dziedziczenia.

W C++ typy główne są najczęściej implementowane za pomocą wskaźników. W tym przypadku klasa Samochód zawiera wskaźnik do egzemplarza klasy CharakterystykaWydajności (patrz rysunek 11.20). Zamianę cech nadwozia i silnika w typy główne pozostawię ambitnym czytelnikom.

Rys. 11.20. Relacja pomiędzy obiektem Samochód a jego typem głównym

0x01 graphic

Class Samochod : public Pojazd

{

public:

Samochod();

~Samochod();

// inne publiczne metody

private:

CharakterystykaWydajnosci * pWydajnosc;

};

I jeszcze jedna uwaga. Typy główne umożliwiają tworzenie nowych typów (a nie tylko egzemplarzy) w czasie działania programu. Ponieważ typy logiczne różnią się od siebie jedynie atrybutami powiązanego z nim typu głównego, atrybuty te mogą być parametrami konstruktora typu głównego. Oznacza to, że w czasie działania programu możesz na bieżąco tworzyć nowe typy samochodów. Przekazując różne rozmiary silnika i różne punkty zmiany biegów do typu głównego, możesz efektywnie tworzyć nowe charakterystyki wydajności. Przypisując te charakterystyki różnym samochodom, możesz zwiększać zestaw typów samochodów w czasie działania programu.

Model dynamiczny

Oprócz modelowania relacji pomiędzy klasami, bardzo ważne jest także wymodelowanie sposobu współpracy pomiędzy klasami. Na przykład, klasy RachunekBieżący, Bankomat oraz Kwit mogą współpracować z klasą Klient, wypełniając przypadek użycia „Wypłata gotówki.” Wkrótce wrócimy do diagramów sekwencji używanych wcześniej w fazie analizy, ale tym razem, na podstawie metod opracowanych dla klas, wypełnimy je szczegółami, tak jak na rysunku 11.21.

Rys. 11.21. Diagram sekwencji

0x01 graphic

Ten prosty diagram interakcji pokazuje współdziałanie pomiędzy klasami projektowymi oraz ich następstwo w czasie. Sugeruje, że klasa Bankomat deleguje na klasę RachunekBieżący całą odpowiedzialność za zarządzanie stanem rachunku, zaś klasa RachunekBieżący przenosi na klasę Bankomat zadanie wyświetlania informacji dla użytkownika.

Diagramy interakcji występują w dwóch odmianach. Odmiana pokazana na rysunku 11.21 jest nazywana diagramem sekwencji. Diagramy współpracy dostarczają innego widoku tych samych informacji. Diagramy sekwencji kładą nacisk na kolejność zdarzeń, zaś diagramy współpracy obrazują współdziałanie pomiędzy klasami. Diagram współpracy można stworzyć bezpośrednio z diagramu sekwencji; programy takie jak Rational Rose potrafią stworzyć taki diagram po jednym kliknięciu na przycisku (patrz rysunek 11.22).

Rys. 11.22. Diagram współpracy

0x01 graphic

Diagramy zmian stanu

Przechodząc do zagadnienia interakcji pomiędzy obiektami, musimy poznać różne możliwe stany każdego z obiektów. Przejścia pomiędzy stanami możemy wymodelować na diagramie stanu (lub diagramie zmian stanów). Rysunek 12.23 przedstawia różne stany obiektu RachunekBieżący w czasie, gdy klient jest zalogowany do systemu.

Rys. 11.23. Stan rachunku klienta

0x01 graphic

Każdy diagram stanu rozpoczyna się od stanu start, a kończy na stanie koniec. Poszczególne stany posiadają nazwy, zaś zmiany stanów mogą być opisane za pomocą etykiet. Strażnik wskazuje warunek, który musi być spełniony, aby obiekt mógł przejść ze stanu do stanu.

Superstany

Klient może w każdej chwili zmienić zamiar i zrezygnować z logowania się. Może to uczynić po włożeniu karty w celu zidentyfikowania swojego rachunku lub już po wprowadzeniu kodu PIN. W obu przypadkach system musi zaakceptować jego żądanie anulowania operacji i powrócić do stanu „nie zalogowany” (patrz rysunek 11.24).

Rys. 11.24. Użytkownik może zrezygnować

0x01 graphic

Jak widać, w bardziej skomplikowanych diagramach, stan Anulowany szybko zaczyna przeszkadzać. Jest to szczególnie irytujące, gdyż anulowanie jest stanem wyjątkowym, który nie powinien dominować w diagramie. Możemy uprościć ten diagram, używając superstanu, tak jak pokazano na rysunku 11.25.

Rys. 11.25. Superstan

0x01 graphic

Diagram z rysunku 11.25 dostarcza takich samych informacji, jak diagram z rysunku 11.24, lecz jest dużo bardziej przejrzysty i łatwiejszy do odczytania. Od momentu rozpoczęcia logowania, aż do chwili jego zakończenia przez system, możesz ten proces anulować. Gdy to uczynisz, powrócisz do stanu „Nie zalogowany.”

Często określenie „język UML” utożsamia się z bardziej ogólnym pojęciem, jakim jest metodyka (projektowania) UML — przyp.red.

Każdy z tych panów był autorem odrębnej metodyki projektowania.

J. Rumbaugh opracował Object Modelling Technique (OMT), która jest wystarczająca w przypadku modelowania dziedziny zagadnienia (problem domain). Nie odzwierciedla jednak dokładnie ani wymagań użytkowników systemów, ani wymagań implementacji.

I. Jacobson rozwinął Object-Oriented System Engineering (OOSE), który w sposób zadowalający uwzględnia aspekty modelowania użytkowników i cyklu życia systemu jako całości. Nie odzwierciedla jednak w sposób wystarczający sposobu modelowania dziedziny oraz aspektu implementacji.

G. Booch jest autorem Object-Oriented Analysis and Design Methods (OOAD), spełniającej wszelkie wymogi w dziedzinie projektowania, konstrukcji i związków ze środowiskiem implementacji. Nie uwzględnia jednak w sposób dostateczny fazy rozpoznania i analizy wymagań użytkowników.

UML ma stanowić syntezę wymienionych metodyk. Ma jednak wielu krytyków. W wielu publikacjach można przeczytać, że jest to rzecz przereklamowana i w niewystarczający sposób zdefiniowana. Konkurencją dla ciągle uzupełnianej metodyki UML jest m.in. metodyka i notacja oparta na tzw. technice “design by contracts”. — przyp.red

5 Część I Podstawy obsługi systemu WhizBang (Nagłówek strony)

F:\korekta\r11-06.doc

1



Wyszukiwarka

Podobne podstrony:
(3660) macierz odwrotna i rząd macierzy
3660
3660
3660
3660
3660
3660
200413 3660
(3660) macierz odwrotna i rząd macierzy
Nokia 3660 pl
Instrukcja obsługi Electrolux ER 3660 BN

więcej podobnych podstron