Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

Introduzione a PureMVC: la prima applicazione

Imparare a conoscere uno dei Framework MVC più tradotti realizzando una semplice applicazione
Imparare a conoscere uno dei Framework MVC più tradotti realizzando una semplice applicazione
Link copiato negli appunti

PureMVC è un framework open-source scritto da Cliff Hall per ActionScript, di cui è stato effettuato il porting su altri 10 linguaggi, tra cui anche Java e Php. PureMVC è un'implementazione, lo dice il nome stesso, dell'affermatissimo design pattern architetturale Model-View-Controller per la realizzazione di applicazioni basate su interfaccia grafica.

Figura 1. Un framework per tante piattaforme
Un framework per tante piattaforme

Per affrontare lo studio di PureMVC e capirne il funzionamento è necessaria una buona conoscenza di alcuni dei design patterns descritti nel libro "Design Patterns: elementi per il riuso di software ad oggetti" della cosiddetta "Gang Of Four", tra i quali Singleton, Observer, Facade, Mediator, Command e Proxy.

I tre livelli dell'applicazione sono rappresentati da tre singleton: Proxy, Mediator e Command rispettivamente per Model, View e Controller. I tre livelli non comunicano direttamente tra di loro ma sfruttano la Facade che permette ai tre livelli di interagire tra di loro senza che l'uno dipenda dall'altro, mettendo così in pratica il principio del "minimo accoppiamento" che devono rispettare le classi e gli oggetti di un'applicazione ben progettata.

Figura 2. La struttura MVC del framework
La struttura MVC del framework

La comunicazione tra i livelli è realizzata grazie al pattern Observer che prevede lo scambio di messaggi, chiamati Notification (notifiche), tra gli oggetti. Le notifiche sono costituite da un nome (identificativo) e da un corpo: nel corpo può essere incluso un oggetto di qualsiasi tipo, utile all'esecuzione di un'azione sia sul model che sull'interfaccia.

Tutte le notifiche che viaggiano tra componenti diversi della nostra applicazione devono essere definite univocamente e dichiarate nella Facade, che è l'unico oggetto con cui tutti gli altri oggetti dell'applicazione possono comunicare direttamente.

Nel seguito dell'articolo ci orienteremo tra i meandri di PureMVC costruendo una semplice applicazione di esempio, cercando di chiarire, ove necessario, gli aspetti teorici del framework pur non soffermandoci eccessivamente sui dettagli funzionali.

La prima applicazione con PureMVC

Come prima applicazione con PureMVC, vogliamo sviluppare un semplice gestore di un datagrid che permetta di inserire, modificare o eliminare elementi all'interno di un component Datagrid.

Prima di iniziare lo sviluppo della nostra applicazione includiamo, tra le librerie del progetto, la libreria swc di PureMVC, che possiamo ottenere dalla sezione Download del sito del progetto. Il file swc può essere copiato nella cartella libs oppure aggiunto manualmente al build path, come mostrato in figura.

Figura 3. Inserire la libreria nel progetto
Inserire la libreria nel progetto

La prima cosa che dobbiamo fare è creare la struttura del progetto. Utilizzando Flash Builder 4 creiamo tre package: model, view e controller. Model conterrà le classi di tipo Proxy, view le classi di tipo Mediator e controller le classi di tipo SimpleCommand o MacroCommand.

All'interno di view creiamo un altro package "components" che conterrà i component mxml dell'interfaccia grafica della nostra applicazione. All'interno del package model, invece, creiamo un sottopackage con nome vo che conterrà tutti i cosiddetti "value objects", che altro non sono che classi che rappresentano il modello dei dati su cui lavora l'applicazione.

Una volta che la struttura del progetto è pronta, creiamo la facade che deve estendere la classe Facade ed implementare l'interfaccia IFacade di PureMVC (la chiamiamo ApplicationFacade e la posizioniamo all'interno del default package del progetto).

