Non dichiarare interfacce per oggetti immutabili


27

Non dichiarare interfacce per oggetti immutabili

[EDIT] Dove gli oggetti in questione rappresentano Data Transfer Objects (DTOs) o Plain Old Data (PODs)

È una linea guida ragionevole?

Fino ad ora, ho spesso creato interfacce per classi sigillate che sono immutabili (i dati non possono essere modificati). Ho cercato di stare attento a non usare l'interfaccia ovunque mi interessi l'immutabilità.

Sfortunatamente, l'interfaccia inizia a pervadere il codice (e non è solo il mio codice di cui mi preoccupo). Finisci per essere passato a un'interfaccia e poi vuoi passarlo a qualche codice che vuole davvero presumere che la cosa che gli viene passata sia immutabile.

A causa di questo problema, sto considerando di non dichiarare mai interfacce per oggetti immutabili.

Ciò potrebbe avere ramificazioni rispetto al test unitario, ma a parte questo, sembra una linea guida ragionevole?

O c'è un altro modello che dovrei usare per evitare il problema "spreading-interface" che sto vedendo?

(Sto usando questi oggetti immutabili per diversi motivi: principalmente per la sicurezza dei thread poiché scrivo molto codice multi-thread; ma anche perché significa che posso evitare di fare copie difensive degli oggetti passati ai metodi. Il codice diventa molto più semplice in molti casi in cui sai che qualcosa è immutabile, cosa che non succede se ti è stata data un'interfaccia. In realtà, spesso non puoi nemmeno fare una copia difensiva di un oggetto a cui fa riferimento un'interfaccia se non fornisce un operazione clone o qualsiasi modo di serializzarlo ...)

[MODIFICARE]

Per fornire un contesto molto più ampio per le mie ragioni per voler rendere gli oggetti immutabili, vedi questo post sul blog di Eric Lippert:

http://blogs.msdn.com/b/ericlippert/archive/tags/immutability/

Dovrei anche sottolineare che sto lavorando con alcuni concetti di livello inferiore qui, come elementi che vengono manipolati / passati in code di lavoro multi-thread. Questi sono essenzialmente DTO.

Anche Joshua Bloch raccomanda l'uso di oggetti immutabili nel suo libro Effective Java .


Azione supplementare

Grazie per il feedback, tutto. Ho deciso di andare avanti e utilizzare questa linea guida per i DTO e il loro genere. Finora funziona bene, ma è passata solo una settimana ... Comunque, sta bene.

Ci sono alcune altre questioni relative a questo che vorrei chiedere; in particolare qualcosa che sto chiamando "Immutabilità profonda o superficiale" (nomenclatura che ho rubato dalla clonazione profonda e superficiale) - ma questa è una domanda per un'altra volta.


3
+1 ottima domanda. Spiega perché è così importante per te che l'oggetto appartiene a quella particolare classe immutabile piuttosto che a qualcosa che implementa semplicemente la stessa interfaccia. È possibile che i tuoi problemi siano radicati altrove. L'iniezione delle dipendenze è estremamente importante per i test unitari, ma ha molti altri importanti vantaggi che svaniscono tutti se si forza il codice a richiedere una particolare classe immutabile.
Steven Doggart,

1
Sono leggermente confuso dal tuo uso del termine immutabile . Per essere chiari, intendi una classe sigillata, che non può essere ereditata e sovrascritta, o intendi che i dati che espone sono di sola lettura e non possono essere cambiati una volta che l'oggetto è stato creato. In altre parole, vuoi garantire che l'oggetto sia di un tipo particolare o che il suo valore non cambierà mai. Nel mio primo commento, ho assunto il significato del primo, ma ora con la tua modifica, sembra più simile al secondo. O ti preoccupi di entrambi?
Steven Doggart,

3
Sono confuso, perché non lasciare i setter fuori dall'interfaccia? D'altra parte, mi stanco di vedere interfacce per oggetti che sono in realtà oggetti di dominio e / o DTO che richiedono fabbriche per quegli oggetti ... causano dissonanza cognitiva per me.
Michael Brown,

2
Che dire delle interfacce per le classi che sono tutte immutabili? Ad esempio, di Java Numero classe permette di definire una List<Number>che può contenere Integer, Float, Long, BigDecimal, ecc ... Tutto ciò sono immutabili se stessi.

