ตรวจสอบการออกแบบการทำให้เป็นอนุกรม C ++


9

ฉันกำลังเขียนแอปพลิเคชัน C ++ แอปพลิเคชันส่วนใหญ่อ่านและเขียนข้อมูลที่จำเป็นและนี่ก็ไม่มีข้อยกเว้น ฉันสร้างการออกแบบระดับสูงสำหรับตัวแบบข้อมูลและตรรกะการทำให้เป็นอนุกรม คำถามนี้ขอให้ตรวจสอบการออกแบบของฉันโดยคำนึงถึงเป้าหมายเฉพาะเหล่านี้:

  • เพื่อให้มีวิธีที่ง่ายและยืดหยุ่นในการอ่านและเขียนข้อมูลในรูปแบบที่กำหนดเอง: raw binary, 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 แนวคิดคือการสรุปลำดับของข้อมูลจาก data ( 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 จะส่งคืนชื่อไฟล์ ตัวจัดการที่ตอบสนองต่อเหตุการณ์นั้นจะต้องเป็นรหัสวัตถุประสงค์ทั่วไปที่ใช้ชื่อไฟล์, รับซีเรียลไลเซอร์และเรียกใช้ฟังก์ชั่นเพื่อทำการยกของหนัก เป็นการดีที่การออกแบบนี้จะทำงานหากโค้ดอื่นกำลังทำงานที่ไม่ใช่ไฟล์ I / O เช่นการส่ง WidgetDatabase ไปยังอุปกรณ์มือถือผ่านซ็อกเก็ต


วิดเจ็ตบันทึกเป็นรูปแบบของตัวเองหรือไม่? มันทำงานร่วมกับรูปแบบที่มีอยู่หรือไม่ ใช่ จากทั้งหมดที่กล่าวมา กลับไปที่กล่องโต้ตอบของไฟล์คิดถึง Microsoft Word Microsoft มีอิสระในการพัฒนารูปแบบ DOCX อย่างไรก็ตามพวกเขาต้องการภายในข้อ จำกัด บางอย่าง ในเวลาเดียวกัน Word ยังอ่านหรือเขียนรูปแบบดั้งเดิมและรูปแบบของบุคคลที่สาม (เช่น PDF) โปรแกรมนี้ไม่แตกต่าง: รูปแบบ "ไบนารี" ที่ฉันพูดถึงเป็นรูปแบบภายในที่ยังไม่ถูกกำหนดที่ออกแบบมาเพื่อความเร็ว ในขณะเดียวกันก็จะต้องสามารถอ่านและเขียนรูปแบบมาตรฐานแบบเปิดในโดเมนของตน (ไม่เกี่ยวข้องกับคำถาม) เพื่อให้สามารถทำงานกับซอฟต์แวร์อื่น ๆ ได้

ในที่สุดมี Widget เพียงประเภทเดียว มันจะมีวัตถุลูก แต่สิ่งเหล่านั้นจะได้รับการจัดการโดยตรรกะการทำให้เป็นอนุกรมนี้ โปรแกรมจะไม่โหลดทั้งวิดเจ็ตและเฟือง การออกแบบนี้เท่านั้นที่จะต้องมีความกังวลกับวิดเจ็ตและ WidgetDatabases


1
คุณได้พิจารณาใช้ห้องสมุดBoost Serialization แล้วหรือยัง? มันรวมเอาเป้าหมายการออกแบบทั้งหมดที่คุณมี
Bart van Ingen Schenau

1
@BartvanIngenSchenau ฉันไม่ได้ส่วนใหญ่เป็นเพราะความสัมพันธ์รัก / เกลียดฉันมีกับ Boost ฉันคิดว่าในกรณีนี้รูปแบบบางรูปแบบที่ฉันต้องการให้การสนับสนุนอาจมีความซับซ้อนมากกว่า Boost Serialization ที่สามารถจัดการได้โดยไม่ต้องเพิ่มความซับซ้อนมากพอที่การใช้มันจะไม่ซื้อฉันมากนัก

อา! ดังนั้นคุณไม่ได้ (ยกเว้น) การทำให้เป็นอินสแตนซ์ของวิดเจ็ตต่อเนื่อง (ที่จะแปลก ... ) แต่วิดเจ็ตเหล่านี้จำเป็นต้องอ่านและเขียนข้อมูลที่มีโครงสร้างหรือไม่ คุณต้องใช้รูปแบบไฟล์ที่มีอยู่หรือคุณมีอิสระที่จะกำหนดรูปแบบเฉพาะกิจหรือไม่? วิดเจ็ตต่าง ๆ ใช้รูปแบบทั่วไปหรือคล้ายกันที่สามารถนำไปใช้เป็นโมเดลทั่วไปได้หรือไม่? จากนั้นคุณสามารถทำส่วนต่อประสานผู้ใช้ - โดเมนลอจิก - โมเดล –DAL แยกได้มากกว่าการรวมทุกอย่างเข้าด้วยกันเป็นวัตถุพระเจ้า WxWidget ที่จริงแล้วฉันไม่เห็นว่าทำไมวิดเจ็ตจึงมีความเกี่ยวข้องที่นี่
อมร

@ ฉันแก้ไขคำถามอีกครั้ง wxWidgets เกี่ยวข้องเฉพาะกับส่วนต่อประสานกับผู้ใช้เท่านั้น: วิดเจ็ตที่ฉันพูดถึงไม่มีส่วนเกี่ยวข้องกับกรอบงาน wxWidgets (เช่นไม่มีวัตถุพระเจ้า) ฉันแค่ใช้คำนั้นเป็นชื่อสามัญสำหรับประเภท DAO

1
@LarsViklund คุณสร้างข้อโต้แย้งที่น่าสนใจและคุณเปลี่ยนความคิดเห็นของฉันในเรื่องนี้ ฉันอัปเดตโค้ดตัวอย่าง

คำตอบ:


7

ฉันอาจจะผิด แต่การออกแบบของคุณดูเหมือนจะถูกครอบงำอย่างน่ากลัว เป็นอันดับหนึ่งWidgetที่คุณต้องการที่จะกำหนดWidgetReader, WidgetWriter, WidgetDatabaseReader, WidgetDatabaseWriterอินเตอร์เฟซที่แต่ละคนมีการใช้งานสำหรับ XML, JSON และการเข้ารหัสไบนารีและโรงงานที่จะผูกเรียนทุกคนร่วมกัน นี่เป็นปัญหาเนื่องจากสาเหตุต่อไปนี้:

  • ถ้าผมต้องการที่จะเป็นอันดับที่ไม่ใช่Widgetชั้นขอเรียกว่าFooผมต้อง reimplement ทั้งหมดนี้สวนสัตว์ของการเรียนและสร้างFooReader, FooWriter, FooDatabaseReader, FooDatabaseWriterอินเตอร์เฟซ, ครั้งที่สามสำหรับรูปแบบอนุกรมแต่ละบวกโรงงานเพื่อให้ได้ใช้งานได้จากระยะไกล อย่าบอกฉันว่าจะไม่มีการคัดลอกและวางเกิดขึ้นที่นั่น! การระเบิดแบบ combinatorial ครั้งนี้ดูเหมือนจะไม่สามารถทำได้อย่างเป็นธรรมแม้ว่าแต่ละคลาสเหล่านั้นจะมีเพียงวิธีเดียวเท่านั้น

  • Widgetไม่สามารถสรุปได้อย่างสมเหตุสมผล ไม่ว่าคุณจะเปิดทุกอย่างที่ควรจะต่อเนื่องถึงโลกที่เปิดด้วยวิธีทะเยอทะยานหรือคุณต้องfriendแต่ละคนและทุกWidgetWriter(และอาจจะยังทั้งหมดWidgetReader) การใช้งาน Widgetในทั้งสองกรณีที่คุณจะแนะนำการมีเพศสัมพันธ์ระหว่างการใช้งานเป็นอันดับและ

  • สวนสัตว์ผู้อ่าน / ผู้เขียนเชิญความไม่สอดคล้องกัน เมื่อใดก็ตามที่คุณเพิ่มสมาชิกWidgetคุณจะต้องอัปเดตคลาสการทำให้เป็นอนุกรมที่เกี่ยวข้องทั้งหมดเพื่อจัดเก็บ / ดึงสมาชิกนั้น นี่คือสิ่งที่ไม่สามารถตรวจสอบความถูกต้องแบบคงที่ดังนั้นคุณจะต้องเขียนการทดสอบแยกต่างหากสำหรับผู้อ่านและนักเขียนแต่ละคน ที่การออกแบบปัจจุบันของคุณนั่นคือ 4 * 3 = 12 การทดสอบต่อคลาสที่คุณต้องการทำให้เป็นอันดับ

    ในอีกทางหนึ่งการเพิ่มรูปแบบการทำให้เป็นอันดับใหม่เช่น YAML ก็เป็นปัญหาเช่นกัน สำหรับแต่ละคลาสที่คุณต้องการทำให้เป็นอันดับคุณจะต้องจำไว้ว่าต้องเพิ่มตัวอ่านและตัวเขียน YAML และเพิ่มเคสดังกล่าวไปยัง enum และไปยังโรงงาน อีกครั้งนี่คือสิ่งที่ไม่สามารถทดสอบแบบคงที่ได้เว้นแต่คุณจะได้รับ (เช่น) ฉลาดและสร้างอินเทอร์เฟซ templated สำหรับโรงงานที่เป็นอิสระจากWidgetและทำให้แน่ใจว่ามีการใช้งานสำหรับแต่ละประเภทอนุกรม

  • บางทีWidgetตอนนี้เป็นไปตาม SRP เนื่องจากไม่รับผิดชอบต่อการทำให้เป็นอนุกรม แต่การใช้งานของเครื่องอ่านและเครื่องเขียนไม่ชัดเจนด้วยการตีความ“ SRP = แต่ละวัตถุมีเหตุผลเดียวที่จะเปลี่ยนแปลง” การใช้งานจะต้องเปลี่ยนเมื่อรูปแบบการเปลี่ยนอนุกรมเป็นอนุกรมหรือเมื่อมีWidgetการเปลี่ยนแปลง

หากคุณสามารถใช้เวลาน้อยที่สุดในการลงทุนล่วงหน้าโปรดลองสร้างกรอบงานการทำให้เป็นอันดับทั่วไปมากขึ้นกว่าคลาสที่ยุ่งเหยิงนี้ ตัวอย่างเช่นคุณสามารถกำหนดเป็นตัวแทนแลกเปลี่ยนทั่วไปขอเรียกว่าSerializationInfoมี JavaScript เหมือนวัตถุรูปแบบวัตถุส่วนใหญ่สามารถมองเห็นเป็นstd::map<std::string, SerializationInfo>หรือเป็นหรือเป็นดั้งเดิมเช่น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 ++ ไม่มีกลไกการพิมพ์ระดับในตัวเพื่อแยกข้อความจากข้อมูลไบนารี


คำแนะนำของคุณจะเปลี่ยนไปอย่างไรเนื่องจาก DAOs เหล่านี้โดยทั่วไปแล้วเป็นระดับ "ข้อมูลการทำให้เป็นอนุกรม" แล้ว เหล่านี้เป็น C ++ เทียบเท่าPOJOs ฉันจะแก้ไขคำถามด้วยข้อมูลเพิ่มเติมเล็กน้อยว่าจะใช้วัตถุเหล่านี้อย่างไร
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.