Duplicazione dati vs Incapsulamento. Quale design usare?

2

Il problema che affronto è come combinare incapsulamento e utilizzo ottimale della memoria.

Non posso mostrarti il mio codice e quindi spiegarlo in un ampio esempio (spero).

Diciamo che abbiamo bisogno di avere un database di mans. Vogliamo sapere solo 2 cose su quelle persone:

  1. Età dell'uomo (in ore dalla nascita).
  2. Nome della città in cui vive.

Il modo comodo e naturale per gestire questi dati è creare un oggetto, che corrisponde a un uomo e memorizzarlo in un array:

class OMan1 {
  public:
    OMan( const int &age, const astring &t ): fAge(age), fTown(t) {}
    const int& age() const: { return fAge; }
    const astring& Town() const: { return fTown; }
    astring FullId() const: { return fTown+fAge; }
  private:
    int fAge;
    astring fTown;
}

OMan mans[N];

Qui i nostri OM sono oggetti che contengono autonomamente e tutto si riempie bene.

Tranne il fatto che cloniamo i nomi delle città migliaia di volte, e spreciamo memoria e tempo di esecuzione in questo modo.

Un miglioramento che possiamo fare è creare un array indipendente per i nomi di città e per ogni OMan, memorizzare solo l'età, un identificativo della città e un puntatore alla serie di città:

class OMan2 {
  // same functionality as for OMan1
    int fAge;
    int fTownId;
    astring* fTowns;
}

l'oggetto è ancora autonomo, sizeof(int) + sizeof(void*) è molto meno di sizeof(astring) , vinciamo molto. Tuttavia, è ancora 2-3 volte più del sizeof(fAge) e ripetiamo fTowns miliardi di volte.

L'ottimizzazione della memoria è fondamentale per me, quindi quello che faccio è mantenere solo fAge e fTownId e spostare tale funzionalità come Town() e FullId() fuori dalla classe OMan in una classe come OManDataBase :

class OMan3 {
  public:
    OMan( const int &age, const int &tid ): fAge(age), fTownId(tid) {}
    const int& age() const: { return fAge; }
    const int& TownId() const: { return fId; }
    // const astring& Town() const: { return fTown; }
    // astring FullId() const: { return fTown+fAge; }
  private:
    int fAge;
    int fTownId;
}

class OManDataBase {
  // constructor, destructor
    const int& age( const int& i) const: { return fMans[i].TownId()]; }
    const astring& Town( const int& i) const: { return fTown[fMans[i].TownId()]; }
    const astring& FullId( const int& i) const: { return Town(i)+age(i); }
  private:
    vector<OMan3> fMans;
    vector<astring> fTowns;
}

E OMan3 ora non è un oggetto autonomo. Ad esempio, non riconosce il suo nome completo. Ciò significa che se ho bisogno di fare un po 'di elaborazione dei dati con un uomo devo usare l'intera% istanza diOManDataBase:

OBillType47 NewBillType47( const OManDataBase &db, int i ) { ... }

invece di

OBillType47 NewBillType47( const OMan &m ) { ... }

l'incapsulamento è stato interrotto qui e la leggibilità del codice è stata chiaramente ridotta. (Ho messo Type47 per sottolineare che posso avere un sacco di funzioni, che funziona con Oman-S e non posso includerle tutte in OManDataBase class).

Mi chiedo c'è un altro modo (-s) per risolvere il problema della duplicazione dei dati, mantenendo gli oggetti il più possibile autosufficienti ?

    
posta klm123 29.11.2013 - 16:50
fonte

5 risposte

4

Dovresti provare a utilizzare il modello Peso vivo . Questo vuol dire che memorizzerai solo l'id della città nell'oggetto man, e il metodo per ottenere il nome della città sarà ancora parte del tuo oggetto "man". Per fare in modo che funzioni, devi passare l'elenco di tutte le città come parametro:

class OMan1 
{
   //..
   const astring& Town( const vector<astring> &allTowns) const: 
   { 
      return allTowns[fTownId]; 
   }
   // ...
 }

Quindi perderai naturalmente un po 'di autocontenimento, dal momento che dovrai fornire la lista della città dappertutto dove chiedi un oggetto "uomo" per la sua città, ma il metodo Town() è tenuto nel luogo in cui la maggior parte della gente lo aspetterebbe: nell'oggetto "man", e non in una sorta di "oggetto divino" (database).

    
risposta data 30.11.2013 - 09:11
fonte
3

Ecco alcuni consigli ingenui Spero che tu trovi utile per conservare la memoria ...

Soluzione 1

