Modello di iteratore: perché è importante non esporre la rappresentazione interna?


24

Sto leggendo C # Design Pattern Essentials . Attualmente sto leggendo il modello iteratore.

Comprendo appieno come implementare, ma non capisco l'importanza o non vedo un caso d'uso. Nel libro viene fornito un esempio in cui qualcuno deve ottenere un elenco di oggetti. Avrebbero potuto farlo esponendo una proprietà pubblica, come IList<T>o un Array.

Il libro scrive

Il problema è che la rappresentazione interna in entrambe queste classi è stata esposta a progetti esterni.

Qual è la rappresentazione interna? Il fatto è un arrayo IList<T>? Davvero non capisco perché questa sia una brutta cosa per il consumatore (il programmatore lo chiama) per sapere ...

Il libro dice quindi che questo modello funziona esponendo la sua GetEnumeratorfunzione, quindi possiamo chiamare GetEnumerator()ed esporre la 'lista' in questo modo.

Presumo che questo schema abbia un posto (come tutti) in certe situazioni, ma non riesco a vedere dove e quando.



4
Perché, l'implementazione potrebbe voler passare dall'uso di un array, all'utilizzo di un elenco collegato senza richiedere modifiche nella classe consumer. Ciò sarebbe impossibile se il consumatore fosse a conoscenza dell'esatta classe restituita.
Mentre l'

11
Non è una brutta cosa per il consumatore sapere , è una brutta cosa per il consumatore fare affidamento . Non mi piace il termine nascondere le informazioni perché ti porta a credere che le informazioni siano "private" come la tua password anziché private come all'interno di un telefono. I componenti interni sono nascosti perché sono irrilevanti per l'utente, non perché sono una sorta di segreto. Tutto quello che devi sapere è comporre qui, parla qui, ascolta qui.
Captain Man,

Supponiamo di avere una bella "interfaccia" Fche mi dia conoscenza (metodi) di a, be c. Va tutto bene, ci possono essere molte cose diverse ma solo Fper me. Pensa al metodo come "vincoli" o clausole di un contratto che le Fclassi impegnano a fare. Supponiamo di aggiungere un dperò, perché ne abbiamo bisogno. Questo aggiunge un ulteriore vincolo, ogni volta che lo facciamo imponiamo sempre di più sulla Fs. Alla fine (nel peggiore dei casi) solo una cosa può essere una, Fquindi potremmo anche non averla. E Flimita così tanto che c'è solo un modo per farlo.
Alec Teal,

@captainman, che commento meraviglioso. Sì, vedo perché "astrarre" molte cose, ma il colpo di scena nel conoscere e nel fare affidamento è ... Francamente .... Fantastico. E una distinzione importante che non ho considerato fino a quando non ho letto il tuo post.
MyDaftQuestions

Risposte:


56

Il software è un gioco di promesse e privilegi. Non è mai una buona idea promettere più di quanto tu possa offrire, o più di quanto i tuoi collaboratori abbiano bisogno.

Questo vale in particolare per i tipi. Il punto di scrivere una raccolta iterabile è che il suo utente può iterare su di essa, né più né meno. Esporre il tipo concreto diArray solito crea molte altre promesse, ad esempio che è possibile ordinare la raccolta in base a una funzione di propria scelta, per non parlare del fatto che un normale Arrayprobabilmente consentirà al collaboratore di modificare i dati memorizzati al suo interno.

Anche se pensi che questa sia una buona cosa ("Se il renderer nota che manca la nuova opzione di esportazione, può semplicemente rattopparla! Neat!"), Nel complesso questo diminuisce la coerenza della base di codice, rendendo più difficile ragionare su - e rendere facile ragionare sul codice è l'obiettivo principale dell'ingegneria del software.

Ora, se il tuo collaboratore ha bisogno di accedere a un certo numero di cose in modo da garantire loro di non perderne nessuna, implementi Iterableun'interfaccia ed esponi solo quei metodi che questa interfaccia dichiara. In questo modo, il prossimo anno, quando nella tua libreria standard comparirà una struttura di dati enormemente più efficiente e più efficiente, sarai in grado di cambiare il codice sottostante e trarne vantaggio senza correggere il tuo codice client ovunque . Ci sono altri vantaggi nel non promettere più di quanto sia necessario, ma questo da solo è così grande che in pratica non ce ne sono altri.


