W trakcie mojej krótkiej przygody z kodem, miałem ogromną przyjemność poznać zasady programowania obiektowego na przykładzie języka C++. Sporo czasu zajmowały mi rozważania, czy można coś zapisać prościej, jak uniknąć redundancji kodu i czy dany obiekt nie potrzebuje jeszcze jednej prywatnej metody.
Pierwszą aplikacją konsolową, którą napisałem „obiektowo” była książka adresowa. Temat był mi już znany, gdyż wcześniej stworzyłem ją w brzydki sposób, prawdziwy rozwojowy koszmar: program składający się z jednego pliku, liczącego dziesiątki zadeklarowanych funkcji z malutkim main’em na końcu, robiącym za menu główne. Niespełna 600 linijek kodu robiących absolutnie wszystko.
Tak ten potworek wyglądał: Link
Problem polegał na tym, że otwierając jeden plik, zmuszony byłem szukać wszystkiego w jednym oknie, losowa kolejność funkcji nie pomagała, a jeden mały błąd powodował wielogodzinne sesje w Debuggerze. Dzięki wsparciu doświadczonego mentora, który pokazał mi piękno obiektowości, udało się mój projekt ucywilizować i uczynić dalsze utrzymanie mniej bolesne, oto diagram UML nowej książki adresowej:

I tutaj wchodzi wzorzec projektowy Fasada, cała na biało… oraz w innych barwach 🙂
Pierwszą użytą przeze mnie (zupełnie nieświadomie) Fasadą był obiekt klasy App, posiadał on tak naprawdę tylko jedną metodę, ponieważ użytkownik końcowy nie potrzebuje nic innego, jak tylko to, aby nasz program działał bez zarzutu. Jedyną dowolność, jaką mu zostawiamy, to możliwość nazwania plików z użytkownikami oraz przypisanymi do nich adresatami. Na tym etapie jeszcze nie pracowałem z bazami danych, więc dane przechowywałem w plikach .txt, teraz już wiem, że to nie jest dobre rozwiązanie 😉
Poniżej treść pliku main().cpp:
int main() {
App app("Users.txt" , "Recipients.txt");
app.run();
return 0;
}
Przyjrzyjmy się bliżej klasie App, gdyż tutaj robi się ciekawie:
class App{
AddressBook addressBook;
public:
App(string userFileName, string recipientFileName):addressBook(userFileName, recipientFileName){}
void run();
};
Zwróćmy uwagę, że konstruktor naszej klasy App przekazuje nazwy plików do klasy AddresBook, nie wykonując tutaj żadnej samodzielnej operacji, pełni jedynie funkcję menu głównego. Przyjrzyjmy się plikowi App.cpp:
#include "App.h"
void App :: run(){
while (true) {
if (!addressBook.isUserLogged()) {
char choice = addressBook.pickOptionMainMenu();
switch (choice) {
case '1':
addressBook.userRegister();
break;
case '2':
addressBook.userSignIn();
break;
case '9':
exit(0);
break;
default:
cout << endl << "Nie ma takiej opcji w menu." << endl << endl;
system("pause");
break;
}
} else {
char choice = addressBook.pickOptionUserMenu();
switch (choice) {
case '1':
addressBook.addRecipient();
break;
case '2':
addressBook.findRecipientByName();
break;
case '3':
addressBook.findRecipientBySurname();
break;
case '4':
addressBook.displayAllRecipients();
break;
case '5':
addressBook.deleteRecipient();
break;
case '6':
addressBook.editRecipient();
break;
case '7':
addressBook.loggedUserPasswordChange();
break;
case '8':
addressBook.userLogout();
break;
}
}
}
}
Nasza klasa App poprzez metodę run() tak naprawdę tylko wywołuje konkretne metody z klasy AddressBook, nie mając pojęcia, jak te metody działają. Zatem jest ona kolejną fasadą dla klasy klienckiej App, oto jej struktura:
class AddressBook {
UserManager userManager;
RecipientManager *recipientManager;
const string RECIPIENT_FILE_NAME;
public:
AddressBook(string userFileName, string recipientFileName)
:userManager(userFileName), RECIPIENT_FILE_NAME(recipientFileName) {
recipientManager = NULL;
};
~AddressBook() {
delete recipientManager;
recipientManager = NULL;
}
void userRegister();
void displayAllUsers();
void userSignIn();
void loggedUserPasswordChange();
void addRecipient();
void displayAllRecipients();
void userLogout();
bool isUserLogged();
char pickOptionMainMenu();
char pickOptionUserMenu();
void findRecipientByName();
void findRecipientBySurname();
void deleteRecipient();
void editRecipient();
};
Ponownie przyjrzyjmy się logice klasy AddressBook:
void AddressBook::userRegister() {
userManager.userRegister();
}
bool AddressBook :: isUserLogged() {
return userManager.isUserLogged();
}
void AddressBook :: userSignIn() {
userManager.userSignIn();
if(userManager.isUserLogged()) {
recipientManager = new RecipientManager(userManager.getLoggedUserId(), RECIPIENT_FILE_NAME);
}
}
void AddressBook :: displayAllUsers() {
userManager.displayAllUsers();
}
void AddressBook :: loggedUserPasswordChange() {
userManager.loggedUserPasswordChange();
}
void AddressBook :: addRecipient() {
if(userManager.isUserLogged()) {
recipientManager->addRecipient();
}
}
void AddressBook :: displayAllRecipients() {
if(userManager.isUserLogged()) {
recipientManager->displayAllRecipients();
}
}
void AddressBook :: userLogout() {
userManager.userLogout();
delete recipientManager;
recipientManager = NULL;
cout << "Pomyslnie wylogowao." << endl;
}
char AddressBook :: pickOptionMainMenu() {
char choice;
system("cls");
cout << " >>> MENU GLOWNE <<<" << endl;
cout << "---------------------------" << endl;
cout << "1. Rejestracja" << endl;
cout << "2. Logowanie" << endl;
cout << "9. Koniec programu" << endl;
cout << "---------------------------" << endl;
cout << "Twoj wybor: ";
choice = AuxiliaryMethod :: loadCharacter();
return choice;
}
char AddressBook :: pickOptionUserMenu() {
char choice;
system("cls");
cout << " >>> MENU UZYTKOWNIKA <<<" << endl;
cout << "---------------------------" << endl;
cout << "1. Dodaj adresata" << endl;
cout << "2. Wyszukaj po imieniu" << endl;
cout << "3. Wyszukaj po nazwisku" << endl;
cout << "4. Wyswietl adresatow" << endl;
cout << "5. Usun adresata" << endl;
cout << "6. Edytuj adresata" << endl;
cout << "---------------------------" << endl;
cout << "7. Zmien haslo" << endl;
cout << "8. Wyloguj sie" << endl;
cout << "---------------------------" << endl;
cout << "Twoj wybor: ";
choice = AuxiliaryMethod :: loadCharacter();
return choice;
}
void AddressBook :: findRecipientByName() {
recipientManager->findRecipientByName();
}
void AddressBook :: findRecipientBySurname() {
recipientManager->findRecipientBySurname();
}
void AddressBook :: deleteRecipient() {
recipientManager->deleteRecipient();
}
void AddressBook :: editRecipient() {
recipientManager->editRecipient();
}
Dalej mamy już klasyczną architekturę warstwową i delegowanie odpowiedzialności. Nasza klasa AddressBook niektóre z zadań wykonuje samodzielnie, natomiast resztę zleca kolejnym dwóm klasom, są nimi tworzony niemal natychmiast obiekt klasy UserManager oraz drugi, tworzony po zalogowaniu się obiekt RecipientManager (po wylogowaniu obiekt ulega zniszczeniu):
class UserManager {
UserFile userFile;
int loggedUserId;
vector <User> users;
User giveNewUserData();
int getNewUserId();
bool userNameExists(string userName);
void saveAllUsersToFile();
public:
UserManager(string userFileName) : userFile(userFileName) {
loggedUserId = 0;
loadUsersFromFile();
};
bool isUserLogged();
int getLoggedUserId();
void userRegister();
void displayAllUsers();
void loadUsersFromFile();
int userSignIn();
void loggedUserPasswordChange();
void userLogout();
};
class RecipientManager {
RecipientFile recipientFile;
const int LOGGED_USER_ID;
int lastRecipientId;
vector <Recipient> recipients;
Recipient giveNewRecipientData(int loggedUserId);
void displayRecipientData(size_t index);
void displayFoundRecipientsAmount(int recipientsAmount);
int giveChosenRecipientId();
char loggedUserPasswordChange();
char chooseRecipientEditMenuOption();
public:
RecipientManager(int loggedUserId, string recipientFileName): recipientFile(recipientFileName), LOGGED_USER_ID(loggedUserId) {
recipients = recipientFile.loadLoggedUserRecipientsFromFile(LOGGED_USER_ID);
};
int addRecipient();
void displayAllRecipients();
void findRecipientByName();
void findRecipientBySurname();
void deleteRecipient();
void editRecipient();
};
Te z kolei delegują część zadań dalej – obiekty klas UserFile oraz RecipientFile, dziedziczące po klasie TextFile:
class RecipientFile : protected TextFile {
Recipient recipient;
int lastRecipientId;
string temporaryRecipientFileName;
string transformRecipientDataToSeparatedByVerticalBars(Recipient recipient);
int getUserIdFromDataSeparatedByVerticakBars(string singleRecipientDataSeparatedByVerticalBars);
Recipient getRecipientData(string recipientDataSeparatedByVerticalBars);
int getRecipientIdFromDataSeparatedByVerticakBars(string recipientDataSeparatedByVerticalBars);
int getLastRecipientId();
void deleteFile(string fileName);
void renameFile(string fileOldName, string fileNewName);
public:
RecipientFile(string RECIPIENTFILENAME) : TextFile(RECIPIENTFILENAME) {
lastRecipientId = 0;
temporaryRecipientFileName = "Recipient_temp.txt";
};
void addRecipientToFile(Recipient recipient);
vector <Recipient> loadLoggedUserRecipientsFromFile(int loggedUserId);
int getNewRecipientId();
void deleteChosenLineFromFile(int deletedRecipientId);
void editChosenLineInFile(string recipientDataLineSeparatedByVerticalBars, int editedRecipientId);
void updateChosenRecipientData(Recipient recipient, int editedRecipientId);
};
class UserFile : protected TextFile{
string singleUserDataSeparatedByVerticalBars;
string transformUserDataToSeparatedByVerticalBars(User user);
User loadSingleUserData(string singleUserDataSeparatedByVerticalBars);
public:
UserFile(string userFileName) : TextFile(userFileName){};
vector <User> loadUsersFromFile();
void addUserToFile(User user);
void saveAllUsersToFile(vector <User> users);
};
I finalnie rodzic powyższych:
class TextFile {
string FILE_NAME;
protected:
TextFile(string fileName): FILE_NAME(fileName){};
bool isFileEmpty();
string getFileName();
};
Podsumowując, w trakcie mojej nauki fasada była najczęściej i zarazem najmniej świadomie wykorzystywanym wzorcem projektowym, wykonującym niezwykle ważną pracę – pośredniczenia między użytkownikiem, a logiką biznesową aplikacji. Pokazała mi zupełnie nowe dla mnie podejście do programowania, gdzie dobrze napisany program przypomina świetnie zarządzaną firmę. Obiekty są pracownikami, z których każdy posiada konkretny zestaw wiedzy(atrybuty) oraz narzędzi i obowiązków (metody).
Oczywiście, nie pokazałem wszystkich klas działających w mojej aplikacji, skoncentrowałem się jedynie na wybranych celem przedstawienia swoich przemyśleń. Zainteresowanych zachęcam do zapoznania się z całością mojego projektu na platformie GitHub:

