Objek membaca dari file, pelanggaran SRP?

8

Saya menulis program simulasi fisika dalam C ++. Saya seorang pemula di OOP dan C ++.

Dalam program saya, beberapa objek perlu diinisialisasi berdasarkan data dari file input.

Misalnya, file input imajiner:

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

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

Untuk contoh ini, katakanlah saya memiliki kelas Turbin dan kelas Airfoil. Objek airfoil perlu mengetahui akord dan bentuknya, dan objek turbin perlu mengetahui tinggi dan jumlah bilah.

Haruskah saya melakukan ini sehingga setiap objek dapat membangun sendiri dari file input?

misalnya:

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

atau harus dilakukan dengan fungsi bebas:

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

Apa kelebihan dari masing-masing metode? Apakah ada cara yang lebih baik?

energi angin
sumber

Jawaban:

5

Pertama-tama, selamat untuk mengambil pemrograman selangkah lebih maju dan bertanya-tanya tentang bagaimana melakukannya dengan lebih baik (dan untuk mengajukan pertanyaan yang bagus). Ini adalah sikap yang baik dan mutlak diperlukan untuk membawa program Anda selangkah lebih maju. Pujian!

Apa yang Anda hadapi di sini adalah masalah yang terkait dengan arsitektur program Anda (atau desain, tergantung pada siapa yang Anda tanya). Ini tidak begitu banyak tentang apa yang dilakukannya, tetapi bagaimana melakukannya (yaitu struktur program Anda alih-alih fungsinya). Sangat penting untuk memperjelas hal ini: Anda benar - benar dapat membuat kelas-kelas itu mengambil Fileobjek sebagai input, dan program Anda masih bisa bekerja. Jika Anda melangkah lebih jauh dan menambahkan semua kode penanganan pengecualian dan menangani kasus tepi yang terkait dengan file dan I / O (yang seharusnyadilakukan di suatu tempat) di kelas-kelas itu (... tetapi tidak di sana), dan mereka menjadi campur aduk I / O dan logika domain (logika domain berarti logika yang terkait dengan masalah aktual yang Anda coba selesaikan), program Anda bisa " kerja". Sasarannya, jika Anda berencana menjadikan ini lebih dari satu hal sederhana, harusnya berfungsi dengan baik , artinya Anda dapat mengubah bagian-bagian itu tanpa memengaruhi orang lain, perbaiki bug saat mereka muncul dan semoga memperpanjangnya tanpa terlalu banyak kesulitan ketika dan jika Anda menemukan fitur baru dan menggunakan kasing yang ingin Anda tambahkan.

OK, sekarang, jawabannya. Pertama: ya, menggunakan File sebagai parameter metode di Turbinekelas melanggar SRP. Kelas Anda Turbinedan Airfoilseharusnya tidak tahu apa-apa tentang file. Dan, ya, ada cara yang lebih baik untuk melakukannya. Saya akan berbicara dengan Anda melalui satu cara saya akan melakukannya terlebih dahulu dan kemudian pergi ke lebih detail tentang mengapa lebih baik nanti. Ingat, ini hanya sebuah contoh (bukan kode yang benar-benar dapat dikompilasi, tetapi semacam pseudocode) dan salah satu cara yang mungkin untuk melakukannya.

// 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, jadi, ide utama di sini adalah untuk mengisolasi, atau menyembunyikan, bagian-bagian berbeda dari program dari satu sama lain. Saya terutama ingin mengisolasi bagian inti dari program, di mana logika domain berada ( Turbinekelas, yang sebenarnya memodelkan dan menyelesaikan masalah), dari detail lain, seperti penyimpanan. Pertama, saya mendefinisikan TurbineDatastruktur untuk menyimpan data untuk Turbineyang berasal dari dunia luar. Kemudian, saya mendeklarasikan TurbineRepositorykelas abstrak (berarti kelas yang tidak dapat dipakai, hanya digunakan sebagai induk untuk warisan) dengan metode virtual, yang pada dasarnya menggambarkan perilaku "menyediakan TurbineDatastruktur dari dunia luar". Kelas abstrak ini juga bisa disebut antarmuka (deskripsi perilaku). The TurbineFileRepositorymengimplementasikan kelas yang metode (dan dengan demikian menyediakan perilaku yang) untukFiles. Terakhir, TurbineFactorypenggunaan TurbineRepositoryuntuk mendapatkan TurbineDatastruktur tersebut dan membuat Turbines:

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

