Class design dilemma: incapsulamento contro la singola responsabilità / separazione delle preoccupazioni

6

Sto lavorando su una classe che rappresenta un oggetto con rappresentazioni multiple: una è una rappresentazione di tipo XML utilizzata da un sistema di ordinamento automatico, l'altra è una rappresentazione basata su POJO utilizzata da uno strumento di monitoraggio.

Il problema che sto incontrando è che mi sembra di dover fare un compromesso tra l'incapsulamento dei dati interni dell'oggetto e la progettazione della mia applicazione secondo il principio della "separazione delle preoccupazioni" o "singola responsabilità per classe".

Diamo un'occhiata alla classe in modo più dettagliato:

public class MyObject {
    private Object field1;
    private Object field2;
    private Object field3;
    ...
    private Object field100;
}

(Sì, è piuttosto un brutto mostro brutto, qualcosa su cui non ho alcun controllo)

Se seguo il principio della separazione delle preoccupazioni (nel modo in cui lo capisco comunque), dovrò aggiungere i getter per ogni campo in questa classe e avere una classe XMLBuilder separata per la rappresentazione XML e un PojoBuilder per il monitoraggio rappresentazione dello strumento:

public class XMLBuilder {

    public XML asXml(MyObject obj) {
        return "<tag1>" + object.getField1() + "</tag1>"
               "<tag2>" + object.getField2() + "</tag2>"
               ...
               ;
        }
}

Questo mi sembra di infrangere il principio dell'incapsulamento: devo esporre molti dati interni sulla mia classe solo per fornire una rappresentazione per questo. Se passo il mio oggetto ad altri sistemi, non ho alcun controllo sul fatto che questi sottosistemi possano diventare dipendenti da campi arbitrari (es. Field45), il che significa che le future modifiche alla struttura di questo oggetto potrebbero diventare difficili dato che ora ci sono altre possibili dipendenze su di esso.

Un approccio alternativo, sostenuto da Allen Holub ( link ) per esempio, è quello di fare la rappresentazione all'interno della classe:

public class MyObject {

    ...

    public XML asXml() {
        return "<tag1>" + field1 + "</tag1>"
               "<tag2>" + field2 + "</tag2>"
               ...
               ;
    }

    public Object asMonitor() {
        ...
    }

 }

Per me questa soluzione sembra ottima, poiché la classe non espone mai campi privati. I generatori di rappresentazione hanno accesso a tutte le informazioni di cui ha bisogno e l'oggetto stesso può essere tranquillamente passato ad altri sottosistemi senza la preoccupazione che un altro modulo dipenda da internals.

Ora, quando lo implemento, tutti i miei colleghi sollevano la questione della "Separazione delle preoccupazioni". Il problema, dicono, è che la classe MyObject ora ha conoscenza di cose come rappresentazioni XML e rappresentazioni POJO, il che significa che se la rappresentazione XML cambia, ora devi apportare modifiche all'interno della classe stessa stessa , invece di aggiornare semplicemente un "set di regole XML" o qualche altra configurazione esterna.

La soluzione di Holub consiste nell'utilizzare un modello di Builder:

public class MyObject {

...

    public void buildContent(MyObjectBuilder builder) {
        builder.setField1(field1);
        builder.setField2(field2);
    }
 }

 public class XMLBuilder implements MyObjectBuilder {

     ...

     public void setField1(Object field1) {
         content += "<tag1>" + field1 + "</tag1>";
     }

     public XML asXML() {
         return content;
     }

     ...
 }

ma questo sembra difficilmente migliore del primo approccio - invece di esporre i dati come getter, ora è implicitamente esposto attraverso un costruttore. OK, capisco che con il builder potrei avere un controllo aggiuntivo su come il mio oggetto si rappresenta, ma è quel limite di controllo che vale la maggiore complessità nella base di codice? Le modifiche alla struttura di MyObject porteranno comunque probabilmente a dipendenze downstream che richiedono aggiornamenti, quindi non sono del tutto sicuro se molto è stato ottenuto qui (eccetto che ci sono tecnicamente nessun getters).

È un caso in cui devi semplicemente scegliere il tuo veleno? O c'è una via di mezzo magica che riduce tutti questi problemi? Quali sono le strategie comuni per affrontare questo?

    
posta firtydank 07.12.2015 - 10:26
fonte

4 risposte

8

Is this a case where you simply have to choose your poison?

Sì, in sostanza.

Uno dei compiti dello sviluppatore software è la gestione del rischio quando si tratta di cambiare. E c'è molta teoria sulla gestione del rischio. In generale, è necessario innanzitutto identificare i rischi. L'hai già fatto: cambio dell'oggetto stesso, cambio di XML, cambio di POJO, ecc. Tutti questi sono possibili rischi. Il prossimo passo sarebbe identificare quali sono le probabilità che si verifichi. Puoi farti un'idea guardando indietro ai cambiamenti che dovevi fare in passato, ma solo l'esperienza reale può aiutarti qui. Quindi, è necessario identificare l'impatto di ciascun rischio. Dici che le modifiche possono propagarsi attraverso le dipendenze, quindi più dipendenze il rischio influenza, più costoso è quando si verifica. Prendendo tutto questo, dovresti creare un design che minimizzi i rischi che hanno alte probabilità e costi elevati.