12
Exposing the concrete type Array usually creates many additional promises, e.g. that you can sort the collection by a function of your own choosing...- Fantastico. Non ci ho pensato. Sì, riporta una 'iterazione' e solo, un'iterazione!
MyDaftQuestions

18

Nascondere l'implementazione è un principio fondamentale di OOP e una buona idea in tutti i paradigmi, ma è particolarmente importante per gli iteratori (o come si chiamano in quel linguaggio specifico) in linguaggi che supportano l'iterazione lenta.

Il problema con l'esposizione del tipo concreto di iterabili - o persino di interfacce simili IList<T>- non sta negli oggetti che li espongono, ma nei metodi che li usano. Ad esempio, supponiamo che tu abbia una funzione per stampare un elenco di Foos:

void PrintFoos(IList<Foo> foos)
{
    foreach (foo in foos)
    {
        Console.WriteLine(foo);
    }
}

Puoi usare quella funzione solo per stampare elenchi di foo, ma solo se implementano IList<Foo>

IList<Foo> foos = //.....
PrintFoos(foos);

Ma cosa succede se si desidera stampare ogni elemento con indice pari dell'elenco? Dovrai creare un nuovo elenco:

IList<Foo> everySecondFoo = new List<T>();
bool isIndexEven = true;
foreach (foo; foos)
{
    if (isIndexEven)
    {
        everySecondFoo.Add(foo);
    }
    isIndexEven = !isIndexEven;
}
PrintFoos(everySecondFoo);

Questo è piuttosto lungo, ma con LINQ possiamo farlo con una sola riga, che in realtà è più leggibile:

PrintFoos(foos.Where((foo, i) => i % 2 == 0).ToList());

Ora, hai notato il .ToList()alla fine? Questo converte la query lazy in un elenco, in modo che possiamo passarla a PrintFoos. Ciò richiede un'allocazione di un secondo elenco e due passaggi sugli elementi (uno sul primo elenco per creare il secondo elenco e un altro sul secondo elenco per stamparlo). Inoltre, se avessimo questo:

void Print6Foos(IList<Foo> foos)
{
    int counter = 0;
    foreach (foo in foos)
    {
        Console.WriteLine(foo);
        ++ counter;
        if (6 < counter)
        {
            return;
        }
    }
}

// ........

Print6Foos(foos.Where((foo, i) => i % 2 == 0).ToList());

E se foosavesse migliaia di voci? Dovremo esaminarli tutti e allocare un enorme elenco solo per stamparne 6!

Immettere gli enumeratori: la versione C # di The Iterator Pattern. Invece di far accettare un elenco alla nostra funzione, lo facciamo accettare Enumerable:

void Print6Foos(Enumerable<Foo> foos)
{
    // everything else stays the same
}

// ........

Print6Foos(foos.Where((foo, i) => i % 2 == 0));

Ora è Print6Foospossibile scorrere pigramente i primi 6 elementi dell'elenco e non è necessario toccarne il resto.

Non esporre la rappresentazione interna è la chiave qui. Quando abbiamo Print6Foosaccettato un elenco, abbiamo dovuto dargli un elenco - qualcosa che supporta l'accesso casuale - e quindi abbiamo dovuto allocare un elenco, perché la firma non garantisce che itererà solo su di esso. Nascondendo l'implementazione, possiamo facilmente creare un Enumerableoggetto più efficiente che supporti solo ciò di cui la funzione ha effettivamente bisogno.


6

Esporre la rappresentazione interna non è praticamente mai una buona idea. Non solo rende il codice più difficile da ragionare, ma anche più difficile da mantenere. Immagina di aver scelto qualsiasi rappresentazione interna - diciamo un IList<T>- ed esposto questo interno. Chiunque utilizzi il tuo codice può accedere all'elenco e il codice può fare affidamento sul fatto che la rappresentazione interna sia un elenco.