È necessario fare in modo che la classe facade sia gestita come un singleton, quindi in tutta l'applicazione è disponibile al più una sola instanza della classe ApplicationFacade. A tal proposito dobbiamo fornire la classe ApplicationFacade di un metodo getInstance() che restituisce l'unica instanza della classe stessa, così come previsto dal pattern Singleton. Inoltre, è una "best practice" definire nella classe Facade tutte le notifiche che potranno essere inviate intercettate dagli oggetti della nostra applicazione.

Inizialmente, ad esempio, la classe ApplicationFacade avrà il seguente codice:

Una cosa importante da notare è che abbiamo effettuato l'override del metodo initializeController() ApplicationFacade Command Command

Cerchiamo di definire un workflow per la creazione di un'applicazione basata su PureMVC

Creare l'interfaccia utente

Dopo aver definito la facade, possiamo iniziare a "disegnare" l'interfaccia grafica della nostra applicazione, che sarà accompagnata dalle classi Mediator che avranno il compito di gestire le funzionalità dei singoli component e lo scambio di messaggi tra essi.

L'interfaccia grafica che vogliamo creare dovrà essere simile a quella rappresentata in figura.

Figura 4. L'interfaccia grafica
L'interfaccia grafica

Abbiamo realizzato un component per la gestione di una riga del datagrid, costituito da due campi di testo e da un pulsante "Aggiungi". Quando viene selezionata una riga del datagrid, il component mostrerà i pulsanti "Modifica" ed "Elimina", per poter rispettivamente modificare o eliminare dal datagrid l'elemento selezionato.

Di seguito è riportato il codice del component.

<?xml version="1.0" encoding="utf-8"?>
<s:Group xmlns:fx="http://ns.adobe.com/mxml/2009"
         xmlns:s="library://ns.adobe.com/flex/spark"
         xmlns:mx="library://ns.adobe.com/flex/halo" width="273" height="164">
  <s:Panel x="0" y="0" width="274" height="164" title="Gestione utenti">
    <mx:Form x="0" y="0" width="100%" height="85">
      <mx:FormItem label="Nome">
        <s:TextInput id="tiNome"/>
      </mx:FormItem>
      <mx:FormItem label="Cognome">
        <s:TextInput id="tiCognome"/>
      </mx:FormItem>
    </mx:Form>
    <s:HGroup x="10" y="93" width="254">
      <s:Button id="btnAggiungi" label="Aggiungi"/>
      <mx:HBox id="boxOperazioni" visible="false">
        <s:Button id="btnDeseleziona" label="Annulla"/>
        <s:Button id="btnModifica" label="Modifica"/>
        <s:Button id="btnElimina" label="Elimina"/>
      </mx:HBox>
    </s:HGroup>
  </s:Panel>
</s:Group>

È particolarmente importante notare che il component non contiene una parte di logica (Actionscript) per poter gestire il riempimento dei TextInput e gli eventi associati ai pulsanti. Gran parte della logica per la gestione dell'interfaccia grafica deve essere gestita dalle classi mediator.

I component mxml dovrebbero soltanto effettuare il dispatch degli eventi, che devono essere intercettati e gestiti dai mediator. In generale, il codice actionscript deve essere ridotto al minimo all'interno dei file mxml.

I mediator

Le classi che hanno il compito di gestire l'interfaccia grafica e l'interazione tra i component mxml devono estendere la classe Mediator del framework. Devono avere una struttura di base simile a quella mostrata di seguito.

È importante che il costruttore invochi, con super(), il costruttore della classe padre passando come parametri il nome del mediator (definito univocamente all'interno di tutta l'applicazione) e l'oggetto che dovrà contenere il riferimento al component dell'interfaccia grafica. Il nome è un identificativo e serve a recuperare l'istanza del mediator dalla facade ogni volta che, da un punto della nostra applicazione, bisognerà effettuare delle operazioni sull'interfaccia.

Man mano che sviluppiamo le funzionalità del nostro software, aggiungiamo al mediator i metodi necessari alla gestione degli eventi che si verificano durante l'interazione dell'utente con l'applicazione. Due metodi fondamentali per il funzionamento del mediator sono handleNotifications() listNotificationInterests()

