Soffro di un uso eccessivo dell'incapsulamento?


11

Ho notato qualcosa nel mio codice in vari progetti che mi sembra odore di codice e qualcosa di brutto da fare, ma non riesco a gestirlo.

Durante il tentativo di scrivere "codice pulito" tendo a utilizzare in modo eccessivo i metodi privati ​​per facilitare la lettura del mio codice. Il problema è che il codice è davvero più pulito ma è anche più difficile da testare (sì, lo so che posso testare metodi privati ​​...) e in generale mi sembra una cattiva abitudine.

Ecco un esempio di una classe che legge alcuni dati da un file .csv e restituisce un gruppo di clienti (un altro oggetto con vari campi e attributi).

public class GroupOfCustomersImporter {
    //... Call fields ....
    public GroupOfCustomersImporter(String filePath) {
        this.filePath = filePath;
        customers = new HashSet<Customer>();
        createCSVReader();
        read();
        constructTTRP_Instance();
    }

    private void createCSVReader() {
        //....
    }

    private void read() {
        //.... Reades the file and initializes the class attributes
    }

    private void readFirstLine(String[] inputLine) {
        //.... Method used by the read() method
    }

    private void readSecondLine(String[] inputLine) {
        //.... Method used by the read() method
    }

    private void readCustomerLine(String[] inputLine) { 
        //.... Method used by the read() method
    }

    private void constructGroupOfCustomers() {
        //this.groupOfCustomers = new GroupOfCustomers(**attributes of the class**);
    }

    public GroupOfCustomers getConstructedGroupOfCustomers() {
        return this.GroupOfCustomers;
    }
}

Come puoi vedere, la classe ha solo un costruttore che chiama alcuni metodi privati ​​per fare il lavoro, so che non è una buona pratica in generale, ma preferisco incapsulare tutte le funzionalità della classe invece di rendere pubblici i metodi nel qual caso un client dovrebbe funzionare in questo modo:

GroupOfCustomersImporter importer = new GroupOfCustomersImporter(filepath)
importer.createCSVReader();
read();
GroupOfCustomer group = constructGoupOfCustomerInstance();

Preferisco questo perché non voglio inserire righe di codice inutili nel codice laterale del client che infastidisce la classe client con i dettagli di implementazione.

Quindi, è davvero una cattiva abitudine? Se sì, come posso evitarlo? Si noti che quanto sopra è solo un semplice esempio. Immagina che la stessa situazione accada in qualcosa di un po 'più complesso.

Risposte:


17

Penso che tu sia effettivamente sulla strada giusta per quanto riguarda il desiderio di nascondere i dettagli di implementazione dal client. Vuoi progettare la tua classe in modo che l'interfaccia che il client vede sia l'API più semplice e concisa che puoi inventare. Non solo il client non sarà "infastidito" con i dettagli dell'implementazione, ma sarai anche libero di refactificare il codice sottostante senza preoccuparti che i chiamanti di quel codice debbano essere modificati. Ridurre l'accoppiamento tra i diversi moduli ha alcuni vantaggi reali e dovresti assolutamente cercare di farlo.

Quindi, se segui i consigli che ho appena fornito, finirai con qualcosa che hai già notato nel tuo codice, un sacco di logica che rimane nascosta dietro un'interfaccia pubblica e non è facilmente accessibile.

Idealmente, dovresti essere in grado di testare l'unità della tua classe solo sulla sua interfaccia pubblica e se ha dipendenze esterne, potrebbe essere necessario introdurre oggetti fake / mock / stub per isolare il codice da testare.

Tuttavia, se lo fai e senti ancora che non puoi facilmente testare ogni parte della classe, allora è molto probabile che la tua unica classe stia facendo troppo. Nello spirito del principio SRP , puoi seguire quello che Michael Feathers chiama "Modello di classe di germogli" ed estrarre un pezzo della tua classe originale in una nuova classe.

Nel tuo esempio hai una classe importatore che è anche responsabile della lettura di un file di testo. Una delle tue opzioni è estrarre un pezzo di codice di lettura del file in una classe InputFileParser separata. Ora tutte quelle funzioni private diventano pubbliche e quindi facilmente testabili. Allo stesso tempo, la classe parser non è qualcosa che è visibile ai client esterni (contrassegnalo come "interno", non pubblicare file di intestazione o semplicemente non pubblicizzarlo come parte dell'API) poiché questi continueranno a utilizzare l'originale importatore la cui interfaccia continuerà a rimanere breve e dolce.


1

In alternativa a mettere molte chiamate di metodo nel costruttore della classe - che sono d'accordo è un'abitudine da evitare generalmente - potresti invece creare un metodo di fabbrica per gestire tutte le cose di inizializzazione extra che non vuoi disturbare il client lato con. Ciò significherebbe che stai esponendo un metodo in modo che assomigli a qualcosa di simile al tuo constructGroupOfCustomers()metodo come pubblico e utilizzalo come metodo statico della tua classe:

GroupOfCustomersImporter importer = 
    new GroupOfCustomersImporter.CreateInstance();

o come metodo di una classe di fabbrica separata forse:

ImporterFactory factory = new ImporterFactory();
GroupOfCustomersImporter importer = factory.CreateGroupOfCustomersImporter();

Quelle sono solo le prime due opzioni pensate direttamente dalla mia testa.

Vale anche la pena considerare che i tuoi istinti stanno cercando di dirti qualcosa. Quando il tuo istinto ti dice che il codice sta iniziando a puzzare, probabilmente ha un odore, ed è meglio fare qualcosa al riguardo prima di puzzare! Sicuramente in superficie potrebbe non esserci nulla di intrinsecamente sbagliato nel tuo codice di per sé, tuttavia un rapido controllo e un refactor ti aiuteranno a risolvere i tuoi pensieri sul problema in modo da poter passare ad altre cose con fiducia senza preoccuparti se stai costruendo un potenziale castello di carte. In questo caso particolare, delegare alcune delle responsabilità del costruttore a - o essere chiamato da - una fabbrica ti assicura di poter esercitare un maggior grado di controllo dell'istanza della tua classe e dei suoi potenziali discendenti futuri.


1

Aggiungendo la mia risposta, poiché è finita troppo a lungo per un commento.

Hai assolutamente ragione che l'incapsulamento e il test sono piuttosto contrari: se nascondi quasi tutto, è difficile testarlo.

Tuttavia, potresti rendere l'esempio più testabile fornendo uno stream anziché un nome file, in questo modo fornisci solo un csv noto dalla memoria, o un file se vuoi. Renderà anche la tua classe più solida quando è necessario aggiungere il supporto http;)

Inoltre, guarda usando lazy-init:

public class Csv
{
  private CustomerList list;

  public CustomerList get()
  {
    if(list == null)
        load();
     return list;
  }
}

1

Il costruttore non dovrebbe fare troppo se non inizializzare alcune variabili o lo stato dell'oggetto. Fare troppo nel costruttore può sollevare eccezioni di runtime. Proverei a evitare che il cliente faccia qualcosa in questo modo:

MyClass a;
try {
   a = new MyClass();
} catch (MyException e) {
   //do something
}

Invece di fare:

MyClass a = new MyClass(); // Or a factory method
try {
   a.doSomething();
} catch (MyException e) {
   //do something
}
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.