È un po 'esoterico dire il minimo come hai già riconosciuto, il che potrebbe farmi grattarmi la testa per un momento quando inizio a incontrare il tuo codice chiedendomi cosa stai facendo e dove vengono implementate queste classi di aiuto fino a quando non inizio a prendere il tuo stile / abitudini (a quel punto potrei abituarmi totalmente).
Mi piace che stai riducendo la quantità di informazioni nelle intestazioni. Soprattutto in basi di codice molto grandi, che possono avere effetti pratici per ridurre le dipendenze in fase di compilazione e in definitiva i tempi di costruzione.
La mia reazione istintiva è che se si sente la necessità di nascondere i dettagli di implementazione in questo modo, per favorire il passaggio di parametri a funzioni indipendenti con collegamento interno nel file di origine. Di solito è possibile implementare funzioni di utilità (o intere classi) utili per l'implementazione di una particolare classe senza avere accesso a tutti gli interni della classe e invece passare solo quelle pertinenti dall'implementazione di un metodo alla funzione (o costruttore). E naturalmente questo ha il vantaggio di ridurre l'accoppiamento tra la tua classe e gli "aiutanti". Ha anche la tendenza a generalizzare ulteriormente quelli che altrimenti avrebbero potuto essere "aiutanti" se scopri che stanno iniziando a servire uno scopo più generalizzato applicabile a più di una implementazione di classe.
A volte anche un po 'rabbrividisco quando vedo un sacco di "aiutanti" nel codice. Non è sempre vero, ma a volte possono essere sintomatici di uno sviluppatore che sta semplicemente decomponendo le funzioni volenti o nolenti per eliminare la duplicazione del codice con enormi blocchi di dati trasferiti a funzioni con nomi / scopi a malapena comprensibili oltre al fatto che riducono la quantità di codice richiesto per implementare alcune altre funzioni. Solo un po 'più di un po' più in anticipo, a volte può portare a una chiarezza molto maggiore in termini di come l'implementazione di una classe è scomposta in ulteriori funzioni e favorire il passaggio di parametri specifici al passaggio di intere istanze del tuo oggetto con pieno accesso agli interni promuovere quello stile di pensiero progettuale. Non sto suggerendo che lo stai facendo, ovviamente (non ne ho idea),
Se diventa ingombrante, prenderei in considerazione una seconda soluzione più idiomatica che è il brufolo (mi rendo conto che hai citato problemi con esso ma penso che puoi generalizzare una soluzione per evitare quelli con il minimo sforzo). Ciò può spostare molte informazioni che la tua classe deve essere implementata, compresi i suoi dati privati, lontano dall'intestazione all'ingrosso. I problemi di prestazioni del pimpl possono essere in gran parte mitigati con un allocatore a tempo costante * economico * sporco come un elenco gratuito preservando la semantica del valore senza dover implementare un ctor di copia definito dall'utente.
- Per quanto riguarda le prestazioni, il brufolo introduce almeno un overhead puntatore, ma penso che i casi debbano essere piuttosto seri laddove ciò pone una preoccupazione pratica. Se la località spaziale non viene degradata in modo significativo attraverso l'allocatore, i loop stretti che iterano sull'oggetto (il che dovrebbe essere generalmente omogeneo se le prestazioni sono così importanti) tenderà comunque a ridurre al minimo le mancate cache nella pratica, a condizione che tu usi qualcosa come un elenco gratuito per allocare il pimpl, inserendo i campi della classe in blocchi di memoria in gran parte contigui.
Personalmente, solo dopo aver esaurito quelle possibilità, prenderei in considerazione qualcosa del genere. Penso che sia un'idea decente se l'alternativa è come più metodi privati esposti all'intestazione con forse solo la natura esoterica di ciò che è la preoccupazione pratica.
Un'alternativa
Un'alternativa che mi è venuta in mente in questo momento e che realizza ampiamente i tuoi stessi scopi in assenza di amici è questa:
struct PredicateListData
{
int somePrivateField;
};
class PredicateList
{
PredicateListData data;
public:
bool match() const;
};
// In source file:
static bool fullMatch(const PredicateListData& p)
{
// Can access p.somePrivateField here.
}
bool PredicateList::match() const
{
return fullMatch(data);
}
Ora potrebbe sembrare una differenza molto discutibile e lo definirei comunque un "aiutante" (in un senso forse dispregiativo dal momento che stiamo ancora passando l'intero stato interno della classe alla funzione se ne ha bisogno o no) tranne che evita il fattore "shock" dell'incontro friend
. In generale friend
sembra un po 'spaventoso vedere spesso assenti ulteriori controlli, dal momento che dice che i tuoi interni di classe sono accessibili altrove (il che implica che potrebbe non essere in grado di mantenere i propri invarianti). Con il modo in cui friend
lo usi diventa piuttosto discutibile se le persone sono consapevoli della pratica dalfriend
risiede semplicemente nello stesso file di origine e aiuta a implementare la funzionalità privata della classe, ma quanto sopra ottiene quasi lo stesso effetto, almeno con l'unico vantaggio probabilmente discutibile che non coinvolge nessun amico che evita quell'intero tipo ("Oh sparare, questa classe ha un amico. In quale altro luogo i suoi privati hanno accesso / mutazione? "). Considerando che la versione immediatamente sopra comunica immediatamente che non c'è modo per i privati di accedere / mutare al di fuori di qualsiasi cosa fatta nell'implementazione di PredicateList
.
Questo forse si sta spostando verso territori un po 'dogmatici con questo livello di sfumatura poiché chiunque può rapidamente capire se si nominano in modo uniforme le cose *Helper*
e le inseriscono tutte nello stesso file sorgente che è tutto raggruppato insieme come parte dell'implementazione privata di una classe. Ma se diventiamo schizzinosi, forse lo stile immediatamente sopra non causerà una reazione istintiva a prima vista in assenza della friend
parola chiave che tende a sembrare un po 'spaventosa.
Per le altre domande:
Un consumatore può definire la propria classe PredicateList_HelperFunctions e consentire loro di accedere ai campi privati. Anche se non lo vedo come un grosso problema (se davvero volessi in quei campi privati potresti fare un casting), forse incoraggerebbe i consumatori ad usarlo in quel modo?
Questa potrebbe essere una possibilità oltre i confini dell'API in cui il client potrebbe definire una seconda classe con lo stesso nome e ottenere l'accesso agli interni in quel modo senza errori di collegamento. Poi di nuovo sono in gran parte un programmatore C che lavora nella grafica in cui i problemi di sicurezza a questo livello di "what if" sono molto bassi nella lista delle priorità, quindi preoccupazioni come queste sono solo quelle su cui tendo agitando le mani e faccio una danza e prova a fingere che non esistano. MrGreen Se stai lavorando in un dominio in cui preoccupazioni come queste sono piuttosto serie, penso che sia una buona considerazione da prendere.
La proposta alternativa di cui sopra evita anche di soffrire di questo problema. Se vuoi comunque continuare a utilizzare friend
, puoi anche evitare quel problema trasformando l'helper in una classe nidificata privata.
class PredicateList
{
...
// Declare nested class.
class Helper;
// Make it a friend.
friend class Helper;
public:
...
};
// In source file:
class PredicateList::Helper
{
...
};
È questo un modello di design noto per il quale c'è un nome?
Nessuno a mia conoscenza. Dubito che ce ne sarebbe uno dal momento che sta davvero entrando nella minuzia dei dettagli e dello stile di implementazione.
"Helper Hell"
Ho ricevuto una richiesta di ulteriori chiarimenti in merito al modo in cui a volte rabbrividisco quando vedo implementazioni con un sacco di codice "helper", e questo potrebbe essere leggermente controverso con alcuni, ma in realtà è un dato di fatto come ho fatto davvero rabbrividire quando stavo eseguendo il debug di alcuni dell'implementazione di una classe da parte dei miei colleghi solo per trovare un sacco di "aiutanti". MrGreen E non ero l'unico della squadra a grattarmi la testa cercando di capire cosa avrebbero dovuto fare esattamente tutti questi aiutanti. Inoltre non voglio venire dogmatico come "Non usare gli aiutanti", ma vorrei suggerire che potrebbe aiutare a pensare a come implementare le cose assenti da loro quando pratico.
Tutte le funzioni dei membri privati non sono utili per definizione?
E sì, sto includendo metodi privati. Se vedo una classe con un'interfaccia pubblica semplice ma come una serie infinita di metodi privati che sono in qualche modo mal definiti in uno scopo come find_impl
o find_detail
o find_helper
, allora anch'io mi rabbrividisco in un modo simile.
Quello che sto suggerendo in alternativa sono funzioni non amichevoli non membro con collegamento interno (dichiarato static
o all'interno di uno spazio dei nomi anonimo) per aiutare a implementare la tua classe con almeno uno scopo più generalizzato di "una funzione che aiuta a implementare gli altri". E posso citare Herb Sutter da "Coding Standards" C ++ qui per il motivo per cui ciò può essere preferibile da un punto di vista generale SE:
Evita le quote associative: ove possibile, preferisci rendere le funzioni non amichevoli. [...] Le funzioni non amichevoli non membri migliorano l'incapsulamento minimizzando le dipendenze: il corpo della funzione non può dipendere dai membri non pubblici della classe (vedi Articolo 11). Inoltre, suddividono le classi monolitiche per liberare la funzionalità separabile, riducendo ulteriormente l'accoppiamento (vedi Articolo 33).
Puoi anche comprendere le "quote associative" di cui parla in una certa misura in termini del principio di base della limitazione dell'ambito variabile. Se immagini, come l'esempio più estremo, un oggetto God che ha tutto il codice necessario per l'esecuzione dell'intero programma, favorendo quindi "aiutanti" di questo tipo (funzioni, che siano funzioni membro o amici) che possono accedere a tutti gli interni ( privati) di una classe rendono sostanzialmente tali variabili non meno problematiche delle variabili globali. Hai tutte le difficoltà nel gestire correttamente lo stato e nel thread della sicurezza e nel mantenimento degli invarianti che otterresti con le variabili globali in questo esempio estremo. E, naturalmente, la maggior parte degli esempi reali non si spera in alcun modo vicino a questo estremo, ma nascondere le informazioni è utile solo in quanto limita la portata delle informazioni a cui si accede.
Ora Sutter dà già una bella spiegazione qui, ma aggiungerei inoltre che il disaccoppiamento tende a promuovere come un miglioramento psicologico (almeno se il tuo cervello funziona come il mio) in termini di come funzioni il design. Quando inizi a progettare funzioni che non possono accedere a tutto nella classe tranne i parametri rilevanti che le passi o, se passi l'istanza della classe come parametro, solo i suoi membri pubblici, tende a promuovere una mentalità di progettazione che favorisce funzioni che hanno uno scopo più chiaro, oltre al disaccoppiamento e alla promozione di un migliore incapsulamento, rispetto a ciò che potresti essere tentato di progettare se solo potessi accedere a tutto.
Se torniamo alle estremità, una base di codice piena di variabili globali non induce esattamente gli sviluppatori a progettare funzioni in modo chiaro e generalizzato allo scopo. Molto rapidamente più informazioni puoi accedere a una funzione, più molti di noi mortali affrontano la tentazione di degeneralizzarla e ridurne la chiarezza a favore dell'accesso a tutte queste informazioni extra che abbiamo invece di accettare parametri più specifici e pertinenti a quella funzione restringere il suo accesso allo stato e ampliarne l'applicabilità e migliorare la chiarezza delle intenzioni. Ciò vale (anche se generalmente in misura minore) con funzioni membro o amici.