Terminiamo la costruzione della nostra interfaccia, posizionando nell'applicazione principale il datagrid e il component custom che abbiamo creato.

Il file MainApplication.mxml

<?xml version="1.0" encoding="utf-8"?>
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
               xmlns:s="library://ns.adobe.com/flex/spark" 
               xmlns:mx="library://ns.adobe.com/flex/halo" minWidth="500" minHeight="400"
               xmlns:components="view.components.*" height="244"
               creationComplete="facade.sendNotification(ApplicationFacade.STARTUP, this);">
  
  <fx:Declarations></fx:Declarations>
  
  <fx:Script>
    <![CDATA[
      import model.vo.UserVO;
      
      import mx.collections.ArrayCollection;
      
      private var facade:ApplicationFacade = ApplicationFacade.getInstance();
      
      public static const SELECT_USER:String = "UserSelectedEvent";
      
      [Bindable] public var utenti:ArrayCollection;
      [Bindable] public var utenteSelezionato:UserVO;
      
      private function sendEvent(name:String) : void {
        dispatchEvent(new Event(name));
      }
    ]]>
  </fx:Script>
  
  <fx:Binding source="dgUtenti.selectedItem as UserVO" destination="utenteSelezionato"/>
  
  <mx:DataGrid x="10" y="10" id="dgUtenti" dataProvider="{ utenti }"
               click="if (dgUtenti.selectedItem != null) sendEvent(SELECT_USER);">

    <mx:columns>
      <mx:DataGridColumn headerText="Nome" dataField="nome"/>
      <mx:DataGridColumn headerText="Cognome" dataField="cognome"/>
    </mx:columns>
  </mx:DataGrid>
  
  <components:UserEdit id="formUserEdit" x="320" y="10"></components:UserEdit>
</s:Application>

Al creationComplete STARTUP ApplicationFacade STARTUP STARTUP StartupCommand

Il pattern Mediator risolve il problema della comunicazione tra component dell'interfaccia grafica, garantendo l'indipendenza e il minimo accoppiamento tra i component. Nel seguito dell'articolo, quando andremo a mettere insieme i diversi pezzi della nostra applicazione verrà chiarito come i diversi oggetti comunicano.

Nella seconda parte dell'articolo completeremo l'applicazione ed esamineremo l'implementazione dei componenti model e controller di PureMVC

I Proxy e il Model

Prima di continuare con lo sviluppo del nostro software, è bene soffermarci sullo strato denominato Model che sarà composto dai "Proxy". Generalmente, il design pattern Proxy prevede lo sviluppo di classi che devono svolgere la funzione di interfaccia di comunicazione verso altri sistemi (filesystem, web-services, grandi aree di memoria etc.).

All'interno del framework PureMvc, oltre ad avere queste funzionalità, le classi proxy fungono anche da "data carrier", ovvero si occupano di conservare e gestire i dati su cui lavora la nostra applicazione.

Quindi, nel costruttore di ogni classe che estende Proxy dobbiamo effettuare una chiamata al costruttore della classe padre specificando due parametri: il nome dell'oggetto che viene istanziato (in modo da poterlo recuperare dalla facade quando necessario), e il tipo di dati su cui il proxy lavora.

Definiamo, per la nostra applicazione, la classe UserProxy: per la costante NAME vale lo stesso discorso fatto per le classi mediator, ovvero che è necessario un identificativo per recuperare il proxy dalla facade tramite il metodo retrieveProxy().

Di seguito è riportato il codice della classe UserProxy:

Da notare l'invocazione del costruttore della classe padre proxy, a cui passiamo il nome (identificativo del proxy all'interno della facade) e la struttura date su cui il proxy dovrà lavorare. Il proxy possiede una variabile di nome "data" con scope protected che contiene la struttura dati gestita.

