Professional Linux Programming, R-01-t, PLP_Rozdział1


1. Projekt aplikacji

Informacje ogólne

Tworzenie profesjonalnych aplikacji udaje się najlepiej przy odpowiednim wyważeniu proporcji między planem, zrozumieniem potrzeb i znajomością stosowanych narzędzi programistycznych. Nikt nie lubi jak coś się nie udaje i z tego powodu całą pracę trzeba rozpoczynać od nowa. Dokładne zaplanowanie aplikacji przed rozpoczęciem tworzenia kodu może zapobiec wielu niepowodzeniom.

Linux jest doskonałą platformą do tworzenia aplikacji. Jego otwarta architektura i dostępność kodu źródłowego powodują, że programowanie staje się naprawdę atrakcyjne.

Niniejszej książki nie należy traktować jako akademickiego podręcznika programowania. Nie mamy zamiaru poświęcać wiele czasu na naukę zarządzania projektem lub naukę metod programowania, ponieważ napisano już wiele dobrych książek na te tematy. W podrozdziale zatytułowanym Materiały źródłowe wskazujemy niektóre zagadnienia wymagające dalszych studiów.

Autorzy książki, jako programiści, doceniają to, że dzięki prostym technikom i narzędziom można zaoszczędzić wiele czasu i wysiłku. W tym rozdziale omówimy sposoby unikania niektórych pułapek, które mogą się niespodziewanie pojawić podczas tworzenia rzeczywistych aplikacji (a nawet aplikacji tworzonych dla zabawy). Opiszemy tu opracowywanie wymagań, zagadnienia inżynierii oprogramowania, architekturę aplikacji oraz specyfikacje interfejsu. W dalszych częściach książki pokażemy metody kontroli kodu, oczyszczanie go z błędów, testowanie, tworzenie dokumentacji oraz zagadnienia wiążące się z wdrożeniem, czyli bazy danych i graficzne interfejsy programu.

Pokazując narzędzia i technikę programowania oraz biblioteki będziemy w całej książce posługiwali się jedną aplikacją. Taki temat przewodni ułatwia łączenie wielu rozdziałów w jednolitą całość. Rozpoczniemy od ogólnego zdefiniowania wymagań, następnie przejdziemy do bardziej szczegółowych zagadnień budowy programu i na zakończenie otrzymamy profesjonalny, elegancki i gotowy do rozpowszechniania system produkcyjny do tworzenia programów.

Nie zamierzmy traktować budowanej w książce przykładowej aplikacji jako gotowego produktu przeznaczonego do sprzedaży. Niektóre sytuacje w naszym przykładzie są zmyślone i wielu programistów prawdopodobnie nie zgodzi się na jego praktyczne wdrożenie, w czym prawdopodobnie będą mieć wiele racji.

Jako kanwę książki wybraliśmy aplikację wspomagającą obsługę wypożyczalni płyt DVD. Rozpoczniemy od prostego programu do przechowywania danych o dostępnych płytach DVD, a następnie będziemy do niego dodawać pewne funkcje, np. interfejs graficzny z możliwością wyszukiwania. Pokażemy także zastosowanie niektórych zasad handlowych, np. uzależnianie opłat od wielu zróżnicowanych czynników (takich jak upusty w okresie od poniedziałku do czwartku). Na zakończenie dodamy interfejs WWW pozwalający klientom na zdalną rezerwację płyt.

Rozpoczniemy więc od kilku wcześniej wybranych założeń dotyczących tworzenia programu.

Zdecydowaliśmy się na użycie języka C. Niezależnie od pojawienia się i wzrostu popularności nowszych i bardziej egzotycznych języków takich jak C++, Java i Perl — język C jest w stanie sprostać większości zadań, których podejmuje się programista, szczególnie w środowisku systemu Linux. Przede wszystkim język C jest językiem systemu UNIX i zostało w nim napisane jądro Linuksa, a więc istnieją dla niego interfejsy do prawie każdej funkcji systemowej. Interfejs aplikacji w Linuksie został zaprojektowany do efektywnej współpracy z programami w języku C. Z poziomu tego języka można łatwo uzyskać dostęp do baz danych i równie łatwo zbudować interfejs graficzny posługując się pakietami GNOME i GTK+.

Decydujemy się również na użycie „pełnokrwistej” bazy danych, mimo że zakres przykładowej aplikacji nie jest na tyle obszerny, aby w pełni taką bazę wykorzystać. Jest to działanie nieco na wyrost, które umożliwi nam prezentację bazy i interfejsu graficznego w następnych rozdziałach książki.

Stopień szczegółowości aplikacji także został ustalony kompromisowo. Opisując wymagania stawiane aplikacji i jej projekt pomijamy niektóre zagadnienia występujące w świecie rzeczywistym. Jako przykład można tu podać użycie pól o stałych rozmiarach tam, gdzie zmienny rozmiar mógłby być bardziej przydatny. Niektóre szczegółowe rozwiązania nie są więc w pełni elastyczne.

Ten rozdział składa się z trzech części. W pierwszej omówimy krótko metody tworzenia programu i ich zależność od zmieniających się wymagań. W następnej części przedstawimy wpływ rzeczywistości na formułowanie założeń i ich analizę, pokazując sposób zamiany wymagań użytkownika na bardziej sformalizowane deklaracje, które można wykorzystać podczas tworzenia projektu. Na zakończenie rozdziału przedstawimy podstawową strukturę aplikacji i zaprojektowany interfejs programowy (API) spełniający nasze założenia. Będziemy go używać w dalszych rozdziałach tematycznych naszej książki przy omawianiu zagadnień wprowadzonych w tym rozdziale.

Formułowanie założeń

Wspomnieliśmy wcześniej, że rozpoczynamy pracę od wymagań określonych dosyć ogólnikowo. Nie jest to celowe działanie, lecz po prostu akceptacja rzeczywistości. W wielu projektach proces precyzyjnego określania potrzeb klienta jest zwykłym oszustwem i jedną z przyczyn późniejszych problemów. Nie ma chyba nic gorszego niż przekonanie się o nieprawidłowości założeń po zbudowaniu całego systemu. W najgorszym przypadku cała praca programistów może wówczas pójść na marne a klient nie zapłaci i może nawet skierować sprawę do sądu!

Jeżeli nie będziemy zwracać uwagi na to, co nasza aplikacja ma robić, to napotkamy wiele problemów. Mogą to być:

Rozrost właściwości programu możemy zaobserwować wówczas, gdy rozpoczniemy pospieszne tworzenie kodu aplikacji. Odbywa się to tak, że mamy ogólne wyobrażenie o tym, co chcemy zrobić i od razu rozpoczynamy tworzenie kodu. Następnie odkrywamy, że trzeba uwzględnić nową właściwość, zmienić jakąś funkcję lub obsłużyć nowy interfejs. Jeżeli nie uzgodniliśmy z klientem precyzyjnie zakresu wykonywanych dla niego prac, to zaczną się pojawiać coraz to nowe żądania. W takich przypadkach może się zdarzyć, że nigdy nie zakończymy tego, co rozpoczęliśmy! Na szczęście są sposoby uniknięcia tego typu sytuacji.

Dowolność wymagań może się pojawić wówczas, gdy nie dbamy o precyzję używanych sformułowań lub gdy użyte sformułowania mają kilka znaczeń. W języku angielskim, podobnie jak w polskim, istnieje wiele takich słów. Np. słowo „skład” w dawnym znaczeniu to „sklep” lub „magazyn”. Znajdziemy się w wielce kłopotliwym położeniu, jeśli zbudujemy aplikację dla niewłaściwego rodzaju „składu”. Widać więc, jak ważne są dokładne definicje używanych terminów i unikanie technicznego żargonu jeśli tylko jest to możliwe.

Przyjmowanie z góry jakichś założeń może być bardzo mylące. Prowadzi to do kłopotów w szczególności wtedy, gdy działamy w nieznanym obszarze zagadnień. Możemy np. nie zdawać sobie sprawy, że numery ISBN nadawane książkom zawierają cyfrę kontrolną i utworzyć aplikację nie uwzględniającą tego faktu. Wydawnictwo będące naszym klientem nie poinformuje nas o konstrukcji numeru, ponieważ jest to rzecz doskonale znana w tym środowisku zawodowym. Wszyscy wiedzą oprócz nas, że systemy wydawnicze muszą to obsługiwać — mamy więc przykład milczącego przyjęcia założenia.

Jako inny przykład niedopowiedzianych założeń możemy podać zaokrąglanie sum do najbliższych pięciu centów przy płatnościach gotówkowych w Australii i Nowej Zelandii. Wynika to z tego, że monetą o najmniejszym nominale jest tam właśnie pięć centów. Spróbujmy postawić się w sytuacji programisty, który nie uwzględni tego faktu! Podobnie będzie w krajach stosujących podatek od wartości dodanej (VAT) — tu wszyscy oczekują, że systemy obsługi sprzedaży będą go wyliczać zgodnie z miejscowymi zasadami.

