22
Iul
2010

Cele mai multe aplicatii existente opereaza cu data care trebuie pastrate de la o rulare la alta. Aceste data sunt adesea salvate in fisiere, fie text, fie, mai uzual, binare, cu un format bine definit.

Problema

Initial, la versiunea 1.0, o aplicatie opereaza cu structuri de date care sunt salvate si incarcate din fisiere. Dar, la versiunea 2.0 aceste structuri de date se schimba (unele campuri sunt adaugate, altele dispar) si prin urmare se schimba si formatul fisierelor in care acestea sunt salvate.

Intrebare: Ce se va intampla in momentul in care aplicatia 2.0 va incerca sa incarce fisiere de tipul vechi (versiunea 1.0)?

Raspuns: In marea majoritate a cazurilor ar aparea problema incompatibilitatii intre versiunea aplicatiei curente si versiunea fisierelor si aplicatia ar putea avea un comportament nedefinit. Prin urmare aplicatia trebuie scrisa astfel incat aceasta sa fie capabila sa trateze operatiile de input pentru ambele versiuni de fisiere.

Rezolvare

Pentru a trata aceasta problema de compatibilitate exista mai multe solutii mai mult sau mai putin profesioniste. O solutie recomandata este serializarea.

Serializarea este un proces de scriere/citire a unui obiect pe/de pe un mediu de stocare persistent. Serializarea este ideala pentru situatii in care se doreste mentinerea starii unei structuri de date. Exista diferite framework-uri care ofera suport pentru serializare, printre care si Microsoft Foundation Classes.

Pentru ca un programator Visual C++ sa beneficieze de suportul pentru serializare va utiliza un obiect al clasei CArchive pentru a intermedia serializarea dintre obiect si mediul de stocare. Intre un astfel de obiect CArchive si unul de tipul CFile exista o stransa legatura.

Dar de la o versiune la alta a aplicatiei, structura fisierelor specifice poate suferi modificari semnificative ale structurii fisierului. Cu alte cuvinte, avem de-a face cu versiuni diferite de fisiere, iar ultima versine de aplicatie trebuie sa fie capabila sa incarce si sa modifice versiuni anterioare de fisiere. In acest scop, MFC-ul ne furnizeaza conceptul de Versionable Schema. Versionable Schema inseamna: constanta VERSIONABLE_SCHEMA (se gaseste in fisierul afx.h si are valoarea 0x80000000) in combinatie "sau logic" cu ultima versiune a aplicatiei ca parametru pentru macroul IMPLEMENT_SERIAL si metodele clasei CArchive: GetObjectSchema() si SetObjectSchema(). Metoda GetObjectSchema() e utilizata pentru aflarea versiunii obiectelor salvate intr-un fisier ce se doreste a se incarca in aplicatie. Complementul acesteia, metoda SetObjectSchema() permite setarea in fiser a versiunii obiectului salvat, astfel incat acest fisier sa poate fi incarcat si in versiunile urmatoare de aplicatie.

Spre deosebire de clasele de I/O stream generale, clasa CArchive este destinata doar pentru operatii de serializare a obiectelor in fisiere binare.

Pentru a serializa o clasa e nevoie sa parcurgem urmatorii cinci pasi:

  • Clasa ce dorim sa o serializam, va trebui sa fie derivata din clasa CObject (sau din una din clasele deja derivate din CObject).
  • Sa suprascriem metoda Serialize() din CObject.
  • Se utilizeaza macroul DECLARE_SERIAL in declaratia clasei.
  • Clasa serializabila trebuie sa aibe un constructor fara argumente.
  • Se utilizeaza macroul IMPLEMENT_SERIAL in fisierul de implementare al clasei serializabila.

Mai multe detalii privind tehnice si teoretice se gasesc in MSDN si link-urile referite din aceasta pagina.

Totusi, de la acesti cinci pasi pana la utilizarea serializarii si a versionarii obiectelor sunt cativa pasi semnificativi de urmat. Mai jos voi prezenta un exemplu de aplicatie de tip Dialog cu suportul pentru serializare si versionare implementat.

Aplicatie exemplu - SerAddressBook

In cadrul acestei aplicatii (bazata pe scheletulul aplicatiilor de tip CDialog) mi-am propus sa realiez o agenda de contacte. Sa presupunem ca initial clientul nostru a cerut ca agenda noastra sa contina: numele, prenumele, adresa si numarul de telefon. Dar odata cu aparitia telefoniei mobile si a extinderii Internetului, clientul nostru are nevoie de noi campuri in agenda sa: numarul de telefon mobil si adresa de email.

