C ++ 직렬화 디자인 검토


9

C ++ 응용 프로그램을 작성 중입니다. 대부분의 응용 프로그램 은 필요한 데이터 인용을 읽고 쓸 있으며 예외는 아닙니다. 데이터 모델 및 직렬화 논리에 대한 높은 수준의 디자인을 만들었습니다. 이 질문은 다음과 같은 특정 목표를 염두에두고 디자인검토 하도록 요청합니다 .

  • 원시 이진, XML, JSON 등 임의의 형식으로 데이터 모델을 읽고 쓸 수있는 쉽고 유연한 방법 알. 데이터 형식은 데이터 자체 및 직렬화를 요청하는 코드와 분리되어야합니다.

  • 가능한 한 직렬화에 오류가 없는지 확인하십시오. I / O는 여러 가지 이유로 본질적으로 위험합니다. 디자인에 실패 할 수있는 더 많은 방법이 있습니까? 그렇다면 어떻게 이러한 위험을 완화하기 위해 설계를 리팩터링 할 수 있습니까?

  • 이 프로젝트는 C ++를 사용합니다. 당신이 그것을 좋아하든 싫어하든, 언어는 자체적으로 일을하는 방식을 가지고 있으며, 디자인은 언어와 반대되는 것이 아니라 언어를 다루는 것을 목표로 합니다 .

  • 마지막으로 프로젝트는 wxWidgets 위에 구축됩니다 . 더 일반적인 경우에 적용 가능한 솔루션을 찾고 있지만이 특정 구현은 해당 툴킷과 잘 작동해야합니다.

다음은 디자인을 보여주는 C ++로 작성된 매우 간단한 클래스 집합입니다. 이것들은 내가 지금까지 부분적으로 작성한 실제 클래스가 아니며,이 코드는 단순히 내가 사용중인 디자인을 보여줍니다.


먼저 일부 샘플 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;
};

다음으로, DAO를 읽고 쓰는 순수한 가상 클래스 (인터페이스)를 정의합니다. 아이디어는 데이터 자체의 데이터 직렬화 ( 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;
};

마지막으로, 원하는 I / O 유형에 적합한 리더 / 라이터를 얻는 코드가 있습니다. 리더 / 라이터의 하위 클래스도 정의되어 있지만 디자인 검토에는 아무 것도 추가하지 않습니다.

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 */);
  }
};

내 디자인의 명시된 목표에 따라 한 가지 특별한 관심사가 있습니다. C ++ 스트림은 텍스트 또는 이진 모드로 열 수 있지만 이미 열린 스트림을 확인할 수있는 방법은 없습니다. 프로그래머 오류를 통해 XML 또는 JSON 리더 / 라이터에 바이너리 스트림을 제공하는 것이 가능할 수 있습니다. 이로 인해 미묘한 오류가 발생할 수 있습니다. 코드가 빨리 실패하는 것을 선호하지만이 디자인이 그렇게 할 것이라고 확신하지 못합니다.

이 문제를 해결하는 한 가지 방법은 스트림을 독자 또는 작가에게 개방하는 책임을 오프로드하는 것이지만 SRP를 위반하고 코드를 더 복잡하게 만들 것이라고 생각합니다. DAO를 작성할 때 작성자는 스트림이 어디로 가고 있는지 신경 쓰지 않아야합니다. 파일, 표준 출력, HTTP 응답, 소켓 등이 될 수 있습니다. 이러한 문제가 직렬화 논리에 캡슐화되면 훨씬 더 복잡해집니다. 특정 유형의 스트림과 호출 할 생성자를 알아야합니다.

이 옵션 외에도 단순하고 유연하며 이러한 객체를 사용하는 코드에서 논리 오류를 방지하는 데 도움이되는 이러한 객체를 모델링하는 더 좋은 방법이 무엇인지 잘 모르겠습니다.


솔루션을 통합해야하는 사용 사례는 간단한 파일 선택 대화 상자 입니다. 사용자는 파일 메뉴에서 "열기 ..."또는 "다른 이름으로 저장 ..."을 선택하고 프로그램에서 WidgetDatabase를 열거 나 저장합니다. 개별 위젯에 대한 "가져 오기 ..."및 "내보내기 ..."옵션도 있습니다.

