2010-05-28 8 views
6

Recibí respuestas sobre temas similares aquí en SO pero no pude encontrar una respuesta satisfactoria. Como sé que este es un tema bastante amplio, trataré de ser más específico.¿Cómo escribir un programa modular flexible con buenas posibilidades de interacción entre módulos?

Quiero escribir un programa que procesa archivos. El procesamiento no es trivial, así que la mejor manera es dividir diferentes fases en módulos independientes que luego se usarían según sea necesario (ya que a veces solo me interesaría la salida del módulo A, a veces necesitaría la salida de otros cinco módulos, etc.) La cuestión es que necesito que los módulos cooperen, porque la salida de uno puede ser la entrada de otro. Y necesito que sea RÁPIDO. Además, quiero evitar hacer cierto procesamiento más de una vez (si el módulo A crea algunos datos que luego deben ser procesados ​​por el módulo B y C, no quiero ejecutar el módulo A dos veces para crear la entrada para los módulos B, C) .

La información que los módulos necesitan compartir sería principalmente bloques de datos binarios y/o desplazamientos en los archivos procesados. La tarea del programa principal sería bastante simple: simplemente analizar los argumentos, ejecutar los módulos necesarios (y tal vez dar algún resultado, ¿o debería ser esta la tarea de los módulos?).

No necesito que los módulos se carguen en tiempo de ejecución. Está perfectamente bien tener libs con un archivo .h y recompilar el programa cada vez que hay un módulo nuevo o algún módulo se actualiza. La idea de los módulos está aquí principalmente debido a la legibilidad del código, mantener y poder tener más personas trabajando en diferentes módulos sin la necesidad de tener alguna interfaz predefinida o lo que sea (por otro lado, algunas "pautas" sobre cómo escribir el probablemente se requerirían módulos, lo sé). Podemos suponer que el procesamiento de archivos es una operación de solo lectura, el archivo original no se modifica.

¿Alguien podría indicarme una buena dirección sobre cómo hacer esto en C++? Cualquier consejo es bienvenido (enlaces, tutoriales, libros pdf ...).

+3

Esta pregunta es básicamente " ¿Cómo escribo el código modular? Como _todo el código debe ser modular, no hay nada específicamente sobre C++ aquí, o sobre su dominio de problema particular. y la respuesta es "aplicando habilidad, talento y experiencia". –

Respuesta

2

Esto se ve muy similar a una arquitectura de complemento. Recomiendo comenzar con un diagrama de flujo de datos (informal) para identificar:

  • cómo estos datos de bloques de proceso
  • qué datos hay que transferir
  • qué resultados vuelven de un bloque a otro (datos/códigos de error/excepciones)

Con esta información puede comenzar a construir interfaces genéricas, que permiten enlazar a otras interfaces en tiempo de ejecución.Luego, agregaría una función de fábrica a cada módulo para solicitar el objeto de procesamiento real. I no recomendamos sacar los objetos de procesamiento directamente de la interfaz del módulo, pero devolver un objeto de fábrica, donde se pueden recuperar los objetos de procesamiento. Estos objetos de procesamiento se usan para construir toda la cadena de procesamiento.

Un esquema muy simplificado sería el siguiente:

struct Processor 
{ 
    void doSomething(Data); 
}; 

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

Fuera de mi mente estos patrones es probable que aparezcan:

  • función de fábrica: para conseguir los objetos de módulos
  • compuestos & & decorador: formando la cadena de procesamiento
+0

Gracias por su respuesta, el enfoque de patrón de fábrica se ve bien! – PeterK

+1

Sin embargo, la implementación de la fábrica parece incorrecta. Use RAII y deje de pedirle al cliente que devuelva su 'Procesador' al' Módulo': ¡sabemos que lo olvidará! –

+0

@Matthieu M. incluso si no hubiera un método de eliminación, el lado del cliente debe realizar la eliminación, ya que los objetos no pueden pasar por valor, sino solo por puntero. Entonces RAII no previene ningún daño en este punto. La razón para tener un método de eliminación es tener más libertad para la implementación de la fábrica y no verse forzado a usar algo nuevo para la construcción del objeto. Utilizo este patrón en un proyecto en el que algunas fábricas crean objetos a pedido, mientras que otras devuelven punteros a objetos únicos u objetos de un conjunto. – Rudi

2

Me pregunto si el C++ es el nivel correcto para pensar con este propósito. En mi experiencia, siempre ha resultado útil tener programas separados que se canalizan juntos, en la filosofía de UNIX.

Si sus datos no son demasiado grandes, hay muchas ventajas en la división. Primero obtiene la capacidad de probar cada fase de su procesamiento de forma independiente, ejecuta un programa y redirige la salida a un archivo: puede verificar fácilmente el resultado. Luego, aprovecha los múltiples sistemas centrales incluso si cada uno de sus programas tiene un único hilo y, por lo tanto, es mucho más fácil de crear y depurar. Y también aprovecha la sincronización del sistema operativo utilizando las canalizaciones entre sus programas. Tal vez también algunos de sus programas podrían hacerse utilizando programas de utilidad ya existentes?

Su programa final creará el pegamento para reunir todas sus utilidades en un solo programa, canalizando datos de un programa a otro (no más archivos en este momento), y replicarlo según sea necesario para todos sus cálculos.

+0

Olvidé decir que estoy obligado al sistema operativo Windows. Y realmente quiero un solo programa, no un conjunto de programas que funcionen juntos (ya que es muy posible que los módulos que creo no se usen solo en mi aplicación, sino también en otros). De todos modos, gracias por tu respuesta. – PeterK

+0

Hay bibliotecas para tuberías independientes del sistema operativo (o más precisamente, para abstraerlas). –

+0

Estar vinculado a Windows no es un obstáculo para la creación de varios programas y unirlos entre sí. ¡Incluso Windows puede hacer esto perfectamente! –

1

Esto realmente parece bastante trivial, así que supongo que echaremos de menos algunos requisitos.

Utilice Memoization para evitar calcular el resultado más de una vez. Esto debe hacerse en el marco.

Puede usar un diagrama de flujo para determinar cómo hacer que la información pase de un módulo a otro ... pero la forma más simple es hacer que cada módulo llame directamente a aquellos de los que dependen. Con la memorización no cuesta mucho, ya que si ya se ha calculado, está bien.

Dado que necesita poder iniciar sobre cualquier módulo, necesita darles identificadores y registrarlos en alguna parte con una forma de buscarlos en el tiempo de ejecución. Hay dos maneras de hacer esto.

  • Ejemplar: Obtiene el único ejemplar de este tipo de módulo y lo ejecuta.
  • Fábrica: crea un módulo del tipo solicitado, lo ejecuta y lo descarta.

La desventaja del método Exemplar es que si se ejecuta el módulo dos veces, usted no estar empezando desde un estado limpio, pero desde el estado que la última (posiblemente fallidos) la ejecución dejó en. Para que memoization puede verse como una ventaja, pero si falla el resultado no se calcula (urgh), por lo que recomendaría no hacerlo.

Entonces, ¿cómo ...?

Comencemos con la fábrica.

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 
}; 

Realmente es una interfaz muy básica. Sin embargo, dado que queremos una nueva instancia del módulo cada vez que invoquemos el Organizer (para evitar el problema de reentrada), necesitaremos trabajar en nuestra interfaz 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 

Y ahora, es fácil:

// 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(); 
} 

y un módulo sencillo/Ejemplo Resultado:

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)); 
    } 
}; 

Y de la principal:

#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; 
} 
Cuestiones relacionadas