[WikiDyd] [TitleIndex] [WordIndex

Zajmiemy się metodami projektowania prostych programów. Nie są do tego niezbędne techniki modelowania, z którymi zapoznacie się Państwo w dalszym toku studiów, ale nawet do pracy nad najprostszym programem nie można przystępować bez projektu.

Ważne jest określenie, co chce się zrobić (specyfikacja funkcjonalna programu). Od odpowiedzi na pytanie "Co ma powstać?" zaczyna się projektowanie programu. W drugiej kolejności pytamy "jak ma to być zrobione?".

Przykład - opis

Jako przykład analizujemy zadanie napisania programu, który w podanych przez użytkownika lokalizacjach wyszuka pliki i katalogi o powtarzających się nazwach.

Co ?

Rozpoczynamy od określenia funkcji programu. W tym celu sporządzimy szkic dokumentacji użytkownika, według schematu uniksowej strony manuala (man-page).

NAZWA

SKŁADNIA

OPIS

OPCJE

BŁĘDY

LICENCJA

AUTOR

Po określeniu w taki sposób wymagań funkcjonalnych dodamy jeszcze jedno wymaganie niefunkcjonalne: program ma działać w środowisku uniksowym.

Jak?

Nasz program podzielimy na moduły. Podział będzie przeprowadzony ze względu na funkcje wypełniane przez poszczególne fragmenty programu i w taki sposób aby moduły były:

Podział programu na moduły pozwala:

Podstawowa sprawa przy podziale na moduły to określenie interfejsów. Interfejs modułu określa jego funkcjonalność, a nie konstrukcję wewnętrzną - odpowiada na pytanie co moduł robi?, a nie jak to robi?. W przypadku języka C moduł składa się co najmniej z pliku z rozszerzeniem .h zawierającego specyfikację interfejsu (zewnętrznie widoczne typy danych i zewnętrznie widoczne funkcje) oraz pliku z rozszerzeniem .c, zawierającego kod. Duży, żłozony moduł będzie składał się z wiekszej liczby plików, ale zapewne będzie też podzielony na pod-moduły: technika podziału kodu na kawałki powinna być stosowana konsekwentnie, aż do poziomu funkcji.

W naszym programie dirdu wydzielimy następujace moduły:

reader

Moduł reader powinien umożliwiać skanowanie drzewa katalogów rozpoczynając od określonej lokalizacji i zagłębiając się nie dalej, niż określił użytkownik. Powinien przekazywać nazwy odnalezionych plików i katalogów do modułu container.

container

Moduł ma przechowywać pary o postaci ( nazwa, ścieżka ) gdzie nazwa i ścieżka to napisy i nazwa jest nazwą pliku lub katalogu, a ściezka to miejsce, gdzie ten plik lub katalog się znajduje. Moduł powinien też potrafić zwrócić powtarzające się nazwy w postaci: ( nazwa, [ lista ścieżek ] ).

output

Moduł powinien zapisywać krotki o postaci ( nazwa, [ lista ścieżek ] ) do pliku lub na standardowe wyjście, w ,,ładnej" formie.

main

Moduł zawierający funkcję main, odpowiedzialny za analizę argumentów wywołania, sterujący innymi modułami.

Zaprojektowanie dobrego interfejsu poszczególnych modułów może wymagać kilku iteracji - przymiarek do projektów modułów i analizy współpracy pomiędzy modułami.

Rozpocznijmy od przymiarki do modułu main.

   1 main ( int argc, char ** argv ) {
   2   init-data( container, max-depth, output );
   3   read-and-process-args( argc, argv ); /* użyjemy getopt */
   4   argc -= optind;
   5   argv += optind;
   6   while( argc ) {
   7     process-directory( *argv++, container-interface, max-depth );
   8   }
   9   output-results( container-interface, output );
  10   return 0;
  11 }

Teraz przymiarka do modułu reader (oparta o [wiki:Jimp1/readDir Przykład użycia funkcji readdir i opendir]).

   1 void onedir( char * path, int level, int max-level, container-interface )
   2 {
   3   char namebuf[1024];    /* bufor do tworzenia nazw podkatalogow */
   4   struct dirent *entry;  /* zm. robocza do iteracji po pozycjach w katalogu */
   5                                                                                                                                                             
   6   DIR *thisdir= opendir( path );  /* proba otwarcia */
   7                                                                                                                                                             
   8   if( thisdir == NULL ) {  /* nie udalo sie otwaorzyc path jako katalogu */
   9     if( errno != ENOTDIR ) /* ignorujemy blad ENOTDIR (nie-katalog) 
  10                               - inne bledy zglaszamy */
  11       fprintf( stderr, "reader: %s : %s\n", path, strerror(errno) );
  12     return;
  13   }
  14                                                                                                                                                             
  15   while( (entry= readdir( thisdir )) != NULL ) { /* czytamy kolejne pozycje */
  16 
  17      if( strcmp( entry->d_name, "." ) != 0 
  18      && strcmp( entry->d_name, ".." ) != 0 ) {
  19        container-interface( entry->d_name, path );
  20        strcpy( namebuf, path );          /* tworzymy nazwe */
  21        strcat( namebuf, "/" );
  22        strcat( namebuf, entry->d_name );
  23        if( level < max-level) 
  24          onedir( namebuf, level+1, max-level, container-interface );      /* i wywolujemy rekurencyjnie listdir */
  25      }
  26   }
  27   closedir( thisdir ); /* na koniec zamykamy katalog */
  28 }

Następnie pierwsza przymiarka do modułu output - na jej podstawie i na podstawie modułu reader określimy oczekiwania względem ostatniego modułu - container.

   1 void output-results( container-interface, output ) {
   2   container-interface-init();
   3   while( (next-duplicate= container-interface()) != NULL )
   4     output += format( next-duplicate );
   5 }

Moduł container powinien udostepniać interfejs dla read i output:

   1 void add-to-container( char * name, char * path ) {
   2    container[ name ] += path;
   3 }
   4 
   5 void container-init() {
   6 }
   7 
   8 void container-next-duplicate() {
   9   if( is-next-duplicate() )
  10     return next-duplicate();
  11   else
  12     return NULL;
  13 }

Kodowanie

Po tych wstepnych przymiarkach możemy przejść do projektowania poszczególnych modułów.

Rozpoczniemy od modułu container: [wiki:Jimp1/DirduContainerDraft Pierwsza przymiarka i testy].

Testy pozwalają nam wykryć pewne błędy i ulepszyć kod: [wiki:Jimp1/DirduContainer w miare ostateczna wersja kontenera].

Następnie zajmiemy się modułem wyjścia: [wiki:Jimp1/DirduOutputDraft pierwsza przymiarka i testy].

Potem modułem skanującym dysk: [wiki:Jimp1/DirduReaderDraft pierwsza przymiarka i testy].

I wreszcie modułem głównym: [wiki:Jimp1/DirduMain kod].


2015-09-23 06:43