Witajcie w kolejnym wpisie z serii o obiektowości, drodzy czytelnicy! W poprzednim poście dowiedzieliście się czym jest dziedziczenie, a także na jakich filarach opiera się programowanie obiektowe. Dzisiaj zajmiemy się kolejnym z nich – omówimy wspólnie czym jest hermetyzacja.
Hermetyzacja – czym ona jest?
Mówiąc krótko hermetyzacja, zwana też enkapsulacją, polega na ograniczaniu dla klas zewnętrznych dostępu do danych i metod tworzonej klasy. Dzięki temu zapobiegamy niekontrolowanemu działaniu programu, a także zachowujemy przejrzystą strukturę obiektów w projekcie. Wyobraź sobie, że tworzysz bazę klientów w programie. Stworzysz najpewniej klasę Client, w której zawrzesz informacje na temat danego klienta, jak jego imię, nazwisko, wiek, zamieszkanie itd. Nie chcemy jednak, aby w trakcie działania programu jego imię, czy nazwisko zostało zmienione przez kogoś innego. Musimy więc uniemożliwić zmianę tych dwóch pól, dając jedynie dostęp do ich odczytu. Na tym właśnie polega hermetyzacja. Aby jak najściślej zachować zasady hermetyzacji, stosuje się różne praktyki:
- korzystanie z modyfikatorów dostępności – jak najmniej danych/metod powinno być widocznych na zewnątrz klasy
- Ukrywanie pól klasy i nadawanie dostępu do nich poprzez funkcje get i set
- Korzystanie z wartości stałych/tylko do odczytu

Hermetyzacja – modyfikatory dostępu
Modyfikatory omawialiśmy już w poprzednim wpisie, jednak warto je sobie przypomnieć:
- Public – metody lub pola oznaczone tym modyfikatorem są dostępne dla każdego, kto posiada instancję naszej klasy, a także dla klas dziedziczących
- Protected – metody lub pola oznaczone tym modyfikatorem są dostępne tylko w obrębie danej klasy i w klasach pochodnych
- Private – metody lub pola oznaczone tym modyfikatorem są dostępne tylko w obrębie danej klasy
Modyfikatory dostępu możemy przypisać do każdej metody, pola, a nawet konstruktorów. W języku C++ użycie modyfikatora dostępu przed nazwą klasy pozwala na określenie zakresu dziedziczenia jej właściwości i zachowania przez klasy pochodne. Więcej o tych zakresach przeczytacie w poprzednim wpisie.
Przejdźmy do prostego przykładu:
Producer.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
#ifndef PRODUCER_H #define PRODUCER_H #include <string> #include <iostream> #include <vector> #include <regex> using namespace std; class Producer { public: //tutaj deklarujemy metody/pola/konstruktory, ktore chcemy, aby byly widoczne dla kazdego, kto posiada dostep do instancji klasy Producer(const string& Name); //publiczny konstruktor klasy producenta, pozwalajcy na ustawienie jej nazwy void produceItem(string productName); //publiczna metoda, ktora wyprodukuje kolejny produkt vector<string> releaseProducts(); //publiczna metoda, ktora wyda wyprodukowane przedmioty z magazynu protected: //tutaj deklarujemy metody/pola/konstruktory, ktore chcemy, aby byly widoczne dla tej klasy i klas pochodnych void checkProducts(); //przed wydaniem chcemy sprawdzic, czy produkty nie sa wadliwe //tworzymy metode protected, bo nie chcemy aby ona byla dostepna dla kazdego posiadajacego dostep do instancji klasy private: //tutaj deklarujemy metody/pola/konstruktory, ktore chcemy, aby byly widoczne tylko dla tej klasy bool isProductValid(string product); //metoda sprawdzajaca, czy produkt nie jest wadliwy - nie chcemy, aby ulegla ona zmianie w klasach pochodnych, dlatego jest ona jako private vector<string> _products; //kontener przechowujacy wyprodukowane przedmioty - nie chcemy, aby byl on zmieniany przez inne klasy string _name = ""; //nazwa producenta - jest ona stala, nie chcemy, aby ulegla zmianie bool _productsChecked = false; //zmienna pomocnicza, ktora informuje o tym, czy produkty w kontenerze zostaly sprawdzone }; #endif // PRODUCER_H |
Producer.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
#include "producer.h" Producer::Producer(const string &Name) : _name(Name) //inicjalizujemy pole _name w konstruktorze { } void Producer::produceItem(string productName) { _products.push_back(productName); //produkujemy nowy przedmiot _productsChecked = false; //oznaczamy wartosc zmiennej pomocniczej na true - produkty wymagaja ponownego sprawdzenia } vector<string> Producer::releaseProducts() { checkProducts(); //przed wydaniem produktow usuwamy z magazynu wadliwe sztuki vector<string> releasedProducts = _products; //chcemy wyczyscic magazyn po zwroceniu przedmiotow - tworzymy kopie obiektu _products, aby moc to zrobic _products.clear(); //czyscimy magazyn return releasedProducts; //wydajemy gotowe produkty } void Producer::checkProducts() { if(_productsChecked){ //jezeli przedmioty zostaly juz sprawdzone return; //przerywamy dzialanie funkcji oszczedzajac czas } vector<string> validProducts; //tworzymy pomocniczy kontener na produkty, ktore nie sa wadliwe for(string product : _products){ //sprawdzamy kazdy z produktow if(isProductValid(product)){ //i dodajemy do kontenera tylko te sprawne validProducts.push_back(product); } } _products = validProducts; //natepnie aktualizujemy magazyn _productsChecked = true; //oznaczamy wartosc zmiennej pomocniczej na true - dzieki temu metoda ta nie wywola sie drugi raz } bool Producer::isProductValid(string product) { return regex_match(product, regex("^[A-Za-z0-9]*$")); //sprawdzamy czy produkt spelnia kryteria } |
Stworzyliśmy przykładową klasę producenta, która zawiera informacje o jej nazwie, przechowywanych produktach, a także metody pozwalające na manipulację nimi. Zauważ, że metoda sprawdzająca produkty przed wydaniem jest oznaczona jako protected, co oznacza, że nie będzie możliwe sprawdzenie produktów poza tą klasą lub klasami pochodnymi. Pola przechowujące nazwę i wyprodukowane przedmioty oznaczyliśmy jako private, ponieważ nie chcemy, aby ktokolwiek modyfikował ich zawartość poza tą klasą. Dodatkowo, dla przykładu, stworzyliśmy również prywatną metodę sprawdzającą poprawność danego produktu – chcemy, aby była ona dostępna tylko dla bazowej metody sprawdzającej wszystkie produkty. Aby nie komplikować, za wadliwy produkt uznajemy każdy, który zawiera w swojej nazwie znaki specjalne.

