Programming Languages Hacks

Importanti regole per linguaggi di programmazione rilevanti come Java, C, C++, C#…

  • Subscribe

  • Lettori

    I miei lettori abituali

  • Twitter

Pattern Flyweight in Pratica: Predisporre un’Applicazione per il Precache

Posted by Ricibald on January 18th, 2010

Il pattern Flyweight consente di riutilizzare gli oggetti in modo da poter evitare allocazioni. Questo risulta utile se:

  1. gli oggetti da creare richiedono molta memoria
  2. gli oggetti da creare sono molti
  3. gli oggetti da creare sono costosi da creare (e distruggere) dal punto di vista computazionale
  4. i vincoli impongono un sistema che non abbia cali improvvisi di performance, derivanti ad esempio dall’inizializzazione pesante di un oggetto
  5. i vincoli impongono un utilizzo molto attento delle risorse
Facciamo un esempio pratico di ognuno di questi punti:
  1. un’animazione può avere tante caratteristiche ingenti da memorizzare come accelerazione, direzione, …
  2. un word processor utilizza migliaia di caratteri con le stesse info su font, dimensione, colore, …
  3. un’immagine: il caricamento richiede ogni volta l’accesso al filesystem
  4. un videogioco deve avere fps costanti e non avere improvvisi picchi negativi di 10fps solo perché si alloca una risorsa
  5. un dispositivo mobile come il Nokia o l’iPhone
La parte critica a mio parere non è implementare il pattern direttamente: con un po’ di sforzo si riesce! Il problema sta nell’adattare un progetto non pensato per questo pattern, in questo caso l’implementazione non diventa affatto banale!
Facciamo un esempio pratico: ho un’applicazione per iPhone che deve gestire delle biglie che rimbalzano di numero crescente. All’inizio le prestazioni risultano buone ma poi iniziano a degradare evidenziando due problemi:
  • la memoria inizia a non essere sufficiente per le info necessarie
  • l’allocazione/deallocazione causa picchi negativi di fps, ora (più) evidenti a causa dello stesso degrado delle performance generali
Per questo vorremmo gestire tutti gli aspetti comuni a ogni singola biglia attraverso il pattern Flyweight. Vorremmo quindi gestire:
  • lo stato intrinseco della biglia nel flyweight
    • posizione, comportamento specifico della biglia
  • lo stato estrinseco della biglia nella nostra app, che lo passa al flyweight
    • immagine, colore, dimensione, comportamenti comuni di animazioni
Facile a dirsi, ma se non abbiamo pensato la app in questo modo ci troveremo tanti problemi. Infatti al momento di creare una biglia prima:
  • veniva creata e associata l’immagine corrispondente
  • veniva aggiunta l’immagine alla scena corrente assegnando uno z-index
  • veniva assegnato un comportamento della biglia: quanto rimbalzare, se esplode, …
Capire effettivamente cosa estrarre da tutto questo diventa una sfida a volte irrisolvibile, anche a causa dei vincoli tecnologici a cui ci sottopongono i framework stessi, che non prevedono il riuso delle strutture.
Per questo, a parte la teoria del pattern, bisognerebbe:

capire come predisporre una applicazione per fare uso di un precaching in modo da abilitare il pattern con poco sforzo

Rivediamo quindi l’obiettivo: non vogliamo applicare alla lettera il pattern Flyweight (difficile da adattare al nostro contesto) ma vogliamo evitare allocazioni inutili. Una strategia sta nel riutilizzare le stesse istanze ogni volta che queste non sono più utili. Infatti se una biglia esce dal gioco la strategia comune è deallocare la biglia stessa e allocarne una nuova quando necessario. Invece decidiamo di riutilizzare le stesse istanze quando escono dal campo visivo: ci sarà l’illusione di distruggere la biglia, ma non sarà così! In questo modo possiamo ottenere due vantaggi:
  • l’allocazione e deallocazione, che causavano picchi negativi di fps sono presenti un’unica volta
  • creiamo le premesse per un precaching di ad es. 40 biglie, utile per impedire completamente il problema di picchi negativi di fps
Tornando al problema precedente, quando costruiamo una app non sapremo se questo servirà o meno, ma nel dubbio la cosa conveniente è senza dubbio:

dividere inizializzazione estrinseca da inizializzazione intrinseca in modo da poter riutilizzare l’inizializzazione quando richiesto

Il trucco è quindi proprio questo: nella creazione degli oggetti dovremmo inizializzare solo quello che non cambia nella biglia come l’immagine il colore, … Dopo aver creato gli oggetti passiamo quindi per una seconda fase di inizializzazione che imposta lo stato intrinseco della biglia stessa come la posizione e il comportamento. Questo lo facciamo a priori!! Un piccolo sforzo, ma in fondo ben gestibile. Se un giorno servirà questo ci verrà di aiuto!
Immaginiamo che sia venuto quel giorno! Basta creare una FlyweightFactory che consente il seguente flusso:

  • allo startup la FlyweightFactory crea ad es. 40 biglie e le mantiene in un proprio pool di precaching
  • quando alla app serve una biglia richiede un’istanza disponibile alla FlyweightFactory
  • la FlyweightFactory restituirà un’istanza dal pool con lo stato intrinseco correttamente inizializzato
  • quando la biglia non viene più referenziata nella nostra app ma esiste solo nella FlyweightFactory deve esistere una strategia per marcare l’istanza come disponibile nel pool (es. un meccanismo di notifica a eventi)

Fare questo a fronte della divisione tra allocazione/inizializzazione è un gioco da ragazzi!

A questo punto rimane solo il problema dell’allocazione dello stato estrinseco, che viene ridondata per ogni biglia. Per questo risulta quindi necessario aggiungere come parametro nell’allocazione (creazione) delle biglie lo stato estrinseco stesso, che viene creato un’unica volta dalla factory. In questo modo risolviamo il problema finale.

Il flusso di inizializzazione diventa quindi:

  • allo startup viene inizializzato lo stato estrinseco
  • la FlyweightFactory alloca un pool di 40 biglie passando lo stato estrinseco
  • le singole istanze memorizzano il riferimento allo stato estrinseco
  • la app richiede un’istanza alla FlyweightFactory
  • la FlyweightFactory inizializza un’istanza disponibile dal pool e la restituisce. Rimangono quindi 39 biglie nel pool
  • la app usa la biglia. Quando non serve più la rimuove dalla app
  • la app lancia un evento di avvenuta rimozione della biglia
  • la FlyweightFactory riceve l’evento e aggiunge la biglia nel pool delle istanze disponibili. Rimangono quindi 40 biglie nel pool
Concludendo abbiamo leggermente modificato il pattern Flyweight per adattarlo a contesti reali: il pattern infatti prevede che le singole operazioni portino con sé lo stato estrinseco su cui lavorano mentre la nostra soluzione prevede un’allocazione che assegni lo stato estrinseco e le singole operazioni che lo usano come se lo avessero creato loro. Questo si sposa bene con il refactoring necessario per applicare il pattern, che altrimenti richiederebbe un ripensamento globale dell’applicazione.

Leave a Reply

You must be logged in to post a comment.