Đánh giá thiết kế serialization C ++


9

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 Sprockets. Thiết kế này chỉ cần được quan tâm với Widgets và WidgetDatabase.


1
Bạn đã cân nhắc sử dụng thư viện Boost serialization cho việc này chưa? Nó kết hợp tất cả các mục tiêu thiết kế mà bạn có.
Bart van Ingen Schenau

1
@BartvanIngenSchenau Tôi không có, chủ yếu là do mối quan hệ yêu / ghét tôi có với Boost. Tôi nghĩ trong trường hợp này, một số định dạng tôi cần hỗ trợ có thể phức tạp hơn Boost serialization có thể xử lý mà không cần thêm độ phức tạp mà việc sử dụng nó không mua cho tôi nhiều.

Ah! Vì vậy, bạn không (de-) tuần tự hóa các trường hợp widget (đó sẽ là kỳ quặc), nhưng các widget này chỉ cần đọc và ghi dữ liệu có cấu trúc? Bạn có phải thực hiện các định dạng tệp hiện có hoặc bạn có thể tự do xác định định dạng đặc biệt không? Các widget khác nhau có sử dụng các định dạng phổ biến hoặc tương tự có thể được triển khai như một Mô hình chung không? Sau đó, bạn có thể thực hiện một giao diện người dùng, tên miền logic Logic mô hình, chia sẻ DAL thay vì kết hợp mọi thứ lại với nhau như một đối tượng thần WxWidget. Trên thực tế, tôi không thấy lý do tại sao các vật dụng có liên quan ở đây.
amon

@amon Mình lại chỉnh sửa câu hỏi. wxWidgets chỉ liên quan đến giao diện với người dùng: Các widget mà tôi nói đến không liên quan gì đến khung wxWidgets (tức là không có đối tượng thần). Tôi chỉ sử dụng thuật ngữ đó làm tên chung cho một loại DAO.

1
@LarsViklund bạn đưa ra một lập luận thuyết phục và bạn đã thay đổi quan điểm của tôi về vấn đề này. Tôi đã cập nhật mã ví dụ.

Câu trả lời:


7

Tôi có thể sai, nhưng thiết kế của bạn dường như bị áp đảo khủng khiếp. Để serialize chỉ là một Widget, bạn muốn xác định WidgetReader, WidgetWriter, WidgetDatabaseReader, WidgetDatabaseWritergiao diện mà mỗi người đều có triển khai cho XML, JSON, và mã hóa nhị phân, và một nhà máy để buộc tất cả những lớp học với nhau. Đây là vấn đề vì những lý do sau:

  • Nếu tôi muốn serialize một phi Widgetlớp, chúng ta hãy gọi nó Foo, tôi phải reimplement toàn Zoo này các lớp học, và tạo FooReader, FooWriter, FooDatabaseReader, FooDatabaseWritergiao diện, lần ba cho mỗi định dạng serialization, cộng thêm một nhà máy để làm cho nó thậm chí từ xa có thể sử dụng. Đừng nói với tôi là sẽ không có bất kỳ bản sao và dán nào đang diễn ra ở đó! Vụ nổ tổ hợp này dường như khá khó hiểu, ngay cả khi mỗi lớp đó về cơ bản chỉ chứa một phương thức duy nhất.

  • Widgetkhông thể gói gọn một cách hợp lý. Hoặc là bạn mở tất cả mọi thứ nên được nối tiếp với thế giới mở bằng các phương thức getter, hoặc bạn phải thực hiện friendtừng WidgetWriter(và có thể là tất cả WidgetReader). Trong cả hai trường hợp, bạn sẽ giới thiệu khớp nối đáng kể giữa các triển khai tuần tự hóa và Widget.

  • Sở thú độc giả / nhà văn mời không nhất quán. Bất cứ khi nào bạn thêm một thành viên Widget, bạn sẽ phải cập nhật tất cả các lớp tuần tự hóa liên quan để lưu trữ / truy xuất thành viên đó. Đây là điều không thể kiểm tra chính xác về tính chính xác, do đó bạn cũng sẽ phải viết một bài kiểm tra riêng cho từng người đọc và người viết. Theo thiết kế hiện tại của bạn, đó là 4 * 3 = 12 bài kiểm tra cho mỗi lớp bạn muốn tuần tự hóa.

    Theo hướng khác, việc thêm một định dạng tuần tự hóa mới như YAML cũng có vấn đề. Đối với mỗi lớp mà bạn muốn tuần tự hóa, bạn sẽ phải nhớ thêm trình đọc và ghi YAML, và thêm trường hợp đó vào enum và cho nhà máy. Một lần nữa, đây là điều không thể được kiểm tra tĩnh, trừ khi bạn thông minh và tạo ra giao diện templated cho các nhà máy độc lập Widgetvà đảm bảo triển khai cho từng loại tuần tự hóa cho mỗi hoạt động vào / ra được cung cấp.

  • Có lẽ Widgetbây giờ thỏa mãn SRP vì nó không chịu trách nhiệm cho việc tuần tự hóa. Nhưng các trình triển khai của người đọc và người viết rõ ràng là không, với SRP = mỗi đối tượng có một lý do để thay đổi cách diễn giải: các cài đặt phải thay đổi khi định dạng tuần tự thay đổi hoặc khi Widgetthay đổi.

