I pericoli della programmazione con i mixin/3

Dagli articoli precedenti dovrebbe essere ampiamente chiaro che io non amo i
mixin, ma vale la vena di spiegare esattamente cos’è che mi dà così
tanto fastidio. Tutti noi sappiamo che il modo migliore di
risolvere un problema complesso è quello di spezzarlo in sottoproblemi più
piccoli disaccoppiati, secondo la massima del dividi et impera. La
cosa fastidiosa dei mixin è che questo principio viene applicato all’inizio (il
problema viene scomposto in pacchetti disgiunti di funzionalità) ma
alla fine tutte le funzionalità separate vengono rimescolate insieme
in un calderone unico, che è il namespace della classe figlia. Che
l’iniezione avvenga direttamente come in Ruby o indirettamente tramite
l’ereditarietà come in Python, il risultato non cambia: alla fine
l’utente della classe figlia si trova a dover gestire centinaia di
metodi tutti assieme e non ha un modo ovvio per capirne la
provenienza.

Overpopulation.jpg

Il problema del sovraffollamento

Il problema dei mixin e soluzioni possibili

Il problema dei mixin è che rendono la vita semplice all’autore
del framework, ma la rendono difficile all’utilizzatore: questo è assolutamente
in contrapposizione con lo Zen di Python che preferisce la facilità
di lettura alla facilità di scrittura.
Lo scenario che io ho in mente è sempre lo stesso: quello di un
povero programmatore che si trova a dovere debuggare un oggetto
proveniente da un framework di cui non sa nulla o quasi, in assenza
totale o parziale di documentazione e con poco tempo a disposizione
per imparare il minimo necessario per risolvere il problema del
momento (questo scenario vi ricorda qualcosa?).
In queste condizioni le tecniche
tipiche di introspezione (l’autocompletamento, pydoc e
strumenti simili) falliscono perché tutti i metodi vengono considerati
equivalenti e la bella e ordinata catalogazione per mixin va a farsi
benedire. Cosa si può fare in questa situazione? Direi che ci sono almeno tre
possibili atteggiamenti:

  1. La rassegnazione. Rendersi conto che ormai il linguaggio
    permette l’ereditarietà multipla e/o i mixin, che molti framework li usano e
    che non cambierà mai. Inventare dei workaround per cercare di
    sopravvivere, come il decoratore warn_overriding che ho
    definito nel primo articolo della serie e scrivere dei tool di
    introspezioni migliori per orientarsi nella selva dei mixin (il
    problema di pydoc è che dà troppa informazione).
  2. L’educazione. Darsi da fare per rendere noti al grande pubblico i
    problemi dei mixin e convincere gli autori dei framework del futuro
    a usare design alternativi. È quello che ho cercato di fare con
    il secondo articolo di questa serie.
  3. La ricerca. Studiare implementazioni migliori dell’idea dei mixin:
    anche se non ci sono speranze per il linguaggio che si sta usando
    per motivi di compatibilità con il passato, la ricerca non è
    inutile perché potrebbe essere implementata nei linguaggi del futuro.
    Lo stesso Python può essere usato come linguaggio di
    sperimentazione ed in questo articolo mostro come implementare
    i mixin in termini di composizione e non di ereditarità.

Un problema di design

Siccome non mi piace parlare in astratto ma preferisco riferirmi
ad esempi concreti, in questo paragrafo discuterò un problema di
design concreto che risolverò con classi di mixin ma senza incorrere
nel problema del sovraffollamento. A tale scopo riprendiamo in mano
l’articolo precedente
e torniamo alla classe PictureContainer2 che derivava dalla
classe DictMixin. Come abbiamo detto,
derivare da DictMixin è un’ottima idea
perché DictMixin fornisce soltanto
19 attributi alle sue istanze (ottenuti con dir(DictMixin))
che sono tutti già noti a chi sa usare
i dizionari in Python e quindi il carico cognitivo è nullo.

