Zpět Obsah Další

Struktura aplikací


Ve dnešním dílu našeho seriálu o programování v Cocoa se samotným prostředím Cocoa ani tolik zabývat nebudeme: namísto toho se podíváme na některá základní pravidla návrhu a struktury (nejen) objektových aplikací. Samozřejmě, kdykoli budeme naše povídání doprovázet konkrétními příklady, použijeme API Cocoa; to, o čem si dnes budeme povídat však je mnohem obecnější a platí v jakémkoli prostředí.

Model, View, Controller

Základem kvalitního objektového designu je dělení na tři přinejmenším logicky nezávislé moduly: ty se tradičně nazývají "model", "view" (uživatelské rozhraní) a "controller" (správce); pro aplikační strukturu, založenou na těchto třech modulech, se často používá zkratka složená z jejich prvních písmen: MVC.

Každý z modulů má jasně stanovenou úlohu:

Při tomto návrhu získáme nejvyšší flexibilitu a vícenásobnou použitelnost kódu: model není nijak závislý na konkrétní aplikační logice, můžeme jej beze změny a snadno využít v jakékoli jiné aplikaci, jež pracuje se shodnými daty. Podobně controller nijak nezávisí na konkrétním vzhledu grafického uživatelského rozhraní — to můžeme podle potřeby (a podle požadavků zákazníků) snadno měnit, aniž bychom museli zasahovat do kódu controlleru (natož pak modelu).

Jednoduchý příklad

Ukažme si pro lepší ilustraci jednoduchý příklad: aplikaci, která zobrazí v grafickém uživatelském rozhraní obsah archivu typu TAR nebo TAR.GZ.

Model

Nejprve navrhneme datový model — objekt (či lépe třídu objektů), representující data, s nimiž pracujeme: v našem případě tedy archivy. Se zkušenostmi, jež máme z minulých dílů s obecnými službami knihovny Foundation, je to jednoduché — API našeho modelu by mohlo vypadat třeba takto:

 @interface Archiver
 +(Archive)archiveWithContentsOfFile:(NSString*)file;
 -(NSArray*)items; // pole NSDictionaries, obsahujících
 extern NSString *AName; // jméno souboru
 extern NSString *ASize; // velikost
 @end

Implementací se již podrobně zabývat nebudeme — programování se službami Foundation jsme si vysvětlili v předcházejících dílech. Zájemci se mohou na zdrojový kód implementace modulu Archive podívat na konci textu.

View

Máme-li model, můžeme se pustit do návrhu grafického uživatelského rozhraní. Stejně dobře jsme ovšem mohli postupovat opačně — nejprve sestavit rozhraní aplikace, a potom připravovat model: to je právě jednou z hlavních výhod MVC. View a model jsou na sobě naprosto nezávislé, a můžeme je sestavovat zcela samostatně — u větších projektů může jeden programátor nebo tým pracovat na modelu, a jiný na rozhraní.

V prostředí Cocoa ovšem pro návrh view využijeme InterfaceBuilder (připomeňme posledních několik dílů našeho seriálu, kde jsme si vysvětlovali jak tato geniální aplikace funguje). Rozhraní bude jednoduché: jediné okno, obsahující tabulku se dvěma sloupci (jméno souboru a jeho velikost), a tlačítko pro otevření nového souboru. Okno již máme připravené ze standardního projektového template; jen do něj uložíme tabulku a tlačítko, a nastavíme jejich atributy. Prozatím není třeba "drátovat" žádné akce ani outlety.

Controller

V takhle jednoduchém případě je nejsnazší připravit jak třídu pro controller tak i její instanci přímo v InterfaceBuilderu. Přejdeme do panelu "Classes", vybereme třídu NSObject, vyžádáme si vytvoření nové podtřídy, a v okně inspektoru nastavíme její atributy — speciálně přidáme outlet "table" třídy NSTableView a akci "openNewArchive". To vidíme na druhém obrázku:

Nyní si vyžádáme vytvoření instance třídy Controller ("Instantiate" z pop-up nabídky), a zajistíme propojení mezi controllerem a uživatelským rozhraním: outlet "table" nové instance "nadrátujeme" na tabulku, a tlačítko "nadrátujeme" na akci "openNewArchive" controlleru.

Poslední nutné nastavení je vazba tabulky na controller. Ta vyžaduje dva kroky:

Tím jsme v InterfaceBuilderu hotovi; ještě si jen vyžádáme vygenerování základních zdrojových souborů pro třídu Controller, a půjdeme do ProjectBuilderu doplnit její zdrojový kód.