Możemy zaoszczędzić wiele czasu niepotrzebnie traconego na borykanie się z tego typu problemami jeżeli zachowamy ostrożność, będziemy rejestrować wymagania w sformalizowany sposób i nadamy im odpowiedni priorytet, aby zawsze orientować się w ich ważności.

Jeżeli uzgodnimy zakres funkcjonalny naszej aplikacji przed rozpoczęciem zaawansowanych prac przy tworzeniu kodu to unikniemy w przyszłości rozrostu wymaganych właściwości — lub jeżeli takie zmiany nastąpią — będziemy mogli żądać dodatkowej zapłaty. Aby zachować kontrolę nad zmianami wymagań możemy użyć „listy życzeń” z zapisem rozrastających się funkcji, na której klient będzie zatwierdzał koszty i skutki żądanych przez niego modyfikacji.

Jeżeli upewnimy się, że na etapie formułowania założeń zgromadzono wystarczająco dużo szczegółów, to prawdopodobieństwo wystąpienia niespodziewanego braku lub przemilczenia wymagań będzie znacznie zmniejszone.

Dowolność w formułowaniu żądań można zmniejszyć stosując wspólne słownictwo i definiując wszystkie specjalistyczne określenia użyte w założeniach. Należy dokładnie wyrazić wymagania używając krótkich zdań, w których słowa takie jak „wymaga” czy „musi” odnoszą się do wymagań obowiązkowych, zaś słowa „powinno” lub „może” dotyczą właściwości opcjonalnych, które aplikacja może mieć przy zapewnieniu odpowiednich środków i czasu na jej realizację. Wszystkie wymagania muszą być sprawdzalne, tzn. powinna istnieć możliwość zbudowania takich testów, które odpowiedzą na pytanie czy system spełnia dane wymagania. Np. nie można zbadać, czy aplikacja działa „szybko”, ale można sprawdzić, czy aplikacja „reaguje w ciągu kilku sekund”.

Konieczność dobrego zrozumienia wymagań użytkownika może być bardzo frustrująca podczas tworzenia kodu. Trzeba jednak pamiętać, że ten użytkownik (lub sponsor) jest naszym klientem i ma wolność wyboru środków opisujących swoje wymagania. Może się więc zdarzyć, że zapis na serwetce będzie wszystkim, co od niego otrzymamy. Programiści muszą być przygotowani na takie sytuacje. Ponieważ klienci nie mają zwykle ochoty na przygotowanie założeń w odpowiedniej postaci, to ten obowiązek spada na nas.

Ogólnie mówiąc, wymagania można podzielić na kilka kategorii, które obejmują:

Po zebraniu wymagań obydwie strony muszą potwierdzić swoje uzgodnienia, najlepiej w postaci podpisanego dokumentu.

Modele tworzenia kodu

Model zstępujący

Jednym z klasycznych modeli tworzenia aplikacji jest tzw. model zstępujący. Polega on na tym, że każdy etap projektu musi zostać zakończony przed rozpoczęciem następnego. Na rysunku poniżej pokazano schemat działania takiego modelu:

Założenia

Projekt

Tworzenie kodu

Testowanie

Wdrożenie

Wśród wad tego modelu można wskazać brak innej możliwości reagowania na zmiany w założeniach niż tylko przerwanie całego projektu. Dodatkowym zagrożeniem jest tu umieszczenie testów pod koniec całego procesu. Jeżeli wykryje się np. błąd w interfejsie lub gdzie indziej, to skutki mogą być wówczas opłakane.

Niektóre odmiany modelu zstępującego umożliwiają powrót do wcześniejszych etapów, ale nie są to zwykle czynności planowe i konieczność ich stosowania oznacza, że model niezbyt dobrze pasuje do rzeczywistości.

Model iteracyjny

Model iteracyjny reprezentuje współczesne podejście do zagadnienia tworzenia aplikacji i pokonuje wady modelu zstępującego polegające na ścisłym wyznaczeniu granic poszczególnych etapów.

Podejście iteracyjne bywa stosowane wtedy, gdy planuje się zmianę założeń podczas realizacji projektu. Na początku zakłada się niewielką liczbę iteracji w celu doskonalenia produktu końcowego. Odzwierciedlają one wyobrażenie o tym, jak odbiorca będzie zmieniał swoje pomysły, nawet jeśli na początku miał na dany temat zupełnie inne zdanie. Elastyczne programy, takie jak np. interfejsy graficzne lub wymyślne systemy wspomagania decyzji, same z siebie są przedmiotem zmiany założeń lub rozrostu wymagań. Trzeba wówczas znaleźć sposób zapobiegania zbyt częstym narzekaniom „Będę to wiedział, jak zobaczę” lub, co gorsza, „Miałem na myśli coś innego, czy możemy to zrobić?” Model iteracyjny pozwala na cofnięcie się do założeń, dzięki czemu użytkownik widzi wczesne wersje programu a programista może utrzymywać uporządkowaną strukturę logiczną swojego środowiska pracy.

W modelu iteracyjnym obowiązuje ogólna zasada umieszczania w początkowych iteracjach obowiązkowych wymagań o najwyższym priorytecie (jest to tzw. wersja 1.0). Jest on zalecany dla programów o modułowej budowie, z powierzchownym interfejsem graficznym i zmieniającymi się metodami dostępu do danych.

Iteracja 1

Projekt

Kod

Test

Iteracja 2

Projekt

Kod

Test

Iteracja 3

Projekt

Kod

Test 1

Z powyższego schematu wynika, że w każdej iteracji wykonywane są zadania jak dla modelu zstępującego. Zazwyczaj planuje się niewielką liczbę iteracji, w których są rozwiązywane ściśle określone zestawy wymagań. Może się okazać, że większe projekty tworzone w zmiennym środowisku będą wymagały przebudowy kodu, ale zawsze jest to planowane przed rozpoczęciem kolejnej iteracji.

Warto podkreślić, że proces projektowania (i uściślania wymagań) przechodzący do procesu tworzenia kodu i potem do procesu testowania trwają przez cały czas realizacji projektu.

Metoda szybkiego śledzenia

Problem określany jako „Będę to wiedział, jak zobaczę” można czasami pokonać projektując i programując prostą aplikację od razu po ustaleniu podstawowych wymagań. Podstawowa aplikacja może więc na początku nie działać dobrze i będzie na pewno pozbawiona wielu funkcji, ale stanowi dobre odniesienie dla procesu gromadzenia założeń.

Zauważmy, że nie jest to dokładnie to samo, co prototyp do jednorazowego użytku. Nie budujemy tutaj aplikacji makietowej przeznaczonej do wyrzucenia (może jednak tak się zdarzyć, jeśli założenia były nierealne). Kod aplikacji ma być kodem rzeczywistym, na którym będzie można sprawdzić słuszność wybranych algorytmów. Plan naszego projektu powinien zawierać dwie lub trzy takie aplikacje cząstkowe, które będą ulepszane, a nie wyrzucane lub tworzone od nowa. Wyjątkami mogą być tu testowe zawartości ekranu, projektowane tylko dla uzgodnienia wyglądu interfejsu.

Wczesne i częste testy

Tuż po utworzeniu wstępnego projektu należy rozpocząć jego testowanie. Zazwyczaj plan testów sporządzany w pisemnej postaci zawiera opis wszystkich niezbędnych testów. Można wówczas weryfikować projekt przeglądając, czy jego właściwości na danym etapie spełniają przyjęte założenia.

Oprócz tego należy określić strategie testowania całej aplikacji. Możemy podjąć decyzję, co należy testować, jakich narzędzi użyć do tego celu oraz co włączyć w samej aplikacji, aby testowanie było bardziej efektywne.

Testowaniem powinny być objęte następujące obszary:

Po dokonaniu zmian należy ponownie przeprowadzić testowanie w celu upewnienia się, że nie powstały nowe problemy. Takie testy nazywane są testami regresyjnymi.

Na zakończenie należy pokazać klientowi, że ukończony system rzeczywiście spełnia uzgodnione wymagania. Jest to tzw. test odbiorczy.

Przed rozpoczęciem tworzenia początkowego kodu należy przetestować środowisko programowania, sprawdzając obecność i poprawność działania kompilatorów i bibliotek.

Po utworzeniu wstępnego kodu przeprowadzamy jego test, sprawdzając poprawność wybranej strategii testowania oraz obecność i poprawność działania niezbędnych narzędzi.

Ogólnie mówiąc, chcąc zmniejszyć ryzyko popełnienia błędu należy testować wszystko i jak najczęściej. Model zstępujący programowania nie sprawdza się więc najlepiej, ponieważ testy są w nim przeprowadzane na zakończenie projektu. Wówczas może być za późno na stwierdzenie, że np. użycie jakiegoś egzotycznego języka programowania było błędem, ponieważ dostępny dla niego debuger nie będzie działał na danym sprzęcie, albo że użyty interpreter nie działa wystarczająco szybko i powoduje spadek wydajności całej aplikacji.

Więcej na temat testów można się dowiedzieć z rozdziału 11.

Wypożyczalnia DVD

Wybierając naszą przykładową aplikację wyobraźmy sobie wypożyczalnię płyt DVD, w której cała dokumentacja prowadzona jest w postaci zapisów na papierze. Właściciel zadecydował, że jest mu potrzebny system wspomagający codzienne operacje, ponieważ pozwoli mu to przezwyciężyć szereg niedogodności związanych z dokumentacją papierową.