Il problema comincia quando si decide
che è necessario aggiungere funzionalità al nostro PictureContainer2.
Per esempio, se stiamo scrivendo un’applicazione GUI, potremo
aver bisogno di metodi tipo set_thumbnails_size, get_thumbnails_size,
generate_thumbnails, show_thumbnails, make_picture_menu, show_picture_menu
,
eccetera; diciamo che abbiamo almeno 50 metodi che hanno a che fare
con la GUI (settaggio dei parametri, dei menu, dei bottoni, metodi
ausiliari e chi più ne ha più ne metta). Potremmo mettere tutti
questi 50 metodi in una classe mixin chiamata GUI ed ereditare
da DictMixin e da GUI.

Fin qui nulla di male. Supponiamo però che
la versione 2 della nostra applicazione debba andare su Web;
può avere allora senso implementare gli 8 metodi del protocollo HTTP
(HEAD, GET, POST, PUT, DELETE, TRACE, OPTIONS, CONNECT) in un’altra
classe mixin; se vogliamo dare anche la possibilità di editare le immagini
ai nostri utenti, può aver senso
anche un’interfaccia WebDAV (quindi con 7 metodi addizionali rispetto
al protocollo HTTP: PROPFIND, PROPPATCH, MKCOL, COPY, MOVE, LOCK, UNLOCK).

D’altra parte, ci sono utenti che potrebbero preferire il buon vecchio
protocollo FPT per trasferire le immagini
(quindi 43 metodi ABOR, ALLO, APPE, CDUP, CWD, DELE, EPRT,
EPSV, FEAT, HELP, LIST, MDTM, MLSD, MLST, MODE, MKD, NLST, NOOP, OPTS, PASS,
PASV, PORT, PWD, QUIT, REIN, REST, RETR, RMD, RNFR, RNTO, SIZE, STAT, STOR,
STOU, STRU, SYST, TYPE, USER, XCUP, XCWD, XMKD, XPWD, XRMD
). Infine,
ci sarà bisogno di metodi di autorizzazione vari (is_admin, is_logged_user,
is_anonymous_user, is_picture_owner, is_friend_of
, eccetera), diciamo altri
20 metodi nella classe mixin AUTH.

A questo punto avremmo sei classi mixin (DictMixin, GUI, HTTP, WEBDAV,
FTP, AUTH) ed un totale di almeno 20 (da DictMixin) + 50 (da GUI) + 8
(da HTTP) + 7 (da WEBDAV) + 44 (da FTP) + 20 (da AUTH) = 148 metodi
derivanti dalle classi mixin. A questi vanno aggiunti i metodi
specifici della classe PictureContainer. Non è un bello scenario,
soprattutto se pensate che domani potrei avere bisogno di supportare
un’altra interfaccia e quindi di aggiungere ancora altri metodi. Nelle
mie stime sono stato conservativo, ma si fa presto a raggiunger le
centinaia di metodi. Questo scenario non è affatto
irrealistico: è esattamente quello che è avvenuto in
Zope/Plone.

Per esempio, date un’occhiata alla gerarchia di un
oggetto Plone Site (la figura mostra tra parentesi quadre il numero
di metodi/attributi non speciali definiti per ogni classe). Il sito in
oggetto è quello di un’applicazione Plone che ho in produzione da tre
anni. Il conteggio totale è di 38 classi, 88 nomi sovrascritti, 42
nomi speciali, 648 nomi normali. Un mostro. In queste condizioni
viene da chiedersi se esistono design alternative ai mixin che evitano
il problema del sovraffollamento dei metodi. La risposta è sì ed è
sempre la solita, usare la composizione al posto dell’ereditarietà,
pratica comunemente consigliata in ogni buon manuale di programmazione
orientata agli oggetti ma messa in opera non abbastanza spesso.

Una possibile soluzione

La mia soluzione, come anticipato nell’articolo precedente sarà
quella di sostituire l’ereditarietà con la composizione + delegazione,
ovverossia fare uso di oggetti proxy.
Il carico cognitivo richiesto da un proxy – un
oggetto che fa dispatch su di un altro oggetto – è molto
inferiore al carico cognitivo imposto dall’ereditarietà.

