Devo incapsulare sempre una struttura di dati interna interamente?

10

Considera questa classe:

class ClassA{

    private Thing[] things; // stores data

    // stuff omitted

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

}

Questa classe espone la matrice che usa per memorizzare i dati, a qualsiasi codice cliente interessato.

L'ho fatto in un'app a cui sto lavorando. Ho avuto una classe ChordProgression che memorizza una sequenza di Chord s (e fa alcune altre cose). Aveva un metodo Chord[] getChords() che restituiva l'array di accordi. Quando la struttura dei dati dovette cambiare (da una matrice a una ArrayList), tutto il codice client si ruppe.

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 struttura dati stessa, tutte le operazioni offerte dalla struttura dati sono ora offerte direttamente dalla classe che lo racchiude, utilizzando metodi pubblici che delegano alla struttura dati.

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

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

Questo approccio è comune? Cosa ne pensi di questo? Quali svantaggi ne ha altri? È ragionevole che la classe che li include implementa almeno tre metodi pubblici solo per delegare alla struttura interna dei dati?

    
posta Aviv Cohn 27.05.2014 - 03:23
fonte

5 risposte

14

Codice simile:

   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[] things come public . L'idea alla base di un metodo di accesso consiste nel creare un'interfaccia che isola i client dai cambiamenti interni e impedisce loro di manipolare la struttura dati effettiva se non in modi discreti come consentito dall'interfaccia. Come hai scoperto quando tutto il tuo codice client si è rotto, il tuo metodo di accesso non lo ha fatto - è solo un codice sprecato. Penso che molti programmatori tendano a scrivere un codice del genere perché hanno imparato da qualche parte che tutto deve essere incapsulato con i metodi di accesso - ma questo è per le ragioni che ho spiegato. Farlo solo per "seguire il modulo" quando il metodo di accesso non serve a nulla è solo rumore.

Consiglio vivamente la soluzione proposta, che soddisfa 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 il la struttura interna dei dati si aspetta nei modi che si decide appropriati - "la legge del privilegio meno necessario". Se osservi i grandi framework OOP popolari, come CLR, STL, VCL, lo schema che hai proposto è molto diffuso, proprio per questo motivo.

Dovresti sempre farlo? Non necessariamente. Ad esempio, se hai classi di helper o di amici che sono essenzialmente un componente della tua classe di lavoratore principale e non sono "frontali", non è necessario - è un eccesso che aggiungerà un sacco di codice non necessario. E in tal caso, non utilizzerei affatto un metodo di accesso - è insensato, come spiegato. Basta dichiarare la struttura dei dati in un modo che è limitato solo alla classe principale che lo utilizza - la maggior parte dei linguaggi supporta modi per farlo - friend , o dichiarandolo nello stesso file della classe principale dell'operatore, ecc.

L'unico lato negativo posso vedere nella tua proposta che è più lavoro da codificare (e ora dovrai ricodificare le classi del tuo consumatore - ma devi / devi fare comunque.) Ma non è proprio un aspetto negativo: devi farlo nel modo giusto, ea volte ciò richiede più lavoro.

Una delle cose che rende buono un buon programmatore è che sanno quando il lavoro extra vale la pena, e quando non lo è. A lungo termine, inserire l'extra adesso ripagherà con grandi dividendi in futuro - se non su questo progetto, poi sugli altri. Impara a codificare nel modo giusto e usa la tua testa su di esso, non solo seguendo i moduli prescritti in modo automatico.

Note that collections more complex than arrays might require the enclosing class to implement even more than three methods just to access the internal data structure.

Se stai esponendo un'intera struttura di dati attraverso una classe contenente, IMO devi riflettere sul motivo per cui questa classe è incapsulata, se non è semplicemente per fornire un'interfaccia più sicura - una "classe wrapper". Stai dicendo che la classe contenitiva non esiste per quello scopo, quindi forse c'è qualcosa che non va nel tuo progetto. Prendi in considerazione la possibilità di suddividere le tue lezioni in moduli più discreti e di metterli a strati.

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

    
risposta data 27.05.2014 - 03:49
fonte
2

Hai chiesto: dovrei sempre incapsulare completamente una struttura dati interna?

Breve risposta: Sì, la maggior parte 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 forniscono la possibilità di ottenere / impostare le coordinate X e Y, ma è possibile nascondere facilmente i dati interni senza troppi problemi. Per tali classi, l'esposizione dei dettagli della struttura dati interna non è richiesta.

  2. Classi contenitore che incapsulano raccolte. STL ha le classiche classi di contenitori. Considero std::string e std::wstring tra quelli. Forniscono una ricca interfaccia per gestire le astrazioni, ma std::vector , std::string e std::wstring forniscono anche la possibilità di ottenere l'accesso ai dati grezzi. Non sarei frettoloso chiamandoli 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 si gestiscono milioni di nodi mesh e dati su tali nodi mesh per motivi di prestazioni.

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

  3. Classi 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 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 è opportuno esporre i loro dati interni caso per caso.

In conclusione: l'unica volta in cui ho ritenuto necessario esporre la struttura interna dei dati di una classe è quando è diventato un collo di bottiglia nelle prestazioni.

    
risposta data 27.05.2014 - 18:20
fonte
0

Invece di restituire direttamente i dati grezzi, prova qualcosa di simile

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 fornisci la retrocompatibilità (a un costo).

    
risposta data 27.05.2014 - 04:31
fonte
0

Dovresti usare le interfacce per queste cose. Non sarebbe d'aiuto nel tuo caso, dal momento che l'array Java non implementa tali interfacce, ma dovresti farlo da 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 puoi cambiare ArrayList in LinkedList o qualsiasi altra cosa, e non spezzerai alcun codice poiché tutte le raccolte Java (oltre agli array) che hanno (pseudo?) l'accesso casuale probabilmente implementeranno List .

Puoi anche usare Collection , che offre meno metodi di List ma può supportare raccolte senza accesso casuale, o Iterable che può anche supportare i flussi ma non offre molto in termini di metodi di accesso ...

    
risposta data 27.05.2014 - 04:18
fonte
-2

È piuttosto comune nascondere la struttura dei dati interni al di fuori del mondo. A volte è eccessivo specialmente nel DTO. Lo consiglio per il modello di dominio. Se è assolutamente necessario esporre, restituire la copia immutabile. Insieme a questo suggerisco di creare un'interfaccia con questi metodi come ottenere, impostare, rimuovere ecc.

    
risposta data 28.05.2014 - 19:53
fonte

Leggi altre domande sui tag