Mengapa saya melakukannya dengan cara ini? Mengapa Anda memisahkan file I / O dari bagian dalam program Anda? Karena dua tujuan utama dari desain atau arsitektur program Anda adalah untuk mengurangi kompleksitas dan untuk mengisolasi perubahan. Mengurangi kerumitan berarti membuat segala sesederhana mungkin (tetapi tidak lebih sederhana) sehingga Anda dapat bernalar tentang bagian-bagian individual dengan benar dan terpisah: ketika Anda berpikir tentang Turbines, Anda seharusnya tidak memikirkan format di mana file yang berisi data turbin ditulis, atau apakah FileAnda sedang membaca ada atau tidak. Anda harus berpikir tentang Turbine, titik.

Mengisolasi perubahan berarti bahwa perubahan harus memengaruhi jumlah tempat yang paling mungkin dalam kode, sehingga kemungkinan bug terjadi (dan area yang memungkinkan terjadinya bug setelah Anda mengubah kode) dikurangi hingga minimum absolut. Juga, hal-hal yang sering berubah, atau cenderung berubah di masa depan, harus terpisah dari hal-hal yang tidak. Dalam kasus Anda, misalnya, jika format penyimpanan Turbinedata dalam file berubah, seharusnya tidak ada alasan bagi Turbinekelas untuk berubah, hanya kelas yang suka TurbineFileRepository. Satu-satunya alasan Turbineharus berubah adalah jika Anda menambahkan pemodelan yang lebih canggih ke dalamnya, atau fisika yang mendasarinya berubah (yang jauh lebih kecil kemungkinannya daripada perubahan format file), atau sesuatu yang serupa.

Rincian di mana dan bagaimana data disimpan harus ditangani secara terpisah oleh kelas, seperti TurbineFileRepository, yang akan, akibatnya, tidak tahu tentang cara Turbinekerjanya, atau bahkan mengapa data yang mereka berikan diperlukan. Kelas-kelas ini benar - benar harus menerapkan penanganan pengecualian I / O, dan semua jenis hal yang membosankan dan sangat penting yang terjadi ketika program Anda berbicara dengan dunia luar, tetapi mereka tidak boleh lebih dari itu. Fungsi TurbineRepositoryadalah untuk menyembunyikan dari TurbineFactorysemua detail dan hanya menyediakannya dengan data vektor. Itu juga apa yang TurbineFileRepositoryditerapkan sehingga tidak ada detail tentang hal itu perlu diketahui siapa pun yang ingin menggunakanTurbineDatastruktur. Sebagai perubahan fitur yang bagus, bayangkan Anda ingin menyimpan data turbin dan airfoil dalam database MySQL. Agar itu berfungsi, yang perlu Anda lakukan hanyalah menerapkan TurbineDatabaseRepositorydan pasang. Tidak lebih. Keren ya

Semoga berhasil dengan pemrograman Anda!

Juan Carlos Coto
sumber
4

Ini biasanya harus diimplementasikan sebagai fungsi bebas. Fungsi itu biasanya harus dinamai operator>>dan mengambil dua argumen: di istreamdan referensi ke Turbine(dan mengembalikan istreamyang diteruskan ke sana). Dalam kasus tertentu, ini akan menjadi friendkelas, karena harus dapat memanipulasi secara langsung yang (dalam banyak kasus) dunia luar tidak boleh disentuh (langsung).

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

Ini tidak hanya memuaskan SRP, tetapi membuat kelas bekerja dengan seluruh perpustakaan standar. Misalnya, jika Anda ingin membaca file yang penuh dengan spesifikasi Turbin (bukan hanya satu), Anda dapat melakukan sesuatu seperti ini:

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

std::vector<Turbine> turbines { 
    std::istream_iterator<Turbine>(in),
    std::istream_iterator<Turbine>()
};
Jerry Coffin
sumber
2
Ini benar-benar terasa seperti Pola Repositori adalah solusi yang lebih tepat. Bagaimana jika Anda beralih dari penyimpanan file ke menggunakan database?
Greg Burghardt
@GregBurghardt Repository Pattern adalah ide yang bagus tetapi eksklusif dengan solusi ini, bisa saja dibuat di atasnya dan menggunakan operator ini secara internal.
kamilk