Se un oggetto è un’ instanza di una classe, mi sento obbligato
a conoscere tutti i suoi metodi (inclusi quelli di tutti i suoi
antenati) se non altro perché potrei sovrascriverli accidentalmente
mentre se un oggetto è un proxy mi basta sapere qual è
l’oggetto a cui si riferisce,
so che se voglio posso andare a vedere i metodi dell’oggetto,
ma non mi sento obbligato a farlo.
È più un motivo psicologico che altro, ma il proxy piace perché
permette di tenere confinata la complessità, mentre l’ereditarietà
la espone direttamente.

Quest’ultimo punto è estremamente importante. Il cervello
umano può memorizzare un numero limitato di cose. Un
oggetto con dieci metodi può essere memorizzato abbastanza agevolmente
mentre un oggetto con cento metodi esce dalle capacità del programmatore
medio. La soluzione è quella di dividire i cento metodi in dieci categorie
con dieci metodi ognuna: a questo punto le dieci categorie possono essere
tenute in mente. La soluzione gerarchica scala: se avessi bisogno di
mille metodi, basta definire dieci macro-categorie, ognuna contenente
dieci categorie semplici, e tenere a mente le macro-categorie. La
catalogazione gerarchica è la maniera naturale per la mente umana
per memorizzare l’informazione, come formalizzato per lo meno dai tempi
di Aristotele
, quindi è questa la cosa giusta
da fare, non tenere i mille metodi tutti sullo stesso piano nello stesso
namespace.

Nel caso in esame, ho deciso di trasformare tutte le classi di mixin in
proxy: se un attributo non viene trovato nel namespace del mixin, viene
cercato anche nel namespace dell’oggetto sottostante.
Tecnicamente questa idea può essere implementata definendo
degli oggetti dispatcher
da usare come attributi di classe (attribute descriptors).
Potete trovare l’implementazione in appendice.
Un esempio d’uso è il seguente:

class PictureContainer(DictMixin, object):
# dispatchers objects have the same methods as the corresponding mixin
# class; moreover they dispatch on self, i.e. on PictureContainer objects
gui = dispatcher(GUI)
http = dispatcher(HTTP)
webdav = dispatcher(WEBDAV)
ftp = dispatcher(FTP)
auth = dispatcher(AUTH)

@property
def log(self):
return logging.getLogger(self.__class__.__name__)

def __init__(self, id, pictures_or_containers):
self.id = id
self.data = {}
for poc in pictures_or_containers:
# both pictures and containers must have an .id
self.data[poc.id] = poc

def __getitem__(self, id):
return self.data[id]

def __setitem__(self, id, value):
self.log.info('Adding or replacing %s into %s', id, self.id)
self.data[id] = value

def __delitem__(self, id):
self.log.warn('Deleting %s', id)
del self.data[id]

def keys(self):
return self.data.keys()

Notate come io abbia rifattorizzato la classe PictureContainer2
dell’articolo precedente per renderla più pulita.
In particolare ho definito esplicitamente la proprietà log invece
di importarla dal modulo di utilità (from utility import log)
come fatto. Tutto sommato, visto che si
tratta di tre sole righe, può avere senso riscriverle ed evitare
al lettore di andare a guardare un altro modulo. C’è sempre da trovare
un punto di equilibrio tra riuso del codice da una parte e codice
spaghetti dall’altra. Nel dubbio, tenete conto che readability counts.

Come vedete è sparita l’ereditarietà da tutte le classi mixin
tranne DictMixin.
Un oggetto PictureContainer è un dizionario e quindi
è giusto che erediti da DictMixin; ha meno senso che erediti
da GUI, HTTP, WEBDAV, FTP, AUTH, perché è uno stiracchiare la
realtà dire che PictureContainer sia anche un oggetto GUI, HTTP, WEBDAV,
FTP, AUTH.

Ho ereditato esplicitamente da object, perché la classe
DictMixin è una cosiddetta classe old-style (per motivi di
compatibilità con il passato), mentre il dispatcher
è pensato per essere usato con classi new-style; ereditare da object
fa sì che PictureContainer diventi una classe new-style. Questo
è uno dei casi in cui l’ereditarietà multipla è comoda, ma il caso
d’uso sparirà in Python 3.0, in cui tutte le classi sono new-style.

Discussione