Zdrojový kód Controlleru už je velmi jednoduchý: nejprve mezi jeho instanční proměnné přidáme model:

 #import <Cocoa/Cocoa.h>
 #import "Archiver.h"
 
 @interface Controller : NSObject {
     IBOutlet NSTableView *table;
     Archiver *model;
 }
 - (IBAction)openNewArchive:(id)sender;
 @end

a pak implementujeme dvě jednoduché metody, které využívá tabulka pro získání svého obsahu — první určí počet řádků, druhá vrátí hodnotu pro daný řádek (identifikovaný pořadovým číslem) a sloupec (identifikovaný právě tím identifikátorem, který jsme zadali v inspektoru v InterfaceBuilderu):

 @implementation Controller
 -(int)numberOfRowsInTableView:(NSTableView*)tv {
     return [[model items] count];
 }
 -(id)tableView:(NSTableView*)tv objectValueForTableColumn:(NSTableColumn*)col row:(int)row {
     return [[[model items] objectAtIndex:row] objectForKey:[col identifier]];
 }
 ...

Vidíme, že jsme si trochu usnadnili práci tím, že jsme použili jako identifikátory přímo konstanty, representující údaje ve slovnících modelu. Kdyby tomu tak nebylo, museli bychom buď použít řadu "ifů", nebo — lépe — vlastní překladový slovník z identifikátorů tabulky na klíče modelu (s ním by kód vypadal nějak takto: "...objectForKey:[translation objectForKey:[col identifier]]];").

Poslední co je třeba udělat je implementace akce "openNewArchive": zde jen použijeme standardní panel pro výběr souborů, a pokud jej uživatel uzavře tlačítkem "OK" (tj. metoda runModalForTypes: vrátí pravdivou hodnotu), vytvoříme nový model, odpovídající zvolenému souboru (jeho jméno nám vrátí panel na základě zprávy "filename"). Nakonec tabulce pošleme zprávu "reloadData", aby nové údaje zobrazila:

 ...
 -(IBAction)openNewArchive:(id)sender {
     NSOpenPanel *panel=[NSOpenPanel openPanel];
     if ([panel runModalForTypes:[NSArray arrayWithObject:@"gz"]]) {
         [model release];
         model=[[Archiver archiverWithContentsOfFile:[panel filename]] retain];
         [table reloadData];
     }
 }
 @end

To je opravdu vše: stačí aplikaci přeložit a spustit, a hned funguje — to ilustruje poslední obrázek:

Zdrojový kód modelu

 #import "Archiver.h"
 @interface TarArchiver:Archiver {
     NSMutableArray *items;
 }
 @end
 @implementation TarArchiver
 -(void)parseOutput:(NSString*)s {
     NSEnumerator *en=[[s componentsSeparatedByString:@"\n"] objectEnumerator];
     items=[[NSMutableArray alloc] init];
     while (s=[en nextObject]) if ([s length]) {
         NSArray *a=[s componentsSeparatedByString:@" "];
         [items addObject:[NSDictionary dictionaryWithObjectsAndKeys:
             [a lastObject],AName,
             [a objectAtIndex:[a count]-4],ASize,
             nil]];
     }
 }
 -(void)readArchive:(NSString*)file {
     NSTask *task=[[[NSTask alloc] init] autorelease];
     NSPipe *pipe=[NSPipe pipe];
     NSFileHandle *fh=[pipe fileHandleForReading];
     NSMutableData *buf=[NSMutableData data];
     NSData *d;
 
     [items release]; items=nil;
     [task setStandardOutput:pipe];
     [task setLaunchPath:@"/usr/bin/gnutar"];
     [task setArguments:[NSArray arrayWithObjects:@"tzvf",file,nil]];
     [task launch];
     while ((d=[fh availableData]) && [d length]) [buf appendData:d];
     [task waitUntilExit];
     if ([task terminationStatus]==0)
         [self parseOutput:[[[NSString alloc] initWithData:buf
          encoding:NSUTF8StringEncoding] autorelease]];
 }
 -initWithContentsOfFile:(NSString*)file {
     [self=[super init] readArchive:file];
     return self;
 }
 -(void)dealloc {
     [items release];
     [super dealloc];
 }
 +(Archiver*)archiverWithContentsOfFile:(NSString*)file {
     return [[[self alloc] initWithContentsOfFile:file] autorelease];
 }
 -(NSArray*)items {
     return items;
 }
 @end


Zpět Obsah Další

Copyright © Chip, O. Čada 2000-2003