Saya menulis aplikasi C ++. Sebagian besar aplikasi membaca dan menulis kutipan data yang diperlukan dan yang satu ini tidak terkecuali. Saya membuat desain tingkat tinggi untuk model data dan logika serialisasi. Pertanyaan ini meminta peninjauan desain saya dengan mengingat tujuan khusus ini:
Untuk memiliki cara yang mudah dan fleksibel untuk membaca dan menulis model data dalam format sewenang-wenang: raw binary, XML, JSON, et. Al. Format data harus dipisahkan dari data itu sendiri serta kode yang meminta serialisasi.
Untuk memastikan bahwa serialisasi bebas dari kesalahan sebisa mungkin dilakukan. I / O secara inheren berisiko karena berbagai alasan: apakah desain saya memperkenalkan lebih banyak cara agar gagal? Jika demikian, bagaimana saya bisa memperbaiki desain untuk mengurangi risiko tersebut?
Proyek ini menggunakan C ++. Apakah Anda suka atau benci itu, bahasa memiliki cara sendiri dalam melakukan sesuatu dan tujuan desain untuk bekerja dengan bahasa, tidak menentangnya .
Akhirnya, proyek ini dibangun di atas wxWidgets . Sementara saya mencari solusi yang berlaku untuk kasus yang lebih umum, implementasi spesifik ini harus bekerja dengan baik dengan toolkit itu.
Berikut ini adalah seperangkat kelas yang sangat sederhana yang ditulis dalam C ++ yang menggambarkan desain. Ini bukan kelas sebenarnya yang telah saya tulis sebagian sejauh ini, kode ini hanya menggambarkan desain yang saya gunakan.
Pertama, beberapa sampel DAO:
#include <iostream>
#include <map>
#include <memory>
#include <string>
#include <vector>
// One widget represents one record in the application.
class Widget {
public:
using id_type = int;
private:
id_type id;
};
// Container for widgets. Much more than a dumb container,
// it will also have indexes and other metadata. This represents
// one data file the user may open in the application.
class WidgetDatabase {
::std::map<Widget::id_type, ::std::shared_ptr<Widget>> widgets;
};
Selanjutnya, saya mendefinisikan kelas virtual murni (interface) untuk membaca dan menulis DAO. Idenya adalah untuk abstrak serialisasi data dari data itu sendiri ( SRP ).
class WidgetReader {
public:
virtual Widget read(::std::istream &in) const abstract;
};
class WidgetWriter {
public:
virtual void write(::std::ostream &out, const Widget &widget) const abstract;
};
class WidgetDatabaseReader {
public:
virtual WidgetDatabase read(::std::istream &in) const abstract;
};
class WidgetDatabaseWriter {
public:
virtual void write(::std::ostream &out, const WidgetDatabase &widgetDb) const abstract;
};
Akhirnya, berikut adalah kode yang mendapatkan pembaca / penulis yang tepat untuk tipe I / O yang diinginkan. Akan ada subkelas dari pembaca / penulis yang juga didefinisikan, tetapi ini tidak menambahkan apa pun pada ulasan desain:
enum class WidgetIoType {
BINARY,
JSON,
XML
// Other types TBD.
};
WidgetIoType forFilename(::std::string &name) { return ...; }
class WidgetIoFactory {
public:
static ::std::unique_ptr<WidgetReader> getWidgetReader(const WidgetIoType &type) {
return ::std::unique_ptr<WidgetReader>(/* TODO */);
}
static ::std::unique_ptr<WidgetWriter> getWidgetWriter(const WidgetIoType &type) {
return ::std::unique_ptr<WidgetWriter>(/* TODO */);
}
static ::std::unique_ptr<WidgetDatabaseReader> getWidgetDatabaseReader(const WidgetIoType &type) {
return ::std::unique_ptr<WidgetDatabaseReader>(/* TODO */);
}
static ::std::unique_ptr<WidgetDatabaseWriter> getWidgetDatabaseWriter(const WidgetIoType &type) {
return ::std::unique_ptr<WidgetDatabaseWriter>(/* TODO */);
}
};
Sesuai tujuan desain saya, saya punya satu masalah khusus. C ++ stream dapat dibuka dalam mode teks atau biner, tetapi tidak ada cara untuk memeriksa aliran yang sudah dibuka. Bisa saja melalui kesalahan programmer untuk menyediakan misalnya aliran biner ke pembaca / penulis XML atau JSON. Ini dapat menyebabkan kesalahan yang halus (atau tidak terlalu halus). Saya lebih suka kode gagal cepat, tetapi saya tidak yakin desain ini akan melakukannya.
Salah satu cara mengatasi hal ini adalah melepaskan tanggung jawab membuka aliran ke pembaca atau penulis, tetapi saya percaya itu melanggar SRP dan akan membuat kode lebih kompleks. Saat menulis DAO, penulis seharusnya tidak peduli ke mana aliran akan mengalir: itu bisa berupa file, standar keluar, respons HTTP, soket, apa pun. Begitu kekhawatiran itu dirangkum dalam logika serialisasi, ia menjadi jauh lebih kompleks: ia harus mengetahui jenis aliran tertentu dan konstruktor mana yang harus dipanggil.
Selain opsi itu, saya tidak yakin apa yang akan menjadi cara yang lebih baik untuk memodelkan objek-objek ini yang sederhana, fleksibel, dan membantu untuk mencegah kesalahan logika dalam kode yang menggunakannya.
Kasus penggunaan dengan mana solusi harus diintegrasikan adalah kotak dialog pemilihan file sederhana . Pengguna memilih "Open ..." atau "Save As ..." dari menu File, dan program membuka atau menyimpan WidgetDatabase. Juga akan ada opsi "Impor ..." dan "Ekspor ..." untuk masing-masing Widget.
Ketika pengguna memilih file untuk dibuka atau disimpan, wxWidgets akan mengembalikan nama file. Pawang yang merespons peristiwa itu harus berupa kode tujuan umum yang mengambil nama file, memperoleh serializer, dan memanggil fungsi untuk melakukan pengangkatan berat. Idealnya desain ini juga berfungsi jika sepotong kode lain melakukan non-file I / O, seperti mengirim WidgetDatabase ke perangkat seluler melalui soket.
Apakah widget menyimpan ke formatnya sendiri? Apakah itu beroperasi dengan format yang ada? Iya! Semua yang di atas. Kembali ke dialog file, pikirkan tentang Microsoft Word. Microsoft bebas untuk mengembangkan format DOCX namun mereka inginkan dalam batasan tertentu. Pada saat yang sama, Word juga membaca atau menulis format lawas dan pihak ketiga (mis. PDF). Program ini tidak berbeda: format "biner" yang saya bicarakan adalah format internal yang belum ditentukan yang dirancang untuk kecepatan. Pada saat yang sama, ia harus dapat membaca dan menulis format standar terbuka di domainnya (tidak relevan dengan pertanyaan) sehingga dapat dapat bekerja dengan perangkat lunak lain.
Akhirnya, hanya ada satu jenis Widget. Ini akan memiliki objek anak, tetapi mereka akan ditangani oleh logika serialisasi ini. Program tidak akan memuat Widget dan Sprocket. Desain ini hanya perlu memperhatikan Widget dan Widget Widget.