Home > python > Python: Pattern MVC per programmatori di coccio

Python: Pattern MVC per programmatori di coccio

2 Luglio 2013

Che cos’è?

Da wikipedia:

Il Model-View-Controller (MVC), in informatica, è un pattern architetturale molto diffuso
nello sviluppo di sistemi software, in particolare nell’ambito della programmazione orientata
agli oggetti, in grado di separare la logica di presentazione dei dati dalla logica di business

Il pattern si basa sulla separazione dei compiti fra i 3 componenti software presenti
nel pattern stesso (Model, View, Controller):

model: fornisce i metodi per accedere ai dati utili all’applicazione (logica di buisiness);
view: visualizza i dati contenuti nel model e interagisce con l’utente (logica di presentazione
ad es. la GUI);
controller: riceve i comandi dell’utente (tramite il view) e li attua modificando lo stato
degli altri due componenti model e view (logica di controllo).

La rappresentazione più semplice dell’interazione tra comopnenti dell’MVC e l’utente è:

           +--------------+
           |     MODEL    |
           +--------------+
          /                \
         /                  \
     aggiorna             manipola
       /                      \
      /                        \
+-----------+            +--------------+
|   VIEW    |            |  CONTROLLER  |
+-----------+            +--------------+
     \                         ^
      \                       /
      vede                  usa
        \                   /
         \                 /
          v               /  
          +---------------+
          |    UTENTE     |
          +---------------+

Prendiamo un esempio che anche un bambino deve capire.
Creiamo un semplicissimo programmino che aggiunga il 22% di IVA al prezzo immesso dall’utente.

Le tre classi rappresentanti il model, il view ed il controller, saranno:

class Model(object):
    def __init__(self):
        self.iva = 0.22
    def calcola(self, valore): # business logic
        self.valore = valore
        return self.valore + self.valore * self.iva 

class View(object):
    def mostra_valore(self, valore): # representation logic
        print "valore ivato: {}".format(valore)

class Controller(object):
    def __init__(self):
        self.model = Model() # riferimento al model
        self.view = View()   # riferimento al view

    def esegui(self): # control logic
        valore = raw_input("Inserisci valore al netto dell' I.V.A.: ") # prende il valore utente
        ivato = self.model.calcola(float(valore)) # lo dà al model che lo modifica
        self.view.mostra_valore(ivato) # lo spedisce al view che lo visualizza

if __name__ == '__main__':
    controller = Controller()
    controller.esegui()

l’esempio è decisamente stupido.
Il model detiene la business logic, ovvero i dati del programma, compreso il metodo che calcola
il valore finale. La view mostra solo il valore che gli viene passato dal controller e non gli
frega nulla di cosa sia tale valore. Chiaramente il codice andrebbe farcito di controlli sul
valore passato come argomento, quanto meno qualche try/except, ma fa nulla…
Il controller fa quello che deve, cioè prende il valore inserito dall’utente, lo dà in pasto
al model del quale mantiene un riferimento, ed una volta manipolato, lo rispedisce al view del
quale possiede un altro riferimento (in realtà il pattern lavorerebbe in
maniera un poco più complicata, ma gli approfondimenti andrebbero fatti in altra sede,
vedi interazione view-model, observers ecc)

Volendolo incasinare un pochino, potremmo utilizzare le librerie wx per creare una vera GUI,
che incarni alla perfezione il concetto di view.

Prima quanche regola:

1 – il model non deve contenere codice inerente la visualizzazione ed il controllo dei dati.
Nel model quindi nessuna interfaccia verso l’utente, quali l’input di dati, nessun widget GUI ecc.
2 – il controller ed il view, non devono contenere codice inerente il model, quale la manipolazione
dei dati. Eventuali calcoli, vanno sempre eseguiti all’interno del model.
3 – Il Model non deve MAI sapere cosa facciano il view ed il controller, mentre non è vero
il contrario, come si vedrà il controller avrà dei riferimenti al model e ne invocherà i metodi
durante la logica di controllo

import wx

class Model(object):
    def __init__(self):
        self.money = 0

    def aggiungi(self, value):
        self.money = self.money + value

    def rimuovi(self, value):
        self.money = self.money - value

    def saldo_attuale(self):
        return self.money


class View(wx.Frame):
    def __init__(self, parent):
        wx.Frame.__init__(self, parent, title='mvc example')
        sizer = wx.BoxSizer(wx.VERTICAL)
        text = wx.StaticText(self, label="saldo attuale")
        self.saldo = wx.TextCtrl(self)
        self.aggiungi = wx.Button(self, label="Aggiungi")
        self.rimuovi = wx.Button(self, label="rimuovi")        
        sizer.Add(text, 0, wx.EXPAND | wx.ALL)
        sizer.Add(self.saldo, 0, wx.EXPAND | wx.ALL)
        sizer.Add(self.aggiungi, 0, wx.EXPAND | wx.ALL)
        sizer.Add(self.rimuovi, 0, wx.EXPAND | wx.ALL)
        self.saldo.SetEditable(False)
        self.SetSizer(sizer)

    def set_money(self, valore):
        self.saldo.SetValue(str(valore))