Nếu bạn có thể đầu tư tối thiểu thời gian trước đó, vui lòng thử vẽ ra một khung tuần tự hóa chung chung hơn so với mớ hỗn độn này của các lớp. Ví dụ: bạn có thể định nghĩa một biểu diễn trao đổi chung, hãy gọi nó SerializationInfo, với một mô hình đối tượng giống như JavaScript: hầu hết các đối tượng có thể được xem như một std::map<std::string, SerializationInfo>, hoặc như một std::vector<SerializationInfo>, hoặc như một nguyên thủy, chẳng hạn như int.

Đối với mỗi định dạng tuần tự hóa, sau đó bạn sẽ có một lớp quản lý việc đọc và viết một biểu diễn tuần tự hóa từ luồng đó. Và đối với mỗi lớp bạn muốn tuần tự hóa, bạn sẽ có một số cơ chế chuyển đổi các thể hiện từ / sang biểu diễn tuần tự hóa.

Tôi đã trải nghiệm thiết kế như vậy với cxxtools ( trang chủ , GitHub , bản demo tuần tự hóa ) và nó hầu như cực kỳ trực quan, áp dụng rộng rãi và thỏa đáng cho các trường hợp sử dụng của tôi - vấn đề duy nhất là mô hình đối tượng khá yếu của biểu diễn tuần tự hóa yêu cầu bạn để biết trong quá trình giải tuần tự hóa chính xác loại đối tượng mà bạn đang mong đợi và việc khử lưu huỳnh đó ngụ ý các đối tượng có thể xây dựng mặc định có thể được khởi tạo sau này. Đây là một ví dụ sử dụng giả định:

class Point {
  int _x;
  int _y;
public:
  Point(x, y) : _x(x), _y(y) {}
  int x() const { return _x; }
  int y() const { return _y; }
};

void operator <<= (SerializationInfo& si, const Point& p) {
  si.addMember("x") <<= p.x();
  si.addMember("y") <<= p.y();
}

void operator >>= (const SerializationInfo& si, Point& p) {
  int x;
  si.getMember("x") >>= x;  // will throw if x entry not found
  int y;
  si.getMember("y") >>= y;
  p = Point(x, y);
}

int main() {
  // cxxtools::Json<T>(T&) wrapper sets up a SerializationInfo and manages Json I/O
  // wrappers for other formats also exist, e.g. cxxtools::Xml<T>(T&)

  Point a(42, -15);
  std::cout << cxxtools::Json(a);
  ...
  Point b(0, 0);
  std::cin >> cxxtools::Json(p);
}

Tôi không nói rằng bạn nên sử dụng cxxtools hoặc sao chép chính xác thiết kế đó, nhưng theo kinh nghiệm của tôi, thiết kế của nó khiến việc thêm tuần tự hóa trở nên tầm thường ngay cả đối với các lớp nhỏ, một lần, với điều kiện bạn không quá quan tâm đến định dạng tuần tự hóa ( ví dụ: đầu ra XML mặc định sẽ sử dụng tên thành viên làm tên thành phần và sẽ không bao giờ sử dụng các thuộc tính cho dữ liệu của bạn).

Vấn đề với chế độ nhị phân / văn bản cho các luồng dường như không thể giải quyết được, nhưng điều đó không quá tệ. Đối với một điều, nó chỉ quan trọng đối với các định dạng nhị phân, trên các nền tảng tôi không có xu hướng lập trình cho ;-) Nghiêm trọng hơn, đó là hạn chế về cơ sở hạ tầng tuần tự hóa của bạn, bạn sẽ phải ghi lại và hy vọng mọi người sử dụng chính xác. Mở các luồng trong độc giả hoặc nhà văn của bạn là quá không linh hoạt và C ++ không có cơ chế cấp độ loại tích hợp để phân biệt văn bản với dữ liệu nhị phân.


Làm thế nào để thay đổi lời khuyên của bạn cho rằng về cơ bản các DAO này đã là một lớp "thông tin tuần tự hóa"? Đây là những C ++ tương đương với POJO . Tôi cũng sẽ chỉnh sửa câu hỏi của mình với một ít thông tin hơn về cách các đối tượng này sẽ được sử dụng.
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.