Hermetyzacja – gettery/settery, czyli ustalanie dostępu do pól
Najbardziej charakterystyczną praktyką hermetyzacji jest korzystanie z tzw. getterów i seterów. Na czym one polegają? Mówiąc krótko są to specjalne metody, które dają dostęp do prywatnych pól danej klasy.
Wróćmy do przykładu naszego producenta. Powiedzmy, że chcemy, aby magazyn produktów mógł być zmieniany przez każdego, kto ma dostęp do instancji klasy producenta – może to być dostawa nowych przedmiotów lub zmiana asortymentu. Można by po prostu zmienić widoczność vectora na publiczną, jednak w praktyce korzysta się z innego rozwiązania. Dodajemy dwie publiczne metody: pierwsza zwraca wartość naszego vectora, a druga typu void pozwala na ustawienie jego wartości. Dlaczego takie rozwiązanie jest lepsze? Dzięki takiemu podejściu mamy większą kontrolę nad danymi, które ktoś wprowadza. Przykładowo przed zmianą wartości możemy od razu pozbyć się wadliwych produktów, a następnie oznaczyć produkty jako sprawdzone, dzięki czemu upewnimy sie, że wszystkie dane są poprawne. Dodatkowo chcemy, aby nazwa naszego producenta była widoczna dla każdego, ale bez możliwości jej zmiany – wtedy wystarczy stworzyć jedynie metodę zwracającą jej wartość.

