Devo sempre incapsulare interamente una struttura di dati interna?


11

Si prega di considerare questa classe:

class ClassA{

    private Thing[] things; // stores data

    // stuff omitted

    public Thing[] getThings(){
        return things;
    }

}

Questa classe espone l'array che utilizza per archiviare i dati, a qualsiasi codice client interessato.

L'ho fatto in un'app a cui sto lavorando. Avevo una ChordProgressionclasse che memorizza una sequenza di Chords (e fa alcune altre cose). Aveva un Chord[] getChords()metodo che restituiva la matrice di accordi. Quando la struttura dei dati ha dovuto cambiare (da un array ad un ArrayList), tutto il codice client si è rotto.

Questo mi ha fatto pensare: forse il seguente approccio è migliore:

class ClassA{

    private Thing[] things; // stores data

    // stuff omitted

    public Thing[] getThing(int index){
        return things[index];
    }

    public int getDataSize(){
        return things.length;
    }

    public void setThing(int index, Thing thing){
        things[index] = thing;
    }

}

Invece di esporre la stessa struttura di dati, tutte le operazioni offerte dalla struttura di dati sono ora offerte direttamente dalla classe che la racchiude, usando metodi pubblici che delegano alla struttura di dati.

Quando la struttura dei dati cambia, solo questi metodi devono cambiare, ma dopo lo fanno, tutto il codice client funziona ancora.

Si noti che le raccolte più complesse degli array potrebbero richiedere alla classe che lo racchiude di implementare anche più di tre metodi solo per accedere alla struttura interna dei dati.


Questo approccio è comune? Cosa ne pensi di questo? Quali aspetti negativi ha altro? È ragionevole che la classe che racchiude implementi almeno tre metodi pubblici solo per delegare alla struttura di dati interna?

Risposte:


14

Codice come:

   public Thing[] getThings(){
        return things;
    }

Non ha molto senso, dal momento che il tuo metodo di accesso non sta facendo altro che restituire direttamente la struttura interna dei dati. Potresti anche dichiarare Thing[] thingsdi esserlo public. L'idea alla base di un metodo di accesso è quella di creare un'interfaccia che isola i client dalle modifiche interne e impedisce loro di manipolare la struttura dei dati effettiva, tranne nei modi discreti consentiti dall'interfaccia. Come hai scoperto quando si è rotto tutto il codice client, il tuo metodo di accesso non ha funzionato, è solo un codice sprecato. Penso che molti programmatori tendano a scrivere codice in questo modo perché hanno imparato da qualche parte che tutto deve essere incapsulato con metodi di accesso - ma questo è per i motivi che ho spiegato. Farlo solo per "seguire il modulo" quando il metodo di accesso non serve a nessuno scopo è solo rumore.

Consiglio vivamente la soluzione proposta, che raggiunge alcuni dei più importanti obiettivi dell'incapsulamento: offrire ai clienti un'interfaccia solida e discreta che li isola dai dettagli di implementazione interna della classe e non consente loro di toccare la struttura interna dei dati aspettarti nei modi che ritieni appropriati - "la legge del privilegio meno necessario". Se guardi ai grandi framework OOP popolari, come CLR, STL, VCL, il modello che hai proposto è diffuso, proprio per questo motivo.

Dovresti sempre farlo? Non necessariamente. Ad esempio, se hai classi di aiuto o di amicizia che sono essenzialmente un componente della tua classe lavoratrice principale e non sono "front side", non è necessario - è un eccesso che aggiungerà molto codice non necessario. E in quel caso, non userei affatto un metodo di accesso - è insensato, come spiegato. Dichiara semplicemente la struttura dei dati in un modo che è limitato solo alla classe principale che la utilizza - la maggior parte delle lingue supporta i modi per farlo - friendo dichiarandola nello stesso file della classe lavoratrice principale, ecc.

L'unico aspetto negativo che posso vedere nella tua proposta è che è più lavoro da codificare (e ora dovrai ricodificare le tue classi di consumatori - ma devi / dovresti farlo comunque.) Ma questo non è davvero un aspetto negativo - devi farlo nel modo giusto, e talvolta ci vuole più lavoro.