1
@MikeBrown Immagino che solo perché l'interfaccia implica l'immutabilità non significa che l'implementazione la imponga. Potresti semplicemente lasciarlo alla convenzione (cioè documentare l'interfaccia che l'immutabilità è un requisito) ma potresti finire con alcuni problemi davvero sgradevoli se qualcuno violasse la convenzione.
Vaughandroid

Risposte:


17

Secondo me, la tua regola è buona (o almeno non è cattiva), ma solo a causa della situazione che stai descrivendo. Non direi che sono d'accordo con esso in tutte le situazioni, quindi, dal punto di vista del mio pedante interiore, dovrei dire che la tua regola è tecnicamente troppo ampia.

In genere non si definiscono oggetti immutabili a meno che non vengano essenzialmente utilizzati come oggetti di trasferimento dati (DTO), il che significa che contengono proprietà dei dati ma pochissima logica e nessuna dipendenza. Se è così, come sembra che sia qui, direi che sei sicuro di usare direttamente i tipi concreti piuttosto che le interfacce.

Sono sicuro che ci saranno alcuni puristi di unit test che non saranno d'accordo, ma secondo me le classi DTO possono essere tranquillamente escluse dai requisiti di unit test e iniezione di dipendenza. Non è necessario utilizzare una factory per creare un DTO, poiché non ha dipendenze. Se tutto crea i DTO direttamente secondo necessità, allora non c'è davvero modo di iniettare un tipo diverso, quindi non c'è bisogno di un'interfaccia. E poiché non contengono alcuna logica, non c'è nulla da testare sull'unità. Anche se contengono qualche logica, purché non abbiano dipendenze, dovrebbe essere banale testare la logica dell'unità, se necessario.

In quanto tale, penso che stabilire una regola secondo cui tutte le classi DTO non devono implementare un'interfaccia, sebbene potenzialmente non necessaria, non danneggerà la progettazione del software. Dato che hai questo requisito secondo cui i dati devono essere immutabili e non puoi applicarli tramite un'interfaccia, direi che è del tutto legittimo stabilire quella regola come standard di codifica.

Il problema più grande, tuttavia, è la necessità di applicare rigorosamente un livello DTO pulito. Finché le classi immutabili senza interfaccia esistono solo nel livello DTO e il livello DTO rimane privo di logica e dipendenze, allora sarai al sicuro. Se inizi a mescolare i tuoi livelli, tuttavia, e hai classi senza interfaccia che si raddoppiano come classi di livelli aziendali, penso che inizierai a incontrare molti problemi.


Il codice che si aspetta specificamente di trattare un DTO o un oggetto valore immutabile dovrebbe usare le caratteristiche di quel tipo piuttosto che quelle di un'interfaccia, ma ciò non significa che non ci sarebbero casi d'uso per un'interfaccia implementata da tale classe e anche da altre classi, con metodi che promettono di restituire valori che saranno validi per qualche tempo minimo (ad es. se l'interfaccia viene passata a una funzione, i metodi dovrebbero restituire valori validi fino a quando tale funzione non ritorna). Tale interfaccia può essere utile se ci sono un certo numero di tipi che incapsulano dati simili e uno desidera ...
Supercat,

... per avere un mezzo per copiare i dati dall'uno all'altro. Se tutti i tipi che includono determinati dati implementano la stessa interfaccia per leggerli, tali dati possono essere copiati facilmente tra tutti i tipi, senza richiedere a ciascuno di loro di saper importare da ciascuno degli altri.
supercat,

A quel punto, puoi anche eliminare i tuoi getter (e setter, se i tuoi DTO sono mutabili per qualche motivo stupido) e rendere pubblici i campi. Non metterai mai nessuna logica lì, giusto?
Kevin,

@Kevin suppongo, ma con la comodità moderna delle proprietà auto, è così facile renderle proprietà, perché no? Suppongo che le prestazioni e l'efficienza siano la massima preoccupazione, quindi forse è importante. In ogni caso, la domanda è: quale livello di "logica" permetti in un DTO? Anche se metti un po 'di logica di validazione nelle sue proprietà, non ci sarebbe alcun problema a testarlo a condizione che non avesse dipendenze che richiedessero beffe. Finché il DTO non utilizza alcun oggetto business di dipendenza, è sicuro avere un po 'di logica incorporata perché saranno comunque testabili.
Steven Doggart,

