C ++ serileştirme tasarım incelemesi


9

Bir C ++ uygulaması yazıyorum. Çoğu uygulama gerekli veri alıntılarını okur ve yazar ve bu bir istisna değildir. Veri modeli ve serileştirme mantığı için üst düzey bir tasarım oluşturdum. Bu soru, tasarımımın bu belirli hedefler göz önünde bulundurularak incelenmesini istiyor:

  • Veri modellerini keyfi formatlarda okumak ve yazmak için kolay ve esnek bir yol elde etmek için: raw binary, XML, JSON, et. ark. Verilerin formatı, verilerin kendisinden ve serileştirme isteyen koddan ayrılmalıdır.

  • Serileştirmenin mümkün olduğunca hatasız olmasını sağlamak. G / Ç, çeşitli nedenlerden dolayı doğası gereği risklidir: Tasarımım başarısız olması için daha fazla yol sunuyor mu? Öyleyse, bu riskleri azaltmak için tasarımı nasıl yeniden düzenleyebilirim?

  • Bu proje C ++ kullanıyor. İster sevin ister nefret edin, dilin kendi şeyleri yapma şekli vardır ve tasarım dil ile değil , dil ile çalışmayı amaçlamaktadır .

  • Son olarak, proje wxWidgets üzerine inşa edilmiştir . Ben daha genel bir dava için geçerli bir çözüm ararken, bu özel uygulama bu araç kiti ile güzel çalışması gerekir.

Aşağıda C ++ ile yazılmış ve tasarımı gösteren çok basit bir sınıflar dizisi verilmiştir. Bunlar şimdiye kadar kısmen yazdığım gerçek sınıflar değil, bu kod kullandığım tasarımı gösteriyor.


İlk olarak, bazı örnek DAO'lar:

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

Sonra, DAO'ları okumak ve yazmak için saf sanal sınıflar (arayüzler) tanımlarım. Fikir, verilerin kendisinden ( SRP ) verilerin serileştirilmesini özetlemektir .

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

Son olarak, istenen G / Ç tipi için uygun okuyucu / yazıcıyı alan kod. Ayrıca tanımlanan okuyucuların / yazarların alt sınıfları olacaktır, ancak bunlar tasarım incelemesine hiçbir şey eklemez:

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

Tasarımımın belirtilen hedeflerine göre, özel bir endişem var. C ++ akışları metin veya ikili modda açılabilir, ancak önceden açılmış bir akışı kontrol etmenin bir yolu yoktur. Bir XML veya JSON okuyucu / yazıcıya ikili bir akım sağlamak programcı hatası yoluyla mümkün olabilir. Bu, ince (veya çok ince olmayan) hatalara neden olabilir. Kodun hızlı başarısız olmasını tercih ederim, ancak bu tasarımın bunu yapacağından emin değilim.

Bunun bir yolu, akışı okuyucuya veya yazara açma sorumluluğunu ortadan kaldırmak olabilir, ancak bunun SRP'yi ihlal ettiğini ve kodu daha karmaşık hale getireceğine inanıyorum. Bir DAO yazarken, yazar akışın nereye gittiğini umursamalıdır: bir dosya, standart çıkış, bir HTTP yanıtı, bir soket, herhangi bir şey olabilir. Bu endişe serileştirme mantığına dahil edildiğinde çok daha karmaşık hale gelir: belirli bir akış türünü ve hangi kurucuyu çağıracağını bilmelidir.

Bu seçeneğin yanı sıra, basit, esnek ve onu kullanan koddaki mantık hatalarını önlemeye yardımcı olan bu nesneleri modellemek için daha iyi bir yol ne olacağından emin değilim.


Çözümün entegre edilmesi gereken kullanım durumu basit bir dosya seçimi iletişim kutusudur . Kullanıcı Dosya menüsünden "Aç ..." veya "Farklı Kaydet ..." i seçer ve program WidgetDatabase'i açar veya kaydeder. Tek tek Widget'lar için "İçe Aktar ..." ve "Dışa Aktar ..." seçenekleri de olacaktır.

Kullanıcı açmak veya kaydetmek için bir dosya seçtiğinde, wxWidgets bir dosya adı döndürür. Bu olaya yanıt veren işleyici, dosya adını alan, bir serileştirici edinen ve ağır kaldırma işlemi için bir işlev çağıran genel amaçlı kod olmalıdır. İdeal olarak bu tasarım, başka bir kod parçası, bir soket üzerinden bir mobil cihaza WidgetDatabase göndermek gibi dosya olmayan G / Ç gerçekleştiriyorsa da işe yarayacaktır.


