Abstract Active Record

1

Cześć, czytając sporo o przechowywaniu danych, wpadłem na pomysł (być może nie tylko ja) lekkiej modyfikacji Active Recordu, która być może lepiej się nada do tego zadania niż inne stosowane rozwiązania.

Głównym założeniem Active Record jest to, że to klasa wie, jak się zapisać i jak się odczytać. I uważam, że to dobre założenie. Weźmy na przykład sytuację, w której możemy zapisać dane do różnych formatów - kilka formatów plików, baza danych i coś tam jeszcze.

(Specjalnie w przykładach stosuję C++, ponieważ dużo ciężej stworzyć tutaj mechanizmy refleksyjne, jak np. w C#. A więc taki język dużo lepiej nadaje się do stosowania Abstract Active Record. Stosuję jak najprostsze kody, żeby nie zaciemniać obrazu)

Przykład modelu:

class Person
{
public:
    const wchar_t * getName() const;
    int getAge() const;
}

Teraz napiszmy klasę do zapisu / odczytu osób, obsługującą plik CSV:

class CSVExportImport
{
  public:
    void exportPerson(const Person & data)
    {
        //pseudokod
       os << data.getName() << L";" << os.data.getAge();
       os.Save();
    }

    Person importPerson()
    {
       //pseudokod
       std::wstring name;
       int age;
       is >> name;
       is >> age;

       return Person(name, age);
    }
}

To prosty zapis i odczyt, który można zrealizować na wiele sposobów. Nie jest to teraz istotne. Podobnie mogą wyglądać klasy do zapisu osoby w innych formatach (binarny, baza danych, inne pliki tekstowe) itd.

I tu pojawia się problem. Ponieważ dodajemy to klasy Person nowe pole:

class Person
{
public:
    const wchar_t * getName() const;
    int getAge() const;
    const wchar_t * country() const;
}

No i teraz trzeba zrobić zmiany w wielu miejscach w kodzie - we wszystkich klasach zapisujących i odczytujących osoby. Oczywiście w każdej z nich trzeba dodatkowo obsłużyć import starszych wersji, gdzie nie było zapisanego państwa. Dodatkowym problemem będzie, jeśli mamy kilka zespołów i osobne zespoły są odpowiedzialne za rozwój serializatorów.

Tutaj jednak nie możemy zastosować typowego active record, bo robi same problemy i wiąże bezpośrednio model z warstwą przechowywania danych.

Ale moglibyśmy posłużyć się omawianym Abstract Active Record. Działa to tak.
Na początek tworzymy wspólny interfejs:

class ISerializer
{
public:
    virtual void writeString(const wchar_t * pName, const wchar_t * pData) = 0;
    virtual void writeInt(const wchar_t * pName, const int data) = 0;

    virtual wchar_t * readString(const wchar_t * pName) = 0;
    virtual int readInt(const wchar_t * pName) = 0;
}

Następnie tworzymy poszczególne klasy do serializacji, np:

class CsvSerializer: public ISerializer
{
public:
    void writeString(const wchar_t * pName, const wchar_t * pData) override
    {
        m_data[pName] = pData;
    }

///itd.
}

m_data to jakaś mapa, która TYLKO w klasie CsvSerializer pomaga przechowywać dane w odpowiedni sposób.

Analogicznie możemy zbudować resztę klas: BinarySerializer, SqlSerializer itd.

Teraz jednak cała magia zapisu dzieje się w klasie Person. Person dokładnie wie, z jakich elementów się składa, dokładnie wie jak się zapisać i jak się odczytać:

class ISerializable
{
public:
  virtual void save(ISerializer & serializer) const = 0;
  virtual void load(ISerializer & serializer) = 0;
}

class Person: public ISerializable
{
public:
    const wchar_t * getName() const;
    int getAge() const;
    const wchar_t * country() const;

    void save(ISerializer & serializer) const override
    {
        //jeśli Person dziedziczy po innych klasach tego typu, to musi to wywołać save z klasy bazowej
        //a następnie:

        serializer.writeInt(L"version", 2);
        serializer.writeString(L"name", getName().c_str());
        serializer.writeInt(L"age", getAge());
        serializer.writeString(L"country", getCountry().c_str());
    }

    void load(ISerializer & serializer) override
    {
        int version = serializer.readInt(L"version");
        m_name = serializer.readString(L"name");
        m_age = serializer.readInt(L"age");
        if(version > 1)
          m_country = serializer.readString(L"country");
    }
}

Co zyskujemy dzięki temu?

  1. Mamy klasy serializatorów odpowiedzialne tylko za jedną rzecz - jak zapisać / odczytać dane (niezależne od modelu)
  2. Mamy model, który dokładnie wie, jak się zapisać i odczytać, i CO NAJWAŻNIEJSZE - nie jest zależny od żadnej implementacji serializatora
  3. Po zmianie modelu nie trzeba zmieniać reszty serializatorów.

Można by się czepiać, ze klasy modelu muszą być zależne od innej warstwy - tej, gdzie mamy zdefiniowane interfejsy do serializacji. Ale moim zdaniem nie jest to problem, bo to jest normalne, zwłaszcza przy ORMach (klasa bazowa w stylu BaseEntity)

Oczywiście przy niektórych językach stosowanie czegoś takiego jest bez sensu. Np. w C#, gdzie bardzo mocno rozwinięta jest refleksja można po prostu przekazać cały obiekt do serializatora, który już doskonale wie co i jak ma zapisać / odczytać. Natomiast w językach niższych poziomów - tak jak C++ ten mechanizm się świetnie sprawdzi.

A co Wy o tym sądzicie? Tylko proszę bez hejtu :)