Personalmente, preferisco tenerli il più possibile puliti da quel genere di cose, ma ci sono sempre momenti in cui è necessario piegare le regole e quindi è meglio lasciare la porta aperta per ogni evenienza.
Steven Doggart,

7

Ero solito fare un gran casino nel rendere invulnerabile il mio codice per abusarne. Ho creato interfacce di sola lettura per nascondere membri mutanti, ho aggiunto molti vincoli alle mie firme generiche, ecc. Ecc. Ho scoperto che la maggior parte delle volte prendevo decisioni di progettazione perché non mi fidavo dei miei colleghi immaginari. "Forse un giorno assumeranno un nuovo ragazzo entry-level e non saprà che la classe XYZ non può aggiornare DTO ABC. Oh no!" Altre volte mi concentravo sul problema sbagliato - ignorando la soluzione ovvia - non vedevo la foresta attraverso gli alberi.

Non creo più interfacce per i miei DTO. Lavoro supponendo che le persone che toccano il mio codice (principalmente me stesso) sappiano cosa è permesso e cosa ha senso. Se continuo a fare lo stesso stupido errore, di solito non provo ad indurire le mie interfacce. Ora passo la maggior parte del mio tempo a cercare di capire perché continuo a fare lo stesso errore. Di solito è perché sto analizzando troppo qualcosa o mi manca un concetto chiave. Il mio codice è stato molto più facile da lavorare da quando ho rinunciato a essere paranoico. Finisco anche con meno "framework" che richiedono la conoscenza di un addetto ai lavori per lavorare sul sistema.

La mia conclusione è stata quella di trovare la cosa più semplice che funzioni. La maggiore complessità derivante dalla creazione di interfacce sicure non fa altro che sprecare tempo di sviluppo e complicare altrimenti il ​​codice. Preoccupati di queste cose quando hai 10.000 sviluppatori che usano le tue librerie. Credimi, ti salverà da molte tensioni inutili.


Solitamente conservo le interfacce per quando ho bisogno dell'iniezione di dipendenza per i test unitari.
Travis Parks,

5

Sembra una linea guida ok, ma per ragioni strane. Ho avuto un certo numero di posti in cui un'interfaccia (o classe base astratta) fornisce un accesso uniforme a una serie di oggetti immutabili. Le strategie tendono a cadere qui. Gli oggetti di stato tendono a cadere qui. Non credo sia troppo irragionevole modellare un'interfaccia per sembrare immutabile e documentarla come tale nella tua API.

Detto questo, le persone tendono a sovra-interfacciare oggetti Plain Old Data (di seguito, POD) e persino strutture semplici (spesso immutabili). Se il tuo codice non ha alternative sane a qualche struttura fondamentale, non ha bisogno di un'interfaccia. No, i test unitari non sono una ragione sufficiente per cambiare il tuo design (deridere l'accesso al database non è il motivo per cui stai fornendo un'interfaccia a questo, è flessibilità per i cambiamenti futuri) - non è la fine del mondo se i tuoi test usa quella struttura fondamentale di base così com'è.


2

Il codice diventa molto più semplice in molti casi quando sai che qualcosa è immutabile, cosa che non succede se ti è stata data un'interfaccia.

Non credo sia una preoccupazione di cui l'implementatore degli accessor deve preoccuparsi. Se interface Xsi intende che è immutabile, allora non è responsabilità dell'implementatore di interfacce assicurarsi di implementare l'interfaccia in modo immutabile?

Tuttavia, nella mia mente, non esiste un'interfaccia immutabile: il contratto di codice specificato da un'interfaccia si applica solo ai metodi esposti di un oggetto e nulla riguardo agli interni di un oggetto.

È molto più comune vedere l'immutabilità implementata come decoratore piuttosto che come interfaccia, ma la fattibilità di quella soluzione dipende davvero dalla struttura del tuo oggetto e dalla complessità della tua implementazione.


