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?
- Mamy klasy serializatorów odpowiedzialne tylko za jedną rzecz - jak zapisać / odczytać dane (niezależne od modelu)
- Mamy model, który dokładnie wie, jak się zapisać i odczytać, i CO NAJWAŻNIEJSZE - nie jest zależny od żadnej implementacji serializatora
- 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 :)