사용자가 열거 나 저장할 파일을 선택하면 wxWidgets가 파일 이름을 리턴합니다. 해당 이벤트에 응답하는 처리기는 파일 이름을 가져오고 serializer를 획득 한 후 무거운 리프팅을 수행하는 함수를 호출하는 범용 코드 여야합니다. 소켓을 통해 WidgetDatabase를 모바일 디바이스로 전송하는 것과 같이 다른 파일이 비 파일 I / O를 수행하는 경우에도이 디자인이 작동합니다.


위젯이 자체 형식으로 저장됩니까? 기존 형식과 상호 운용됩니까? 예! 무엇보다도. 파일 대화 상자로 돌아가서 Microsoft Word에 대해 생각해보십시오. Microsoft는 DOCX 형식을 자유롭게 개발할 수 있었지만 특정 제약 조건 내에서 원했습니다. 동시에 Word는 레거시 및 타사 형식 (예 : PDF)을 읽거나 씁니다. 이 프로그램은 다르지 않습니다. 제가 이야기하는 "이진"형식은 속도를 위해 아직 정의되지 않은 내부 형식입니다. 동시에 다른 소프트웨어와 작업 할 수 있도록 도메인에서 공개 표준 형식을 읽고 쓸 수 있어야합니다 (질문과 무관).

마지막으로 한 가지 유형의 위젯 만 있습니다. 자식 개체가 있지만이 직렬화 논리에 의해 처리됩니다. 프로그램은 위젯 스프로킷을 모두로드하지 않습니다 . 이 디자인 은 위젯 및 위젯 데이터베이스 에만 관심이 있으면됩니다.


1
이를 위해 Boost Serialization 라이브러리 사용을 고려 했습니까 ? 그것은 당신이 가진 모든 디자인 목표를 통합합니다.
Bart van Ingen Schenau

1
@BartvanIngenSchenau 나는 Boost와의 사랑 / 증오 관계 때문에 주로하지 않았습니다. 이 경우 지원 해야하는 일부 형식이 Boost Serialization이 처리 할 수있는 것보다 더 복잡 할 수 있다고 생각합니다.

아! 따라서 위젯 인스턴스를 직렬화 해제하는 것은 아니지만 (이상한 것입니다 ...)이 위젯은 구조화 된 데이터를 읽고 쓰면됩니까? 기존 파일 형식을 구현해야합니까, 아니면 임시 형식을 자유롭게 정의 할 수 있습니까? 다른 위젯이 공통 모델로 구현 될 수있는 공통 또는 유사한 형식을 사용합니까? 그런 다음 모든 것을 WxWidget god 오브젝트로 병합하지 않고 사용자 인터페이스 (도메인 로직 모델) DAL 분할을 수행 할 수 있습니다. 실제로 위젯이 왜 관련이 있는지 알 수 없습니다.
amon

@ amon 질문을 다시 편집했습니다. wxWidgets는 사용자와의 인터페이스에 한해서만 관련이 있습니다. 제가 이야기하는 위젯은 wxWidgets 프레임 워크와 아무 관련이 없습니다 (즉, god 객체는 없습니다). 그 용어를 DAO 유형의 일반 이름으로 사용합니다.

1
@LarsViklund 당신은 설득력있는 주장을하고 그 문제에 대한 나의 의견을 바 꾸었습니다. 예제 코드를 업데이트했습니다.

답변:


7