Una delle cose che rende buono un buon programmatore è che sanno quando vale il lavoro extra e quando non lo è. A lungo termine, mettere gli extra ora ripagherà con grandi dividendi in futuro - se non su questo progetto, poi su altri. Impara a programmare nel modo giusto e usa la tua testa al riguardo, non solo seguire roboticamente i moduli prescritti.

Si noti che le raccolte più complesse degli array potrebbero richiedere alla classe che lo racchiude di implementare anche più di tre metodi solo per accedere alla struttura interna dei dati.

Se stai esponendo un'intera struttura di dati attraverso una classe contenente, IMO devi pensare al motivo per cui quella classe è incapsulata, se non è semplicemente fornire un'interfaccia più sicura - una "classe wrapper". Stai dicendo che la classe contenente non esiste a tale scopo - quindi forse c'è qualcosa che non va nel tuo design. Valuta di suddividere le tue classi in moduli più discreti e stratificarli.

Una classe dovrebbe avere uno scopo chiaro e discreto e fornire un'interfaccia per supportare tale funzionalità - non di più. Potresti provare a raggruppare cose che non appartengono. Quando lo fai, le cose si rompono ogni volta che devi attuare un cambiamento. Più piccole e discrete sono le tue lezioni, più facile è cambiare le cose: pensa a LEGO.


1
Grazie per aver risposto. Una domanda: che dire se la struttura interna dei dati ha, forse, 5 metodi pubblici - che tutti devono essere presenti nell'interfaccia pubblica della mia classe? Ad esempio, un ArrayList Java ha i seguenti metodi: get(index), add(), size(), remove(index), e remove(Object). Utilizzando la tecnica proposta, la classe che contiene questo ArrayList deve avere cinque metodi pubblici solo per delegare alla raccolta interna. E lo scopo di questa classe nel programma è molto probabilmente non incapsulare questa ArrayList, ma piuttosto fare qualcos'altro. L'ArrayList è solo un dettaglio. [...]
Aviv Cohn,

La struttura interna dei dati è solo un membro ordinario, che usando la tecnica sopra - richiede che contenga classe per includere altri cinque metodi pubblici. Secondo te - è ragionevole? E anche - è comune?
Aviv Cohn,

@Prog - Che dire se la struttura interna dei dati ha, forse, 5 metodi pubblici ... IMO se trovi che devi avvolgere un'intera classe di supporto all'interno della tua classe principale ed esporla in quel modo, devi ripensare che design -la tua classe pubblica sta facendo troppo / o non presenta l'interfaccia appropriata. Una classe dovrebbe avere un ruolo molto discreto e chiaramente definito, e la sua interfaccia dovrebbe supportare quel ruolo e solo quel ruolo. Pensa a rompere e stratificare le tue lezioni. Una classe non dovrebbe essere un "lavello da cucina" che contiene tutti i tipi di oggetti nel nome dell'incapsulamento.
Vettore

Se stai esponendo un'intera struttura di dati attraverso una classe wrapper, IMO devi pensare al motivo per cui quella classe è incapsulata se non è semplicemente fornire un'interfaccia più sicura. Stai dicendo che la classe contenente non esiste a tale scopo - quindi c'è qualcosa di sbagliato in questo progetto.
Vettore

1
@Phoshi - La parola chiave è di sola lettura - Sono d'accordo. Ma l'OP non sta parlando di sola lettura. per esempio removenon è di sola lettura. La mia comprensione è che l'OP vuole rendere tutto pubblico - come nel codice originale prima della modifica proposta public Thing[] getThings(){return things;}Questo è ciò che non mi piace.
Vettore

2

Hai chiesto: dovrei sempre incapsulare interamente una struttura di dati interna?

Risposta breve: Sì, il più delle volte ma non sempre .

Risposta lunga: Penso che le lezioni seguano le seguenti categorie:

  1. Classi che incapsulano dati semplici. Esempio: punto 2D. È facile creare funzioni pubbliche che offrano la possibilità di ottenere / impostare le coordinate X e Y ma è possibile nascondere facilmente i dati interni senza troppi problemi. Per tali classi, non è necessario esporre i dettagli della struttura dei dati interni.

  2. Classi contenitore che incapsulano raccolte. STL ha le classiche classi di contenitori. Considero std::stringe anche std::wstringtra quelli. Essi forniscono una ricca interfaccia per affrontare le astrazioni, ma std::vector, std::stringe std::wstringanche fornire la possibilità di ottenere l'accesso ai dati grezzi. Non avrei fretta di chiamarle classi mal progettate. Non conosco la giustificazione per queste classi che espongono i loro dati grezzi. Tuttavia, nel mio lavoro, ho trovato necessario esporre i dati grezzi quando ho a che fare con milioni di nodi mesh e dati su quei nodi mesh per motivi di prestazioni.

    L'importante di esporre la struttura interna di una classe è che devi pensare a lungo prima di dare un segnale verde. Se l'interfaccia è interna a un progetto, sarà costoso cambiarlo in futuro, ma non impossibile. Se l'interfaccia è esterna al progetto (come quando si sta sviluppando una libreria che verrà utilizzata da altri sviluppatori di applicazioni), potrebbe essere impossibile cambiare l'interfaccia senza perdere i client.

  3. Classi che sono per lo più funzionali in natura. Esempi: std::istream, std::ostream, iteratori dei contenitori STL. È assolutamente sciocco esporre i dettagli interni di queste classi.

  4. Classi ibride. Queste sono classi che incapsulano alcune strutture di dati ma forniscono anche funzionalità algoritmiche. Personalmente, penso che questi siano il risultato di un design mal pensato. Tuttavia, se li trovi, devi decidere se ha senso esporre i loro dati interni caso per caso.

In conclusione: l'unica volta che ho trovato necessario esporre la struttura interna dei dati di una classe è quando è diventato un collo di bottiglia delle prestazioni.


Penso che la ragione più importante per cui STL espone i loro dati interni sia la compatibilità con tutte le funzioni che prevedono puntatori, che sono molte.
Siyuan Ren,

0

Invece di restituire direttamente i dati grezzi, prova qualcosa del genere

class ClassA {
  private Things[] things;
  ...
  public Things[] asArray() { return things; }
  public List<Thing> asList() { ... }
  ...
}

Quindi, stai essenzialmente fornendo una collezione personalizzata che presenta qualunque faccia al mondo sia desiderata. Nella tua nuova implementazione quindi,

class ClassA {
  private List<Thing> things;
  ...
  public Things[] asArray() { return things.asArray(); }
  public List<Thing> asList() { return things; }
  ...
}

Ora hai l'incapsulamento corretto, nascondi i dettagli di implementazione e offri la retrocompatibilità (a un costo).


Un'idea intelligente per la retrocompatibilità. Ma: ora hai l'incapsulamento corretto, nascondi i dettagli di implementazione - non proprio. I clienti devono ancora affrontare le sfumature di List. I metodi di accesso che restituiscono semplicemente i membri dei dati, anche con un cast per rendere le cose più robuste, non sono davvero un buon incapsulamento IMO. La classe operaia dovrebbe gestire tutto ciò, non il client. Più "stupido" deve essere un client, più robusto sarà. Per inciso, non sono sicuro che tu abbia risposto alla domanda ...
Vector

1
@Vector - hai ragione. La struttura dei dati restituita è ancora mutabile e gli effetti collaterali uccideranno il nascondimento delle informazioni.
BobDalgleish,

La struttura dei dati restituita è ancora mutabile e gli effetti collaterali uccideranno il nascondiglio delle informazioni - sì, anche quello - è pericoloso. Stavo semplicemente pensando in termini di ciò che è richiesto dal cliente, che era al centro della domanda.
Vettore

@BobDalgleish: perché non restituire una copia della collezione originale?
Giorgio,

1
@BobDalgleish: A meno che non ci siano buone ragioni di prestazione, prenderei in considerazione la possibilità di restituire un riferimento a strutture di dati interne per consentire ai suoi utenti di modificarle come una pessima decisione di progettazione. Lo stato interno di un oggetto deve essere modificato solo con metodi pubblici appropriati.
Giorgio,

0