Właściciel wypożyczalni nie jest rzeczywistą postacią. Będziemy posługiwali się jego słowami głównie w sprawach dotyczących ograniczenia kosztów (co nie wymaga naszej pracy) oraz użycia otwartego oprogramowania. Zakładamy także, że właściciel rozumie, iż określenie „System operacyjny” nie oznacza automatycznie Windows.

Ponieważ nasza książka jest poświęcona programowaniu w systemie Linux, to byłoby wielką niezręcznością jeśli tworzona aplikacja miałaby charakter programu zamkniętego, bez udostępnienia wersji źródłowej, używającego zastrzeżonej bazy danych z systemu Windows NT. Takie wewnętrzne ograniczenia nie występują w systemie Linux. Oczywiście, programista może zawsze zadecydować się na tworzenie rozwiązań zastrzeżonych, stosując komercyjne produkty.

Musimy porozmawiać z kierownikiem wypożyczalni aby zapoznać się z jej działaniem i zrozumieć występujące tam problemy. Warto również poobserwować funkcjonowanie wypożyczalni i porozmawiać z jej klientami. Wszystko to ma na celu dobre poznanie otoczenia, w którym ma działać system.

Na tym etapie gromadzenia założeń prawie zawsze odkrywamy fakt, że klient mówi jak są wykonywane jakieś operacje, a nie czego one dotyczą. Bardzo ważne jest zdobycie informacji o tym, co poszczególne operacje robią — w przeciwnym wypadku powstanie skomputeryzowany system obsługi istniejących problemów, a nie system rozwiązujący te problemy.

Założenia wstępne

Rozmawiając z właścicielem wypożyczalni zdobędziemy pewne wymagania początkowe. Każdemu z tych wymagań należy nadać unikatowy identyfikator. Wymagania użytkownika (klienta) będziemy wyróżniać początkowymi literami „UR”:

UR1 — Klienci zwracają płyty DVD do skrytki jeszcze przed otwarciem wypożyczalni. Jeśli zwrócono kilka kopii tego samego filmu, to nie mogę ich rozróżnić.

UR2 — Nie mam innego sposobu na sprawdzenie, które płyty są w danym momencie wypożyczone przez klientów, jak pójście na zaplecze i rzut oka na regał. Zniechęca to klientów, którzy muszą odstawić pudełko na półkę, bo okazało się, że film został wypożyczony.

UR3 — System musi być przyjazny.

UR4 — Chciałbym utrzymać kasę sklepową, ponieważ niedawno ją kupiłem.

UR5 — Słyszałem o tym, że programiści udostępniają klientom kody źródłowe systemów komputerowych i chciałbym taki kod uzyskać. W przyszłości, jeśli coś będzie źle działać lub zechcę coś zmienić, wykonanie poprawek będę mógł zlecić komu zechcę.

UR6 — Na system nie mogę wydać więcej niż 1000 dolarów, a na wykonaną pracę sporządzimy oddzielną umowę.

Zwróćmy uwagę na to, że żądania są dość niejasne i można je spełnić w różny sposób. Podstawowym zadaniem na tym etapie jest więc wyjaśnienie i sprecyzowanie wymagań oraz podział żądań bardziej złożonych na kilka prostszych. Numeracja wymagań użytkownika ułatwia ich późniejsze śledzenia. W tej fazie projektu nie należy zbyt mocno koncentrować się na wyniku końcowym, jednak warto określić w sposób ostateczny, czy niektórych z wymagań nie będzie można spełnić lub czy nie są one zbyt kosztowne.

Pierwotna lista wymagań wygląda całkiem nieźle, ale nie odpowiada na podstawowe pytanie: ile płyt DVD i ilu klientów obsługuje wypożyczalnia? Te dane musimy poznać z aktualnego stanu wypożyczalni.

Wprowadzamy więc kolejne wymagania:

UR7 — Dysponuję pięcioma tysiącami tytułów na siedmiu tysiącach płyt.

UR8 — Niedawno wydałem kartę klienta o numerze 9000, przypuszczam także, że kilku klientów zrezygnowało z usług mojej wypożyczalni nie żądając usunięcia swoich danych.

Analiza wymagań klienta

Mamy już niektóre wymagania naszego klienta i możemy zostawić właściciela wypożyczalni w spokoju. Spróbujemy teraz zrozumieć żądania i zapisać je w sposób bardziej precyzyjny.

Rozpoczniemy od wymagań UR1 i UR7:

UR1 — Klienci zwracają płyty DVD do skrytki jeszcze przed otwarciem wypożyczalni. Jeśli zwrócono kilka kopii tego samego filmu, to nie mogę ich rozróżnić.

UR7 — Dysponuję pięcioma tysiącami tytułów na siedmiu tysiącach płyt.

W powyższych stwierdzeniach widzimy tu bardzo ważny fakt: wypożyczalnia ma wiele kopii wielu płyt i potrzebna jest informacja o tym, kto zwrócił daną kopię płyty (nie wystarcza znajomość samego tytułu zwracanej płyty). Jeżeli nie zauważymy tego, że tytuł płyty DVD (np. film „2001”) musi być obsługiwany przez system inaczej niż dana płyta DVD (kopia nr 3 filmu „2001”), to czeka nas później pracochłonna przeróbka aplikacji.

Możemy przedstawić te wymagania w sposób bardzie sformalizowany, rozbijając je na więcej treściwych deklaracji:

R1: Wypożyczalnia musi obsługiwać ponad 5000 różnych tytułów.

R2: Wypożyczalnia musi obsługiwać ponad 7000 różnych płyt.

R3: Trzeba obsłużyć co najmniej 5 różnych kopii każdego tytułu.

R4: Na podstawie zwracanej płyty trzeba mieć możliwość określenia, który klient ją zwrócił.

Przejdźmy teraz do wymagania UR2:

UR2 — Nie mam innego sposobu na sprawdzenie, które płyty są w danym momencie wypożyczone przez klientów, jak pójście na zaplecze i rzut oka na regał. Zniechęca to klientów, którzy muszą odstawić pudełko na półkę, bo okazało się, że film został wypożyczony.

Jest to trochę dziwny problem, który można rozwiązać wieloma metodami nie posługując się przy tym komputerem. Problem polega na tym, że klienci wybierają pudełko płyty DVD z półki, podchodzą do lady, muszą czekać aż ktoś z obsługi sprawdzi dostępność płyty na zapleczu i jeśli płyty nie ma — muszą odnieść pudełko na półkę. Na podstawie znajomości wcześniej widzianych systemów rejestracji „zeszytowej” możemy też stwierdzić, że obsługa wypożyczalni na ogół nie wie kiedy zostaną zwrócone wypożyczone kopie.

Jako rozwiązanie nie wymagające użycia komputera można zaproponować stosowanie przywieszek na pudełkach płyt, które oznaczałyby wypożyczenie wszystkich kopii. Coś takiego na pewno pomoże, ale jest dosyć pracochłonne.

Do naszego proponowanego systemu możemy dodać funkcję, która będzie informować klienta, że wszystkie kopie są wypożyczone oraz kiedy nastąpi zwrot. Powinno być to zupełnie proste, ponieważ trudno sobie wyobrazić sprawnie działający system, który nie udostępnia informacji o wypożyczonych płytach. Jedyną wadą takiego rozwiązania będzie to, że klient nadal musi podejść do lady z pudełkiem po płycie aby uzyskać informację, że płyta nie jest dostępna. Usunięcie znacznika wypożyczenia będzie prostsze i może zmniejszyć kolejkę przy ladzie. Na podstawie osobistych doświadczeń wiemy jednak, że wcale tak nie musi być.

Jeżeli chcemy działać zdecydowanie, to możemy zaproponować następny krok. Załóżmy, że wstawimy do wypożyczalni terminal dla klientów, na którym sami będą mogli sprawdzić dostępność danej płyty. Mogłoby to zmniejszyć kolejkę przy ladzie, pracochłonność a nawet liczbę personelu. A gdyby rozwinąć ten pomysł szerzej i zezwolić klientom na wyszukiwanie nowych tytułów z ich ulubionymi gwiazdami? Właściciel wypożyczalni mógłby przystać na takie rozwiązanie, jeżeli nie byłoby zbyt drogie... ale musimy pamiętać o drzemiącej możliwości rozrostu funkcji.

Aby uniknąć poszerzania własnych pomysłów powinniśmy ponownie porozmawiać z właścicielem wypożyczalni i sprawdzić jego reakcję na nasze propozycje. Zostawiamy więc otwarte wymagania:

R5: Potrzeba efektywnego sprawdzania, czy wszystkie kopie danego tytułu są wypożyczone i gdzie się one znajdują.

R6: Potrzeba wyszukiwania dostępnych tytułów w bazie danych.

Powyższe wymagania musimy uściślić później (i być może nadać im odpowiedni priorytet).

Przejdźmy teraz do wymagania UR3:

UR3 — System musi być przyjazny.