class Controller(object):
    def __init__(self):
        self.model = Model() # riferimento al model
        self.view = View(None) # riferimento al view
        self.saldo_iniziale = self.model.saldo_attuale()
        self.view.set_money(self.saldo_iniziale)
        self.view.aggiungi.Bind(wx.EVT_BUTTON, self.aggiungi_valore)
        self.view.rimuovi.Bind(wx.EVT_BUTTON, self.rimuovi_valore)
        self.view.Show()

    def aggiungi_valore(self, evt):
        self.model.aggiungi(10)
        self.aggiorna_saldo(self.model.saldo_attuale())

    def rimuovi_valore(self, evt):
        self.model.rimuovi(10)
        self.aggiorna_saldo(self.model.saldo_attuale())

    def aggiorna_saldo(self, valore):
        self.view.set_money(valore)

if __name__ == '__main__':
    app = wx.App(False)
    controller = Controller()
    app.MainLoop()

Appunto stilistico:
in questo esempio i bind dei pulsanti della view, sono stati fatti esternamente al view
stesso e più precisamente all’interno del controller.

Domanda:
nella teoria del pattern MVC, il Model dovrebbe essere in grado di informare gli altri elementi
view e controller, del proprio cambiamento di stato.
Nel nostro esempio il model non lo fa assolutamente, è il controller che si prende la briga
di aggiornarsi sempre sul valore del saldo dopo ogni evento.
Come può il model avvisare gli altri ad ogni cambiamento dei dati che gestisce, se non gli è
nemmeno concesso di conoscere l’esistenza degli altri due?
Mandando dei messaggi! E qui entra magicamente in ballo l’Observer (ahi…).

In una puntata precedente se ne era già parlato.
Il programma precedente con tanto di observer, diventa:

import wx


class Observable(object):
    def __init__(self, value=None):
        self.data = value
        self.callbacks = []

    def add_callback(self, func):
        self.callbacks.append(func)

    def _do_callbacks(self):
        for func in self.callbacks:
            func(self.data)

    def set_money(self, data):
        self.data = data
        print "Model's data changed!"
        self._do_callbacks()

    def get_money(self):
        return self.data



class Model(object):
    def __init__(self):
        self.money = Observable(0) # observer

    def aggiungi(self, value):
        self.money.set_money(self.money.get_money() + value)

    def rimuovi(self, value):
        self.money.set_money(self.money.get_money() - value)

    def saldo_attuale(self):
        return self.money


class View(wx.Frame):
    def __init__(self, parent):
        wx.Frame.__init__(self, parent, title='mvc example')
        sizer = wx.BoxSizer(wx.VERTICAL)
        text = wx.StaticText(self, label="saldo attuale")
        self.saldo = wx.TextCtrl(self)
        self.aggiungi = wx.Button(self, label="Aggiungi")
        self.rimuovi = wx.Button(self, label="rimuovi")        
        sizer.Add(text, 0, wx.EXPAND | wx.ALL)
        sizer.Add(self.saldo, 0, wx.EXPAND | wx.ALL)
        sizer.Add(self.aggiungi, 0, wx.EXPAND | wx.ALL)
        sizer.Add(self.rimuovi, 0, wx.EXPAND | wx.ALL)
        self.saldo.SetEditable(False)
        self.SetSizer(sizer)

    def set_money(self, valore):
        self.saldo.SetValue(str(valore))

class Controller(object):
    def __init__(self):
        self.model = Model() # riferimento al model
        self.view = View(None) # riferimento al view
        self.saldo_iniziale = self.model.money.get_money() # observer
        self.view.set_money(self.saldo_iniziale)
        self.view.aggiungi.Bind(wx.EVT_BUTTON, self.aggiungi_valore)
        self.view.rimuovi.Bind(wx.EVT_BUTTON, self.rimuovi_valore)
        self.model.money.add_callback(self.aggiorna_saldo) # func observed
        self.view.Show()

    def aggiungi_valore(self, evt):
        self.model.aggiungi(10)

    def rimuovi_valore(self, evt):
        self.model.rimuovi(10)

    def aggiorna_saldo(self, valore):
        self.view.set_money(valore)

if __name__ == '__main__':
    app = wx.App(False)
    controller = Controller()
    app.MainLoop()

Come si nota, il valore inizializzato nel model, passa attraverso l’Observer e
diventa il valore inizializzato di quest’ultimo, di conseguenza i metodi setter
del model, diventano semplicemente quelli dell’Observer.
Ad ogni ‘set’ di observer viene chiamato il metodo _do_callbacks() che itera sul
dizionario callbacks al quale è stato aggiunto il metodo aggiorna_saldo() del
controller, tramite il metodo add_callback di Observer:

self.model.money.add_callback(self.aggiorna_saldo)

Purtroppo andando più nello specifico si complicherebbero ulteriormente le cose,
ma non è nell’intento di questo post, che fa pur sempre parte della saga:
‘programmatori di coccio’ (lacune a go-go).

🙂

altri link utili de coccio:
pattern observer

versione MVC con observer per pincipianti

Categorie:python Tag:
I commenti sono chiusi.