Tak wygląda nasza klasa po zmianach:
Producer.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
#ifndef PRODUCER_H #define PRODUCER_H #include <string> #include <iostream> #include <vector> #include <regex> using namespace std; class Producer { public: Producer(const string& Name); void produceItem(string productName); vector<string> releaseProducts(); void setProducts(vector<string> products); //tworzymy setter dla produktow vector<string> getActualProducts(); //tworzymy getter dla produktow string getName(); //tworzymy getter dla nazwy producenta protected: void checkProducts(); private: bool isProductValid(string product); vector<string> _products; string _name = ""; bool _productsChecked = false; }; #endif // PRODUCER_H |
Producer.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
#include "producer.h" Producer::Producer(const string &Name) : _name(Name) { } void Producer::produceItem(string productName) { _products.push_back(productName); _productsChecked = false; } vector<string> Producer::releaseProducts() { checkProducts(); vector<string> releasedProducts = _products; _products.clear(); return releasedProducts; } void Producer::setProducts(vector<string> products) { _products = products; //podmieniamy zawartosc magazynu na podana wartosc _productsChecked = false; //produkty nalezy sprawdzic checkProducts(); //od razu sprawdzamy ktore przedmioty nie sa wadliwe } vector<string> Producer::getActualProducts() { return _products; //zwracamy magazyn produktow } string Producer::getName() { return _name; //zwracamy nazwe producenta } void Producer::checkProducts() { if(_productsChecked){ return; } vector<string> validProducts; for(string product : _products){ if(isProductValid(product)){ validProducts.push_back(product); } } _products = validProducts; _productsChecked = true; } bool Producer::isProductValid(string product) { return regex_match(product, regex("^[A-Za-z0-9]*$")); } |
Hermetyzacja – stałe
Pola stałe inicjalizowane są ze słowem kluczowym const. Pola takie po inicjalizacji nie mogą już zostać w żaden sposób zmienione – ani przez klasę je zawierająca, ani przez nikogo innego. Stałe przydają się, gdy w programie często korzystamy z tej samej wartości. Dobrą praktyką jest wtedy stworzenie stałej, która zainicjalizuje dany obiekt. Dzięki temu nie musimy potem wielokrotnie wywoływać jego konstruktora w czasie działania programu. W naszym przykładzie możemy stworzyć pole zawierające zainicjalizowany regex – jego wartość z założenia ma być zawsze taka sama, więc użycie stałej jest tutaj dobrym rozwiązaniem.
Producer.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
#include "producer.h" Producer::Producer(const string &Name) : _name(Name) { } void Producer::produceItem(string productName) { _products.push_back(productName); _productsChecked = false; } vector<string> Producer::releaseProducts() { checkProducts(); vector<string> releasedProducts = _products; _products.clear(); return releasedProducts; } void Producer::setProducts(vector<string> products) { _products = products; _productsChecked = false; checkProducts(); } vector<string> Producer::getActualProducts() { return _products; } string Producer::getName() { return _name; } void Producer::checkProducts() { if(_productsChecked){ return; } vector<string> validProducts; for(string product : _products){ if(isProductValid(product)){ validProducts.push_back(product); } } _products = validProducts; _productsChecked = true; } bool Producer::isProductValid(string product) { //_criteriaRegex = regex("^[A-Za-z0-9]*$"); <---- taki kod zwrocilby blad return regex_match(product, _criteriaRegex); //korzystamy z naszej stalej } |
Producer.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
#ifndef PRODUCER_H #define PRODUCER_H #include <string> #include <iostream> #include <vector> #include <regex> using namespace std; class Producer { public: Producer(const string& Name); void produceItem(string productName); vector<string> releaseProducts(); void setProducts(vector<string> products); vector<string> getActualProducts(); string getName(); protected: void checkProducts(); private: bool isProductValid(string product); vector<string> _products; string _name = ""; bool _productsChecked = false; const regex _criteriaRegex = regex("^[A-Za-z0-9]*$"); //tworzymy stala zawierajaca nasz regex }; #endif // PRODUCER_H |
Zbierzmy teraz wszystkie przykłady do kupy. Stworzymy 3 instancje klasy producenta, a następnie wyprodukujemy 3 serie po 5 produktów, których nazwy wybierzemy (pseudo)losowo z przygotowanej listy. W liście tej celowo zawrzemy kilka wadliwych produktów, aby sprawdzić działanie naszego programu. Następnie wydrukujemy zawartość listy produktów przed i po sprawdzeniu.
Main.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
#include <iostream> #include <vector> #include "producer.h" using namespace std; void produceProducts(Producer f, string productsList[]){ cout << "Obslugujemy producenta o nazwie: " << f.getName() << endl; //pobieramy nazwe producenta z publicznego gettera for (int i = 1; i <= 3; i++){ //dwie serie produktow cout << " Seria numer: " << i << endl; if(i == 3){ //dla ostatniej serii skorzyamy z settera: vector<string> tempProducts; cout << " Dostawa produktow: " << endl; for(int j = 0; j < 6; j++){ int index = rand() % productsList->length(); //wybieramy losowy przedmiot tempProducts.push_back(productsList[index]); //i dodajemy go do listy cout << " " << productsList[index] << endl; } f.setProducts(tempProducts); //korzystamy z naszego settera cout << " Produkty w magazynie po dostawie: " << endl; for(auto a : f.getActualProducts()){ //sprawdzamy produkty w magazynie - powinny byc one bez wadliwych produktow: cout << " " << a << endl; } } else{ for(int j = 0; j < 6; j++){ //dodajemy 5 produktow int index = rand() % productsList->length(); //wybieramy losowy przedmiot f.produceItem(productsList[index]); //i go produkujemy } cout << " Produkty przed wydaniem: " << endl; for(auto a : f.getActualProducts()){ //iterujemy poprzez produkty w magazynie - korzystamy z gettera cout << " " << a << endl; } cout << " Produkty po wydaniu: " << endl; for(auto a : f.releaseProducts()){ //iterujemy poprzez produkty po wydaniu cout << " " << a << endl; } } } } int main() { Producer f1("Producent 1"), f2("Producent 2"), f3("Producent 3"); //tworzymy instancje 3 producentow string products[10] = {"Buty", "Telefon", "r$4D", "Wadliwy*%", "Szczotka", "Smiglowiec szturmowy", "$u$", "Czekolada", "Kalkulator", "Umywa!ka"}; produceProducts(f1, products); produceProducts(f2, products); produceProducts(f3, products); return 0; } |
A oto część wyniku działania naszego programu:

Podsumowanie
Dowiedzieliście się dzisiaj czym jest hermetyzacja, a także w jaki należy ją praktykować. W następnym wpisie z serii omówimy czym jest kolejny jego filar: polimorfizm.