Nie jest to zagadnienie, nad którym można łatwo przejść do porządku dziennego. Nic na nie upoważnia, aby takie żądanie zignorować. Czy oznacza to, że obsługa systemu ma być tak intuicyjna, że niepotrzebne będzie żadne szkolenie? Czy oznacza to, że system prowadzi użytkownika „za rękę” przy każdym podejmowanym przez niego działaniu? Prawdopodobnie są w tym żądaniu ukryte jakieś założenia na temat wydajności systemu — przecież system działający powoli nie będzie systemem przyjaznym! Być może, najlepiej byłoby zastosować interfejs graficzny ułatwiający obsługę poszczególnych funkcji, a następnie po utworzeniu wstępnego projektu aplikacji uzyskać od zamawiającego zgodę na przyjęte rozwiązania.

Zajmiemy się teraz wymaganiem UR4:

UR4 — Chciałbym utrzymać kasę sklepową, ponieważ niedawno ją kupiłem.

Musimy wyjaśnić, co właściciel miał na myśli formułując takie żądanie. Czy myślał, że system wymaga integracji z istniejącą kasą sklepową, czy nie chciał aby system ją zastąpił ponieważ mogłoby to kosztować drożej? Wyjaśnienie tych pytań jest ważne, ponieważ mogą one oznaczać zarówno brak wymagań, jak i ich nadmiar.

W rozmowie z właścicielem dowiedzieliśmy się, że spodziewa się on systemu, który będzie oddzielony od kasy sklepowej i na będzie jedynie wyświetlał na ekranie należną opłatę. Oznacza to duży sukces i mniejszą pracochłonność aplikacji, a więc formułujemy następujące wymaganie:

R7: System ma wyświetlać na ekranie kwotę należnej opłaty bez integracji z systemem kasowym.

Można tu sobie wyobrazić, jak wiele pracy musielibyśmy włożyć w opracowanie systemu współpracującego w jakiś sposób z systemem kasowym.

Przejdziemy teraz do wymagania UR5:

UR5 — Słyszałem o tym, że programiści udostępniają klientom kody źródłowe systemów komputerowych i chciałbym taki kod uzyskać. W przyszłości, jeśli coś będzie źle działać lub zechcę coś zmienić, wykonanie poprawek będę mógł zlecić komu zechcę.

Zakładamy tu, że właściciel miał na myśli jakiś rodzaj systemu otwartego, co w rzeczywistości ma kilka znaczeń. Nie jest tu problemem kod programu, który będziemy tworzyć, ale problemy mogą się pojawić w innych elementach systemu. Dotychczas zakładaliśmy, że wystarczające będzie przekazanie właścicielowi wypożyczalni źródłowego kodu tworzonej aplikacji, ale kod źródłowy innych elementów może nie być dostępny. Na tym etapie nie chcemy tego wyraźnie zaznaczać, ale rozwiązanie wykorzystujące system Linux wydaje się interesujące jeśli całość ma być systemem otwartym.

R8: Musimy udostępnić właścicielowi wypożyczalni kod źródłowy aplikacji.

R9: Powinny być dostępne kody źródłowe innych składników systemu.

Dodaliśmy tu obowiązkowe wymaganie R8 i opcjonalne wymaganie R9. Wymagania obowiązkowe muszą zostać spełnione przed odbiorem całej aplikacji przez klienta. Preferujemy system operacyjny, który udostępnia cały kod źródłowy, ale możemy się bez niego obejść. Użycie słów „musimy” i „powinny być” różnicuje kategorie powyższych wymagań.

Przechodzimy teraz do wymagania UR6:

UR6 — Na system nie mogę wydać więcej niż 1000 dolarów, a na wykonaną pracę sporządzimy oddzielną umowę.

Nie spodziewamy się chyba, że po wykonaniu tego jednego zadania będziemy mogli pójść na emeryturę? Jest to dość ograniczony budżet i w związku z tym należy zrezygnować z wielu komercyjnych pakietów oprogramowania, które mogłyby być wykorzystane.

R10: Całkowity koszt zakupu systemu (sprzętu i licencji) nie może przekroczyć 1000 dolarów.

Teraz zajmiemy się wymaganiami UR7 i UR8, ponieważ obydwa dotyczą rozmiarów systemu.

UR7 — Dysponuję pięcioma tysiącami tytułów na siedmiu tysiącach płyt.

UR8 — Niedawno wydałem kartę klienta o numerze 9000, przypuszczam także, że kilku klientów zrezygnowało z usług mojej wypożyczalni nie żądając usunięcia swoich danych.

W rzeczywistości pierwsze z nich zostało już uwzględnione w założeniach R1, R2 i R3. Tutaj posłuży nam ono do określenia wymagań na numerację klientów i przewidywanego wzrostu ich liczby.

R11: System musi obsługiwać nie mniej niż 9000 klientów, umożliwiać dopisywanie nowych klientów i usuwanie informacji o klientach rezygnujących z usług.

R12: System musi umożliwić co najmniej dwukrotny wzrost liczby danych w porównaniu ze stanem początkowym.

W tym momencie można zasugerować własne wymagania, o których zamawiający nie pomyślał i które wydają się ważne, np. z punktu widzenia elastyczności programu czy łatwości przystosowania go do innych zadań. Można np. dołączyć pewne wymagania nie związane z funkcjami systemu lecz dotyczące wydajności (np. szybkość reakcji zapełnionej bazy danych) lub jakości (aplikacja nie może przerywać pracy jeśli użytkownik naciśnie nieodpowiedni klawisz).

Pomimo tego, że najlepszym miejscem na następne wymagania jest „lista życzeń”, to istnieje kilka zagadnień, których uwzględnienie może się przydać w przyszłości. Po pierwsze, należy do nich dostęp do aplikacji z przeglądarki WWW:

R13: System musi umożliwiać rozbudowę i dołączenie interfejsu WWW dostępnego przez Internet.

Ostatnio obserwuje się bardzo szybki rozwój języka XML jako narzędzia dostępu do różnych struktur danych, co pociąga za sobą następne wymaganie:

R14: System musi umożliwiać dodanie funkcji importu danych w formacie XML, ponieważ dostawca płyt DVD planuje użycie takiego formatu.

Innym interesującym zjawiskiem jest rozwój usług katalogowych LDAP. Wydaje się, że mogą się pojawić serwery LDAP przechowujące np. dane mieszkańców danej miejscowości lub informacje o planowanych wydaniach nowych płyt DVD.

R15: System musi umożliwiać rozbudowę w zakresie korzystania z usług serwerów LDAP.

W rozdziałach 7., 16. i 23. zobaczymy, w jaki sposób dołączyć te właściwości do naszej aplikacji.

Nie mówiliśmy do tej pory prawie nic o klientach korzystających z systemu, ale dopóki nie wyjaśnimy wymagania UR2 oraz sposobu rozwiązania problemów podanych w R5 i R6, to nie wiemy czy klienci będą mogli bezpośrednio korzystać z systemu. Nawet gdyby właściciel dał się przekonać do takiego pomysłu, to jego realizacja wymagałaby zakupu dodatkowego komputera i udostępnienia go klientom. Przy niewielkim budżecie może się to okazać niewykonalne.

Po przemyśleniu wymagań wróćmy więc do właściciela wypożyczalni i przekonajmy się, czy potrafi on sprecyzować wymagania R5 i R6 dotyczące dostępu jego klientów do systemu. Poprosimy go również o informacje na temat formatów używanych przy numeracji płyt DVD i numeracji klientów. Ponieważ nie wydaje się prawdopodobne, aby właściciel chciał zmieniać kody wszystkich płyt na nowe i wydawać na nowo 9000 kart klienta, to lepiej posłużyć się istniejącymi danymi.

Musimy także sprawdzić, czy żadne wymaganie nie zostało pominięte i im wcześniej to wykryjemy, tym lepiej dla nas.

Przebywając ponownie w wypożyczalni stwierdzamy, że zmieniły się ceny za wypożyczenie filmów. Widać, że wypożyczenie filmu jest tańsze w niektóre dni tygodnia i w czasie letnich wakacji, kiedy liczba klientów jest mniejsza. Oprócz tego właściciel udziela rabatu klientom wypożyczającym kilka płyt. Dodajmy więc kolejne wymaganie:

R16: System musi obsługiwać zniżki za wypożyczenie kilku płyt oraz różnicować ceny za wypożyczenie zależnie od dnia tygodnia.

W kolejnej rozmowie z właścicielem na temat naszego rozumienia wymagań stwierdzamy, że bardzo mu się spodobał pomysł prostego interfejsu dla klientów, którzy mogliby sprawdzać w wypożyczalni, czy wybrany przez nich tytuł jest dostępny lub czy został zarezerwowany. Jest to interesujące, ponieważ nie rozważaliśmy do tej pory pomysłu wcześniejszej rezerwacji płyt. Niestety, właściciel nie chce płacić zbyt wiele za stanowisko dla klientów w wypożyczalni. Sądzi, że nie powinno ono kosztować więcej niż 200 dolarów. Może to stanowić problem, a więc jedyny sposób budowy takiego stanowiska to użycie przestarzałego komputera, nawet bez dysku, uruchamianego przez sieć.

