È buona o cattiva pratica fornire classi separate per un oggetto: uno per costruirlo e uno per usarlo?

3

Supponiamo che sto scrivendo un codice C ++ per visualizzare oggetti "Foo". Ho due modi per ottenere un "Foo": calcolarlo dai dati, o prendere i pezzi di un "Foo" precompilato e costruire un nuovo "Foo".

Ora, una volta che viene calcolato un "Foo", è garantito che sia buono per la visualizzazione, ma cambiarlo può rompere questa ipotesi. Pertanto, ho deciso di rappresentare "Foos" nel mio codice da una classe Foo che non ha metodi di muting: una volta costruita e inizializzata, non cambia.

Ma c'è un secondo modo per creare un "Foo": costruirlo da componenti di "Foo" precalcolati. Ho trovato diversi metodi per creare un Foo dai dati precompilati:

Metodo 1: metodi Costruttore / Statici

Forse il metodo più ovvio sarebbe aggiungere un nuovo costruttore o un metodo statico a Foo , chiamarlo fromPrecomputed , che leggerà i componenti del Foo precompilato e creerà un nuovo oggetto Foo , controllando che è valido Per spiegare perché mi piacerebbe evitarlo, devo complicare il mio esempio: diciamo che un componente di "Foo" è una raccolta di "Bars". Ora, in termini di implementazione, a volte una "Barra" è rappresentata come std::vector<std::vector<Bar> > , a volte come Bar array[][2] , a volte come std::vector<std::pair<Bar,Bar> > , e così via ... Potrei avere l'utente di riorganizzare i loro dati in un modulo standardizzato e un singolo costruttore per questo standard, ma questo potrebbe richiedere all'utente di eseguire una copia extra. Non voglio fornire un metodo statico per ogni formato: readPrecomputedFormatA , readPrecomputedFormatB e così via: questo ingombra l'API.

Metodo 2: Crea Foo mutabile

Se esponessi il metodo addBar(Bar) di Foo , potrei consentire all'utente di scorrere la loro raccolta di "Bar" a modo loro. Questo, tuttavia, rende Foo mutabile. Quindi ho potuto calcolare un Foo che abbia senso per la visualizzazione, quindi usare addBar per aggiungere un Bar che rende Foo non più un "Foo". Non va bene.

Metodo 3: Crea una classe "builder" di un amico

Creo una classe chiamata FooBuilder che ha il metodo addBar(Bar) esposto. Rendo FooBuilder un amico di Foo e aggiungo un costruttore a Foo che prende un FooBuilder . Chiamando questo costruttore, controlla che FooBuilder contenga un oggetto "Foo" valido, quindi scambia la sua rappresentazione vuota di Foo con ciò che è all'interno di FooBuilder . Tutti sono felici.

L'unica "confusione" sul metodo n. 3 è che richiede un'amicizia, ma credo valga la pena mantenere l'incapsulamento. Ma questo mi ha fatto pensare: è un modello stabilito? O c'è un altro, migliore modo di farlo che non conosco?

    
posta jme 20.01.2014 - 20:31
fonte

1 risposta

9

Ciò che consideri, FooBuilder e Foo sono in realtà parte di un consolidato schema Builder . L'altro approccio menzionato con la creazione di un'istanza di Foo basata su un'istanza esistente è chiamato Modello di prototipo . Entrambe sono ben note alternative alla creazione di oggetti e diversi libri (in particolare "Modelli di progettazione", ovvero GoF) sono stati scritti descrivendoli (non sono sicuro che sia chiaro sul tuo secondo esempio mutevole, quindi confrontiamo da # 1 a # 3 qui)

Ogni modello ha ovviamente i suoi vantaggi e svantaggi e solo tu puoi decidere quale è meglio per la tua situazione specifica. Ad esempio, di solito uso i builder quando ci sono molti modi diversi per inizializzare Foo. Ad esempio, al momento sto lavorando su una classe di generazione di query e molti client la usano in modo diverso per creare oggetti di query. Diversi client vogliono che vengano restituiti campi diversi, alcuni clienti vogliono solo le ultime 10 righe, alcuni vogliono l'ordinamento mentre altri no. Builder è perfetto in quanto può somigliare a questo:

qb = QueryBuilder()
query = qb.fields("name", "address")
          .sort("zip", "asc")
          .limit(10)
          .build()

Quindi più setter, ciascuna istanza di ritorno del builder stesso così da poter fare daisy-chain fondamentalmente serve come un costruttore molto flessibile. Rendere una classe un amico di un'altra classe ha il suo tempo e un posto e potenzialmente questo potrebbe essere uno di quei posti.

In alternativa, è possibile definire Foo come se avesse un costruttore che accetta un set piuttosto complesso di parametri, o potenzialmente si potrebbe definire un'altra classe FooData, che FooBuilder potrebbe inizializzare e passare alla creazione di Foo. Poi di nuovo, forse Foo e FooBuilder essere amici in questo caso non è una cosa così brutta. Tieni presente che quando una classe è amica di un'altra, stai espandendo il limite di incapsulamento, il che potrebbe essere altrettanto grave quanto inserire sempre più codice all'interno di una classe (cioè più codice conosce gli interni)

Alla fine della giornata, potresti pensare e considerare tutte queste diverse opzioni e tutto si ridurrà a 60/40. Se non c'è un vincitore chiaro, puoi sempre lasciare che una moneta scelga il vincitore e vada avanti con essa. Molto probabilmente una delle scelte progettuali funzionerebbe perfettamente e, seguendo le proposte e vedendo il tuo codice in azione, imparerai preziose lezioni per il futuro. A volte, quando ho delle decisioni di progettazione e non riesco a decidere tra A e B, potrei finire per scegliere B e poi, se mai dovessi incontrare una situazione simile, sceglierei deliberatamente l'altra scelta. Il lavoro in entrambi i casi viene completato e il software mi permette di fare ciò che voglio, ma ottieni un sacco di benefici nel vedere le tue decisioni in azione e poter confrontare i due approcci lavorativi , piuttosto piuttosto che discuterne e pensarci.

    
risposta data 21.01.2014 - 04:27
fonte

Leggi altre domande sui tag