Objekt aus Datei lesen, Verletzung von SRP?

8

Ich schreibe ein Physik-Simulationsprogramm in C ++. Ich bin ein Anfänger in OOP und C ++.

In meinem Programm müssen mehrere Objekte basierend auf Daten aus einer Eingabedatei initialisiert werden.

Zum Beispiel eine imaginäre Eingabedatei:

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

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

Nehmen wir für dieses Beispiel an, ich habe eine Turbinenklasse und eine Tragflächenklasse. Tragflächenobjekte müssen ihre Sehne und Form kennen, und das Turbinenobjekt muss die Höhe und Anzahl der Schaufeln kennen.

Sollte ich dies tun, damit sich jedes Objekt aus einer Eingabedatei selbst erstellen kann?

z.B:

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

oder sollte es mit einer freien Funktion gemacht werden:

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

Was sind die Vor- und Nachteile jeder Methode? Gibt es einen besseren Weg?

Windenergie
quelle

Antworten:

5

Zunächst einmal herzlichen Glückwunsch, dass Sie die Programmierung weiterentwickelt und sich gefragt haben, wie Sie es besser machen können (und eine gute Frage gestellt haben). Es ist eine großartige Einstellung und absolut notwendig, um Ihre Programme einen Schritt weiter zu bringen. Ein großes Lob!

Hier geht es um ein Problem im Zusammenhang mit der Architektur Ihres Programms (oder dem Design, je nachdem, wen Sie fragen). Es geht nicht so sehr darum, was es tut, sondern wie es es tut (dh die Struktur Ihres Programms anstelle seiner Funktionalität). Es ist sehr wichtig , darüber im Klaren zu sein: Sie könnten vollständig machen diese Klassen nehmen FileObjekte als Eingabe und Ihr Programm könnte noch funktionieren. Wenn Sie noch einen Schritt weiter gegangen sind und den gesamten Code für die Ausnahmebehandlung hinzugefügt und sich um Randfälle im Zusammenhang mit Dateien und E / A gekümmert haben (was sollte)irgendwo gemacht werden) in diesen Klassen (... aber nicht dort), und sie wurden zu einem Durcheinander von E / A- und Domänenlogik (Domänenlogik bedeutet Logik in Bezug auf das eigentliche Problem, das Sie lösen möchten), könnte Ihr Programm " Arbeit". Das Ziel, wenn Sie vorhaben, dies mehr als eine einfache, einmalige Sache zu machen, sollte sein, dass es richtig funktioniert , was bedeutet, dass Sie Teile davon ändern können, ohne andere zu beeinflussen, Fehler beheben, wenn sie auftauchen, und sie hoffentlich ohne zu viel erweitern Schwierigkeiten, wann und ob Sie neue Funktionen und Anwendungsfälle finden, die Sie hinzufügen möchten.

OK, jetzt die Antwort. Erstens: Ja, die Verwendung von Dateien als Methodenparameter in der TurbineKlasse verstößt gegen die SRP. Ihre Turbineund AirfoilKlassen sollten nichts über Dateien wissen. Und ja, es gibt bessere Möglichkeiten, dies zu tun. Ich werde Ihnen eine Möglichkeit erläutern, wie ich es zuerst tun und dann genauer erläutern würde, warum es später besser ist. Denken Sie daran, dies ist nur ein Beispiel (nicht wirklich kompilierbarer Code, sondern eine Art Pseudocode) und eine Möglichkeit, dies zu tun.

// 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, die Hauptidee hier ist es, die verschiedenen Teile des Programms voneinander zu isolieren oder zu verbergen. Ich möchte insbesondere den Kernteil des Programms, in dem sich die Domänenlogik befindet (die TurbineKlasse, die das Problem tatsächlich modelliert und löst), von anderen Details wie dem Speicher isolieren . Zuerst definiere ich eine TurbineDataStruktur, um die Daten für Turbines zu speichern , die von der Außenwelt kommen. Dann deklariere ich eine TurbineRepositoryabstrakte Klasse (dh eine Klasse, die nicht instanziiert werden kann und nur als übergeordnetes Element für die Vererbung verwendet wird) mit einer virtuellen Methode, die im Wesentlichen das Verhalten des "Bereitstellens von TurbineDataStrukturen von außen" beschreibt. Diese abstrakte Klasse kann auch als Schnittstelle (Beschreibung des Verhaltens) bezeichnet werden. Die TurbineFileRepositoryKlasse implementiert diese Methode (und stellt somit dieses Verhalten bereit) fürFiles. Schließlich TurbineFactoryverwendet das a TurbineRepository, um diese TurbineDataStrukturen zu erhalten und Turbines zu erstellen :

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