Stwierdziliśmy także, że właściciel nie widzi żadnych powodów, dla których trzeba byłoby różnicować jego dostęp do systemu i dostęp pracowników. Chce także, aby dostęp z dodatkowego stanowiska lub z Internetu także nie był różnicowany pod względem uprawnień, lecz tylko ograniczał liczbę funkcji systemu. Ułatwia to zadanie programiście, a więc nie będziemy prawdopodobnie martwić się o bezpieczeństwo i wystarczy samo logowanie do systemu. W aplikacji nie będziemy także rozróżniać w skomplikowany sposób kategorii użytkowników systemu.

Pytaliśmy także właściciela o możliwość naklejania etykiet z kodem kreskowym na pudełka zawierające płyty i dołączenie do systemu czytnika takiego kodu. Wszystko się podobało do momentu podania ceny — wtedy propozycja użycia kodu kreskowego została odrzucona. Być może ten pomysł będzie zrealizowany w następnym projekcie.

Mamy więc zmienione wymagania R5, R6 i dodaliśmy wymaganie R17 dotyczące rezerwacji tytułów:

R5: System musi sygnalizować wypożyczenie wszystkich kopii danego tytułu, jeśli klient chce wypożyczyć niedostępną w danym momencie płytę.

R6: W systemie powinien znaleźć się publicznie dostępny terminal do wyszukiwania i sprawdzania dostępności tytułów, jeśli będzie to możliwe za kwotę mniejszą niż 200 dolarów.

R17: System musi umożliwiać rezerwacje tytułów. Każdy klient może rezerwować przynajmniej jeden tytuł na tydzień przed planowanym terminem wypożyczenia. Rezerwacja ma być bezpłatna, ale zarezerwowany film musi zostać odebrany do godziny 16:00 w dniu rezerwacji — w przeciwnym wypadku może być wypożyczony komuś innemu.

Odpowiedź na pytanie o formaty numerów płyt i klientów była prosta: płyty mają pięciocyfrowe oznaczenia, podobnie jak klienci. Pytaliśmy się również, co się dzieje jeśli klient nie ma przy sobie swojej karty. Zdarza się to bardzo często i wówczas personel prosi klienta o kod pocztowy oraz nazwisko i na tej podstawie wyszukuje numer wpisany do książki. Jeśli dane klienta z książki potwierdzą się, to film jest wypożyczany bez karty. Mamy więc trzy dodatkowe wymagania:

R18: System musi posługiwać się pięciocyfrowymi numerami płyt.

R19: System musi obsługiwać istniejące pięciocyfrowe numery klientów wypożyczalni.

R20: System musi umożliwiać określenie numeru klienta na podstawie informacji podanych przez klienta, nawet gdy nie przedstawi on swojej karty.

Spis wymagań

Sądzimy, że mamy już większość wymagań, a więc nadszedł czas na ich uporządkowanie, łącznie z dodaniem słów „musi” i „powinien”. Zazwyczaj taki spis powinien mieć postać formalnego dokumentu, o podpisanie którego należy poprosić naszego klienta:

R1: System musi obsługiwać ponad 5000 różnych tytułów.

R2: System musi obsługiwać ponad 7000 różnych płyt.

R3: System musi obsłużyć co najmniej 5 różnych kopii każdego tytułu.

R4: Musi istnieć możliwość określenia klienta na podstawie numeru zwracanej płyty.

R5: System musi sygnalizować wypożyczenie wszystkich kopii danego tytułu, jeśli klient chce wypożyczyć niedostępną w danym momencie płytę.

R6: W systemie powinien znaleźć się publicznie dostępny terminal do wyszukiwania i sprawdzania dostępności tytułów, jeśli będzie to możliwe za kwotę mniejszą niż 200 dolarów.

R7: System musi wyświetlać na ekranie kwotę należnej opłaty bez integracji z systemem kasowym.

R8: Musimy udostępnić właścicielowi wypożyczalni kod źródłowy aplikacji.

R9: Powinny być dostępne kody źródłowe innych składników systemu.

R10: Całkowity koszt systemu musi być mniejszy niż 1000 dolarów.

R11: System musi obsługiwać więcej niż 9000 klientów.

R12: System musi umożliwić co najmniej dwukrotny wzrost liczby danych w porównaniu ze stanem początkowym.

R13: System musi umożliwiać rozbudowę i dołączenie interfejsu WWW dostępnego przez Internet.

R14: System musi umożliwiać dodanie funkcji importu danych w formacie XML.

R15: System musi umożliwiać rozbudowę w zakresie korzystania z usług serwerów LDAP.

R16: System musi obsługiwać zniżki za wypożyczenie kilku płyt oraz różnicować ceny za wypożyczenie zależnie od dnia tygodnia.

R17: System musi umożliwiać rezerwacje tytułów. Każdy klient może rezerwować przynajmniej jeden tytuł na tydzień z góry. Rezerwacja ma być bezpłatna, ale zarezerwowany film musi zostać odebrany do godziny 16:00 w dniu rezerwacji — w przeciwnym wypadku może być wypożyczony komuś innemu.

R18: System musi posługiwać się pięciocyfrowymi numerami płyt.

R19: System musi obsługiwać istniejące pięciocyfrowe numery klientów wypożyczalni.

R20: System musi umożliwiać określenie numeru klienta na podstawie informacji podanych przez klienta w wypożyczalni.

Należy sprawdzić, czy wszystkie pierwotne wymagania klienta znalazły się na tej liście. Odkrywamy, że nie ma tu nic na temat wymagania:

UR3 — System musi być przyjazny.

Trzeba więc dodać coś na temat interfejsu graficznego i wydajności:

R21: System powinien być wyposażony w interfejs graficzny.

R22: Czas reakcji systemu na działanie użytkownika powinien być mniejszy niż 2 sekundy.

Wymaganie R21 nadal nie jest precyzyjne. W rzeczywistości prawdopodobnie zechcemy, aby wymaganie to można było w jakiś sposób testować, np. na podstawie rysunków pokazujących sposób pracy z interfejsem graficznym. Jeśli klient będzie zadowolony z układu elementów interfejsu na ekranie, to powinniśmy zachować szkice, aby można było je porównać z końcowym wynikiem naszej pracy.

Chcąc uprościć naszą aplikację pominęliśmy także wiele innych wymagań, spotykanych w rzeczywistości. Można tu wymienić czytniki kodu kreskowego, bardziej elastyczny system wypożyczania, pełnoekranowe prezentacje filmów DVD — wszystko to zostało przeniesione do etapu drugiego.

Dzieląc wymagania na formalne kategorie, o których wcześniej wspominaliśmy, otrzymujemy:

Większość wymagań z naszej listy ma charakter mieszany. Jeśli powstanie lista wymagań, na której żadna pozycja nie może być zaliczona do którejś z głównych kategorii, to oznacza, że w fazie tworzenia założeń pominięto pewne ważne zagadnienia.

W tym momencie musimy zdefiniować klientów stykających się z naszym proponowanym systemem: będą to albo rzeczywiści ludzie, albo jakieś interfejsy zewnętrzne.

Przypadki użycia

Osoby korzystające z systemu będziemy nazywać aktorami, używając terminologii stosowanej przy projektowaniu systemów. Sposoby komunikacji tych osób z systemem nazywamy przypadkami użycia. Taka terminologia została po raz pierwszy użyta przez Ivara Jacobsona i obecnie jest stosowana w zunifikowanym języku modelowania UML.

Musimy także poznać następną warstwę funkcjonalną naszego systemu, związaną np. ze sposobem wprowadzania nowych tytułów do wypożyczalni.

Spróbujmy najpierw sformułować własne wyobrażenia, które następnie przedstawimy właścicielowi wypożyczalni do zatwierdzenia. Poniżej pokazano schemat przypadku użycia, który może być wykorzystany do przedstawienia podstawowych funkcji systemu.

Właściciel wypożyczalni

Dodanie nowego klienta

Wyszukanie klienta

Dopisanie szczegółów

Wypożyczenie płyty DVD

Klient przy terminalu

Klient z Internetu

Wyszukiwanie płyty DVD

Rezerwacja płyty DVD

Przeglądanie katalogu

Mamy tu trójkę aktorów: właściciela lub pracownika wypożyczalni, klienta wypożyczalni korzystającego z terminala i klienta łączącego się przez Internet. Dwaj ostatni są na rysunku oznaczeni jednym symbolem, ponieważ dotyczą ich te same przypadki użycia.

Właściciel wypożyczalni może wykonywać następujące czynności:

Klient korzystający z terminala w wypożyczalni może wykonywać następujące czynności:

Klient łączący się przez Internet może wykonywać dokładnie takie same czynności, co klient przy terminalu w wypożyczalni. Jedyną różnicą jest tu korzystanie z przeglądarki WWW.

Funkcje, które przydzieliliśmy poszczególnym użytkownikom systemu nie są kompletne, ale służą tu tylko jako przykład. Możemy posłużyć się przypadkami użycia omawiając działanie systemu z właścicielem i jego klientami. Jest to bardzo wygodny i łatwy do zrozumienia sposób prezentacji różnych zagadnień, dzięki któremu wymagania staną się bardziej zrozumiałe i można będzie je później uściślić.

