Python: MVC con Observer per principianti
Dopo l’assaggio in questo articolo, passiamo a questa versione di pattern MVC un po’ più elaborata.
Comprende anche il concetto di observer.
Per rendere il tutto il più chiaro possibile, utilizzeremo un programma molto semplice, ovvero un incrementatore di numero.
Come già visto, il pattern MVC si fonda su tre elementi principali:
– Il model che, nelle “retrovie” si occupa dei dati dell’applicazione e delle operazioni che si effettuano su di essi.
– La View, che si occupa di editare e visualizzare i dati.
– Il Controller che si occupa di gestire il flusso di dati, dal Model alla View e viceversa. Fa da collante tra i primi due oggetti.
Come dicevamo, all’Interno del Model, sarà implementato un Observer Pattern, che verrà spiegato di seguito.
Cominciamo con il definire il Model
class ModelInterface(object): """Definisce una interfaccia per il Model che gestisce l'incremento di un numero""" def __init__(self): super(ModelInterface, self).__init__() self.num = 0 self.observers = [] # Inizializzo una lista di observers def incrementa(self): """Metodo interfaccia da implementare nelle sottoclassi""" raise NotImplementedError def set_value(self, num): self.num = num self.notifica_observer() # Ogni volta che viene modificato il numero def get_value(self): return self.num def notifica_observer(self): """Notifica chiamata ad ogni modifica di numero""" for observer in self.observers: observer() print "Click pulsante: numero incrementato a %s" \ % self.get_value() def registra_observer(self, callback): """registro la callback dell'observer""" print "Registro la callback <%s>" % callback.__name__ self.observers.append(callback) class ModelIncrementa(ModelInterface): def incrementa(self): num = self.get_value() + 1 self.set_value(num)
La prima classe, è l’interfaccia che verrà estesa con la successiva sottoclasse.
In questo specifico caso, l’interfaccia è il metodo incrementa, esteso da ModelIncrementa.
La View, avrà a che fare con tale metodo…
Nel Model, come si nota, sono previsti anche i due metodi relativi all’observer.
Il primo è collegato all’evento click sul pulsante, come vedremo dopo.
Il secondo è responsabile della registrazione della callback-observer.
Come si nota, Model non ha nessun riferimento alla View o al Controller, è all’oscuro di tutto.
Ora vediamo la View:
import wx class View(wx.Frame): def __init__(self, parent, controller, model, title): super(View, self).__init__(parent=parent, title=title) self.panel = Panel(self, controller, model) sizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(self.panel, 1, wx.EXPAND) self.SetSizer(sizer) self.SetInitialSize((150, 150)) def button_activated(self, state=True): self.panel.button.Enable(state) self.panel.button.SetBackgroundColour('Yellow') class Panel(wx.Panel): def __init__(self, parent, controller, model): super(Panel, self).__init__(parent) #Attributi self.model = model self.controller = controller start_number = self.model.get_value() self.text = wx.TextCtrl(self, value='%s' % start_number) self.button = wx.Button(self, label='Incrementa') self.button.SetBackgroundColour('White') # Registro l'Observer self.model.registra_observer(self.on_model_update) # Handler evento click self.Bind(wx.EVT_BUTTON, self.on_click) # Creo il Layout self.__crea_layout() def __crea_layout(self): v_sizer = wx.BoxSizer(wx.VERTICAL) v_sizer.Add(self.text, 0, wx.ALIGN_CENTER|wx.ALL, 5) v_sizer.Add(self.button, 0, wx.ALIGN_CENTER) self.SetSizer(v_sizer) def on_model_update(self): """Metodo Observer""" numero = self.model.get_value() self.text.SetValue(str(numero)) self.button.Enable(True) self.button.SetBackgroundColour('White') def on_click(self, event): self.controller.incrementa()
Qui come si nota, in fase di inizializzazione si crea un Panel, nel quale si instanziano il model esteso ed il controller esteso (tramite i metodi interfaccia).
self.model = model self.controller = controller
Poi si inizializza il numero di partenza per visualizzarlo nel widget apposito:
start_number = self.model.get_value()
quello che sarà “start_number”, lo decido nel Model. La View non ha nessun potere su questo.
In seguito registriamo l’observer:
self.model.registra_observer(self.on_model_update) ... def on_model_update(self): """Metodo Observer""" numero = self.model.get_value() self.text.SetValue(str(numero)) self.button.Enable(True) self.button.SetBackgroundColour('White')
In pratica: in fase di inizializzazione della View, registro l’observer, che nel model viene inserito nella lista degli observers:
# model.py def registra_observer(self, callback): """registro la callback dell'observer""" print "Registro la callback <%s>" % callback.__name__ self.observers.append(callback)
Vediamo il Controller per concludere il ragionamento:
import wx from view import View from model import ModelIncrementa import time class ControllerInterface(object): """Definisce una interfaccia per il Controller che gestisce l'incremento di un numero""" def __init__(self, model): super(ControllerInterface, self).__init__() self.model = model self.view = View(parent=None, controller=self, model=self.model, title='Incrementa') self.view.Show() def incrementa(self): raise NotImplementedError class ControllerIncrementa(ControllerInterface): def incrementa(self): self.view.button_activated(False) self.model.incrementa() time.sleep(1) class Core(wx.App): def OnInit(self): self.model = ModelIncrementa() self.controller = ControllerIncrementa(self.model) return True if __name__ == '__main__': app = Core(False) app.MainLoop()
Torniamo alla View…
Creiamo la callback legata all’evento click del bottone:
self.Bind(wx.EVT_BUTTON, self.on_click) def on_click(self, event): self.controller.incrementa()
Qui la View chiede al Controller cosa fare, anche qui c’è una netta separazione dei compiti.
Quando il bottone viene premuto, viene chiamato il metodo incrementa dell’interfaccia-controller per sapere cosa fare.
#controller.py def incrementa(self): self.view.button_activated(False) self.model.incrementa() time.sleep(1)
questo metodo agisce sulla View disattivando il bottone, poi
chiama il metodo incrementa dell’interfaccia model estesa…
class ModelIncrementa(ModelInterface): def incrementa(self): num = self.get_value() + 1 self.set_value(num)
che preleva il numero dal ModelInterface e lo incrementa di 1…
num = self.get_value() + 1
…e lo sostituisce al precedente…
self.set_value(num)
…chiamando il metodo set_value di ModelInterface, che succede?
#model.py ModelInterface def set_value(self, num): self.num = num self.notifica_observer()
il numero viene sostituito dal valore incrementato e l’observer se ne accorge…
self.notifica_observer()
ed esegue la callback registrata precedentemente;
def notifica_observer(self): """Notifica chiamata ad ogni modifica di numero""" for observer in self.observers: observer() # on_model_update() di Panel
prelevando il numero…
numero = self.model.get_value()
visualizzandolo sulla textcontrol…
self.text.SetValue(str(numero))
riattivando e ricolorando il bottono come all’inizio.
self.button.Enable(True) self.button.SetBackgroundColour('White')
Ribadiamo i punti fondamentali del pattern MVC:
Il Model NON sa nulla del Controller e della View, ne è completamente estraneo.
La View, dal canto suo, non sa nulla di come il Model ed il Controller interagiscano tra loro e non sa come essi siano strutturati, poichè interagisce con loro, solo tramite le interfacce che li estendono, nel nostro caso i metodo incrementa() di ModelInterface esteso da ModelIncrementa e ControllerInterface esteso da ControllerIncrementa.
Quando premiamo il bottone, la View chiede al Controller cosa fare e per sapere quando un dato del Model viene modificato, registra una callback-observer, sul model stesso, per sapere quando la callback legata al pulsante, viene chiamata.
Il Controller come già detto, funge da collante tra i due oggetti precedenti.
Implementa il comportamento della View, in funzione dei cambiamenti di stato del Model.
Questa netta separazione dei compiti torna utile nel caso di riutilizzo del codice.
Potremmo ad esempio, invece di incrementare, decrementare il numero, senza dover mettere mano alla View, che non implementa nessuna operazione sui dati che visualizza.
Inoltre, potremmo effettuare semplici Unittest sul Model senza impazzire scorporando parti di codice innestati nella View o nel Controller.
Commenti recenti