Bir widget kendi biçimine kaydediyor mu? Mevcut biçimlerle birlikte çalışır mı? Evet! Yukarıdakilerin hepsi. Dosya iletişim kutusuna geri dönerek Microsoft Word'ü düşünün. Microsoft, DOCX formatını geliştirmekte özgürdü, ancak belirli kısıtlamalar içinde istediler. Aynı zamanda, Word eski ve üçüncü taraf biçimlerini (örneğin PDF) okur veya yazar. Bu program farklı değil: bahsettiğim "ikili" biçim, hız için tasarlanmış henüz tanımlanmış bir iç biçim. Aynı zamanda, kendi alanındaki açık standart formatları (soru ile ilgisiz) okuyabilmeli ve yazabilmelidir, böylece diğer yazılımlarla çalışabilmelidir.

Son olarak, yalnızca bir tür Widget vardır. Alt nesneleri olacaktır, ancak bu serileştirme mantığı tarafından işlenecektir. Program hem Widget'larını yük asla ve dişlisi. Bu tasarımın yalnızca Widget'lar ve WidgetDatabases ile ilgili olması gerekir.


1
Bunun için Serileştirmeyi Artırma kütüphanesini kullanmayı düşündünüz mü ? Sahip olduğunuz tüm tasarım hedeflerini içerir.
Bart van Ingen Schenau

1
@BartvanIngenSchenau Daha çok Boost ile olan aşk / nefret ilişkim yüzünden olmadı. Bu durumda desteklemem gereken bazı formatların, Boost Serialization'un onu kullanmanın beni fazla satın almadığı kadar karmaşıklık eklemeden işleyebileceğinden daha karmaşık olabileceğini düşünüyorum.

Ah! Yani widget örneklerini (de-) serileştirmiyorsunuz (bu garip olurdu…), ancak bu widget'ların sadece yapılandırılmış verileri okuması ve yazması gerekiyor mu? Mevcut dosya formatlarını uygulamak zorunda mısınız yoksa geçici bir format tanımlamakta serbest misiniz? Farklı widget'lar ortak bir Model olarak uygulanabilecek ortak veya benzer formatları kullanıyor mu? Daha sonra her şeyi bir WxWidget tanrı nesnesi olarak birleştirmek yerine bir kullanıcı arabirimi - etki alanı mantığı - model - DAL ayrımı yapabilirsiniz. Aslında, widget'ların neden alakalı olduğunu anlamıyorum.
amon

@ amon Soruyu tekrar düzenledim. wxWidgets sadece kullanıcı ile arayüz kadar önemlidir: bahsettiğim Widget'lar wxWidgets çerçevesi (yani tanrı nesnesi) ile ilgisi yoktur. Bu terimi sadece bir DAO türü için genel bir ad olarak kullanıyorum.

1
@LarsViklund zorlayıcı bir tartışma yaratıyorsunuz ve konu hakkındaki fikrimi değiştirdiniz. Örnek kodu güncelledim.

Yanıtlar:


7