1
Potresti fornire un esempio di implementazione dell'immutabilità come decoratore?
Vaughandroid

Non banalmente, non credo, ma è un esercizio di pensiero relativamente semplice: se MutableObjectha nmetodi che cambiano stato e mmetodi che restituiscono stato, ImmutableDecoratorpuò continuare a esporre i metodi che restituiscono stato ( m) e, a seconda dell'ambiente, affermare o genera un'eccezione quando viene chiamato uno dei metodi mutabili.
Jonathan Rich

Non mi piace molto convertire la certezza della compilazione in possibilità di eccezioni di runtime ...
Matthew Watson

OK, ma come può ImmutableDecorator sapere se un determinato metodo cambia stato o no?
Vaughandroid,

Non puoi essere certo in ogni caso se una classe che implementa la tua interfaccia è immutabile rispetto ai metodi definiti dalla tua interfaccia. @Baqueta Il decoratore deve avere conoscenza dell'implementazione della classe base.
Jonathan Rich

0

Finisci per essere passato a un'interfaccia e poi vuoi passarlo a qualche codice che vuole davvero presumere che la cosa che gli viene passata sia immutabile.

In C #, un metodo che prevede un oggetto di tipo "immutabile" non dovrebbe avere un parametro di un tipo di interfaccia poiché le interfacce C # non possono specificare il contratto di immutabilità. Pertanto, la linea guida che stai proponendo è insignificante in C # perché non puoi farlo in primo luogo (e è improbabile che le versioni future delle lingue ti consentano di farlo).

La tua domanda nasce da un sottile fraintendimento degli articoli di Eric Lippert sull'immutabilità. Eric non ha definito le interfacce IStack<T>e ha IQueue<T>specificato i contratti per stack e code immutabili. Non lo fanno. Li ha definiti per comodità. Queste interfacce gli hanno permesso di definire diversi tipi per stack e code vuoti. Possiamo proporre una diversa progettazione e implementazione di uno stack immutabile utilizzando un singolo tipo senza richiedere un'interfaccia o un tipo separato per rappresentare lo stack vuoto, ma il codice risultante non sembrerà pulito e sarebbe un po 'meno efficiente.

Ora atteniamoci al design di Eric. Un metodo che richiede uno stack immutabile deve avere un parametro di tipo Stack<T>anziché l'interfaccia generale IStack<T>che rappresenta il tipo di dati astratto di uno stack in senso generale. Non è ovvio come farlo quando si utilizza lo stack immutabile di Eric e non ne ha discusso nei suoi articoli, ma è possibile. Il problema è con il tipo di stack vuoto. Puoi risolvere questo problema assicurandoti di non avere mai uno stack vuoto. Questo può essere assicurato spingendo un valore fittizio come primo valore nello stack e non saltandolo mai. In questo modo, puoi tranquillamente trasmettere i risultati di Pushe Popto Stack<T>.

Avere Stack<T>attrezzi IStack<T>può essere utile. È possibile definire metodi che richiedono uno stack, qualsiasi stack, non necessariamente uno stack immutabile. Questi metodi possono avere un parametro di tipo IStack<T>. Ciò ti consente di passargli pile immutabili. Idealmente, IStack<T>sarebbe parte della libreria standard stessa. In .NET non esiste IStack<T>, ma esistono altre interfacce standard che lo stack immutabile può implementare, rendendo il tipo più utile.

La progettazione alternativa e l'implementazione dello stack immutabile a cui ho fatto riferimento in precedenza utilizza un'interfaccia chiamata IImmutableStack<T>. Naturalmente, l'inserimento di "Immutable" nel nome dell'interfaccia non rende immutabili tutti i tipi che lo implementano. Tuttavia, in questa interfaccia, il contratto di immutabilità è solo verbale. Un buon sviluppatore dovrebbe rispettarlo.

Se stai sviluppando una piccola libreria interna, puoi concordare con tutti i membri del team per onorare questo contratto e puoi utilizzare IImmutableStack<T>come tipo di parametri. Altrimenti, non dovresti usare il tipo di interfaccia.

Vorrei aggiungere, dal momento che hai taggato la domanda C #, che nelle specifiche C # non esistono DTO e POD. Pertanto, scartarli o definirli con precisione migliora la domanda.

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.