Na podstawie przypadków użycia określimy zakres funkcjonalny naszej aplikacji i opis wszystkich operacji, które system musi wykonać. Znając te dane możemy przystąpić do tworzenia struktury aplikacji.

Architektura aplikacji

Ponieważ znamy już podstawowe wymagania, to możemy teraz zastanowić się na sposobem zbudowania systemu.

Zastanawiając się nad tym, jakie współdziałające ze sobą elementy można użyć w systemie, aby zrealizować jego wymagane funkcje, będziemy posługiwać się informacjami pochodzącymi z etapu gromadzenia wymagań i analizy przypadków użycia. Architektura systemu musi być udokumentowana w taki sposób, aby dostarczać wskazówek przy szczegółowym projekcie i służyć pomocą po zakończeniu wszystkich prac.

Na dokładny wybór architektury wpływa wiele czynników. W naszym przykładzie występuje logiczna separacja graficznego interfejsu od zapisów dotyczących płyt DVD i klientów wypożyczalni. Dzieląc aplikację możemy uruchomić jej obydwie części na oddzielnych komputerach i zbudować system wielodostępny lub z dostępem przez Internet. Mamy także zupełnie wyjątkowe ograniczenia cenowe.

Wymagania odnośnie do kosztów i udostępniania kodu źródłowego sugerują z pewnością zastosowanie systemu Linux. Zdolność do działania tego systemu na tanim sprzęcie także umożliwia zachowanie niskich kosztów. Dodatkowo terminal w wypożyczalni ma być bardzo tani. Uważamy, że jedyny sposób realizacji tego zadania to użycie bezpłatnej przeglądarki WWW na jakimś mocno okrojonym komputerze, być może kupionym z drugiej ręki. Jak już wspomnieliśmy wcześniej, bezdyskowa stacja robocza uruchamiana poprzez sieć może być całkiem tania i dodatkową jej zaletą może być możliwość usunięcia stacji dyskietek, co zapobiegnie „zabawom” użytkowników terminala.

Jasne jest, że potrzebna będzie jakaś baza danych. W systemie Linux mamy cały szereg możliwości do wyboru, zarówno komercyjnych, jak i bezpłatnych. Dostępne są bazy posługujące się zwykłymi plikami tekstowymi, proste bazy indeksowe a nawet produkty używane na skalę przemysłową.

Wybraliśmy PostgreSQL, ponieważ jest to w pełni funkcjonalna baza wykorzystująca język SQL, która dodatkowo jest bezpłatna. Wybierając standard SQL pozostawiamy sobie możliwość przejścia na inną bazę danych jeżeli zajdzie taka potrzeba. Niektóre czynniki wpływające na wybór mechanizmu przechowywania danych podano w rozdziale 3.

Struktura aplikacji będzie wyglądać następująco:

Klient z Internetu

Internet

Interfejs

System w wypożyczalni

Baza danych

Terminal

LAN

Jeżeli z aplikacji ma korzystać jednocześnie więcej niż jeden użytkownik, to należy wziąć pod uwagę problem integralności danych. Co się bowiem stanie, jeżeli klient łączący się przez Internet zechce zarezerwować ten sam tytuł, co klient stojący przy terminalu w wypożyczalni? W naszych dotychczasowych rozważaniach nie zastanawialiśmy się, który z nich „zwycięży” i być może dopiero pod koniec dnia pracy będziemy znać poprawną liczbę klientów, którzy wypożyczyli lub zarezerwowali określone tytuły.

W tym momencie musimy rozważyć podział naszej aplikacji.

Wyobraźmy sobie, że kilka osób mieszkających z dala od siebie będzie pisało kod programu. Idealnym rozwiązaniem byłoby całkowite oddzielenie części funkcjonalnej od interfejsu graficznego, tak aby w przyszłości zupełnie inne osoby mogły utworzyć nowy interfejs wykorzystując ten sam system podstawowy. W następnych rozdziałach przy okazji omawiania różnych zastosowań interfejsu graficznego i funkcji baz danych zobaczymy w jaki sposób można się uporać z tym problemem. W rozdziale 2. zapoznamy się także z metodami zespołowego tworzenia kodu programu przez wielu programistów.

Tutaj musimy zdefiniować zasady współpracy aplikacji z interfejsem graficznym, oddzielając go od bazy danych i całej logiki aplikacji. Musimy więc utworzyć zestaw kilku programowych interfejsów aplikacji (API).

Projekt szczegółowy

Naszą aplikację będziemy budować w taki sposób, aby nadawane jej właściwości spełniały wymagania od R1 do R22 i jednocześnie były zgodne z założonymi przypadkami użycia. W kompletnym przykładzie rozwinęliśmy szerzej przypadki użycia i bardziej szczegółowo opisaliśmy wymagane funkcje.

W tej fazie wybiegniemy nieco w przód i przedstawimy API łączący interfejs przeznaczony dla użytkownika z podstawowymi funkcjami przetwarzania danych w naszym systemie. Po przyjęciu API powinno stać się jasne, w jaki sposób trzeba dodawać elementy interfejsu graficznego spełniające wymagania naszej aplikacji. API odnosi się bezpośrednio do niskopoziomowych funkcji wymaganych przez wcześniej opisane przypadki użycia. Ponieważ zdecydowaliśmy się na użycie języka C, to API również wyrażamy w konwencji używanej przy wywoływaniu funkcji tego języka.

Aby zachować możliwość pełnej kontroli nad aplikacją użyjemy uproszczonej metody konstrukcji API, przyjmując pewne ustalone struktury. Możemy sobie wyobrazić, że jest to część związana z tworzeniem kodu podczas pierwszej iteracji, która będzie użyta do sprawdzenia, czy API obsługuje wszystkie wymagane funkcje systemu.

Przy takich założeniach można już oddzielnie zająć się bazą danych, interfejsem graficznym oraz interfejsem WWW. Fizyczną strukturę bazy danych opisujemy w rozdziale 4.

Uwagi podane niżej dotyczą sposobu korzystania z funkcji bazy danych obsługującej wypożyczalnię płyt DVD. Wszystkie opisane tu struktury, stałe i funkcje stają się dostępne po dołączeniu pliku dvd.h i skonsolidowaniu z odpowiednim interfejsem.

Chcemy zwrócić uwagę na to, że nie należy używać podanego tutaj API do tworzenia szeroko udostępnianej biblioteki, ponieważ tak zdefiniowany specyficzny interfejs jest przeznaczony jako pomoc dla kilku programistów tworzących naszą przykładową aplikację.

Przykład zastosowania podstawowego interfejsu można znaleźć w pliku flatfile.c. Przedstawiono tam bardzo uproszczony sposób korzystania z danych zapisanych w prostym pliku tekstowym, bez żadnej optymalizacji. Przy większej liczbie płyt lub klientów (liczonych w tysiącach) bardzo szybko dojdzie do spowolnienia działania takiej bazy, ponieważ wykorzystuje się tu sekwencyjne przeszukiwanie. Dane w takiej postaci pozwalają jednak na niezależne tworzenie i testowanie interfejsu graficznego oraz funkcji obsługujących bazę danych.

Przykład użycia API dla rzeczywistej bazy danych będzie podany w rozdziale 4.

Jeżeli nie podano inaczej, to funkcje zwracają status błędu i przekazują dane wyjściowe za pomocą argumentów wskaźnikowych. Jeżeli wszystkie operacje zakończą się pomyślnie, to będzie zwrócony kod DVD_SUCCESS. W przypadku nieudanego wyszukania zwracany będzie kod DVD_ERR_NOT_FOUND, jeżeli np. szukana płyta jest wypożyczona lub karta klienta jest nieważna.

Zauważmy, że funkcje wyszukiwania mogą tworzyć pustą listę dopasowań i zwracać DVD_SUCCESS.

Funkcje dostępu do danych

Przed wywołaniem funkcji opisanych w tym podrozdziale należy zainicjować połączenie z bazą danych. Służy do tego wywołanie dvd_open_db:

int dvd_open_db()

Otwiera ono połączenie z bazą danych. Jeżeli otwarcie połączenia się uda, to funkcja zwróci DVD_SUCCESS, w przeciwnym wypadku będzie zwrócony kod błędu DVD_ERR_*.

Jeżeli połączenie z bazą danych ma wykonać określony użytkownik, a nie użytkownik domyślny (ten, który uruchomił aplikację), to możemy użyć jego nazwy i hasła. W takim przypadku do otwarcia połączenia z bazą wykorzystywana jest funkcja dvd_open_db_login:

int dvd_open_db_login(const char *user, const char *password)

Do uzyskania treści komunikatu o błędzie, którego numer jest zwracany przez funkcję bazy danych, używana jest funkcja dvd_err_text:

int dvd_err_text(const int error, char **message)

Na podstawie podanego numeru błędu funkcja dvd_err_text przekształca podany wskaźnik tak, aby odnosił się on do statycznego napisu zawierającego czytelny komunikat o błędzie. Funkcja ta zwraca DVD_SUCCESS.