Andiamo, quindi, a definire le operazioni che possono essere effettuate sull'ArrayCollection degli utenti. Le operazioni saranno dichiarate con scope public in modo che, attraverso i Command, sarà possibile gestire i dati contenuti nei proxy in relazione alle operazioni effettuate sull'interfaccia grafica.

Di seguito è riportato il codice della classe UserProxy completa:

public class UserProxy extends Proxy implements IProxy {
  
  public static const NAME = "UserProxy";
  
  public function UserProxy() {
    super(NAME, new ArrayCollection());
  }
  
  public function addItem(item:UserVO) : void {
    users.addItem(item);
  }
  
  public function setItemAt(item:UserVO, index:int) : void {
    users.setItemAt(item, index);
  }
  
  public function removeItemAt(index:int) : void {
    users.removeItemAt(index);
  }
  
  public function get users() : ArrayCollection {
    return data as ArrayCollection;
  }
}

Sono state implementate le funzioni di gestione dell'ArrayCollection ArrayCollection ArrayCollection UserVO

Ad esempio il metodo addItem() RemoteObject WebService

I "Command" che implementano il Controller

Il livello Controller del pattern MVC è implementato, nel framework, attraverso le classi SimpleCommand e MacroCommand. Il pattern Command è stato formalizzato per risolvere il problema dell'accoppiamento del codice che esegue delle operazioni sui dati con il codice che gestisce l'interfaccia grafica.

Così, SimpleCommand di PureMvc ha lo scopo di gestire l'esecuzione di una singola azione. Il punto di forza di questo approccio è che il disaccoppiamento è massimo, visto che la richiesta di esecuzione di un'operazione non deve assolutamente preoccuparsi di come l'operazione viene eseguita, poiché tutta la logica è confinata nei Proxy, mentre le classi che estendono SimpleCommand realizzano una sorta di interfaccia per l'esecuzione delle operazioni sui dati.

Dal punto di vista pratico, l'implementazione di un SimpleCommand è molto semplice.

Notiamo subito che la classe AddUserCommand SimpleCommand ICommand SimpleCommand INotification

Ogni classe che estende SimpleCommand Notification

Occorre, a questo punto, "registrare un Command"

Per fare ciò, utilizziamo il metodo registerCommand Facade inizializeController() ApplicationFacade

Anche se effettuare la registrazione di un Command all'interno di un altro Command può sembrare una pratica anomala, in realtà questa è una tecnica molto comoda se si utilizzano dei command che hanno l'obiettivo di effettuare lo startup di un component

Ad esempio, possiamo osservare il Command che si preoccupa di gestire lo startup dell'applicazione:

public class StartupCommand extends SimpleCommand implements ICommand {

  override public function execute(notification:INotification) : void {
  
    var userProxy:UserProxy = new UserProxy();
    facade.registerProxy(userProxy);
    
    var mainApplication:MainApplication = notification.getBody() as MainApplication;
    
    // leghiamo la lista di utenti dell'interfaccia con 
    // la lista degli utenti contenuta nel proxy UserProxy
    // mainApplication.utenti è una variabile [Bindable]
    mainApplication.utenti = userProxy.users;
    
    facade.registerMediator(new MainApplicationMediator(mainApplication));
    facade.registerMediator(new UserEditMediator(mainApplication.formUserEdit));
    facade.registerCommand(ApplicationFacade.USER_ADD, AddUserCommand);
  }
}

Oltre alla registrazione dei Mediator ApplicationFacade.USER_ADD ApplicationFacade.USER_ADD AddUserCommand

Così come vengono registrati, i Command possono anche essere rimossi removeCommand()

Le operazioni sull'interfaccia

Per gestire l'interazione con l'interfaccia grafica dobbiamo intercettare gli eventi Flex che vengono generati dai component grafici quando l'utente interagisce con l'applicazione.

È fondamentale che questi eventi siano intercettati solo ed esclusivamente dalla classe Mediator che gestisce il component. Questo è importante perchè, in questo modo, si evita di legare l'interfaccia grafica con il resto dell'applicazione (Command e Proxy) e le stesse business logic e data logic possono essere utilizzate con un'interfaccia costruita in Adobe AIR, Flex o Flash.