Per qualunque motivo tu decida di cambiare la rappresentazione interna IAmWhatever<T>in un momento successivo. Invece semplicemente cambiando gli interni della tua classe dovrai cambiare ogni riga di codice e metodo basandoti sul fatto che la rappresentazione interna sia di tipo IList<T>. Questo è ingombrante e incline agli errori nella migliore delle ipotesi, quando sei l'unico ad usare la classe, ma può rompere il codice di altre persone usando la tua classe. Se hai appena esposto una serie di metodi pubblici senza esporre alcun interno, potresti aver cambiato la rappresentazione interna senza che nessuna riga di codice esterna alla tua classe se ne accorgesse, funzionando come se nulla fosse cambiato.

Questo è il motivo per cui l'incapsulamento è uno degli aspetti più importanti di qualsiasi progetto software non banale.


6

Meno dici, meno devi continuare a dire.

Meno chiedi, meno devi dirti.

Se il tuo codice espone solo IEnumerable<T>, che supporta solo GetEnumrator()esso può essere sostituito da qualsiasi altro codice in grado di supportare IEnumerable<T>. Questo aggiunge flessibilità.

Se il tuo codice IEnumerable<T>lo utilizza solo , può essere supportato da qualsiasi codice implementato IEnumerable<T>. Ancora una volta, c'è una maggiore flessibilità.

Tutti i linq-to-object, ad esempio, dipendono solo da IEnumerable<T>. Sebbene segua rapidamente alcune particolari implementazioni, IEnumerable<T>lo fa in modo test-and-use che può sempre ricadere sul solo utilizzo GetEnumerator()e IEnumerator<T>sull'implementazione che restituisce. Questo gli dà molta più flessibilità rispetto a se fosse costruito su array o liste.


Bella risposta chiara. Molto appropriato in quanto lo mantieni breve e quindi segui i tuoi consigli nella prima frase. Bravo!
Daniel Hollinrake,

0

Una formulazione che mi piace usare i mirror di commento di @CaptainMan: "Va bene per un consumatore conoscere i dettagli interni. È male per un cliente avere bisogno di conoscere i dettagli interni".

Se un cliente deve digitare arrayo IList<T>nel proprio codice, quando quei tipi erano dettagli interni, il consumatore deve trovarli nel modo giusto. Se si sbagliano, il consumatore potrebbe non compilare o persino ottenere risultati sbagliati. Ciò produce gli stessi comportamenti delle altre risposte di "nascondi sempre i dettagli di implementazione perché non dovresti esporli", ma inizia a scavare con i vantaggi di non esporre. Se non esponi mai un dettaglio di implementazione, il tuo cliente non potrà mai mettersi in difficoltà usandolo!

Mi piace pensarlo in questa luce perché apre il concetto di nascondere l'implementazione a sfumature di significato, piuttosto che un draconiano "nascondi sempre i dettagli dell'implementazione". Esistono molte situazioni in cui l'esposizione dell'implementazione è totalmente valida. Considera il caso dei driver di dispositivo in tempo reale, in cui la capacità di saltare strati di astrazione e spingere byte nei punti giusti può fare la differenza tra fare il tempismo o meno. Oppure prendi in considerazione un ambiente di sviluppo in cui non hai tempo per creare "buone API" e devi utilizzare immediatamente le cose. Spesso esporre le API sottostanti in tali ambienti può fare la differenza tra stabilire una scadenza o meno.

Tuttavia, quando si lasciano quei casi speciali alle spalle e si esaminano i casi più generali, inizia a diventare sempre più importante nascondere l'implementazione. Esporre i dettagli dell'implementazione è come una corda. Se usato correttamente, puoi fare ogni sorta di cose con esso, come scalare alte montagne. Usato in modo improprio e può legarti in un cappio. Meno sai su come verrà utilizzato il tuo prodotto, più attenta devi essere.

Il case study che vedo più volte è quello in cui uno sviluppatore crea un'API che non è molto attenta a nascondere i dettagli dell'implementazione. Un secondo sviluppatore, utilizzando quella libreria, scopre che all'API mancano alcune funzionalità chiave che vorrebbero. Invece di scrivere una richiesta di funzionalità (che potrebbe avere un tempo di consegna di mesi), decidono invece di abusare del loro accesso ai dettagli di implementazione nascosti (che richiede pochi secondi). Questo non è male in sé e per sé, ma spesso lo sviluppatore API non ha mai pianificato che facesse parte dell'API. Successivamente, quando migliorano l'API e modificano i dettagli sottostanti, scoprono di essere incatenati alla vecchia implementazione, perché qualcuno l'ha utilizzata come parte dell'API. La versione peggiore di questa è la posizione in cui qualcuno ha utilizzato la tua API per fornire una funzionalità incentrata sul cliente e ora la tua azienda " La busta paga è legata a questa API difettosa! Il semplice fatto di esporre i dettagli dell'implementazione quando non sapevi abbastanza dei tuoi clienti si è rivelato essere sufficiente per legare un cappio attorno alla tua API!


