¿Lectura de objetos del archivo, violación de SRP?

8

Estoy escribiendo un programa de simulación de física en C ++. Soy un novato en OOP y C ++.

En mi programa, es necesario inicializar varios objetos según los datos de un archivo de entrada.

Por ejemplo, un archivo de entrada imaginario:

# Wind turbine input file:
number_of_blades = 2
hub_height = 120

# Airfoil data:
airfoil1 = { chord = 2, shape = naca0012}
airfoil2 = { chord = 3, shape = naca0016}

Para este ejemplo, digamos que tengo una clase Turbine y una clase Airfoil. Los objetos de superficie aerodinámica necesitan saber su cuerda y su forma, y ​​el objeto de la turbina necesita saber la altura y el número de álabes.

¿Debo hacer esto para que cada objeto pueda construirse a partir de un archivo de entrada?

p.ej:

class Turbine {
 public:
    Turbine(File input_file);  // reads input file to get the number of blades
 private:
    int num_blades_;
    double height_;
};

o debería hacerse con una función libre:

Turbine create_turbine_from_file(File input_file)
{
    Turbine t;
    t.set_num_blades(input_file.parse_num_blades());
    t.set_height(input_file.parse_height());
    return t;
};

class Turbine {
 public:
    Turbine();

    void set_height();
    void set_num_blades();

 private:
    int num_blades_;
    double height_;
};

¿Cuáles son las ventajas desventajas de cada método? ¿Hay una mejor manera?

energía eólica
fuente

Respuestas:

5

En primer lugar, felicidades por llevar la programación un paso más allá y preguntarse cómo hacerlo mejor (y por hacer una buena pregunta). Es una gran actitud y absolutamente necesaria para llevar sus programas un paso más allá. ¡Prestigio!

Lo que está tratando aquí es un problema relacionado con la arquitectura de su programa (o diseño, dependiendo de a quién le pregunte). No se trata tanto de lo que hace, sino de cómo lo hace (es decir, la estructura de su programa en lugar de su funcionalidad). Es muy importante ser claro en esto: usted podría totalmente hacer esas clases tienen Fileobjetos como entrada, y su programa todavía se podía trabajar. Si fue un paso más allá y agregó todo el código de manejo de excepciones y se ocupó de casos extremos relacionados con archivos y E / S (que deberíanhacer en algún lugar) en esas clases (... pero no allí), y se convirtieron en una mezcolanza de E / S y lógica de dominio (la lógica de dominio significa lógica relacionada con el problema real que está tratando de resolver), su programa podría " trabajo". El objetivo, si planea hacer que esto sea algo más que simple, debe ser que funcione correctamente , lo que significa que puede cambiar partes sin afectar a otras, corregir errores a medida que surgen y, con suerte, extenderlo sin demasiado dificultad cuando y si encuentra nuevas características y casos de uso que desea agregar.

Bien, ahora, la respuesta. Primero: sí, el uso de Archivos como parámetros de método en la Turbineclase viola el SRP. Tu Turbiney las Airfoilclases no deben saber nada sobre archivos. Y sí, hay mejores formas de hacerlo. Te hablaré de una forma en que lo haría primero y luego entraré en más detalles sobre por qué es mejor más tarde. Recuerde, este es solo un ejemplo (no un código realmente compilable, sino una especie de pseudocódigo) y una posible forma de hacerlo.

// TurbineData struct (to hold the data for turbines)

struct TurbineData
{
    int number_of_blades;
    double hub_height;
}

// TurbineRepository (abstract) class

class TurbineRepository
{
    // Defines an interface for Turbine repositories, which return Vectors of TurbineData structures.
    public: 
        virtual std::Vector<TurbineData> getAll();
}

// TurbineFileRepository class

class TurbineFileRepository: public TurbineRepository
{
    // Implements the TurbineRepository "interface".
    public:
        TurbineRepository(File inFile);
        std::Vector<TurbineData> getAll();
    private:
        File file;
}

TurbineFileRepository::TurbineFileRepository(File inFile)
{
    // Process the File and handle everything you need to read from it
    // At some point, do something like:
    // file = inFile
}

std::Vector<TurbineData> TurbineFileRepository::getAll()
{
    // Get the data from the file here and return it as a Vector
}

// TurbineFactory class

class TurbineFactory
{
    public:
        TurbineFactory(TurbineRepository *repo);
        std::Vector<Turbine> createTurbines();
    private:
        TurbineRepository *repository;
}

TurbineFactory::TurbineFactory(TurbineRepository *repo)
{
    // Create the factory here and eventually do something like:
    // repository = repo;
}

TurbineFactory::createTurbines()
{
    // Create a new Turbine for each of the structs yielded by the repository

    // Do something like...
    std::Vector<Turbine> results;

    for (auto const &data : repo->getAll())
    {
        results.push_back(Turbine(data.number_of_blades, data.hub_height));
    }

    return results;
}

// And finally, you would use it like:

int main()
{
    TurbineFileRepository repo = TurbineFileRepository(/* your file here */);
    TurbineFactory factory = TurbineFactory(&repo);
    std::Vector<Turbines> my_turbines = factory.createTurbines();
    // Do stuff with your newly created Turbines
}

OK, entonces, la idea principal aquí es aislar u ocultar las diferentes partes del programa entre sí. Especialmente quiero aislar la parte central del programa, donde está la lógica del dominio (la Turbineclase, que realmente modela y resuelve el problema), de otros detalles, como el almacenamiento. Primero, defino una TurbineDataestructura para contener los datos de Turbines que provienen del mundo exterior. Luego, declaro una TurbineRepositoryclase abstracta (es decir, una clase que no se puede instanciar, solo se usa como padre para la herencia) con un método virtual, que básicamente describe el comportamiento de "proporcionar TurbineDataestructuras del mundo exterior". Esta clase abstracta también se puede llamar una interfaz (una descripción del comportamiento). La TurbineFileRepositoryclase implementa ese método (y por lo tanto proporciona ese comportamiento) paraFiles. Por último, TurbineFactoryusa a TurbineRepositorypara obtener esas TurbineDataestructuras y crear Turbines:

TurbineFactory -> TurbineRepo -> Turbine // with TurbineData as a means of passing data.

¿Por qué lo hago de esta manera? ¿Por qué debería separar las E / S de archivo del funcionamiento interno de su programa? Porque los dos objetivos principales del diseño o la arquitectura de sus programas son reducir la complejidad y aislar el cambio. Reducir la complejidad significa hacer las cosas lo más simples posible (pero no más simples) para que pueda razonar sobre las partes individuales de manera adecuada y por separado: cuando está pensando en Turbines, no debería haber pensado en el formato en el que se encuentran los archivos que contienen se escriben los datos de la turbina, o si el Fileque está leyendo está allí o no. Deberías estar pensando en Turbines, punto.

Aislar el cambio significa que los cambios deberían afectar la menor cantidad posible de lugares en el código, de modo que las posibilidades de que ocurran errores (y las posibles áreas donde pueden ocurrir después de cambiar el código) se reducen al mínimo absoluto. Además, las cosas que cambian a menudo, o es probable que cambien en el futuro, deben estar separadas de las cosas que no lo son. En su caso, por ejemplo, si Turbinecambia el formato en el que se almacenan los datos en los archivos, no debería haber ninguna razón para que la Turbineclase cambie, solo clases como TurbineFileRepository. La única razón por la que Turbinedebería cambiar es si le agregaste un modelado más sofisticado o si la física subyacente cambió (que es considerablemente menos probable que cambie el formato del archivo), o algo similar.

El detalle de dónde y cómo se almacenan los datos debe manejarse por separado por clases, como, por lo tanto TurbineFileRepository, que, en consecuencia, no tienen idea de cómo Turbinefuncionan, o incluso por qué se necesitan los datos que proporcionan. Estas clases deberían implementar totalmente el manejo de excepciones de E / S y todo el tipo de cosas aburridas e increíblemente importantes que suceden cuando su programa habla con el mundo exterior, pero no deberían ir más allá de eso. La función de TurbineRepositoryes esconderse de TurbineFactorytodos esos detalles y solo proporcionarle un vector de datos. También es lo que se TurbineFileRepositoryimplementa para que no se necesite conocer ningún detalle sobre quien quiera usarTurbineDataestructuras Como un posible cambio de características, imagine que desea almacenar datos de turbinas y perfiles en una base de datos MySQL. Para que eso funcione, todo lo que necesita hacer es implementar TurbineDatabaseRepositoryay enchufarlo. Nada más. Genial, ¿eh?

¡Mucha suerte con tu programación!

Juan Carlos Coto
fuente
4

Normalmente debería implementarse como una función libre. Esa función normalmente debería nombrarse operator>>y tomar dos argumentos: en istreamy una referencia a a Turbine(y devolver lo istreamque se le pasó). En un caso típico, será uno friendde la clase, ya que debe ser capaz de manipular directamente los elementos internos que (en muchos casos) el mundo exterior no debe tocar (directamente).

class Turbine {
    // ...

    friend std::istream &operator>>(std::istream &is, Turbine &t) {
        // Simplifying a bit here, but you get the idea. 
        return is >> t.num_blades_ >> t.height_;
    }
};

Esto no solo satisface SRP, sino que hace que la clase funcione con el resto de la biblioteca estándar. Por ejemplo, si desea leer un archivo lleno de especificaciones de Turbines (no solo uno), puede hacer algo como esto:

std::ifstream in("Turbines.txt");

std::vector<Turbine> turbines { 
    std::istream_iterator<Turbine>(in),
    std::istream_iterator<Turbine>()
};
Jerry Coffin
fuente
2
Esto realmente parece que el Patrón de repositorio es la solución más apropiada. ¿Qué pasa si pasa del almacenamiento de archivos a usar una base de datos?
Greg Burghardt
@GregBurghardt Repository Pattern es una buena idea, pero es exclusiva con esta solución, simplemente podría construir sobre ella y usar este operador internamente.
kamilk