Invece di usare una classe C ++ per archiviare i dati, usa strutture "compresse" (guarda la documentazione del tuo compilatore per capire come creare una struttura compressa, di solito c'è un flag del compilatore). Quindi limita la dimensione delle tue impronte di memoria delle variabili, ad esempio un uint8 dovrebbe essere abbastanza grande da contenere l'età di un uomo. Una volta che hai fatto questo negozio, la struttura impacchettata in un array in stile c (per la massima compressione) o usa std::vector<OManStruct> se vuoi semplificarti la vita. Invece di memorizzare il nome della città come una stringa nella struct, crea un std::map<uint32_t, string> che mappa i TownId in nomi di città. Memorizza il townId nella struct.

La tua definizione di struct potrebbe essere simile a questa (se usi GCC):

typedef struct __attribute__((__packed__)) OManStruct 
{
    uint8_t fAge;
    uint32_t fTownId; //maybe you could use uint16_t here but you might be cutting it close
};

Architettonicamente vorrei racchiudere la conoscenza di queste strutture dati in una classe che mantiene l'array, i dati associati (numero di elementi, ecc.) ed espone alcuni simpatici getter e setter in modo che il mondo esterno non conosca i dettagli di questa implementazione. O meglio ancora crea, un OManFactory che può essere trattato come un singleton nel tuo progetto e restituisce oggetti OMan su richiesta.

Soluzione 2

Potresti prendere seriamente in considerazione il database dei dati usando SQLite, Postgres ecc. o qualche database nosql come Redis. Se la quantità di dati crescerà nel tempo, non c'è alcuna garanzia che tu non possa saltare attraverso tutta la tua ram anche se tu implementa la soluzione 1. Un database ti dà anche la possibilità di memorizzare i dati in modo persistente e ti dà un buon meccanismo (se si utilizza un database di tipo sql) per eseguire query sui dati.

    
risposta data 29.11.2013 - 18:13
fonte
1

Data duplication vs Encapsulating. Which design to use?

In contesti sufficientemente critici, trovo un design che non mi impone di comprometterne uno per l'altro. Se non riesci a creare qualcosa di autosufficiente senza una ridondanza non banale di un tipo che non supporta, ad esempio, il multithreading, allora la mia soluzione è progettare a un livello più rozzo che non richiede tali compromessi.

// Notice "Men", not "Man"
class OMen
{
public:
    ...

private:
    std::vector<int> age;
    std::vector<int> town_idx;
    std::vector<astring> town_names;
};

Si sta utilizzando la suddivisione del campo potenzialmente calda / fredda (anche se è possibile combinare l'età e l'indice della città se i pattern di accesso critici sono casuali) e magari l'interning stringa molto semplicistico (un tipo di peso mosca). Questo ti dà anche un carico in barca di spazio per sintonizzare e ritoccare il rep di dati di cui hai bisogno con il senno di poi senza costose modifiche al design.

La tua interfaccia pubblica potrebbe ancora tornare come un proxy a un OManProxy a operator[] che ti consente di accedervi come un oggetto, ma in realtà sta solo indicizzando e puntando a dati in questo aggregato (collezione di uomini) e usi è un proxy temporaneo per comodità.

È come anziché cercare di passare tutto il giorno a concentrarsi su come progettare una classe Pixel che sia efficiente, ridurre lo zoom e invece progettare una classe Image e trasformare i "pixel" in un dettaglio di implementazione interno di questo aggregato . Il più grande valore del design orientato ai dati per me non è come progettare i dati in modo efficiente per un layout e un accesso di memoria ottimali, ma come modellare le cose a un livello sufficientemente grossolano per darti tutto il respiro necessario per sperimentare con le rappresentazioni dei dati, profilo, tweak e tune, senza costosi e intrusivi cambiamenti centrali alle interfacce pubbliche, ampiamente utilizzati, perché la cosa reale che vogliamo evitare sono cambiamenti di design costosi più che altro. Se modellerai piccoli oggetti piccoli che conservano a malapena i dati che sono ampiamente utilizzati, allora ti sei già intrappolato in un angolo del design.

Questo è, naturalmente, per contesti sufficientemente critici in cui è necessario tutto il respiro ottimizzato che si può ottenere per il futuro. A volte è un po 'più ingombrante nell'implementazione per implementare tipi di contenitore di questo tipo (il% analogico% co_de in opposizione a Image , o il% analogico% co_de in opposizione a Pixel ), specialmente se si iniziano a fornire proxy e tale per uso conveniente. Ma ha migliaia di dipendenze che amano ParticleSystem dappertutto solo per rendersi conto che la sua rappresentazione interna ha un strong bisogno di cambiamenti in modi che richiederebbero modifiche all'interfaccia pubblica. E questa è una strategia di progettazione utile in generale, non solo per le aree critiche per le prestazioni. Se ti stai impiccando su come progettare qualcosa di minuscolo, chiedilo se dovrebbe essere davvero qualcosa di pubblico e dipende direttamente da molti posti, o se sarebbe più appropriato trasformarlo in un dettaglio di implementazione di alcuni più grezzi, più alti livello, forse a volte un disegno più astratto, perché qualsiasi progetto che ti fa costantemente pensare a te stesso non è probabilmente uno a cui vuoi iniziare a lanciare molte dipendenze.

    
risposta data 21.12.2018 - 00:34
fonte
0

Il modello di peso leggero è l'idea giusta.

Ho trovato questo post Overflow dello stack precedente sulla Libreria Boost Flyweight che potrebbe soddisfare le tue esigenze. Non sono un utente C ++ e non l'ho mai usato, quindi usalo a tuo rischio!

    
risposta data 01.12.2013 - 01:26
fonte
0

Quando decidi solo come costruire le tue strutture dati, valuta le problematiche dei database relazionali quando i dati sono normalizzati e denormalizzati

Non reinventare la ruota perché stai osservando il problema da un livello di dettaglio troppo basso.

    
risposta data 09.12.2013 - 04:51
fonte

Leggi altre domande sui tag