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
#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
#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
#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
#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
#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
#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
#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.


