2010-05-28 15 views
6

Ho trovato risposte su argomenti simili qui su SO ma non ho trovato una risposta soddisfacente. Poiché so che questo è un argomento piuttosto ampio, cercherò di essere più specifico.Come scrivere un programma modulare flessibile con buone possibilità di interazione tra i moduli?

Desidero scrivere un programma che elabora i file. L'elaborazione è non banale, quindi il modo migliore è dividere le diverse fasi in moduli standalone che poi sarebbero usati come necessario (dato che a volte mi interesserebbe solo l'output del modulo A, a volte avrei bisogno dell'output di altri cinque moduli, ecc.). Il fatto è che ho bisogno che i moduli collaborino, perché l'output di uno potrebbe essere l'input di un altro. E ho bisogno che sia veloce. Inoltre voglio evitare di eseguire determinate elaborazioni più di una volta (se il modulo A crea dei dati che devono essere elaborati dai moduli B e C, non voglio eseguire il modulo A due volte per creare l'input per i moduli B, C) .

Le informazioni che i moduli devono condividere sarebbero principalmente blocchi di dati binari e/o offset nei file elaborati. Il compito del programma principale sarebbe abbastanza semplice: basta analizzare gli argomenti, eseguire i moduli richiesti (e magari dare un po 'di output, o dovrebbe essere questo il compito dei moduli?).

Non ho bisogno di caricare i moduli in fase di esecuzione. Va perfettamente bene avere librerie con un file .h e ricompilare il programma ogni volta che c'è un nuovo modulo o qualche modulo viene aggiornato. L'idea dei moduli è qui principalmente a causa della leggibilità del codice, del mantenimento e della possibilità di avere più persone che lavorano su moduli diversi senza la necessità di avere un'interfaccia predefinita o altro (d'altra parte, alcune "linee guida" su come scrivere il i moduli sarebbero probabilmente richiesti, lo so). Possiamo supporre che l'elaborazione dei file sia un'operazione di sola lettura, il file originale non sia cambiato.

Qualcuno potrebbe indicarmi una buona direzione su come farlo in C++? Qualche consiglio è benvenuto (link, tutorial, libri pdf ...).

+3

Questa domanda è fondamentalmente " come scrivere codice modulare "? Dato che il codice _all_ dovrebbe essere modulare, qui non c'è nulla di specifico sul C++ o sul tuo particolare dominio del problema. e la risposta è "applicando abilità, talento ed esperienza". –

risposta

2

Questo sembra molto simile a un'architettura di plugin. Consiglio di iniziare con un diagramma di flusso di dati (informale) individuare:

  • come questi blocchi dati di processo
  • che dati devono essere trasferiti
  • quali risultati tornare da un blocco all'altro (dati/codici di errore/eccezioni)

Con queste informazioni è possibile iniziare a creare interfacce generiche, che consentono il collegamento ad altre interfacce in fase di esecuzione.Quindi aggiungerei una funzione factory a ciascun modulo per richiedere l'oggetto di elaborazione reale al di fuori di esso. I non consiglia di richiamare gli oggetti di elaborazione direttamente dall'interfaccia del modulo, ma di restituire un oggetto di fabbrica, in cui è possibile recuperare gli oggetti di elaborazione. Questi oggetti di elaborazione vengono quindi utilizzati per creare l'intera catena di elaborazione.

Un contorno semplificato sarebbe simile a questa:

struct Processor 
{ 
    void doSomething(Data); 
}; 

struct Module 
{ 
    string name(); 
    Processor* getProcessor(WhichDoIWant); 
    deleteprocessor(Processor*); 
}; 

Out of my mind questi modelli sono probabili comparire:

  • funzione di fabbrica: per ottenere gli oggetti da moduli
  • compositi & & decoratore: formando la catena di elaborazione
+0

Grazie per la risposta, l'approccio del modello di fabbrica sembra buono! – PeterK

+1

L'implementazione della fabbrica sembra però sbagliata. Usa RAII e smetti di chiedere al client di restituire il suo 'Processore' al' Modulo': sappiamo che dimenticherà! –

+0

@Matthieu M. anche se non c'era alcun metodo di cancellazione, il lato client deve eseguire la cancellazione, poiché gli oggetti non possono passare per valore, ma solo per puntatore. Quindi RAII non previene alcun danno a questo punto. La ragione per avere un metodo di cancellazione è avere più libertà per l'implementazione di fabbrica, e non essere costretti a usare nuovi per la costruzione dell'oggetto. Uso questo modello in un progetto in cui alcune fabbriche creano oggetti su richiesta, mentre altri restituiscono puntatori a singoletti o oggetti da un pool. – Rudi

2

Mi chiedo se il C++ è il livello giusto a cui pensare per questo scopo. Nella mia esperienza, si è sempre dimostrato utile avere programmi separati che sono collegati, nella filosofia UNIX.

Se i dati non sono eccessivamente grandi, ci sono molti vantaggi nella divisione. Per prima cosa acquisisci la capacità di testare indipendentemente ogni fase del tuo processo, esegui un programma e reindirizza l'output a un file: puoi facilmente controllare il risultato. Quindi, puoi usufruire di più sistemi core anche se ognuno dei tuoi programmi è a thread singolo e quindi molto più facile da creare e eseguire il debug. Inoltre, puoi sfruttare la sincronizzazione del sistema operativo utilizzando le pipe tra i tuoi programmi. Forse anche alcuni dei tuoi programmi potrebbero essere fatti usando programmi di utilità già esistenti?