Aplicatia SerAddressBook cu un fisier incarcat de versiunea 2, arata ca mai jos:

Un design bun ne ajuta ca in cazul unor unor modificari ulterioare, schimbarile ce necesita a fi facute sa implice cat mai putine modificari, de preferinta doar adaugare de cod. Pentru aceasta, designul ales de mine are in linii mari cam urmatoarea structura de clase si legaturi dintre acestea:

Desi atat clasa Contact cat si CAddressBook sunt serializabile, serializarea propriu-zisa am realizat-o in cadrul clasei Contact, iar obiectele serializate si versionabile sunt din aceasta clasa.

Clasa Contact

Din antetul clasei Contact se poate observa:

  • derivarea acesteia din clasa abstracta CObject,
  • apelarea macro-ului DECLARE_SERIAL,
  • declararea metodei Serialize() ce va suprascrie metoda din clasa parinte,
  • atributele ce caracterizeaza obiectele clasei noastre.
class Contact : public CObject
{
public:
	DECLARE_SERIAL(Contact);

	Contact();
	Contact(const Contact& rhs);
	Contact(const CString& strFirstName, const CString& strLastName, const CString& strAddress, const CString& strPhone, 
	// these two must have a default value because they are not used in the first version
	const CString& strMobilePhone = _T(""), const CString& strEmail = _T(""));
	Contact& operator=(const Contact& rhs);

	virtual ~Contact();

	void Serialize( CArchive& ar );

	CString GetFirstName() const {return m_strFirstName;}
	CString GetLastName() const {return m_strLastName;}
	CString GetAddress() const {return m_strAddress;}
	CString GetPhoneNumber() const {return m_strPhone;}
	CString GetMobileNumber() const {return m_strMobilePhone;}
	CString GetEmail() const {return m_strEmail;}

	void SetFirstName(const CString& name) {m_strFirstName = name;}
	void SetLastName(const CString& name) {m_strLastName = name;}
	void SetAddress(const CString& addr) {m_strAddress = addr;}
	void SetPhoneNumber(const CString& phone) {m_strPhone = phone;}
	void SetMobileNumber(const CString& mobile) {m_strMobilePhone = mobile;}
	void SetEmail(const CString& email) {m_strEmail = email;}

	static void SetCurrentFileVersion(int nCV) {	CURRENT_VERSION = nCV;	}
	static int	GetCurrentFileVersion() {	return CURRENT_VERSION;	}

private:
	static int	      CURRENT_VERSION;
	CString		m_strFirstName;
	CString		m_strLastName;
	CString		m_strAddress;
	CString		m_strPhone;
	CString		m_strMobilePhone;
	CString		m_strEmail;
};

typedef CList<Contact, Contact>		ContactList;

Ultima linie reprezinta definirea unui tip alias de lista de contacte, utilizand implementarea MFC a listei de obiecte dublu inlantuite. Aceasta lista va fi utilizata in clasa CAddressBook pentru a administra obiectele Contact.

In cadrul fisierului de implementare se declara macroul IMPLEMENT_SERIAL cat si se initializeaza variabila statica ce va contine versiunea curenta a obiectelor.

IMPLEMENT_SERIAL( Contact, CObject, VERSIONABLE_SCHEMA | 2);

int Contact::CURRENT_VERSION = 2;

In cadrul acestui macro se poate observa prezenta constantei VERSIOABLE_SCHEMA in combinatie "sau logic" cu 2 (ultima versiune a aplicatiei demo). Acest al treilea parametru al macroului IMPLEMENT_SERIAL este esential pentru a versionarea obiectelor clasei noastre si se utilizeaza in combinatie cu metodele CArchive::GetObjectSchema() cat si CArchive::SetObjectSchema(). Mai multe detalii despre acestasta constanta, utilizarea ei si a acestor metode se pot gasi in MSDN.

Implementarea metodei Contacte::Serialize() este prezentata mai jos:

void Contact::Serialize( CArchive& ar )
{
	if (ar.IsStoring())
	{
		CRuntimeClass* pruntime = Contact::GetRuntimeClass();
		int oldnr = pruntime->m_wSchema;
		pruntime->m_wSchema = CURRENT_VERSION;

		ar.SerializeClass(pruntime);

		switch (CURRENT_VERSION)
		{
		case 1:
			ar << m_strFirstName << m_strLastName << m_strAddress << m_strPhone;
			break;

		case 2:
			ar << m_strFirstName << m_strLastName << m_strAddress << m_strPhone << m_strMobilePhone << m_strEmail;
			break;

		default:
			// unknown version for this object
			AfxMessageBox(_T("Unknown file version."), MB_ICONSTOP);
			break;
		}

		pruntime->m_wSchema = oldnr;
	}
	else     // loading code
	{
		ar.SerializeClass(RUNTIME_CLASS(Contact));

		UINT nVersion = ar.GetObjectSchema();

		switch (nVersion)
		{
		case 1:
			ar >> m_strFirstName >> m_strLastName >> m_strAddress >> m_strPhone;
			m_strMobilePhone = _T("");
			m_strEmail = _T("");
			break;

		case 2:
			ar >> m_strFirstName >> m_strLastName >> m_strAddress >> m_strPhone >> m_strMobilePhone >> m_strEmail;
			break;

		default:
			// unknown version for this object
			AfxThrowArchiveException(CArchiveException::badSchema);
			break;
		}
	}
}

Daca in constructorul objectului CArchive se seteaza flagul de stocare-incarcare pe CArchive::store (s-a ales salvarea de date intr-un fisier) atunci se va intra pe prima ramura a blocului if si, pe langa atributele ce se trimit la "arhiva", se seteaza si versiunea fisierului ( ex. ar.SetObjectSchema(1); ).

In momentul in care se doreste deschiderea unui fisier existent pe disc, constructorul obiectului nostru CArchive v-a primii flagul CArchive::load si va intra pe ramura else din cadrul blocului if al metodei Serialize(). Se va extrage versiunea obiectelor stocate in fisier si apoi se va incarca continutul obiectului.

Clasa CAddressBook

Clasa CAddressBook face legatura dintre clasa dialog si clasa serializabila Contact, contine o lista de obiecte Contact, administrand lista de contacte, realizand si operatiile de incarcare/stocare o a obiectelor versionabile. Interfata acestei clase este prezentata mai jos:

class CAddressBook : public CObject
{
	DECLARE_SERIAL(CAddressBook);

	ContactList m_cContactsList;

public:
	CAddressBook();
	virtual ~CAddressBook();

	void   Serialize( CArchive& ar );

	bool   AddContact(const Contact& contact);
	bool   RemoveContact(const CString& firstname, 
                                 const CString& lastname);
	POSITION FindContact(const CString& firstname) const;
	bool   FindContact(const CString& firstname, Contact& contact) const;
	
	const  ContactList& GetContacts() const {return m_cContactsList;}

	void	 SetFileVersion(int nFV) {	m_uiFileVersion = nFV;	}
	int	 GetFileVersion() { return m_uiFileVersion; }

private:
	int		m_uiFileVersion;
};

Se poate observa prezenta obiectului la lista de obiecte (m_cContactsList). De asemenea aceasta clasa operatii de agaugate, stergere si gasire in lista obiectelor de contactelor.

Clasa noastra fiind serializabila s-a suprascris metoda Serialize(), metoda ce va fi utilizata de catre clasa client ( in cazul nostru clasa CSerAddressBookDlg ).

void CAddressBook::Serialize( CArchive& ar )
{
	ar.SerializeClass(RUNTIME_CLASS(CAddressBook));

	// storing
	if (ar.IsStoring())
	{
		ar << m_uiFileVersion;

		// write the number of contacts
		ar << (int)m_cContactsList.GetCount();

		Contact::SetCurrentFileVersion(m_uiFileVersion);

		// write all the contacts
		POSITION pos = m_cContactsList.GetHeadPosition();
		while(pos != NULL)
		{
			Contact contact = m_cContactsList.GetNext(pos);

			contact.Serialize(ar);
		}
	}
	else    // loading
	{
		ar >> m_uiFileVersion;

		m_cContactsList.RemoveAll();

		int count = 0;
		ar >> count;

		// read the number of contacts
		for(INT_PTR i = 0; i < count; ++i)
		{
			Contact contact;
			contact.Serialize(ar);

			m_cContactsList.AddTail(contact);
		}
	}
}

Datorita faptului ca in cadrul clasei CArchive nu exista vreo metoda care sa-mi spuna cate obiecte exista intr-un fisier am ales ca de fiecare data cand salvez o lista de obiecte in fisier sa salvez si numarul de obiecte salvate. Pentru aceasta, in cadrul blocului TRUE al if (ar.IsStoring()) am adaugat linia:

ar << m_cContactsList.GetCount();

La fel, si pentru versiunea fisierului, inainte de a trece la scrierea contactelor in agenda fac salvarea versiunii fisierului:

ar << m_uiFileVersion;

Apoi, intr-o bucla while parcurg lista de contacte, serializez si stochez informatiile.

Daca incarc un fisier de pe disc (sunt pe ramura ELSE) atunci parcurg urmatoarele etape:

  • curat lista de contacte,
  • iau numarul de obiecte din fisier,
  • iau versiunea obiectelor si serializez toate obiectele adaugandu-le in lista de obiecte Contact.