1
Ri: "O considera un ambiente di sviluppo in cui non hai tempo per creare" buone API "e devi usare immediatamente le cose": se ti trovi in ​​un ambiente simile e per qualche motivo non vuoi uscire ( o non ne sono sicuro), raccomando il libro Death March: The Complete Software Developer's Guide to Surviving 'Mission Impossible' Projects . Ha un eccellente consiglio in materia. (Inoltre, ti consiglio di cambiare idea sul non smettere.)
ruakh

@ruakh Ho scoperto che è sempre importante capire come lavorare in un ambiente in cui non si ha il tempo di creare buone API. Francamente, per quanto amiamo l'approccio della torre d'avorio, il vero codice è ciò che effettivamente paga le bollette. Prima possiamo ammettere che, prima possiamo iniziare a considerare il software come un equilibrio, bilanciando la qualità dell'API con il codice produttivo, per adattarsi al nostro ambiente esatto. Ho visto troppi giovani sviluppatori intrappolati in un casino di ideali, senza rendermi conto che devono fletterli. (Sono stato anche quel giovane sviluppatore .. mi sto ancora riprendendo da 5 anni di "buona API")
Cort Ammon - Ripristina Monica il

1
Il codice reale paga le bollette, sì. Una buona progettazione dell'API è il modo in cui ti assicuri di poter continuare a generare codice reale. Il codice scadente smette di pagare per se stesso quando un progetto diventa non realizzabile e diventa impossibile correggere i bug senza crearne di nuovi. (E comunque, questo è un falso dilemma. Ciò che richiede un buon codice non è tanto tempo quanto una spina dorsale .)
ruakh

1
@ruakh Quello che ho scoperto è che è possibile creare un buon codice senza "buone API". Se data l'opportunità, le buone API sono belle, ma trattarle come una necessità provoca più conflitti di quanto valga la pena. Considera: l'API per i corpi umani è orrenda, ma continuiamo a pensare che siano piuttosto buone piccole abilità ingegneristiche =)
Cort Ammon - Reinstalla Monica il

L'altro esempio che darei è quello che è stato l'ispirazione per l'argomento sul tempismo. Uno dei gruppi con cui lavoro aveva alcune cose di driver di dispositivo che dovevano sviluppare, con requisiti in tempo reale difficili. Avevano 2 API con cui lavorare. Uno era buono, uno era meno. L'API valida non è stata in grado di eseguire il timing perché ha nascosto l'implementazione. L'API minore ha esposto l'implementazione e, in tal modo, consente di utilizzare tecniche specifiche del processore per creare un prodotto funzionante. La buona API è stata educatamente messa sullo scaffale e hanno continuato a soddisfare i requisiti di temporizzazione.
Cort Ammon - Ripristina Monica il

0

Non sai necessariamente quale sia la rappresentazione interna dei tuoi dati - o potrebbe diventare in futuro.

Supponiamo di avere un'origine dati che rappresenti come un array, potresti iterare su di esso con un normale ciclo for.

Ora dì che vuoi refactoring. Il tuo capo ha chiesto che l'origine dati sia un oggetto database. Inoltre vorrebbe avere la possibilità di estrarre dati da un'API, da una hashmap, da un elenco collegato, da un tamburo magnetico rotante, da un microfono o da un conteggio in tempo reale di trucioli di yak trovati nella Mongolia Esterna.

Bene, se ne hai solo uno per il ciclo, puoi modificare facilmente il tuo codice per accedere all'oggetto dati nel modo in cui prevede di accedervi.

Se hai 1000 classi che tentano di accedere all'oggetto dati, ora hai un grosso problema di refactoring.

Un iteratore è un modo standard per accedere a un'origine dati basata su elenco. È un'interfaccia comune. Usarlo renderà agnostico l'origine dati del codice.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.