Il mediator ha il compito, importantissimo, di dichiarare tutti gli EventListener sull'interfaccia grafica e gestire l'invio delle notifiche al resto dell'applicazione.

Ogni mediator dovrà estendere la classe Mediator e implementare l'interfaccia Imediator. Così come per i Proxy, anche per i Mediator dobbiamo definire un NAME che fa da identificatore all'interno di tutta l'applicazione. Di seguito è riportata l'implementazione, parziale, del Mediator dell'interfaccia principale dell'applicazione.

Il costruttore del mediator riceve come parametro il riferimento al component e lo salva all'interno della proprietà protetta viewComponent

Da notare il listener onUserSelected "trasformare" l'evento Flex in una Notification PureMVC

Buona pratica è la definizione di una get function

Un'importantissima funzionalità dei Mediator, come già detto in precedenza, è l'invio e la ricezione dei messaggi Notification sendNotification() handleNotification() listNotificationInterests()

Lo vediamo nell'implementazione della classe UserEditMediator:

public class UserEditMediator extends Mediator implements IMediator {	

  public static var NAME:String = "UserEditMediator";
  
  public function UserEditMediator(view:Object) {
    super(NAME, view);
    userEditForm.btnAggiungi.addEventListener(MouseEvent.CLICK, onAddClick);
  }
  
  override public function handleNotification(notification:INotification) : void {
    
    switch (notification.getName()) {
      
      case ApplicationFacade.USER_SELECT:
        var userVO:UserVO = notification.getBody() as UserVO;
        userEditForm.tiNome.text = userVO.nome;
        userEditForm.tiCognome.text = userVO.cognome;
        
        userEditForm.btnAggiungi.visible = false;
        userEditForm.btnAggiungi.includeInLayout = false;
        
        userEditForm.boxOperazioni.visible = true;
        userEditForm.boxOperazioni.includeInLayout = true;
        break;
    }
  }
  
  override public function listNotificationInterests() : Array {
    return [ ApplicationFacade.USER_SELECT ];
  }
  
  private function onAddClick(evt:MouseEvent) : void {
    var userVO:UserVO = new UserVO();
    userVO.nome = userEditForm.tiNome.text;
    userVO.cognome = userEditForm.tiCognome.text;
  
    sendNotification(ApplicationFacade.USER_ADD, userVO);
  }
  
  public function get userEditForm() : UserEdit {
    return viewComponent as UserEdit;
  }
}

Il metodo listNotificationInterests() handleNotification() listNotificationInterests

Nota in allegato all'articolo c'è il progetto Flex sviluppato in questo articolo. Non sono implementate le funzionalità di cancellazione e di modifica. Volutamente non ho voluto completare l'esempio in modo da dare a voi il compito di esercitarvi e di implementare le funzionalità mancanti.

Tiriamo le somme...

Lo sviluppo di web application di medie e grandi dimensioni o complessità deve essere affrontato seguendo una metodologia ed effettuando delle scelte progettuali che devono garantire una semplificazione del lavoro ed un'ottimizzazione di tutto il processo di sviluppo. Inoltre è quasi obbligatorio rendere tutto il software, in ogni sua parte, testabile attraverso test unitari sia automatici che manuali.

La scelta di PureMVC come framework di sviluppo garantisce il minimo accoppiamento tra i moduli che a sua volta permette di testare separatamente le singole classi. Inoltre il meccanismo delle notifiche, anche se inizialmente può sembrare contorto o inefficiente, in realtà è un'arma potentissima nella mani degli sviluppatori. Molti meccanismi (difficilmente implementabili in maniera "tradizionale") sono di semplicissima implementazione sfruttando le notifiche come sistema di comunicazione tra le classi.

Inoltre, il framework permette lo sviluppo di software scalabile e manutenibile, in quanto è molto semplice aggiungere funzionalità e correggere eventuali bug.

Ti consigliamo anche