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.
Jawaban:
Saya mungkin salah, tetapi desain Anda tampaknya terlalu direkayasa. Cerita bersambung hanya satu
Widget
, Anda ingin mendefinisikanWidgetReader
,WidgetWriter
,WidgetDatabaseReader
,WidgetDatabaseWriter
antarmuka yang masing-masing memiliki implementasi untuk XML, JSON, dan pengkodean biner, dan pabrik untuk mengikat semua kelas bersama-sama. Ini bermasalah karena alasan berikut:Jika saya ingin cerita bersambung non
Widget
kelas, sebut sajaFoo
, saya harus reimplement seluruh Zoo ini kelas, dan menciptakanFooReader
,FooWriter
,FooDatabaseReader
,FooDatabaseWriter
interface, kali tiga untuk setiap format serialisasi, ditambah pabrik untuk membuatnya bahkan jauh digunakan. Jangan bilang tidak ada copy & paste yang terjadi di sana! Ledakan kombinatorial ini tampaknya cukup dapat dipelihara, bahkan jika masing-masing kelas tersebut pada dasarnya hanya berisi metode tunggal.Widget
tidak dapat dienkapsulasi secara wajar. Entah Anda membuka semua yang harus diserialkan ke dunia terbuka dengan metode pengambil, atau Anda harus menerapkanfriend
setiapWidgetWriter
(dan mungkin juga semuaWidgetReader
) semua . Dalam kedua kasus tersebut, Anda akan memperkenalkan banyak kaitan antara implementasi serialisasi danWidget
.Kebun binatang pembaca / penulis mengundang ketidakkonsistenan. Setiap kali Anda menambahkan anggota
Widget
, Anda harus memperbarui semua kelas serialisasi terkait untuk menyimpan / mengambil anggota itu. Ini adalah sesuatu yang tidak dapat diperiksa kebenarannya secara statis, jadi Anda juga harus menulis tes terpisah untuk setiap pembaca dan penulis. Pada desain Anda saat ini, itu 4 * 3 = 12 tes per kelas yang ingin Anda serialkan.Di arah lain, menambahkan format serialisasi baru seperti YAML juga bermasalah. Untuk setiap kelas yang ingin Anda serialkan, Anda harus ingat untuk menambahkan pembaca dan penulis YAML, dan menambahkan kasing itu ke enum dan ke pabrik. Sekali lagi, ini adalah sesuatu yang tidak dapat diuji secara statis, kecuali jika Anda (terlalu) pintar dan membuat antarmuka templated untuk pabrik yang independen
Widget
dan memastikan implementasi untuk setiap jenis serialisasi untuk setiap operasi masuk / keluar disediakan.Mungkin
Widget
sekarang memenuhi SRP karena tidak bertanggung jawab atas serialisasi. Tetapi implementasi pembaca dan penulis jelas tidak, dengan interpretasi “SRP = setiap objek memiliki satu alasan untuk berubah”: implementasi harus berubah ketika format serialisasi berubah, atau ketikaWidget
perubahan.Jika Anda dapat menginvestasikan waktu minimum sebelumnya, silakan coba untuk membuat kerangka serialisasi yang lebih umum daripada kusut kelas ad-hoc ini. Misalnya, Anda dapat mendefinisikan representasi pertukaran yang umum, sebut saja
SerializationInfo
, dengan model objek yang mirip JavaScript: sebagian besar objek dapat dilihat sebagaistd::map<std::string, SerializationInfo>
, atau sebagaistd::vector<SerializationInfo>
, atau sebagai primitif sepertiint
.Untuk setiap format serialisasi, Anda kemudian akan memiliki satu kelas yang mengelola membaca dan menulis representasi serialisasi dari aliran itu. Dan untuk setiap kelas yang Anda ingin serialkan, Anda akan memiliki beberapa mekanisme yang mengubah instance dari / ke representasi serialisasi.
Saya telah mengalami desain dengan cxxtools ( beranda , GitHub , demo serialisasi ), dan sebagian besar sangat intuitif, berlaku luas, dan memuaskan untuk kasus penggunaan saya - satu-satunya masalah adalah model objek representasi serialisasi yang cukup lemah yang mengharuskan Anda untuk mengetahui selama deserialisasi secara tepat objek apa yang Anda harapkan, dan deserialisasi itu menyiratkan objek-objek yang dapat dibangun secara default yang dapat diinisialisasi nanti. Berikut contoh penggunaan yang dibuat-buat:
Saya tidak mengatakan bahwa Anda harus menggunakan cxxtools atau persis menyalin desain itu, tetapi dalam pengalaman saya desainnya membuatnya sepele untuk menambahkan serialisasi bahkan untuk kelas kecil, sekali saja, asalkan Anda tidak terlalu peduli tentang format serialisasi ( misalnya output XML default akan menggunakan nama anggota sebagai nama elemen, dan tidak akan pernah menggunakan atribut untuk data Anda).
Masalah dengan mode biner / teks untuk streaming sepertinya tidak bisa dipecahkan, tapi itu tidak terlalu buruk. Untuk satu hal, itu hanya penting untuk format biner, pada platform saya tidak cenderung memprogram untuk ;-) Lebih serius, ini adalah pembatasan infrastruktur serialisasi Anda, Anda hanya perlu mendokumentasikan dan berharap semua orang menggunakan dengan benar. Membuka aliran di dalam pembaca atau penulis Anda terlalu tidak fleksibel, dan C ++ tidak memiliki mekanisme tipe-tingkat bawaan untuk membedakan teks dari data biner.
sumber