Non posso dirtelo adesso, perché il modo in cui descrivi il tuo problema è troppo generico. E diversi sistemi potrebbero incontrare diversi rischi a diverse probabilità e costi. Ad esempio, se XML viene utilizzato come comunicazione tra i sistemi, l'aggiornamento dello schema potrebbe non essere un problema. Ma se è usato per la persistenza, allora è indispensabile mantenere la compatibilità con le versioni precedenti. Etc ..

L'unica cosa che posso suggerire è avere più esperienza. Più esperienza ti darà più idee sui possibili rischi e le loro proprietà e modi di progettazione per evitare tali rischi.

    
risposta data 07.12.2015 - 12:05
fonte
2

This seems to me to break the principle of encapsulation - I have to expose a lot of the internal data on my class just to provide a representation for it.

Non proprio. In un approccio, si espongono i dati e la struttura attraverso 100 getters, nell'altro si espongono essenzialmente gli stessi dati e la stessa struttura attraverso un XML con 100 tag. Dal punto di vista delle dipendenze e dell'incapsulamento, c'è una differenza no (!) tra queste due soluzioni.

Inoltre, dovresti considerare di non implementare questo "manualmente". Ci sono alcune librerie basate su riflessioni (come XStream ) che possono fare una serializzazione / deserializzazione Xml per te (alcuni altri sono menzionato in questo precedente post SO ). Funzionano bene con attributi privati, quindi non è necessario scrivere getter e setter aggiuntivi. L'utilizzo di una tale libreria ti consentirà di aderire all'SRP - il tuo oggetto non saprà nulla sul formato di serializzazione.

Se davvero non è possibile utilizzare uno di questi serializzatori XML "out of the box", si può prendere in considerazione l'implementazione di questo in base alla riflessione da soli. Questo sicuramente ti proteggerà dallo scrivere sempre lo stesso tipo di codice della piastra.

Se non vuoi utilizzare la riflessione e seguire la strada della scrittura di codice simile per ogni singolo attributo, considera di fornire un metodo asMap nella tua classe MyObject che ti offre una rappresentazione più o meno "neutrale" del tuo oggetto come Map<String,String> (con i nomi dei campi oi nomi dei tag come chiavi). Il tuo XmlBuilder quindi può prendere questo come input e riformattare la mappa nel formato xml finale.

    
risposta data 07.12.2015 - 16:37
fonte
1

Alcuni framework di serializzazione implementano il seguente approccio:

  • Ogni oggetto modello è necessario per implementare una determinata interfaccia
    • L'interfaccia accetta un oggetto contenitore simile a un visitatore astratto
    • L'oggetto modello è responsabile della compilazione dell'oggetto contenitore
    • Il contenitore fornisce operazioni banali come la memorizzazione di coppie chiave-valore, sequenze, oggetti nidificati, può supportare l'archiviazione di tutti i tipi primitivi
  • Esistono diverse implementazioni di contenitori, uno per XML, uno per POJO, uno per JSON ecc.

Questo approccio rivela la struttura dell'oggetto modello, ma lo rende scomodo da abusare.

Vedi boost :: serialization libreria per riferimento.

Ecco la mia bozza semplificata per Java:

public interface Container implements Closable {
  // Repeat for all primitives
  void put(String key, String value);
  Container getChild(String key);
  //TODO: add sequences support
}

public class ConcreteModelObject {
  private final String datum;
  private final String id;
  private final Child child;
  private class Child {
    private final String childId;
    void store(Container container) {
      container.put("id", childId);
    }
  }
  // The only structure exposure point
  public void store(Container container) throws IOException {
    container.put("id", id);
    container.put("datum", datum);
    // Child container should be closed after use, to allow closing of XML tags, for example
    try (Container childContainer: container.getChild("child")) {
      child.store(childContainer);
    }
  }
}

Questo approccio riduce l'accoppiamento tra la serializzazione e l'oggetto del modello, ma non lo elimina. Risultati simili possono essere ottenuti scegliendo un metodo di serializzazione più potente, implementandolo nel modello, quindi convertendo il formato risultante in altri (ad esempio, rendere il modello serializzabile in XML, fornire un modo per convertire XML in JSON).

La deserializzazione è più o meno simmetrica. Si noti inoltre che questo approccio può consumare memoria se implementato in modo errato. Vedi boost :: serializzazione per soluzioni ottimizzate.

    
risposta data 11.12.2015 - 08:54
fonte
-1

Esporre le proprietà come pubbliche e avere classi separate la rappresentazione XML è uno schema piuttosto standard. Esistono molte librerie XML / JSON ecc. Per la conversione di oggetti di classe in varie rappresentazioni di dati e il tuo codice sarà molto utile se lo lasci compatibile con tutti questi.

E per quello che vale, non vedo davvero la differenza in termini di dipendenze di terze parti. Se farà parte della rappresentazione XML, allora è parte dell'interfaccia pubblica e dovrebbe essere pubblica.

    
risposta data 07.12.2015 - 16:21
fonte