Proviamo adesso che la nostra soluzione al problema di design
è consistente con i principi enunciati
nello Zen di Python. Il fatto che l’implementazione del dispatcher
sia semplice – circa 20 righe di codice –
è già un primo passo nella direzione giusta
(if the implementation is hard to
explain, it’s a bad idea
). Il fatto che tutti i metodi dei mixin restino
localizzati nel loro proprio namespace senza inquinare il namespace
della classe figlia è ancora più pythonico
(namespaces are one honking great idea — let’s do more of those!).
Infine, il fatto che per accedere al metodo POST del mixin HTTP
dobbiamo scrivere esplicitamente self.http.POST è pythonicissimo
perché explicit is better than implicit; in questo modo
non c’è bisogno di tirare ad indovinare per capire la provenienza
di un metodo (in the face of ambiguity, refuse the temptation to guess).

La nostra soluzione, comunque, non è soltanto pythonica, ma anche
usabile (practicality beats purity): provate ad istanziare un
oggetto PictureContainer e a fare
qualche esperimento dalla console interattiva:

>>> pc = PictureContainer('root', [])

Vedrete che l’autocompletamento funziona perfettamente

>>> pc.ftp. # press TAB
pc.ftp.RECV
pc.ftp.SEND
...

che l’help
non vi sommerge di informazioni inutili

>>> help(pc)
Help on PictureContainer in module mixins2 object:
class PictureContainer(UserDict.DictMixin, __builtin__.object)
| Method resolution order:
| PictureContainer
| UserDict.DictMixin
| __builtin__.object
|
| Methods defined here:
| ...
|
| auth = <AUTHDispatcher {is_admin ...} >
| ftp = <FTPDispatcher {RECV, SEND ...} >
| gui = <GUIDispatcher {draw_button, draw_buttons ...} >
| http = <HTTPDispatcher {GET, POST ...} >
| webdav = <WEBDAVDispatcher {GET, POST, LOCK, UNLOCK ...} >
...

e potete fare introspezione soltanto sulle caratteristiche che
vi interessano, senza che tutto sia mescolato (sparse is better than dense):

>>> print dir(pc.http)
['GET, 'POST', ...]

È anche molto facile adattare un oggetto composto da dispatchers,
per renderlo compatibile con diverse interfacce. Ma per spiegare
propriamente questo punto mi servirebbe
un’altra serie di articoli, che potrei intitolare I vantaggi della
programmazione a componenti
. Per il momento, mi fermo qui,
sperando di avervi dato qualche spunto interessante. Ecco per i
più curiosi l’implementazione del dispatcher:

$ cat mdispatcher.py
class BaseDispatcher(object):
def __init__(self, obj=None):
self.__obj = obj
def __get__(self, obj, objcls=None):
if obj is None:
return self
return self.__class__(obj)
def __getattr__(self, name):
obj = self.__obj
if obj is None:
raise AttributeError(name)
return getattr(obj, name)
def __repr__(self):
names = ', '.join(n for n in dir(self) if not n.startswith('__'))
if self.__obj is not None:
msg = 'bound to %r' % self.__obj
else:
msg = ''
return '<%s {%s} %s>' % (
self.__class__.__name__, names, msg)

def dispatcher(mixin):
"Converts a mixin class into a dispatcher"
# could be implemented with single-inheritance only, but why bother?
return type(mixin.__name__ + 'Dispatcher', (BaseDispatcher, mixin), {})()

Comments

  1. giovacchino says:

    come al solito un articolo interessantissimo!

  2. Fantastica serie. Michele, non puoi fermarti qui… 😉

  3. Ho letto tutti e 3 gli articoli uno di seguito all’altro e devo dire che è stato molto utile!
    Complimenti per la spiegazione e per la chiarezza nell’esposizione! 🙂

  4. I lettori di questa miniserie saranno interessati
    a questo mio recente post, in cui descrivo
    un approccio per evitare i mixin usando
    le funzioni generiche:
    http://www.artima.com/weblogs/viewpost.jsp?thread=237764

Policy per i commenti: Apprezzo moltissimo i vostri commenti, critiche incluse. Per evitare spam e troll, e far rimanere il discorso civile, i commenti sono moderati e prontamente approvati poco dopo il loro invio.