Przed zakończeniem działania aplikacja musi zamknąć bazę danych wywołując funkcję dvd_close_db. Wymagają tego procesy niskiego poziomu zachodzące w bazie danych.

int dvd_close_db()

Jeśli zamknięcie bazy się uda, to funkcja zwraca DVD_SUCCESS.

Funkcje klienta

Płyty DVD mogą być wypożyczane tylko klientom legitymującym się kartą klienta. Karta ma unikatowy numer, który jest przydzielany automatycznie w momencie wpisu klienta do bazy danych. System odwołuje się do danych klienta za pomocą wewnętrznego identyfikatora (liczby typu integer).

Wszystkie tablice znakowe są zakończone bajtem wartości NULL, co ułatwia tworzenie kodu programu.

Struktura opisująca klienta nazywa się dvd_store_member i jest zdefiniowana następująco:

typedef struct {

int member_id; /* ident. wewn. [1..] */

char member_no[MEMBER_KNOWN_ID_LEN]; /* numer klienta */

char title[PERSON_TITLE_LEN]; /* zwrot grzeczn. Pani, Pan */

char fname[NAME_LEN]; /* imię */

char lname[NAME_LEN]; /* nazwisko */

char house_flat_ref[NAME_LEN]; /* nr lub nazwa domu */

char address1[ADDRESS_LEN]; /* wiersz adresu 1 */

char address2[ADDRESS_LEN]; /* wiersz adresu 2 */

char town[ADDRESS_LEN]; /* miejscowość */

char state[STATE_LEN]; /* stan - tylko USA */

char phone[PHONE_NO_LEN]; /* telefon: +48(22)1234567 */

char zipcode[ZIP_CODE_LEN]; /* kod pocztowy, np. 01-234 */

} dvd_store_member;

Nowego klienta dopisujemy za pomocą funkcji dvd_member_create:

int dvd_member_create(dvd_store_member *member, int *member_id);

Aplikacja musi utworzyć prototypowego klienta nadając wartości wszystkim polom struktury dvd_store_member oprócz pól member_id i member_no. Wywołanie dvd_member_create spowoduje dopisanie klienta do bazy danych i zwróci w postaci argumentu wyjściowego nowo przydzielony identyfikator member_id. Identyfikator ten będzie użyty do pobierania danych klienta z bazy. Nowy numer klienta zostanie utworzony i dopisany do bazy danych. Zwróćmy uwagę na to, że przekazywana struktura dvd_store_member nie jest przy tym modyfikowana, a więc aby odczytać nowy numer klienta wymagane jest wywołanie dvd_member_get:

int dvd_member_get(const int member_id, dvd_store_member *member);

Wywołanie to modyfikuje dane klienta we wpisie do bazy danych znalezionym na podstawie identyfikatora tego klienta.

Jako podsumowanie pokazujemy więc sekwencję operacji wymaganych przy dopisywaniu nowego klienta:

Funkcja dvd_member_get_id_from_number odtwarza wewnętrzny identyfikator klienta na podstawie numeru karty klienta zapisanego na karcie:

int dvd_member_get_id_from_number(const char *member_no, int *member_id);

Funkcja ta pobiera numer klienta z tablicy znakowej wskazywanej przez member_no (pięć znaków i końcowy NULL) i zapisuje odpowiadający mu identyfikator do zmiennej typu integer wskazywanej przez member_id.

Aby zmienić dane istniejącego klienta należy posłużyć się wywołaniem dvd_member_set:

int dvd_member_set(const dvd_store_member *member);

Powoduje to aktualizację wpisu w bazie danych pasującego dokładnie do podanej struktury klienta. Aby być pewnym, że dane w polach wpisu są poprawne należy go zainicjować wywołując dvd_member_get.

Sekwencja działań przy zmianie danych klienta jest więc następująca:

Aby odnaleźć szczegóły dotyczące danego klienta bez znajomości numeru jego karty należy użyć wywołania dvd_member_search:

int dvd_member_search(const char *name, int *ids[], int *count);

Funkcja ta pobiera napis będący częścią nazwiska i wyszukuje w bazie wszystkich klientów, których pola z nazwiskiem (pola lname w strukturze opisującej klienta) zawierają podany napis. Liczba znalezionych pasujących danych (nawet gdy wynosi zero) jest wpisywana do zmiennej typu integer wskazywanej przez count. Wynikiem jest tablica identyfikatorów klienta, na którą po modyfikacji będzie wskazywał ids. Wskaźnik ids musi być przekazany do funkcji free, aby zwolnić zajmowany przez niego obszar pamięci.

Aby zidentyfikować klienta należy:

Klient może być usunięty za pomocą wywołania funkcji dvd_member_delete:

int dvd_member_delete(const int member_id);

Identyfikator klienta nie musi koniecznie tracić ważności po rezygnacji klienta z usług, czyli nie musi być udostępniany dla nowych klientów (w naszym przykładowym rozwiązaniu nie traci on ważności). Powtórne użycie takiego zwolnionego identyfikatora nie jest najlepszym rozwiązaniem i należy tego unikać. Stare identyfikatory mogą, niejako przy okazji, wiązać się z jakimś przypadkowym „bagażem”, np. z nierozliczonymi wypożyczeniami, zaś użycie nowego identyfikatora przy każdym dodaniu nowego klienta jest prostym, choć prymitywnym rozwiązaniem. W drugiej fazie tworzenia aplikacji można dołączyć funkcje obsługujące przeterminowane numery klienta lub szukujące przeterminowanych wypożyczeń.

Funkcje związane z tytułem filmu

Każda wypożyczana płyta zawiera kopię filmu o jakimś tytule. Możemy mieć kilka lub nie mieć żadnych kopii danego tytułu. Zestaw API pozwala systemowi na obsługę bazy danych zawierającej tytuły filmów DVD oraz ich dane produkcyjne (reżyser, główni aktorzy itp.). Struktura opisująca dane o filmie jest strukturą publiczną:

typedef struct {

int title_id; /* wewnętrzny ID [1..] */

char title_text[DVD_TITLE_LEN]; /* np. 'Milczenie owiec' */

char asin[ASIN_LEN]; /* 10-cyfrowy nr odniesienia */

char director[NAME_LEN]; /* reżyser (jedno nazwisko) */

char genre[GENRE_LEN]; /* rodzaj 'horror', 'komedia' */

/* przyszły API standardowej listy */

char classification[CLASS_LEN]; /* przyszły API standardowej listy */

char actor1[NAME_LEN]; /* aktor, np. 'Jeremy Irons' */

char actor2[NAME_LEN]; /* aktor, np. 'Ingmar Bergman' */

char release_date[DAY_DATE_LEN]; /* data YYYYMMDD plus null */

char rental_cost[COST_LEN]; /* cena wypożyczenia: $$$.cc */

} dvd_title;

Interfejs obsługujący tytuł filmu działa w taki sam sposób jak API dla obsługi klientów, korzystając z wewnętrznego identyfikatora title_id jako klucza.

int dvd_title_set(const dvd_title *title_record_to_update);

int dvd_title_get(const int title_id, dvd_title *title_record_to_complete);

int dvd_title_create(dvd_title *title_record_to_add, int *title_id);

int dvd_title_delete(const int title_id);

Pola zawierające rodzaj filmu i dane klasyfikujące muszą być zgodne z jednym z ograniczonych zestawów napisów używanych przy określaniu rodzaju filmu i jego klasyfikacji. Dane te są pobierane z pomocniczych funkcji dvd_genre_list i dvd_get_classification_list:

int dvd_genre_list(char **genre_list[], int *count);

int dvd_get_classification_list(char **class_list[], int *count);

Funkcja wyszukująca działa nieco inaczej i umożliwia wyszukiwanie albo na podstawie tytułu filmu albo na podstawie nazwisk osób zaangażowanych w danym filmie:

int dvd_title_search(const char *title, const char *name, int *result_ids[],

int *count);

Funkcja ta zwraca listę dopasowanych identyfikatorów filmu. Napis string może być częścią tytułu dopasowaną do pola w bazie danych. Jeżeli jego wartością jest NULL, to nie będzie pasował do żadnego tytułu, jeżeli jest to pusty napis, to będzie pasował do wszystkich tytułów. Napis name może być częścią nazwiska reżysera lub aktora dopasowywaną do pól w bazie danych. Jeśli zostanie znalezione jakiekolwiek dopasowanie, to zostanie ono dołączone do wyniku wyszukiwania.

Funkcje opisujące płytę DVD

Filmy (czyli tytuły) i fizyczne płyty DVD traktujemy oddzielnie, ponieważ można wypożyczyć tylko specyficzną płytę, zaś chcemy rezerwować tytuł do wypożyczenia w przyszłości. Rezerwacja nie dotyczy więc konkretnej płyty, lecz tytułu. System obsługujący wypożyczalnię będzie przydzielał fizyczną płytę w momencie wypożyczenia.

Każda płyta DVD ma unikatowy identyfikator. Obowiązuje tu zasada, że każda kopia jest oznaczona tym samym numerem. Wpis dla każdej fizycznej płyty w bazie danych zawiera informację o tym, który film znajduje się na danej kopii. Informacja ta musi być wprowadzona przez właściciela wypożyczalni w momencie otrzymania płyt.