Clasa aplicatiei CSerAddressBookDlg

Avand acest mecanism de serializare implementat, utilizarea acestuia in cadrul aplicatiei client devine foarte usoara. Astfel, metoda apelata in momentul in care se doreste salvarea in fisier va arata astfel:

void CSerAddressBookDlg::SaveDataContentToFile(CString strSaveFile)
{
	CFile wFile(strSaveFile, CFile::modeCreate | CFile::modeWrite);

	// Create a storing archive
	CArchive arStore(&wFile, CArchive::store);
	m_c_AddressBook.Serialize(arStore);

	// Close the storing archive
	arStore.Close();

	PopulateList();
}

Dupa cum se poate observa, am un obiect CFile ce il utilizez pentru a salva datele in fisiere pe disc. De asemenea, am un obiect local de tip CArchive, in constructorul caruia trimit ca prim parametru adresa handle-lui de fisier si flagul de stocare CArchive::store. Urmeaza apelarea metodei CAddressBook::Serialize() prin intermediul obiectului m_c_AddressBook si apoi inchiderea operatiei de stocare (implicit ruperea de obiectul local CFile).

Incarcarea obiectelor stocate in fisiere, bazandu-ne pe mecanismul de serializare se face astfel:

void CSerAddressBookDlg::LoadDataContentFromFile(CString strLoadedFile)
{
	CFile rFile(strLoadedFile, CFile::modeRead);

	// Create a loading archive
	CArchive arLoad(&rFile, CArchive::load);
	m_c_AddressBook.Serialize(arLoad);
	
	// Close the loading archive
	arLoad.Close();

	switch (m_c_AddressBook.GetFileVersion())
	{
		case 1:
			((CButton*)GetDlgItem(IDC_RADIO_VERSION_1))->SetCheck(BST_CHECKED);
			((CButton*)GetDlgItem(IDC_RADIO_VERSION_2))->SetCheck(BST_UNCHECKED);
			OnBnClickedRadioVersion1();
			break;
		case 2:
			((CButton*)GetDlgItem(IDC_RADIO_VERSION_1))->SetCheck(BST_UNCHECKED);
			((CButton*)GetDlgItem(IDC_RADIO_VERSION_2))->SetCheck(BST_CHECKED);
			OnBnClickedRadioVersion2();
			break;
		default:
			break;
	}

	// repopulate the list
	PopulateList();
}

Dupa cum se poate observa, se creeaza un obiect local CFile, necesar pentru operatiile de citire. De asemenea, se creeaza un obiect local CArchive care are pasat in constructor adresa handle-ului de fisier cat si flagul CArchive::load. Apoi se apeleaza metoda Serialize() a clasei CAddressBook, intrandu-se pe ramura else si se deconecteaza obiectul de fisier.

Ultima linie contine apelul metodei PopulateList() si este metoada de populare, cu date citite din fisier, a controlului lista din cadrul dialogului nostru.

void CSerAddressBookDlg::PopulateList()
{
	// delete all current members
	m_cList.DeleteAllItems();

	// get a reference to the contacts list
	const ContactList& contacts = m_c_AddressBook.GetContacts();

	// iterate over all contacts add add them to the list
	int nCurrentItem = 0;
	POSITION pos = contacts.GetHeadPosition();
	while(pos != NULL)
	{
		const Contact contact = contacts.GetNext(pos);

		nCurrentItem = m_cList.InsertItem(nCurrentItem, contact.GetFirstName());
		m_cList.SetItemText(nCurrentItem, 1, contact.GetLastName());
		m_cList.SetItemText(nCurrentItem, 2, contact.GetAddress());
		m_cList.SetItemText(nCurrentItem, 3, contact.GetPhoneNumber());

		
		switch(m_c_AddressBook.GetFileVersion())
		{
		case 1:
			break;
		case 2:
			m_cList.SetItemText(nCurrentItem, 4, contact.GetMobileNumber());
			m_cList.SetItemText(nCurrentItem, 5, contact.GetEmail());
			break;
		}
	}
}

Concluzii

Arhitectura Doc/View din MFC ofera suport complet pentru serializare. Orice aplicatie MDI/SDI contine implicit scheletul pentru serializare si versionare. Totusi, ideea serializarii si versionarii obiectelor, prezentata mai sus in cadrul unei aplicatii sample de tip dialog este aceiasi.

[phpBB Debug] PHP Warning: in file [ROOT]/phpbb/db/driver/mysqli.php on line 317: mysqli_free_result(): Couldn't fetch mysqli_result