Objeto lendo do arquivo, violação do SRP?

8

Estou escrevendo um programa de simulação de física em C ++. Eu sou um novato em OOP e C ++.

No meu programa, vários objetos precisam ser inicializados com base nos dados de um arquivo de entrada.

Por exemplo, um arquivo de entrada imaginário:

# 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 exemplo, digamos que eu tenho uma classe Turbine e uma classe Airfoil. Os objetos do aerofólio precisam conhecer seu acorde e sua forma, e o objeto da turbina precisa conhecer a altura e o número de pás.

Devo fazer isso para que cada objeto possa se construir a partir de um arquivo de entrada?

por exemplo:

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

ou deve ser feito com uma função livre:

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

Quais são as vantagens e desvantagens de cada método? Existe uma maneira melhor?

energia eólica
fonte

Respostas:

5

Antes de tudo, parabéns por dar mais um passo à programação e pensar em como fazê-lo melhor (e por fazer uma boa pergunta). É uma ótima atitude e é absolutamente necessário levar seus programas um passo adiante. Parabéns!

Você está lidando com um problema relacionado à arquitetura do seu programa (ou design, dependendo de quem você pergunta). Não é tanto sobre o que ele faz, mas como ele faz (ou seja, a estrutura do seu programa em vez de sua funcionalidade). É muito importante ser claro sobre isso: você poderia totalmente fazer essas aulas são Fileobjetos como entrada, e seu programa ainda podia trabalhar. Se você deu um passo adiante e adicionou todo o código de tratamento de exceções e cuidou de casos extremos relacionados a arquivos e E / S (que devemser feito em algum lugar) nessas classes (... mas não lá), e elas se tornaram uma mistura de lógica de E / S e de domínio (lógica de domínio significa lógica relacionada ao problema real que você está tentando resolver), seu programa poderia " trabalhos". O objetivo, se você planeja fazer isso mais do que uma coisa simples e pontual, deve ser que funcione corretamente , o que significa que você pode alterar partes dela sem afetar os outros, corrigir os erros à medida que surgem e, esperançosamente, estendê-lo sem muito dificuldade em quando e se você encontrar novos recursos e casos de uso que deseja adicionar.

OK, agora, a resposta. Primeiro: sim, o uso de Arquivos como parâmetros de método na Turbineclasse viola o SRP. Suas classes Turbinee Airfoilnão devem saber nada sobre arquivos. E, sim, existem maneiras melhores de fazer isso. Vou falar com você sobre uma maneira de fazer isso primeiro e depois entrar em mais detalhes sobre por que é melhor depois. Lembre-se, este é apenas um exemplo (não é realmente um código compilável, mas uma espécie de pseudocódigo) e uma maneira possível de fazê-lo.

// 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, então a idéia principal aqui é isolar ou ocultar as diferentes partes do programa uma da outra. Quero especialmente isolar a parte principal do programa, onde está a lógica do domínio (a Turbineclasse, que realmente modela e resolve o problema), de outros detalhes, como armazenamento. Primeiro, defino uma TurbineDataestrutura para armazenar os dados de Turbines que vêm do mundo exterior. Então, declaro uma TurbineRepositoryclasse abstrata (significando uma classe que não pode ser instanciada, usada apenas como pai da herança) com um método virtual, que basicamente descreve o comportamento de "fornecer TurbineDataestruturas do mundo exterior". Essa classe abstrata também pode ser chamada de interface (uma descrição do comportamento). A TurbineFileRepositoryclasse implementa esse método (e, portanto, fornece esse comportamento) paraFiles. Por fim, o TurbineFactoryusa a TurbineRepositorypara obter essas TurbineDataestruturas e criar Turbines:

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

Por que estou fazendo assim? Por que você deve separar a E / S de arquivo do funcionamento interno do seu programa? Porque os dois principais objetivos do design ou arquitetura dos seus programas são reduzir a complexidade e isolar as alterações. Reduzir a complexidade significa tornar as coisas o mais simples possível (mas não mais simples), para que você possa raciocinar sobre as partes individuais de maneira adequada e separada: quando você pensa em Turbines, não deve pensar no formato em que os arquivos que contêm os dados da turbina são gravados ou se Filevocê está lendo ou não. Você deveria estar pensando em Turbines, ponto final.

Isolar a mudança significa que as mudanças devem afetar a menor quantidade possível de lugares no código, para que as chances de erros ocorram (e as possíveis áreas em que eles podem ocorrer após a alteração do código) sejam reduzidas ao mínimo absoluto. Além disso, as coisas que mudam frequentemente, ou que provavelmente mudam no futuro, devem ser separadas das coisas que não são. No seu caso, por exemplo, se o formato no qual os Turbinedados são armazenados nos arquivos mudarem, não deverá haver motivo para a Turbineclasse mudar, apenas classes como TurbineFileRepository. O único motivo Turbinepara mudar é se você adicionou modelagem mais sofisticada ou se a física subjacente foi alterada (o que é consideravelmente menos provável que a alteração do formato do arquivo) ou algo semelhante.

Os detalhes de onde e como os dados são armazenados devem ser tratados separadamente por classes, como, por exemplo TurbineFileRepository, que não têm idéia de como Turbinefunciona, ou mesmo por que os dados fornecidos são necessários. Essas classes devem implementar totalmente o tratamento de exceções de E / S, e todo o tipo de coisa chata e incrivelmente importante que acontece quando o programa fala com o mundo exterior, mas elas não devem ir além disso. A função de TurbineRepositoryé ocultar TurbineFactorytodos esses detalhes e fornecer apenas um vetor de dados. É também o que TurbineFileRepositoryimplementa, para que nenhum detalhe seja necessário para quem quiser usarTurbineDataestruturas. Como uma boa mudança de recurso possível, imagine que você queira armazenar dados de turbinas e aerofólios em um banco de dados MySQL. Para que isso funcione, tudo que você precisa fazer é implementar um TurbineDatabaseRepositorye conectá-lo. Nada mais. Legal né?

Boa sorte com sua programação!

Juan Carlos Coto
fonte
4

Normalmente deve ser implementado como uma função livre. Essa função normalmente deve ser nomeada operator>>e receber dois argumentos: in istreame uma referência a Turbine(e retornar o istreamque foi passado para ela). Em um caso típico, será friendda classe, pois precisa ser capaz de manipular diretamente as partes internas que (em muitos casos) o mundo externo não deve tocar (diretamente).

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

Isso não apenas satisfaz o SRP, mas faz a classe funcionar com o restante da biblioteca padrão. Por exemplo, se você quiser ler um arquivo cheio de especificações de turbinas (não apenas uma), poderá fazer algo assim:

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

std::vector<Turbine> turbines { 
    std::istream_iterator<Turbine>(in),
    std::istream_iterator<Turbine>()
};
Jerry Coffin
fonte
2
Parece realmente que o Padrão de Repositório é a solução mais apropriada. E se você for do armazenamento de arquivos para o uso de um banco de dados?
precisa
O @GregBurghardt Repository Pattern é uma boa idéia, mas é exclusivo desta solução, ele pode ser construído sobre ela e usar esse operador internamente.
kamilk