Dovresti usare le interfacce per quelle cose. Non sarebbe d'aiuto nel tuo caso, poiché l'array Java non implementa quelle interfacce, ma dovresti farlo d'ora in poi:

class ClassA{

    public ClassA(){
        things = new ArrayList<Thing>();
    }

    private List<Thing> things; // stores data

    // stuff omitted

    public List<Thing> getThings(){
        return things;
    }

}

In questo modo è possibile passare ArrayLista LinkedListqualsiasi altra cosa e non si romperà alcun codice poiché probabilmente verranno implementate tutte le raccolte Java (oltre alle matrici) che hanno (pseudo?) Accesso casuale List.

Puoi anche utilizzare Collection, che offrono meno metodi rispetto a Listma possono supportare raccolte senza accesso casuale o Iterableche possono persino supportare flussi ma non offrono molto in termini di metodi di accesso ...


-1 - scarso compromesso e IMO non particolarmente sicuro: stai esponendo la struttura interna dei dati al client, semplicemente mascherandola e sperando nel meglio perché "Le raccolte Java ... probabilmente implementeranno List". Se la tua soluzione fosse veramente polimorfica / basata sull'ereditarietà - che tutte le collezioni implementassero invariabilmente Listcome classi derivate, avrebbe più senso, ma solo "sperare nel meglio" non è una buona idea. "Un buon programmatore guarda in entrambe le direzioni su una strada a senso unico".
Vettore

@Vector Sì, presumo che verranno implementate le future raccolte Java List(o Collection, o almeno Iterable). Questo è il punto centrale di queste interfacce ed è un peccato che gli array Java non li implementino, ma sono un'interfaccia ufficiale per le raccolte in Java, quindi non è così inverosimile supporre che qualsiasi raccolta Java li implementerà - a meno che quella raccolta non sia più vecchia rispetto List, e in tal caso è molto facile per avvolgere con AbstractList .
Idan Arye,

Stai dicendo che il tuo presupposto è praticamente garantito per essere vero, quindi OK - Rimuoverò il voto negativo (quando mi sarà permesso) perché eri abbastanza decente da spiegare, e non sono un ragazzo Java tranne per osmosi. Tuttavia, non sostengo l'idea di esporre la struttura interna dei dati, indipendentemente da come è stata fatta, e non hai risposto direttamente alla domanda del PO, che riguarda davvero l'incapsulamento. cioè limitare l'accesso alla struttura interna dei dati.
Vettore

1
@Vector Sì, gli utenti possono eseguire il cast Listin ArrayList, ma non è che l'implementazione possa essere protetta al 100%: puoi sempre usare la riflessione per accedere ai campi privati. Fare ciò è malvisto, ma anche il casting è malvisto (non tanto però). Il punto di incapsulamento non è impedire l'hacking dannoso, ma piuttosto impedire agli utenti di dipendere dai dettagli di implementazione che potresti voler cambiare. L'uso Listdell'interfaccia fa esattamente questo: gli utenti della classe possono dipendere Listdall'interfaccia anziché dalla ArrayListclasse concreta che potrebbe cambiare.
Idan Arye,

puoi sempre usare la riflessione per accedere sicuramente ai campi privati : se qualcuno vuole scrivere un codice errato e sovvertire un disegno, può farlo. piuttosto, è per impedire agli utenti ... - questo è uno dei motivi dell'incapsulamento. Un altro è garantire l'integrità e la coerenza dello stato interno della tua classe. Il problema non è "l'hacking malevolo", ma una cattiva organizzazione che porta a cattivi bug. "Legge del privilegio meno necessario": dai al consumatore della tua classe solo ciò che è obbligatorio, non di più. Se è obbligatorio rendere pubblica un'intera struttura di dati interna, si è verificato un problema di progettazione.
Vettore

-2

Questo è abbastanza comune per nascondere la struttura dei dati interni al di fuori del mondo. A volte è eccessivo soprattutto in DTO. Lo consiglio per il modello di dominio. Se è necessario esporre, restituisci la copia immutabile. Insieme a questo suggerisco di creare un'interfaccia con questi metodi come get, set, remove etc.


1
questo non sembra offrire nulla di sostanziale su 3 risposte precedenti
moscerino del
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.