Un oggetto dovrebbe caricarsi da solo?

3

Anche se sto programmando in C ++ da un po 'di tempo, sono sempre di fronte a decisioni di progettazione (probabilmente a causa della flessibilità della lingua). Uno di questi problemi è decidere se un metodo dovrebbe essere parte della classe o meno.

Ad esempio, spesso ho una classe Simulation simile alla seguente,


class Simulation{
public:
    Simulation(void);
    bool init(const char* init_file){
        //...
        particles_ = read_particles(init_file);
        //etc
    }
    //or even,
    bool init(int argc, char* argv[]);
    void run(void);
private:
    std::vector particles_;
    //Other private members
};

Qui, non sono sicuro se questo metodo init, che analizza un file e inizializzi la simulazione, dovrebbe essere davvero un metodo di classe. Un modo diverso per implementare funzionalità simili sarebbe avere una funzione o una classe esterna che gestisce l'analisi degli argomenti della riga di comando o del file di inizializzazione e chiama i metodi appropriati della classe Simulation .


bool init_simulation(Simulation& sim, const char* init_file){
    //...
    Particles particles = read_particles(init_file);
    sim.set_particles(particles);
    //etc
}

Da un lato è più semplice implementare la funzionalità come metodo di classe ed evita di dover introdurre nuove classi / strutture, in particolare per il passaggio dei parametri. D'altra parte, la gestione esterna dell'inizializzazione significa che è possibile estenderla più facilmente, sebbene eventuali modifiche alla classe Simulation si propagheranno alla funzione.

Qual è il design preferito in situazioni simili a quelle sopra descritte, e come dovrei affrontare simili decisioni di design?

    
posta Grieverheart 24.11.2014 - 18:22
fonte

3 risposte

9

Il tuo problema non è avere una classe con un metodo di costruzione, il tuo problema è ciò che hai inserito. L'analisi di un file di testo dovrebbe non essere parte dell'oggetto - il suo compito è creare e gestire le simulazioni. Il metodo costruttore / init dovrebbe prendere una semplice lista o mappa di particelle come uno dei suoi parametri e usare quei dati durante l'inizializzazione. Leggere i dati da un file e convertirli in una semplice lista o mappa è il lavoro di qualche altra parte del tuo codice.

  • Se si modifica il formato del file o si modifica completamente la modalità di memorizzazione dei dati (ad esempio in un database relazionale), non è necessario riscrivere la classe di simulazione.
  • Non dovresti avere bisogno di un file di input per scrivere test per questa classe. I tuoi test dovrebbero essere in grado di creare semplici dati di input e trasmetterli direttamente all'oggetto.

Separazione delle preoccupazioni. Il file (la sua posizione, il formato, ecc.) Non riguarda il tuo oggetto di simulazione.

    
risposta data 24.11.2014 - 18:37
fonte
3

Per la discussione, presenterò un contro-argomento a @itsbruce. YMMV.

Che cosa è più probabile che cambi, il formato del file o l'oggetto stesso? (come nei campi aggiuntivi o modificati).

Secondo la mia esperienza, se il formato del file è standard e accessibile tramite interfacce astratte (ad esempio, XML standard, librerie JSON), il codice di base per questo non cambia. È molto più probabile che l'oggetto cambi rispetto al formato del file.

In tal caso, ha senso che l'oggetto sia in grado di leggere e scrivere se stesso. Quando l'oggetto Foo cambia, solo un file, Foo, deve essere modificato.

Se hai un FooPersister separato e l'oggetto Foo aggiunge o cambia un campo, è probabile che tu debba modificare FooPersister. ad esempio, in Foo, devi aggiungere un .newField, i getter e la logica dei setter appropriati. E, in FooPersister.read(Foo) e FooPersister.write(Foo) , devi ricordare di aggiungere quel campo a entrambi i metodi. Cosa succede se un campo cambia da int a doppio? Probabilmente alcuni codici di FooPersister cambiano lì.

Ovviamente, dipende dal tuo framework di persistenza, se legge automaticamente i campi, o è semplice come aggiungere un'annotazione da qualche parte. E quanto è maturo il tuo progetto, se il tuo oggetto Foo è già un gazillion di linee di codice, ecc. Come detto prima, YMMV. (Puoi generare il codice di persistenza effettivo in una classe di amici o, in Java-ese, una classe interna privata, quindi il tuo codice rimane in qualche modo separato, ma l'API pubblica si trova nella classe Foo.

Un enorme vantaggio di avere l'oggetto in grado di persistere è che potresti essere in grado di eliminare molti dei tuoi getter e setter . Se hai un FooPersister, devi praticamente esporre tutti i tuoi campi interni non transitori ad esso. Se ti ostini, a volte puoi mantenere molto più interno e nascosto (e finale )

Ora, non farei esattamente come fa OP, leggendo / scrivendo sui file:

bool init(const char* init_file)

Probabilmente avrei letto / scrivere da / su un flusso, un nodo o un elemento, o qualcosa del genere, più astratto di un file. Inoltre, anche se "sembra strano", considera che i metodi di lettura / scrittura siano metodi di istanza (read dovrebbe restituire un nuovo Foo, non sovrascrivere quello vecchio), non una funzione di classe statica. Questo apre molte porte interessanti nel tuo design.

    
risposta data 24.11.2014 - 22:16
fonte
2

[Nota: questa domanda ha già una risposta accettata e questa risposta aggiunge solo altri argomenti, ma è troppo lunga per un commento].

One such problem is deciding if a method should be part of the class or not.

Ho trovato un'ottima linea guida offerta da A. Stepanov su questa domanda:

In pratica dice che gli oggetti dovrebbero essere costruiti da membri di dati completamente costruiti. Se la costruzione di un oggetto richiede un'ulteriore elaborazione (ad esempio, la lettura dei dati da un file), dovrebbe essere eseguita da una funzione di fabbrica esterna. Ciò massimizza la flessibilità e la riusabilità. (Eventualmente, è possibile impostare la funzione di fabbrica come amico).

Un esempio:

class Simulation{
public:
    Simulation(std::vector<particle> particles);
        // instance keeps it's own copy,
        // so we might as well construct
        // it in the argument itself and
        // pass it into particles_ using
        // std::move

    void run();
private:
    std::vector<particle> particles_;
};

// factory function only 
Simulation load(std::istream& in) // use an istream instance here
                                  // (see below for reasoning)
{
    std::vector<particle> particles;
    particle p;
    while(in >> particle)
        particles.emplace_back(std::move(p));
    return Simulation{ std::move(particles) };
}

Codice di prova (funzionante con particelle codificate):

std::vector<particle> p = test::get_particles();
Simulation s{ std::move(p) };

Codice di prova (funzione caricamento particelle):

std::string serialized_particles = "jdfkdfhaljkdfhlakjdfhalsjkfh";
std::istringstream in{ serialized_particles };
auto s = load( in );

Codice di produzione:

std::string path_to_file{ "/tmp/aaaaa" };
std::ifstream in{ path_to_file };
auto s = load( in );

Al di fuori di questo esempio (e del tuo caso particolare), la linea guida che uso per determinare se una funzione dovrebbe essere un membro o meno, viene risolta rispondendo a queste domande:

  • la funzionalità si applica a tutte le istanze? (se sì, dovrebbe essere un membro)
  • la funzionalità richiede un'istanza attiva? (se no, non dovrebbe essere un membro)
  • la funzionalità si applica a un'istanza in qualsiasi stato? (se no, la funzione probabilmente non dovrebbe essere un membro)
risposta data 01.12.2014 - 14:29
fonte

Leggi altre domande sui tag