Tipi di dati personalizzati e a responsabilità singola


10

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 Fontclasse non è più autosufficiente (è inutile senza la fabbrica) e FontFactorydeve 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 fabbrica ResourceManager<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::fstreamautosufficiente e fornisce funzioni come opene close. 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 Fontgestisce i suoi dati o il modo in cui FontFactoryi dati caricati? Non lo sai davvero. Avere le classi autosufficienti riduce questo problema: puoi testare da Fontsolo. Se poi devi testare la fabbrica e sai che Fontfunziona 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.) FontFa 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 Fontme 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. :)


2
Mi ricorda l' ActiveRecord vs DataMapper di Martin Fowler .
Utente

Fornisci praticità (il tuo progetto attuale) nell'interfaccia più esterna, rivolta verso l'utente. Utilizzare SRP internamente in modo da facilitare le future modifiche all'implementazione. Posso pensare agli abbellimenti del caricatore di font che salta corsivo e grassetto; che carica solo il BMP Unicode, ecc.
rwong


@rwong Conosco quella presentazione, avevo un segnalibro ( video ). :) Ma non capisco cosa stai dicendo nell'altro tuo commento ...
Paul

1
@rwong Non è già una fodera? È necessaria solo una riga, sia che si carichi un carattere direttamente o tramite ResourceManager. E cosa mi impedisce di reimplementare l'RM se gli utenti si lamentano?
Paul,

Risposte:


7

Non c'è nulla di sbagliato in quel codice secondo me, fa quello che ti serve in modo ragionevole e ragionevolmente facile da mantenere.

Tuttavia , il problema che hai con questo codice è che se vuoi che faccia qualsiasi altra cosa dovrai cambiare tutto .

Il punto dell'SRP è che se hai un singolo componente "CompA" che fa l'algoritmo A () e devi cambiare l'algoritmo A () non dovresti cambiare anche "CompB".

Le mie abilità in C ++ sono troppo arrugginite per suggerire uno scenario decente in cui dovrai cambiare la tua soluzione di gestione dei caratteri, ma il solito caso che faccio è l'idea di scivolare in un livello di cache. Idealmente non vuoi che la cosa che carica le cose sappia da dove proviene, né la cosa caricata dovrebbe preoccuparsi da dove viene, perché apportare modifiche è più semplice. Si tratta di manutenibilità.

Un esempio potrebbe essere il caricamento di un font da una terza fonte (ad esempio un'immagine di sprite di caratteri). Per raggiungere questo obiettivo dovrai cambiare il tuo caricatore (per chiamare il terzo metodo se i primi due falliscono) e la classe Font stessa per implementare questa terza chiamata. Idealmente, dovresti semplicemente creare un altro factory (SpriteFontFactory o qualsiasi altra cosa), implementare lo stesso metodo loadFont (...) e inserirlo in un elenco di fabbriche da qualche parte che possono essere utilizzate per caricare il font.


1
Ah, vedo: se aggiungo un altro modo per caricare un font, dovrò aggiungere un'altra funzione di acquisizione al gestore e un'altra funzione di caricamento alla risorsa. In effetti, questo è uno svantaggio. A seconda di quale potrebbe essere questa nuova fonte, tuttavia, probabilmente dovrai gestire i dati in modo diverso (i TTF sono una cosa, gli sprite dei caratteri sono un'altra), quindi non puoi davvero prevedere quanto sarà flessibile un determinato design. Vedo il tuo punto, però.
Paul,

Sì, come ho detto, le mie abilità in C ++ sono piuttosto arrugginite, quindi ho faticato a trovare una dimostrazione praticabile del problema, sono d'accordo sulla questione della flessibilità. Dipende davvero da cosa stai cercando con il tuo codice, come ho detto, penso che il tuo codice originale sia perfettamente ragionevolmente soluzione al problema.
Ed James,

Ottima domanda e ottima risposta, e la cosa migliore è che più sviluppatori possono imparare da essa. Questo è il motivo per cui adoro uscire qui :). Oh, quindi il mio commento non è completamente ridondante, SRP può essere un po 'complicato perché devi chiederti "what if", il che può sembrare contrario a: "l'ottimizzazione prematura è la radice di tutti i mali" o " Filosofie di YAGNI. Non c'è mai una risposta in bianco e nero!
Martijn Verburg,

0

Una cosa che mi infastidisce della tua classe è che hai loadFromMemorye loadFromFilemetodi. Idealmente, dovresti avere solo un loadFromMemorymetodo; un font non dovrebbe preoccuparsi di come sono diventati i dati in memoria. Un'altra cosa è che dovresti usare costruttore / distruttore invece di carico e freemetodi. Quindi, loadFromMemorysarebbe diventato Font(const void *buf, int len)e free()sarebbe diventato ~Font().


Le funzioni di caricamento sono accessibili da due costruttori e free viene chiamato nel distruttore - non l'ho mostrato qui. Trovo conveniente poter caricare il font direttamente da un file, anziché prima aprire il file, scrivere i dati in un buffer e poi passarlo a Font. A volte, però, devo anche caricare da un buffer, motivo per cui ho entrambi i metodi.
Paul,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.