Negli ultimi mesi ho chiesto persone qui su SE e su altri siti mi offrono alcune critiche costruttive riguardo al mio codice. C'è una cosa che è saltata fuori quasi ogni volta e ancora non sono d'accordo con quella raccomandazione; : P Vorrei discuterne qui e forse le cose diventeranno più chiare per me.
Riguarda il principio della responsabilità singola (SRP). Fondamentalmente, ho una classe di dati Font
, che contiene non solo funzioni per manipolare i dati, ma anche per caricarli. Mi hanno detto che i due dovrebbero essere separati, che le funzioni di caricamento dovrebbero essere collocate all'interno di una classe di fabbrica; Penso che questa sia una cattiva interpretazione dell'SRP ...
Un frammento dalla mia classe di font
class Font
{
public:
bool isLoaded() const;
void loadFromFile(const std::string& file);
void loadFromMemory(const void* buffer, std::size_t size);
void free();
void some();
void another();
};
Design suggerito
class Font
{
public:
void some();
void another();
};
class FontFactory
{
public:
virtual std::unique_ptr<Font> createFromFile(...) = 0;
virtual std::unique_ptr<Font> createFromMemory(...) = 0;
};
Il design suggerito presumibilmente segue l'SRP, ma non sono d'accordo - penso che vada troppo lontano. La Font
classe non è più autosufficiente (è inutile senza la fabbrica) e FontFactory
deve conoscere i dettagli sull'implementazione della risorsa, che probabilmente viene fatta attraverso l'amicizia o il pubblico, che espone ulteriormente l'implementazione di Font
. Penso che questo sia piuttosto un caso di responsabilità frammentata .
Ecco perché penso che il mio approccio sia migliore:
Font
è autosufficiente: essendo autosufficiente, è più facile da capire e mantenere. Inoltre, puoi utilizzare la classe senza dover includere nient'altro. Se, tuttavia, scopri che hai bisogno di una gestione più complessa delle risorse (una fabbrica), puoi farlo facilmente (in seguito parlerò della mia fabbricaResourceManager<Font>
).Segue la libreria standard: credo che i tipi definiti dall'utente dovrebbero cercare il più possibile di copiare il comportamento dei tipi standard in quella lingua. È
std::fstream
autosufficiente e fornisce funzioni comeopen
eclose
. Seguire la libreria standard significa che non è necessario dedicare sforzi all'apprendimento di un altro modo di fare le cose. Inoltre, in generale, il comitato standard C ++ probabilmente conosce di più sul design di chiunque altro qui, quindi se hai dei dubbi, copia ciò che fanno.Testabilità - Qualcosa non va, dove potrebbe essere il problema? - È il modo in cui
Font
gestisce i suoi dati o il modo in cuiFontFactory
i dati caricati? Non lo sai davvero. Avere le classi autosufficienti riduce questo problema: puoi testare daFont
solo. Se poi devi testare la fabbrica e sai cheFont
funziona bene, saprai anche che ogni volta che si verifica un problema deve essere all'interno della fabbrica.È agnostico dal contesto - (Questo si interseca un po 'con il mio primo punto.)
Font
Fa la sua cosa e non fa ipotesi su come lo userai: puoi usarlo come preferisci. Costringere l'utente a utilizzare una fabbrica aumenta l'accoppiamento tra le classi.
Anch'io ho una fabbrica
(Perché il design di Font
me lo permette.)
O meglio un manager, non solo una fabbrica ... Font
è autosufficiente, quindi il manager non ha bisogno di sapere come costruirne uno; invece il gestore si assicura che lo stesso file o buffer non sia caricato in memoria più di una volta. Si potrebbe dire che una fabbrica può fare lo stesso, ma ciò non spezzerebbe l'SRP? La fabbrica non dovrebbe quindi solo costruire oggetti, ma anche gestirli.
template<class T>
class ResourceManager
{
public:
ResourcePtr<T> acquire(const std::string& file);
ResourcePtr<T> acquire(const void* buffer, std::size_t size);
};
Ecco una dimostrazione di come utilizzare il gestore. Si noti che viene utilizzato praticamente esattamente come farebbe una fabbrica.
void test(ResourceManager<Font>* rm)
{
// The same file isn't loaded twice into memory.
// I can still have as many Fonts using that file as I want, though.
ResourcePtr<Font> font1 = rm->acquire("fonts/arial.ttf");
ResourcePtr<Font> font2 = rm->acquire("fonts/arial.ttf");
// Print something with the two fonts...
}
Linea di fondo...
(Mi piacerebbe metterne un dott. Qui, ma non riesco a pensarne uno.: \)
Bene, il gioco è fatto, ho reso il mio caso il migliore possibile. Si prega di pubblicare eventuali contro-argomenti che avete e anche tutti i vantaggi che ritieni che il design suggerito abbia sul mio design. Fondamentalmente, prova a mostrarmi che mi sbaglio. :)