Che cosa rende l'Iterator un modello di progettazione?

9

Mi sono chiesto che cosa rende speciale Iterator rispetto ad altri costrutti simili, e questo ha reso la Gang of Four elencalo come schema di progettazione.

L'Iterator si basa sul polimorfismo (una gerarchia di raccolte con un'interfaccia comune) e sulla separazione delle preoccupazioni (l'iterazione sulle raccolte dovrebbe essere indipendente dal modo in cui i dati sono strutturati).

Ma cosa succede se sostituiamo la gerarchia delle collezioni con, ad esempio, una gerarchia di oggetti matematici (intero, float, complesso, matrice ecc.) e l'iteratore di una classe che rappresenta alcune operazioni correlate su questi oggetti, ad esempio il potere funzioni. Il diagramma di classe sarebbe lo stesso.

Probabilmente potremmo trovare molti altri esempi simili come Writer, Painter, Encoder e probabilmente quelli migliori, che funzionano allo stesso modo. Tuttavia, non ho mai sentito nessuno di questi essere chiamato Design Pattern.

Quindi cosa rende speciale Iterator?

È il fatto che è più complicato perché richiede uno stato mutabile per la memorizzazione della posizione corrente all'interno della raccolta? Ma poi, lo stato mutabile di solito non è considerato desiderabile.

Per chiarire il mio punto, vorrei fare un esempio più dettagliato.

Ecco il nostro problema di progettazione:

Diciamo che abbiamo una gerarchia di classi e un'operazione definita sugli oggetti di queste classi. L'interfaccia di questa operazione è la stessa per ogni classe, ma le implementazioni possono essere completamente diverse. Si presume inoltre che abbia senso applicare l'operazione più volte sullo stesso oggetto, ad esempio con parametri diversi.

Ecco una soluzione ragionevole per il nostro problema di progettazione (praticamente una generalizzazione del pattern iteratore):

Per la separazione delle preoccupazioni, le implementazioni dell'operazione non dovrebbero essere aggiunte come funzioni alla gerarchia di classi originale (oggetti operandi). Poiché desideriamo applicare l'operazione più volte sullo stesso operando, dovrebbe essere rappresentata da un oggetto che contiene un riferimento all'operando, non solo da una funzione. Pertanto l'oggetto operando dovrebbe fornire una funzione che restituisca l'oggetto che rappresenta l'operazione. Questo oggetto fornisce una funzione che esegue l'operazione effettiva.

Un esempio:

C'è una classe base o un'interfaccia MathObject (nome stupido, lo so, forse qualcuno ha un'idea migliore.) con classi derivate MyInteger e MyMatrix . Per ogni MathObject deve essere definita un'operazione Power che consente il calcolo di quadrati, cubi e così via. Quindi potremmo scrivere (in Java):

MathObject i = new MyInteger(5);
Power powerOfFive = i.getPower();
MyInteger square = powerOfFive.calculate(2); // should return 25
MyInteger cube = powerOfFive.calculate(3); // should return 125
    
posta Frank Puffer 26.11.2016 - 20:52
fonte

2 risposte

9

La maggior parte degli schemi del libro GoF hanno le seguenti cose in comune:

  • risolvono i problemi di progettazione di base , usando mezzi orientati agli oggetti
  • le persone spesso affrontano questi problemi gentili in programmi arbitrari, indipendentemente dal dominio o dall'azienda
  • sono ricette per rendere il codice più riutilizzabile, spesso rendendolo più SOLID
  • presentano soluzioni canoniche a questi problemi

I problemi risolti da questi pattern sono così basilari che molti sviluppatori li comprendono principalmente come soluzioni alternative per le funzionalità del linguaggio di programmazione mancanti , che è IMHO un punto di vista valido (si noti che il libro GoF è del 1995, dove Java e C ++ non offrivano così tante funzionalità come oggi).

Il pattern iteratore si adatta bene a questa descrizione: risolve un problema di base che si verifica molto spesso, indipendentemente da un dominio specifico, e come hai scritto da solo è un buon esempio di "separazione delle preoccupazioni". Come sicuramente sai, il supporto per iteratore diretto è qualcosa che trovi oggi in molti linguaggi di programmazione contemporanei.

Ora confronta questo con i problemi che hai scelto:

  • scrivere su un file - questo è IMHO semplicemente non abbastanza "di base". È un problema molto specifico. Né esiste una buona soluzione canonica: ci sono molti approcci diversi su come scrivere su un file e nessuna chiara "best practice".
  • Painter, Encoder: qualsiasi cosa tu abbia in mente, i problemi mi sembrano ancora meno basilari, e nemmeno indipendenti dal dominio.
  • con la funzione "power" disponibile per diversi tipi di oggetti: a prima vista, potrebbe valere la pena di essere uno schema, ma la tua soluzione proposta non mi convince - sembra più un tentativo di calzare la funzione di potenza in qualcosa di simile al modello iteratore. Ho implementato un sacco di codice con calcoli ingegneristici, ma non riesco a ricordare una situazione in cui un approccio simile al tuo oggetto funzione di potenza mi avrebbe aiutato (tuttavia, gli iteratori sono qualcosa su cui devo confrontarmi quotidianamente).

Inoltre, non vedo nulla nel tuo esempio di funzione di potere che non possa essere interpretato come un'applicazione del modello di strategia o del modello di comando, il che significa che quelle parti di base sono già nel libro GoF. Una soluzione migliore potrebbe contenere sia l'overloading dell'operatore che i metodi di estensione, ma quelli sono cose soggette a funzionalità linguistiche, e questo è esattamente quello che la "OO" usata dalla "Gang" non può fornire.

    
risposta data 27.11.2016 - 20:37
fonte
8

The Gang of Four cita la definizione del modello di Christopher Alexander:

Each pattern describes a problem which occurs over and over again in our environment, and then describes the core of the solution to that problem […]

Qual è il problema risolto dagli iteratori?

Intent: Provide a way to access the elements of an aggregate object sequentially without exposing its underlying representation.

Applicability: Use the Iterator pattern

  • to access an aggregate object's contents without exposing its internal representation
  • to support multiple traversals of aggregate objects
  • to provide a uniform interface for traversing different aggregate structures (that is, to support polymorphic iteration).

Quindi si potrebbe sostenere che lo schema iteratore è per definizione specifico del dominio per le collezioni. E questo è perfettamente OK. Altri pattern come il pattern dell'interprete sono specifici del dominio per i linguaggi specifici del dominio, i pattern factory sono specifici del dominio per la creazione dell'oggetto, .... Ovviamente questa è una comprensione piuttosto sciocca di "dominio specifico". Finché si tratta di una coppia di problemi ricorrenti, possiamo chiamarla schema.

Ed è positivo che esista lo schema di Iterator. Le cose brutte accadono se non la usi. Il mio anti-esempio preferito è Perl. Qui, ogni collezione (array o hash) include lo stato iteratore come parte della collezione. Perché questo è cattivo? Possiamo facilmente eseguire iterazioni su un hash con un ciclo while-each:

while (my ($key, $value) = each %$hash) {
  say "$key => $value";
}

E se chiamassimo una funzione nel corpo del loop?

while (my ($key, $value) = each %$hash) {
  do_something_with($key, $value, $hash);
}

Questa funzione ora può fare praticamente tutto ciò che vuole, eccetto:

  • aggiungi o elimina le voci di hash, poiché queste potrebbero alterare l'ordine di iterazione in modo imprevedibile (in C ++ - speak, invaliderebbero l'iteratore).
  • itera la stessa tabella hash senza fare una copia, poiché ciò consumerebbe lo stesso stato di iterazione. Oops.

Se la funzione chiamata deve usare l'iteratore, il comportamento del nostro loop diventa indefinito. Questo è un problema E il pattern iteratore ha una soluzione: inserisci tutto lo stato di iterazione in un oggetto separato creato per iterazione.

Sì, ovviamente lo schema iteratore è correlato ad altri pattern. Ad esempio, come viene istanziato l'iteratore? In Java, abbiamo un'interfaccia generica Iterable<T> e Iterator<T> . Un iterabile concreto come ArrayList<T> crea un particolare tipo di iteratore, mentre un HashSet<T> potrebbe fornire un tipo di iteratore completamente diverso. Questo mi ricorda molto il pattern factory astratto, dove Iterable<T> è la fabbrica astratta e Iterator è il prodotto.

Un iteratore polimorfico può anche essere interpretato come un esempio del modello di strategia. Per esempio. un albero potrebbe offrire diversi tipi di iteratori (pre-ordine, in ordine, post-ordine, ...). Esternamente, tutti condividono un'interfaccia iteratore e producono elementi in una sequenza. Il codice cliente deve solo dipendere dall'interfaccia iteratore, non da alcun particolare algoritmo di attraversamento dell'albero.

I modelli non esistono isolati, indipendenti l'uno dall'altro. Alcuni modelli sono soluzioni diverse allo stesso problema e alcuni modelli descrivono la stessa soluzione in diversi contesti. Alcuni modelli implicano un altro. Inoltre, lo spazio pattern non viene chiuso quando si ruota l'ultima pagina del libro Pattern Designs (vedere anche la domanda precedente Did the Gang di Quattro esplorare a fondo "Pattern Space"? ). Gli schemi descritti nel libro Design Patterns sono molto flessibili e ampi, aperti a variazioni infinite e in definitiva non sono gli unici modelli esistenti.

I concetti che elenchi (scrittura, pittura, codifica) non sono schemi perché non descrivono una combinazione di problemi-soluzioni. Un compito come "I need to write data" o "I need to encode data" non è realmente un problema di progettazione e non include una soluzione; una "soluzione" che consiste solo in "So che creerò una classe di scrittori" non ha senso. Ma se abbiamo un problema del tipo "Non voglio che la grafica a mezzo render sia dipinta sullo schermo", può esistere un pattern: "Lo so, userò una grafica a doppio buffer!"

    
risposta data 26.11.2016 - 23:27
fonte

Leggi altre domande sui tag