Wpis zawierający dane płyty jest struktura publiczną:

typedef struct {

int disk_id; /* wewn. ID [1..] (nie wiąże się z title_id) */

int title_id; /* title_id - ID filmu, którego to dotyczy */

} dvd_disk;

filmu działa w taki sam sposób jak API dla obsługi tytułów. Identyfikator jest przydzielany wewnętrznie. Funkcja wyszukiwania zwraca listę identyfikatorów płyt dla podanego identyfikatora tytułu filmu:

int dvd_disk_set(const dvd_disk *disk_record_to_update);

int dvd_disk_get(const int disk_id, dvd_disk *disk_record_to_complete);

int dvd_disk_create(dvd_disk *disk_record_to_add, int *disk_id);

int dvd_disk_delete(const int disk_id);

int dvd_disk_search(const int title_id, int *result_ids[], int *count);

Funkcje związane z wypożyczaniem

Każdy klient wypożyczalni może wypożyczyć dowolną liczbę płyt. Każde wypożyczenie jest rejestrowane, łącznie z datą wypożyczenia. Każdy klient może zarezerwować jeden tytuł na podany dzień.

Format daty używany w systemie to YYYYMMDD. Bieżąca data jest uzyskiwana z wywołania funkcji pomocniczej dvd_today. Funkcja ta przekształca przekazany wskaźnik tak, aby wskazywał on na statyczną strukturę przechowującą bieżącą datę w poprawnej postaci:

int dvd_today(char **date);

Do sprawdzenia czy dany tytuł będzie dostępny danego dnia służy funkcja dvd_title_available. Data musi być podana w formacie YYYYMMDD.

int dvd_title_available(const int title_id, const char *date, int *count);

Argument count po modyfikacji zawiera liczbę kopii filmu, które będą prawdopodobnie dostępne danego dnia (może to być wartość zerowa).

Film DVD może być wypożyczony i wiąże się to z przydzieleniem fizycznej płyty za pomocą wywołania dvd_rent_title.

int dvd_rent_title(const int member_id, const int title_id, int *disk_id);

Kopia filmu o danym identyfikatorze na fizycznej płycie (jeśli jest dostępna) jest przydzielana i zwracana jako wartość argumentu disk_id. Dla klienta o danym identyfikatorze dokonywany jest wówczas wpis wypożyczenia w bazie danych. Jeżeli płyta nie jest dostępna, to będzie zwrócone DVD_ERR_NOT_FOUND.

Informacje o wypożyczeniu i zwrot płyty DVD obsługują wywołania dvd_rented_disk_info i dvd_disk_return.

int dvd_rented_disk_info(const int disk_id, int *member_id, char *date_rented);

int dvd_disk_return(const int disk_id, int *member_id, char *date_rented);

Dla podanego identyfikatora płyty powyższe funkcje zwracają identyfikator klienta, który daną płytę wypożyczył oraz datę wypożyczenia. Przy wywołaniu dvd_disk_return informacja o wypożyczeniu jest usuwana.

int dvd_reserve_title(

const char *date, const int title_id, const int member_id);

int dvd_reserve_title_cancel(const int member_id);

Druga rezerwacja dokonana przez klienta spowoduje anulowanie jego poprzedniej rezerwacji.

Dane na temat ostatniego żądania rezerwacji dla danego klienta mogą być uzyskane za pomocą wywołania dvd_reserve_title_query_by_member:

int dvd_reserve_title_query_by_member(const int member_id, int *title_id);

Dodatkowe funkcje, które nie występują w przykładowej aplikacji, lecz są nadal przedmiotem naszego zainteresowania są następujące:

int dvd_reserve_title_query_by_titledate(

const int title_id, const char *date, int *member_ids[])

Funkcja ta zwraca listę członków, którzy zarezerwowali podany tytuł na dany dzień. Data o wartości NULL oznacza dowolną datę:

int dvd_overdue_disks(

const char *date1, const char *date2, int *disk_ids[], int *count)

Powyższa funkcja przegląda tabelę wypożyczeń szukając płyt, dla których data wypożyczenia zwiera się między date1 a date2. Wartości NULL dla tych dat oznaczają odpowiednio początek odliczania czasu (czyli 1. stycznia 1970) i dzień jutrzejszy.

Przykładowa aplikacja

Przy aplikacji podzielonej na kilka współdziałających ze sobą składników, takiej jak nasza, przydaje się bardzo dodatkowe utworzenie aplikacji przykładowej dla określonego interfejsu. W naszym przypadku został utworzony taki prawie kompletny, chociaż nieefektywny produkt, który uniezależni twórców interfejsu graficznego od bazy danych. Będą oni wówczas mogli utworzyć działający program i uzyskiwać np. sensowne wyniki przeszukiwania itp.

Dla obsługi wypożyczalni DVD utworzyliśmy aplikację wykorzystującą wszystkie zdefiniowane API współpracujące z prostym plikiem tekstowym, a nie z pełną bazą danych. Kod programu nie jest skomplikowany, ale wystarcza do sprawdzenia, czy zdefiniowany zestaw API pozwoli utworzyć końcową, w pełni funkcjonalną aplikację. Kod ten nie jest też zoptymalizowany i prawdopodobnie będzie działał zbyt wolno przy większej liczbie płyt DVD. Kod musi być poprawny, a nie „szybki”, aby można było go sprawdzać i śledzić jego działanie. Praca z takim przykładowym kodem jest znacznie bardziej efektywna i umożliwia tworzenie interfejsu graficznego, w przeciwieństwie do pracy z kodem roboczym gdy właściwa baza danych nie jest jeszcze zbudowana. Takie podejście umożliwia również porównanie bazy danych z działającą wersją roboczą.

Użycie aplikacji przykładowej może wprowadzać pewne ograniczenia, np. związane z rozmiarem bazy danych. W naszym przypadku we wszelkich operacjach wyszukiwania uwzględniana jest wielkość liter, a więc zalecamy wymuszoną zmianę w danych wejściowych wszystkich liter na wielkie i dokładne podawanie wyszukiwanych napisów.

Opracowaliśmy także program testujący, uruchamiany z wiersza poleceń, który służy do sprawdzenia pliku tekstowego przechowującego dane. Program ten omówimy szczegółowo w rozdziale 11.

Materiały źródłowe

Polecamy lekturę podanych niżej pozycji związanych programowaniem i projektowaniem systemów:

Rapid Development, Steve McConnell, Microsoft Press (ISBN 1-55615-900-5)

eXtreme Programming Explained, Kent Beck, Addison Wesley (ISBN 0-201-61641-6)

Clouds to Code, Jesse Liberty, Wrox Press (ISBN 1-861000952)

The Cathedral and The Bazaar, Eric S. Raymond, O'Reilly & Associates (ISBN 1-56592-724-9) http://www.tuxedo.org/~esr/writings/cathedral-bazaar

Instant UML, Pierre-Alain Muller, Wrox Press (ISBN 1-861000871)

Object-Oriented Systems Analysis and Design, Bennet, McRobb & Farmer, McGraw Hill (ISBN 0-07-709497-2)

Podsumowanie

W tym rozdziale bardzo krótko omówiliśmy pierwsze kroki na drodze do utworzenia aplikacji w sposób strukturalny. Korzystając z przykładu wypożyczalni płyt DVD, o którym bardzo dużo będziemy pisać w dalszych częściach książki, pokazaliśmy sposób definiowania specyfikacji i projektowania szkieletu użytecznego systemu.

Rozpatrzyliśmy różnego rodzaju wymagania i związane z nimi problemy. Pokazaliśmy też iteracyjny model produkcji planowanego kodu.

Ustaliliśmy, że struktura systemu będzie zawierać interfejs użytkownika oddzielony od głównej części aplikacji. Na zakończenie zdefiniowaliśmy szczegóły interfejsu łączącego te dwa składniki systemu.

Od tego momentu możemy już przystąpić do tworzenia kodu.

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

18 F:\helion\R-01-t.doc



Wyszukiwarka

Podobne podstrony:
Professional Linux Programming, R-04-t, PLP - Rozdział 4: Interfejsy PostgreSQL
Professional Linux Programming, R-02-t, PLP_Rozdział2
Linux Programming Professional, r-25-01, PLP_Rozdział25
Linux Programming Professional, r-22-01, PLP_Rozdział_22
Linux Programming Professional, r-20-01, PLP - Rozdział 20
Linux Programming Professional, r-24-01, PLP_Rozdział_24
Professional Linux Programming, R-12-01, Szablon dla tlumaczy
Professional Linux Programming, R-08-t, Szablon dla tlumaczy
Linux Programming Professional, r-13-01, Szablon dla tlumaczy
Linux Programming Professional, R-16-t, Szablon dla tlumaczy
Advanced Linux Programming Chapter 01 advanced unix programming with linux
Asembler wykład 16-10-2000, Zaczynamy (pracę) z programem Turbo Assembler, Rozdział 1
PL Linux+ 2005 01 (osiol NET) www!OSIOLEK!com

więcej podobnych podstron