나는 틀릴 지 모르지만, 당신의 디자인은 엄청나게 과장되어 보인다. 하나를 직렬화하기 위해 Widget, 당신은 정의 할 WidgetReader, WidgetWriter, WidgetDatabaseReader, WidgetDatabaseWriter각 XML, JSON, 이진 인코딩과 함께 모든 클래스를 묶을 수있는 공장 구현을 인터페이스. 다음과 같은 이유로 문제가 있습니다.

  • 나는 비 직렬화하려면 Widget클래스를, 현실을 부르 자 Foo나는 수업이 모든 동물원을 다시 구현해야하고, 생성 FooReader, FooWriter, FooDatabaseReader, FooDatabaseWriter각 직렬화 형식, 플러스 공장 인터페이스, 세 배는 심지어 원격으로 사용할 수 있도록. 거기에 복사 및 붙여 넣기가 없을 것이라고 말하지 마십시오! 이러한 조합 폭발은 각각의 클래스가 본질적으로 단일 방법 만 포함하더라도 상당히 유지하기 어려운 것으로 보입니다.

  • Widget합리적으로 캡슐화 할 수 없습니다. getter 메소드를 사용하여 열린 세계에 직렬화해야하는 모든 것을 열거 나 friend각각 및 모든 WidgetWriter(그리고 아마도 모든 WidgetReader) 구현 에 대해 열려야 합니다. 두 경우 모두 직렬화 구현과와 Widget.

  • 리더 / 라이터 동물원은 불일치를 초대합니다. 에 멤버를 추가 할 때마다 Widget해당 멤버를 저장 / 검색하려면 관련된 모든 직렬화 클래스를 업데이트해야합니다. 이것은 정적으로 검사하여 정확성을 확인할 수 없으므로 각 독자와 작성자마다 별도의 테스트를 작성해야합니다. 현재 디자인에서 직렬화하려는 클래스 당 4 * 3 = 12 테스트입니다.

    반대로 YAML과 같은 새로운 직렬화 형식을 추가하는 것도 문제가됩니다. 직렬화하려는 각 클래스에 대해 YAML 판독기 및 작성기를 추가하고 해당 사례를 열거 형 및 팩토리에 추가해야합니다. 다시 말하지만, 당신은 (너무) 영리하고 공장 Widget마다 독립적 인 템플릿 인터페이스를 작성 하지 않고 각 입출력 작업에 대한 각 직렬화 유형에 대한 구현이 제공 되지 않는 한 정적으로 테스트 할 수없는 것 입니다.

  • 어쩌면 Widget지금 만족 SRP를가 직렬화에 대한 책임을지지 않습니다 이후. 그러나 "SRP = 각 객체에는 변경해야 할 이유가 하나 있습니다"라는 해석과 함께 독자와 작가의 구현은 명확하지 않습니다. 직렬화 형식이 변경되거나 변경 될 때 구현이 변경되어야합니다 Widget.

사전에 최소한의 시간을 투자 할 수 있다면이 임시 클래스 엉킴보다 더 일반적인 직렬화 프레임 워크를 작성하십시오. 예를 들어, 일반적인 교환 표현을 정의 할 수 있습니다,하자가 호출 SerializationInfo객체 자바 스크립트와 같은 모델 : 대부분의 개체는 볼 수있다 std::map<std::string, SerializationInfo>A와, 나 std::vector<SerializationInfo>, 또는 원시와 같은 int.

각 직렬화 형식마다 해당 스트림에서 직렬화 표현을 읽고 쓰는 것을 관리하는 하나의 클래스가 있습니다. 그리고 직렬화하려는 각 클래스에 대해 인스턴스를 직렬화 표현으로 변환하거나 직렬화 표현으로 변환하는 메커니즘이 있습니다.

cxxtools ( 홈페이지 , GitHub , 직렬화 데모 )를 사용 하여 이러한 디자인을 경험했으며 주로 사용이 매우 직관적이고 광범위하게 적용 가능하며 사용 사례에 만족합니다. 역 직렬화 중에는 어떤 종류의 객체를 예상하는지 정확하게 알고 있어야하며 역 직렬화는 나중에 초기화 할 수있는 기본 구성 가능한 객체를 의미합니다. 고안된 사용 예는 다음과 같습니다.

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

나는 당신이 cxxtools를 사용하거나 그 디자인을 정확하게 복사해야한다고 말하지는 않지만, 내 경험상 디자인이 작은 일회성 클래스에 대해서도 직렬화를 추가하는 것은 사소한 일입니다. 예를 들어, 기본 XML 출력은 멤버 이름을 요소 이름으로 사용하며 데이터 속성은 사용하지 않습니다.

스트림에 대한 이진 / 텍스트 모드의 문제는 해결할 수없는 것처럼 보이지만 그렇게 나쁘지는 않습니다. 우선, 그것은 이진 형식에만 중요합니다. ;-)를 위해 프로그래밍하지 않는 플랫폼에서는 더 중요합니다. 직렬화 인프라의 제한 사항이므로 문서화하고 모든 사람이 올바르게 사용하기를 바랍니다. 리더 또는 라이터 내에서 스트림을 여는 것은 너무 융통성이 없으며 C ++에는 이진 데이터와 텍스트를 구분하는 기본 제공 형식 수준 메커니즘이 없습니다.


이러한 DAO가 기본적으로 이미 "직렬화 정보"클래스라는 점을 감안하면 조언이 어떻게 바뀔까요? 이것들은 POJO 와 동등한 C ++입니다 . 이 객체들이 어떻게 사용 될지에 대한 정보를 조금 더 가지고 질문을 편집 할 것입니다.
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.