Tôi đang viết một ứng dụng C ++. Hầu hết các ứng dụng đọc và ghi trích dẫn dữ liệu cần thiết và ứng dụng này cũng không ngoại lệ. Tôi đã tạo ra một thiết kế cấp cao cho mô hình dữ liệu và logic tuần tự hóa. Câu hỏi này đang yêu cầu xem xét lại thiết kế của tôi với các mục tiêu cụ thể này:
Để có một cách dễ dàng và linh hoạt để đọc và ghi các mô hình dữ liệu theo các định dạng tùy ý: nhị phân thô, XML, JSON, et. al. Định dạng của dữ liệu nên được tách rời khỏi chính dữ liệu cũng như mã đang yêu cầu tuần tự hóa.
Để đảm bảo rằng việc xê-ri hóa không có lỗi một cách hợp lý nhất có thể. I / O vốn đã có rủi ro vì nhiều lý do: thiết kế của tôi có giới thiệu nhiều cách để nó thất bại không? Nếu vậy, làm thế nào tôi có thể cấu trúc lại thiết kế để giảm thiểu những rủi ro đó?
Dự án này sử dụng C ++. Cho dù bạn yêu thích hay ghét nó, ngôn ngữ có cách làm việc riêng và thiết kế nhằm mục đích làm việc với ngôn ngữ chứ không phải chống lại nó .
Cuối cùng, dự án được xây dựng trên đầu trang của wxWidgets . Trong khi tôi đang tìm kiếm một giải pháp áp dụng cho một trường hợp tổng quát hơn, thì việc triển khai cụ thể này sẽ hoạt động tốt với bộ công cụ đó.
Dưới đây là một tập hợp các lớp rất đơn giản được viết bằng C ++ để minh họa cho thiết kế. Đây không phải là các lớp thực tế mà tôi đã viết một phần cho đến nay, mã này chỉ đơn giản minh họa thiết kế tôi đang sử dụng.
Đầu tiên, một số DAO mẫu:
#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;
};
Tiếp theo, tôi định nghĩa các lớp ảo (giao diện) thuần để đọc và viết DAO. Ý tưởng là để trừu tượng hóa việc tuần tự hóa dữ liệu từ chính dữ liệu ( 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;
};
Cuối cùng, đây là mã nhận được trình đọc / ghi thích hợp cho loại I / O mong muốn. Sẽ có các lớp con của người đọc / người viết cũng được xác định, nhưng những điều này không thêm gì vào đánh giá thiết kế:
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 */);
}
};
Theo các mục tiêu đã nêu trong thiết kế của tôi, tôi có một mối quan tâm cụ thể. Các luồng C ++ có thể được mở trong chế độ văn bản hoặc nhị phân, nhưng không có cách nào để kiểm tra một luồng đã được mở. Có thể thông qua lỗi lập trình viên để cung cấp ví dụ luồng nhị phân cho trình đọc / ghi XML hoặc JSON. Điều này có thể gây ra lỗi tinh vi (hoặc không quá tinh tế). Tôi muốn mã bị lỗi nhanh hơn, nhưng tôi không chắc thiết kế này sẽ làm được điều đó.
Một cách để giải quyết vấn đề này có thể là giảm trách nhiệm mở luồng cho người đọc hoặc người viết, nhưng tôi tin rằng việc vi phạm SRP và sẽ làm cho mã phức tạp hơn. Khi viết DAO, người viết không nên quan tâm đến việc luồng sẽ đi đến đâu: đó có thể là một tệp, tiêu chuẩn, phản hồi HTTP, ổ cắm, bất cứ thứ gì. Một khi mối quan tâm đó được gói gọn trong logic tuần tự hóa, nó trở nên phức tạp hơn nhiều: nó phải biết loại luồng cụ thể và hàm tạo nào sẽ gọi.
Ngoài tùy chọn đó, tôi không chắc điều gì sẽ là cách tốt hơn để mô hình hóa các đối tượng đơn giản, linh hoạt này và giúp ngăn ngừa các lỗi logic trong mã sử dụng nó.
Trường hợp sử dụng mà giải pháp phải được tích hợp là một hộp thoại chọn tệp đơn giản . Người dùng chọn "Mở ..." hoặc "Lưu dưới dạng ..." từ menu Tệp và chương trình sẽ mở hoặc lưu WidgetDatabase. Cũng sẽ có các tùy chọn "Nhập ..." và "Xuất ..." cho các Widget riêng lẻ.
Khi người dùng chọn một tệp để mở hoặc lưu, wxWidgets sẽ trả lại tên tệp. Trình xử lý đáp ứng với sự kiện đó phải là mã mục đích chung lấy tên tệp, lấy một bộ nối tiếp và gọi một hàm để thực hiện việc nâng vật nặng. Lý tưởng nhất là thiết kế này cũng sẽ hoạt động nếu một đoạn mã khác đang thực hiện I / O không phải tệp, chẳng hạn như gửi WidgetDatabase đến thiết bị di động qua ổ cắm.
Liệu một widget lưu vào định dạng riêng của nó? Liệu nó có tương tác với các định dạng hiện có? Đúng! Tất cả những điều trên. Quay trở lại hộp thoại tập tin, hãy nghĩ về Microsoft Word. Microsoft được tự do phát triển định dạng DOCX tuy nhiên họ muốn trong một số ràng buộc nhất định. Đồng thời, Word cũng đọc hoặc ghi các định dạng của bên thứ ba và bên thứ ba (ví dụ: PDF). Chương trình này không khác: định dạng "nhị phân" mà tôi nói đến là định dạng nội bộ chưa được xác định được thiết kế cho tốc độ. Đồng thời, nó phải có khả năng đọc và viết các định dạng chuẩn mở trong miền của nó (không liên quan đến câu hỏi) để có thể làm việc với các phần mềm khác.
Cuối cùng, chỉ có một loại Widget. Nó sẽ có các đối tượng con, nhưng chúng sẽ được xử lý theo logic tuần tự hóa này. Chương trình sẽ không bao giờ tải cả Widgets và Sprockets. Thiết kế này chỉ cần được quan tâm với Widgets và WidgetDatabase.