Il programma finale creerà la colla per raccogliere tutte le utilità in un singolo programma, piping dati da un programma a un altro (non più file in questo momento), e la replica come richiesto per tutti i calcoli.

+0

Ho dimenticato di essere legato al sistema operativo Windows. E voglio davvero un solo programma, non un insieme di programmi che possano funzionare insieme (dato che è del tutto possibile che i moduli che creo non vengano usati solo nella mia app, ma anche in altri). Comunque, grazie per la tua risposta. – PeterK

+0

Esistono librerie per piping indipendenti dal sistema operativo (o più precisamente, astratte). –

+0

Essere legato a Windows non è un ostacolo alla presentazione per la creazione di diversi programmi e il loro piping. Anche Windows può farlo perfettamente! –

1

Questo sembra davvero piuttosto banale, quindi suppongo che manchino alcuni requisiti.

Utilizzare Memoization per evitare di calcolare il risultato più di una volta. Questo dovrebbe essere fatto nel quadro.

È possibile utilizzare un diagramma di flusso per determinare come passare le informazioni da un modulo a un altro ... ma il modo più semplice è che ogni modulo inviti direttamente le persone da cui dipendono. Con la memoizzazione non costa molto perché se è già stato calcolato, stai bene.

Dal momento che è necessario essere in grado di avviare qualsiasi modulo, è necessario fornire ID e registrarli da qualche parte con un modo per visualizzarli in fase di runtime. Ci sono due modi per farlo.

  • Esempio: ottieni l'esemplare unico di questo tipo di modulo ed eseguilo.
  • Fabbrica: crei un modulo del tipo richiesto, eseguilo e buttalo via.

Lo svantaggio del metodo Exemplar è che se si esegue il modulo due volte, sarete non essere a partire da uno stato pulito ma dallo stato che l'ultimo (forse fallita) esecuzione lasciato in. Per Memoizzazione esso potrebbe essere visto come un vantaggio, ma se fallisce il risultato non è calcolato (urgh), quindi consiglierei contro di esso.

Quindi come stai ...?

Iniziamo dalla fabbrica.

class Module; 
class Result; 

class Organizer 
{ 
public: 
    void AddModule(std::string id, const Module& module); 
    void RemoveModule(const std::string& id); 

    const Result* GetResult(const std::string& id) const; 

private: 
    typedef std::map< std::string, std::shared_ptr<const Module> > ModulesType; 
    typedef std::map< std::string, std::shared_ptr<const Result> > ResultsType; 

    ModulesType mModules; 
    mutable ResultsType mResults; // Memoization 
}; 

È un'interfaccia molto semplice. Tuttavia, dal momento che vogliamo una nuova istanza del modulo ogni volta che invochiamo il numero Organizer (per evitare problemi di riaccensione), abbiamo bisogno di lavorare sulla nostra interfaccia Module.

class Module 
{ 
public: 
    typedef std::auto_ptr<const Result> ResultPointer; 

    virtual ~Module() {}    // it's a base class 
    virtual Module* Clone() const = 0; // traditional cloning concept 

    virtual ResultPointer Execute(const Organizer& organizer) = 0; 
}; // class Module 

Ed ora, è facile:

// Organizer implementation 
const Result* Organizer::GetResult(const std::string& id) 
{ 
    ResultsType::const_iterator res = mResults.find(id); 

    // Memoized ? 
    if (res != mResults.end()) return *(it->second); 

    // Need to compute it 
    // Look module up 
    ModulesType::const_iterator mod = mModules.find(id); 
    if (mod != mModules.end()) return 0; 

    // Create a throw away clone 
    std::auto_ptr<Module> module(it->second->Clone()); 

    // Compute 
    std::shared_ptr<const Result> result(module->Execute(*this).release()); 
    if (!result.get()) return 0; 

    // Store result as part of the Memoization thingy 
    mResults[id] = result; 

    return result.get(); 
} 

E un semplice modulo/Risultato esempio:

struct FooResult: Result { FooResult(int r): mResult(r) {} int mResult; }; 

struct FooModule: Module 
{ 
    virtual FooModule* Clone() const { return new FooModule(*this); } 

    virtual ResultPointer Execute(const Organizer& organizer) 
    { 
    // check that the file has the correct format 
    if(!organizer.GetResult("CheckModule")) return ResultPointer(); 

    return ResultPointer(new FooResult(42)); 
    } 
}; 

E dal principale:

#include "project/organizer.h" 
#include "project/foo.h" 
#include "project/bar.h" 


int main(int argc, char* argv[]) 
{ 
    Organizer org; 

    org.AddModule("FooModule", FooModule()); 
    org.AddModule("BarModule", BarModule()); 

    for (int i = 1; i < argc; ++i) 
    { 
    const Result* result = org.GetResult(argv[i]); 
    if (result) result->print(); 
    else std::cout << "Error while playing: " << argv[i] << "\n"; 
    } 
    return 0; 
}