Yanılıyor olabilirim, ancak tasarımınız korkunç bir şekilde değiştirilmiş gibi görünüyor. Sadece bir tane dizi haline getirmek Widget, tanımlamak istediğiniz WidgetReader, WidgetWriter, WidgetDatabaseReader, WidgetDatabaseWriterher XML, JSON ve ikili kodlamaları ve birlikte tüm bu sınıfları bağlamak için bir fabrika için uygulamaları var arabirimleri. Bu, aşağıdaki nedenlerle sorunludur:

  • Ben olmayan seri hale getirmek istiyorsanız Widgetsınıfı, diyelim Fooben sınıfların bütün bu Zoo reimplement zorunda ve oluşturmak FooReader, FooWriter, FooDatabaseReader, FooDatabaseWriterher seri hale getirme biçimi, artı bir fabrika için arayüzleri, üç kat hatta uzaktan kullanışlı hale getirmek için. Bana orada herhangi bir kopyala yapıştır olmayacağını söyleme! Bu kombinatoryal patlama, bu sınıfların her biri esasen sadece tek bir yöntem içeriyor olsa bile, oldukça sürdürülemez gibi görünüyor.

  • Widgetmakul şekilde kapsüllenemez. Ya açık dünyaya serileştirilmesi gereken her şeyi alıcı yöntemleri ile açarsınız ya da friendher WidgetWriter(ve muhtemelen tüm WidgetReader) uygulamalara sahip olursunuz . Her iki durumda da, serileştirme uygulamaları ile Widget.

  • Okuyucu / yazar hayvanat bahçesi tutarsızlıkları davet ediyor. Bir üye eklediğinizde Widget, o üyeyi depolamak / almak için ilgili tüm serileştirme sınıflarını güncellemeniz gerekir. Bu, statik olarak doğruluk açısından kontrol edilemeyen bir şeydir, bu nedenle her okuyucu ve yazar için ayrı bir test yazmanız gerekecektir. Mevcut tasarımınızda, serileştirmek istediğiniz sınıf başına 4 * 3 = 12 test.

    Diğer yönde, YAML gibi yeni bir serileştirme formatı eklemek de sorunludur. Serileştirmek istediğiniz her sınıf için bir YAML okuyucu ve yazar eklemeyi ve bu durumu numaralamaya ve fabrikaya eklemeyi hatırlamanız gerekir. Yine, bu, bağımsız olan Widgetve her giriş / çıkış işlemi için her serileştirme türü için bir uygulama sağlandığından emin olan (çok) akıllı olmadıkça ve templated bir arayüz çizmediğiniz sürece, statik olarak test edilemeyen bir şeydir .

  • Belki Widgetşimdi SRP'yi tatmin ediyor çünkü serileştirmeden sorumlu değil. Ancak okuyucu ve yazar uygulamaları açıkça “SRP = her nesnenin değişmek için bir nedeni vardır” yorumuyla: serileştirme formatı değiştiğinde veya değiştiğinde uygulamalar Widgetdeğişmelidir.

Önceden minimum zaman ayırabiliyorsanız, lütfen bu geçici sınıf karmaşasından daha genel bir serileştirme çerçevesi çizmeye çalışın. Örneğin, ortak bir değişim temsili tanımlayabilir, diyelim ki SerializationInfoJavaScript benzeri bir nesne modeliyle bunu çağırabiliriz : çoğu nesne a std::map<std::string, SerializationInfo>, veya a std::vector<SerializationInfo>veya gibi bir ilkel olarak görülebilir int.

Her serileştirme biçimi için, o akıştan bir serileştirme gösterimini okumayı ve yazmayı yöneten bir sınıfınız olur. Serileştirmek istediğiniz her sınıf için, örnekleri serileştirme gösterimine / serileştirme gösterimine dönüştüren bir mekanizmanız olacaktır.

Cxxtools ( ana sayfa , GitHub , serileştirme demosu ) ile böyle bir tasarım yaşadım ve kullanım durumlarım için çoğunlukla son derece sezgisel, geniş çapta uygulanabilir ve tatmin edici - tek sorun, sizi gerektiren serileştirme temsilinin oldukça zayıf nesne modeli olması serileştirme sırasında tam olarak ne tür bir nesne beklediğinizi bilmek ve bu serileştirme daha sonra başlatılabilen varsayılan olarak oluşturulabilir nesneleri ifade eder. Aşağıda, kullanımı kanıtlanmış bir kullanım örneği verilmiştir:

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 kullanmanız veya bu tasarımı tam olarak kopyalamanız gerektiğini söylemiyorum, ancak benim deneyimime göre, tasarımı, serileştirme formatını ( örneğin, varsayılan XML çıktısı öğe adları olarak üye adlarını kullanır ve verileriniz için hiçbir zaman öznitelik kullanmaz).

Akışlar için ikili / metin moduyla ilgili sorun çözülebilir görünmüyor, ancak bu çok da kötü değil. Birincisi, sadece ikili formatlar için önemlidir; Programlama eğiliminde olmadığım platformlarda ;-) Daha da önemlisi, serileştirme altyapınızın bir kısıtlamasıdır ve herkesin doğru bir şekilde belgelendirmesi ve umması gerekir. Akışları okuyucularınızda veya yazarlarınızda açmak çok esnek değildir ve C ++, metni ikili verilerden ayırt etmek için yerleşik bir tür düzey mekanizmasına sahip değildir.


Bu DAO'ların temelde zaten bir "serileştirme bilgisi" sınıfı olduğu düşünüldüğünde tavsiyeniz nasıl değişir? Bunlar POJO'ların C ++ eşdeğeri . Bu nesneleri nasıl kullanacağım hakkında biraz daha fazla bilgi ile sorumu da düzenleyeceğim.
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.