Warum mache ich das so? Warum sollten Sie Datei-E / A vom Innenleben Ihres Programms trennen? Denn die beiden Hauptziele des Designs oder der Architektur Ihrer Programme sind die Reduzierung der Komplexität und die Isolierung von Änderungen. Um die Komplexität zu reduzieren, müssen Sie die Dinge so einfach wie möglich (aber nicht einfacher) gestalten, damit Sie die einzelnen Teile richtig und getrennt betrachten können: Wenn Sie an Turbines denken , sollten Sie nicht über das Format nachdenken, in dem die Dateien enthalten sind Die Turbinendaten werden geschrieben oder ob das, was FileSie lesen, vorhanden ist oder nicht. Sie sollten über Turbines, Punkt nachdenken .

Das Isolieren von Änderungen bedeutet, dass Änderungen die geringstmögliche Anzahl von Stellen im Code betreffen sollten, damit die Wahrscheinlichkeit, dass Fehler auftreten (und die möglichen Bereiche, in denen sie nach dem Ändern des Codes auftreten können), auf das absolute Minimum reduziert wird. Außerdem sollten Dinge, die sich häufig ändern oder sich in Zukunft wahrscheinlich ändern, von den Dingen getrennt sein, die dies nicht sind. Wenn sich in Ihrem Fall beispielsweise das Format Turbineändert, in dem Daten in den Dateien gespeichert sind, sollte es keinen Grund für die TurbineÄnderung der Klasse geben, sondern nur Klassen wie TurbineFileRepository. Der einzige Grund, der Turbinesich ändern sollte, besteht darin, dass Sie eine komplexere Modellierung hinzugefügt oder die zugrunde liegende Physik geändert haben (was erheblich weniger wahrscheinlich ist als die Änderung des Dateiformats) oder ähnliches.

Die Details darüber, wo und wie die Daten gespeichert werden, sollten von Klassen getrennt behandelt werden, z. B. TurbineFileRepositorydie keine Ahnung haben, wie sie Turbinefunktionieren oder warum die von ihnen bereitgestellten Daten benötigt werden. Diese Klassen sollten die Behandlung von E / A-Ausnahmen und all die langweiligen und unglaublich wichtigen Dinge, die passieren, wenn Ihr Programm mit der Außenwelt spricht, vollständig implementieren, aber sie sollten nicht darüber hinausgehen. Die Funktion von TurbineRepositorybesteht darin, sich vor TurbineFactoryall diesen Details zu verstecken und nur einen Datenvektor bereitzustellen. Es ist auch das, was TurbineFileRepositoryimplementiert wird, so dass keine Details darüber bekannt sein müssen, wer es verwenden möchteTurbineDataStrukturen. Stellen Sie sich als nette mögliche Funktionsänderung vor, Sie möchten Turbinen- und Tragflächendaten in einer MySQL-Datenbank speichern. Damit dies funktioniert, müssen Sie lediglich ein implementieren TurbineDatabaseRepositoryund einstecken. Mehr nicht. Cool was?

Viel Glück bei Ihrer Programmierung!

Juan Carlos Coto
quelle
4

Es sollte normalerweise als freie Funktion implementiert werden. Diese Funktion sollte normalerweise benannt werden operator>>und zwei Argumente annehmen: in istreamund einen Verweis auf a Turbine(und das zurückgegebene istream, das an sie übergeben wurde). In einem typischen Fall gehört es friendzur Klasse, da es in der Lage sein muss, Interna direkt zu manipulieren, die (in vielen Fällen) die Außenwelt (direkt) nicht berühren sollte.

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

Dies erfüllt nicht nur SRP, sondern lässt die Klasse auch mit dem Rest der Standardbibliothek zusammenarbeiten. Wenn Sie beispielsweise eine Datei mit Spezifikationen von Turbinen lesen möchten (nicht nur eine), können Sie Folgendes tun:

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

std::vector<Turbine> turbines { 
    std::istream_iterator<Turbine>(in),
    std::istream_iterator<Turbine>()
};
Jerry Sarg
quelle
2
Dies scheint wirklich das Repository-Muster zu sein, das die geeignetere Lösung ist. Was ist, wenn Sie vom Dateispeicher zur Verwendung einer Datenbank wechseln?
Greg Burghardt
@ GregBurghardt Repository Pattern ist eine gute Idee, aber es ist exklusiv für diese Lösung. Es kann einfach darauf aufbauen und diesen Operator intern verwenden.
Kamilk