0
  1. Dlaczego chcesz używać różnych serializatorów i jaki problem w ten sposób rozwiązujesz? Bo rozwiązanie jest, ale problemu nie ma? ;-)
  2. Gdzie będzie rezydować logika decydująca o tym jakiego serializatora chcesz użyć, bo jak rozumiem, gdzieś w kodzie wystąpi:
  model.save(specialSerializer); 

Czy może zakładasz, że produkcyjnie użyjesz jednego serializatora, a do testów innego? (wówczas pewnie w kodzie pojawi się fabryczka, która dostarcza testową albo produkcyjną instancję serializatora). Co prawda można uprościć testy jednostkowe, ale czy na integracyjnych nie wybije szambo?

0

Rozwiązuje to problem eksportu do różnych formatów. Poza tym oddziela warstwę modelu od warstwy dostępu do danych w standardowym Active Record.

Załóżmy, że piszesz system, gdzie masz mieć zapis do bazy danych. Ale w pewnym momencie ktoś mówi, że on chce eksport do CSV. A później ktoś jeszcze chce eksport do SAP. Itd. Mój Abstract Active Record to właśnie rozwiązuje. Każdy nowy format zapisu wiąże się tylko z napisaniem nowego serializatora. Nie ruszamy niczego w modelu ani nigdzie indziej. Dokładamy po prostu nowy komponent. No to oczywiście powinno być obsłużone w jakiejś fabryce. Coś w stylu:

switch(extension)
{
    case ".csv": return new CsvSerializer();
    case ".sap": return new SapSerializer();
}

Może to być fabryka abstrakcyjna, wtedy mamy uproszczoną sytuację z testami jednostkowymi.

2

Głównym założeniem Active Record jest to, że to klasa wie, jak się zapisać i jak się odczytać. I uważam, że to dobre założenie

Wręcz odwrotnie. To tak jakby obiad sam wiedział jak się zjeść.

0
grzesiek51114 napisał(a):

Głównym założeniem Active Record jest to, że to klasa wie, jak się zapisać i jak się odczytać. I uważam, że to dobre założenie

Wręcz odwrotnie. To tak jakby obiad sam wiedział jak się zjeść.

No, problem pojawia się, gdy zrobisz zmianę w modelu. Wtedy musiałbyś pamiętać o zmianach we wszystkich serializatorach. A w tym rozwiązaniu dopisujesz tylko pole.

0

Czyli w skrócie wymyśliłeś przeciążanie operatorów strumieni, tylko zamiast stream jest seriailzer i zamiast operatora funkcja?
Jakby spoko, tylko wszystkie implementacje Active Record jakie znam, służą do trzymania obiektów w bazie, wyszukiwania ich, etc. Tutaj tego nie ma.

0

Teoretycznie nie ma wyszukiwania. Ale można to oprogramować w serializatorze do bazy danych. Można tu się pobawić też w lazy loading itd. Wszystko na poziomie konkretnego serializatora.

0

No czaje, że można, ale..

  1. To wyżej to na pewno nie ma za dużo wspólnego z Active Record.
  2. Tak jak już napisałem, standardowy mechanizm w C++ do czegoś takiego to strumienie.
  3. W innych językach są inne sposoby na serializację, na przykład w C# refleksją.

Jakby to co napisałeś jest spoko, bo pisanie oddzielnej klasy serializującej dla każdego typu jest bez sensu i faktycznie jak chcesz mieć "serializowalny" obiekt, to z reguły się to implementuje tak jak napisałeś, przy pomocy jakiejś formy interfejsu.

Tylko pozostaje pytanie jaka jest idea tego wątku? :)

0

Nie widziałem nigdzie takiego rozwiązania, więc uznałem, że sam na coś takiego wpadłem :)

1 użytkowników online, w tym zalogowanych: 0, gości: 1