Home > python, wxPython > Python: MVC con Observer per principianti

Python: MVC con Observer per principianti

23 Ottobre 2014

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.

Categorie:python, wxPython Tag:
I commenti sono chiusi.