Policy Based Design (PBD) łączy szablony C++ ze wzorcem Strategy. Polityka to parametr szablonu, który dostarcza jedno zachowanie: sortowanie, logowanie albo alokację pamięci. Kompilator rozwiązuje politykę, więc abstrakcja nie kosztuje nic w czasie wykonania.
Podstawowa koncepcja
Polityka to parametr szablonu, który definiuje zachowanie albo algorytm. Zwykłe parametry szablonu to najczęściej konkretne typy. Polityka jest szablonem lub klasą i opisuje zestaw operacji, których oczekuje klasa gospodarz. Gospodarz używa polityk, żeby dopasować swoje zachowanie:
// Polityka 1: Strategia sortowania
struct QuickSortPolicy {
template<typename T>
void sort(std::vector<T>& data) const {
std::sort(data.begin(), data.end());
}
};
struct MergeSortPolicy {
template<typename T>
void sort(std::vector<T>& data) const {
std::stable_sort(data.begin(), data.end());
}
};
// Polityka 2: Zachowanie logowania
struct NoLoggingPolicy {
void log(const std::string& msg) const {}
};
struct FileLoggingPolicy {
void log(const std::string& msg) const {
// Loguj do pliku
std::ofstream file("debug.log", std::ios::app);
file << msg << "\n";
}
};
// Klasa gospodarz: dostosowana przez polityki
template<typename SortPolicy, typename LoggingPolicy>
class DataProcessor {
private:
SortPolicy sort_policy;
LoggingPolicy log_policy;
std::vector<int> data;
public:
void add_data(int value) {
data.push_back(value);
}
void process() {
log_policy.log("Rozpoczęcie sortowania...");
sort_policy.sort(data);
log_policy.log("Sortowanie zakończone");
}
void print() const {
for (int val : data) {
std::cout << val << " ";
}
std::cout << "\n";
}
};
// Użycie: komponuj zachowanie w czasie kompilacji
int main() {
// Szybkie przetwarzanie bez logowania
DataProcessor<QuickSortPolicy, NoLoggingPolicy> fast_processor;
fast_processor.add_data(30);
fast_processor.add_data(10);
fast_processor.add_data(20);
fast_processor.process();
fast_processor.print();
// Stabilne sortowanie ze szczegółowym logowaniem
DataProcessor<MergeSortPolicy, FileLoggingPolicy> debug_processor;
debug_processor.add_data(30);
debug_processor.add_data(10);
debug_processor.add_data(20);
debug_processor.process(); // Loguje do pliku
debug_processor.print();
return 0;
}
Anatomia polityki
Polityka opisuje umowę, której oczekuje klasa gospodarz:
// Przykład: interfejs Sorting Policy
struct SortingPolicy {
// Musi być wywoływalna (funktor lub wskaźnik funkcji)
// lub mieć członkowską funkcję taką jak:
template<typename Container>
void sort(Container& data) const;
};
// Przykład: interfejs Hashing Policy
struct HashingPolicy {
template<typename T>
size_t hash(const T& value) const;
};
// Przykład: interfejs Memory Allocation Policy
struct MemoryAllocationPolicy {
void* allocate(size_t size);
void deallocate(void* ptr);
};
Polityki są zwykle:
- Bezstanowe albo z minimalnym stanem. Zwykle zawierają tylko algorytmy.
- Świadome szablonów. Pracują z parametrami szablonu.
- Oparte na koncepcjach. Definiują niejawny interfejs zamiast jawnego dziedziczenia.
Porównanie: polityki i cechy
Cechy opisują typy i dostarczają metadane:
template<typename T>
struct numeric_traits {
static constexpr T max_value() { return std::numeric_limits<T>::max(); }
static constexpr T min_value() { return std::numeric_limits<T>::min(); }
};
Polityki dostarczają zachowanie i algorytmy:
struct SaturatingArithmetic {
template<typename T>
T add(T a, T b) const {
// Nasycaj zamiast przepełnienia
if (a > std::numeric_limits<T>::max() - b) {
return std::numeric_limits<T>::max();
}
return a + b;
}
};
struct WrapAroundArithmetic {
template<typename T>
T add(T a, T b) const {
// Tradycyjne owijanie się na przepełnieniu
return a + b;
}
};
Przykład z życia: inteligentny kontener
// Polityka magazynu
struct VectorStoragePolicy {
template<typename T>
using container = std::vector<T>;
};
struct DequeStoragePolicy {
template<typename T>
using container = std::deque<T>;
};
// Polityka bezpieczeństwa wątków
struct SingleThreadPolicy {
void lock() const {}
void unlock() const {}
};
struct MutexThreadPolicy {
mutable std::mutex mtx;
void lock() const { mtx.lock(); }
void unlock() const { mtx.unlock(); }
};
// Polityka wzrostu
struct ExponentialGrowthPolicy {
static constexpr float growth_factor() { return 1.5f; }
};
struct LinearGrowthPolicy {
static constexpr float growth_factor() { return 0.25f; }
};
// Kontener oparty na politykach
template<typename T,
typename StoragePolicy = VectorStoragePolicy,
typename ThreadPolicy = SingleThreadPolicy,
typename GrowthPolicy = ExponentialGrowthPolicy>
class SmartContainer {
private:
typename StoragePolicy::template container<T> data;
ThreadPolicy thread_policy;
GrowthPolicy growth_policy;
public:
void push_back(const T& value) {
thread_policy.lock();
data.push_back(value);
if (data.size() >= data.capacity() * 0.8f) {
reserve(static_cast<size_t>(
data.capacity() * growth_policy.growth_factor()
));
}
thread_policy.unlock();
}
void reserve(size_t new_capacity) {
// Implementacja rezerwacji przy użyciu polityki magazynu
}
size_t size() const {
return data.size();
}
};
// Użycie: różne konfiguracje
int main() {
// Jednowątkowy z wektorem, wzrost wykładniczy
SmartContainer<int> fast_container;
// Wielowątkowy z deque, wzrost liniowy
SmartContainer<int,
DequeStoragePolicy,
MutexThreadPolicy,
LinearGrowthPolicy> safe_container;
fast_container.push_back(42);
safe_container.push_back(99);
return 0;
}
Zalety Policy Based Design
| Zaleta | Wyjaśnienie |
|---|---|
| Abstrakcja bez kosztów | Polityki działają w czasie kompilacji, więc nie ma narzutu w czasie wykonania |
| Elastyczność | Polityki łączysz dowolnie, bez ograniczeń dziedziczenia |
| Ponowne użycie kodu | Składasz ortogonalne zachowania w różnych kombinacjach |
| Bezpieczeństwo typów | Decyzje zapadają w czasie kompilacji, typy są sprawdzane |
| Brak funkcji wirtualnych | Wywołania są bezpośrednie, kompilator może je wstawiać |
| Kontrola nad rozmiarem binarnym | Instancjonujesz tylko kombinacje, których używasz |
| Ortogonalne zagadnienia | Każda polityka zajmuje się osobnym aspektem |
| Budowa frameworków | Rozszerzalne biblioteki bez hierarchii dziedziczenia |
Wady Policy Based Design
| Wada | Wyjaśnienie |
|---|---|
| Czas kompilacji | Duża liczba szablonów znacznie wydłuża kompilację |
| Rozprzestrzenianie kodu | Każda kombinacja polityk generuje nowy kod |
| Komunikaty błędów | Błędy szablonów bywają nieczytelne i trudne w debugowaniu |
| Krzywa uczenia | To zaawansowany C++, potrzeba doświadczenia |
| Brak elastyczności w czasie wykonania | Polityki trzeba ustalić przy kompilacji |
| Wsparcie IDE | Uzupełnianie kodu i refaktoryzacja szablonów wypadają słabo |
| Debugowanie | Przechodzenie przez wygenerowany kod szablonów jest męczące |
| Dokumentacja | Niejawne interfejsy trzeba dokładnie opisać |
Biblioteka oparta na politykach: podejście Loki
Biblioteka Loki to klasyczny przykład Policy Based Design:
// Z Loki (uproszczony przykład)
template<typename T,
template<class> class CreationPolicy = OpNewCreator,
template<class> class LifetimePolicy = DefaultLifetime,
template<class, class> class ThreadingPolicy = SingleThreaded>
class SmartPtr {
// Implementacja używająca polityk
};
// Użycie: dostosuj zachowanie SmartPtr
SmartPtr<MyClass, // Typ
OpNewCreator, // Jak stworzyć
DefaultLifetime, // Jak zarządzać czasem życia
SingleThreaded> // Bezpieczeństwo wątków
my_ptr;
// Do użytku wielowątkowego:
SmartPtr<MyClass,
OpNewCreator,
DefaultLifetime,
ObjectLevelLocking> // Inna polityka wątków
thread_safe_ptr;
Kiedy używać Policy Based Design
Dobre przypadki użycia:
- Budujesz bibliotekę wielokrotnego użytku z ortogonalnymi wariantami.
- Masz kilka niezależnych osi dostosowania: magazyn, wątki, wzrost.
- Potrzebujesz abstrakcji bez narzutu.
- Wszystkie kombinacje polityk da się ustalić w czasie kompilacji.
- Piszesz framework albo bibliotekę.
Unikaj, gdy:
- Potrzebujesz tylko 1-2 punktów dostosowania, a szablony to przerost formy.
- Algorytm ma być wybierany w czasie wykonania.
- Zespół ma małe doświadczenie z szablonami.
- Czas kompilacji jest ograniczony.
- Wystarczyłoby proste dziedziczenie.
Porównanie podejść Strategy
| Aspekt | Oparte na szablonach | Oparte na politykach | Wybór w czasie wykonania |
|---|---|---|---|
| Czas wyboru | Czas kompilacji | Czas kompilacji | Czas wykonania |
| Narzut | Zero | Zero | 1-3 cykle (vtable) |
| Osie dostosowania | 1-2 | Wiele (ortogonalne) | Jedna (polimorfizm) |
| Czas kompilacji | Dobry | Słaby (ciężkie szablony) | Szybki |
| Rozmiar binarny | Umiarkowany | Może być duży | Mały |
| Elastyczność w czasie wykonania | Brak | Brak | Pełna |
| Bezpieczeństwo typów | Silne | Silne | Słabe |
| Krzywa uczenia | Łatwa | Trudna | Łatwa |
| Wsparcie IDE | Dobre | Słabe | Dobre |
| Odpowiednie dla | Prostych algorytmów | Frameworków bibliotecznych | Kodu aplikacji |
Przykład: pula połączeń bazodanowych
// Polityki do dostosowania
struct FIFOQueuePolicy {
template<typename T>
using container = std::queue<T>;
};
struct LIFOStackPolicy {
template<typename T>
using container = std::stack<T>;
};
struct NoValidationPolicy {
void validate(const Connection& conn) const {}
};
struct PingValidationPolicy {
void validate(const Connection& conn) const {
conn.ping(); // Zweryfikuj, czy połączenie żyje
}
};
// Pula połączeń oparta na politykach
template<typename QueuePolicy, typename ValidationPolicy>
class ConnectionPool {
private:
typename QueuePolicy::template container<Connection> available;
ValidationPolicy validator;
public:
Connection get_connection() {
if (available.empty()) {
return Connection::create();
}
auto conn = available.front(); // lub top() dla LIFO
available.pop();
validator.validate(conn); // Może być no-op w zależności od polityki
return conn;
}
void return_connection(Connection conn) {
available.push(conn);
}
};
// Konfiguracje
using FastPool = ConnectionPool<FIFOQueuePolicy, NoValidationPolicy>;
using SafePool = ConnectionPool<LIFOStackPolicy, PingValidationPolicy>;
Podsumowanie
Policy Based Design pozwala bibliotece mieć kilka niezależnych punktów dostosowania bez kosztu w czasie wykonania. Działa, bo każda polityka jest typem, który kompilator widzi na wylot. Rachunek przychodzi przy kompilacji: więcej instancjacji, dłuższy build i komunikaty błędów, które trudno czytać. PBD ma sens w bibliotekach i frameworkach. W kodzie aplikacji prostsze warianty Strategy zwykle wystarczą.
Patrz też
- Strategy oparty na szablonach (kompilacja): podstawowa strategia szablonowa.
- Wybór w czasie wykonania (funkcje wirtualne): strategia z funkcjami wirtualnymi.
- Cechy i polityki: cechy opisują typy, polityki dostarczają zachowanie.
- Książka Andrei Alexandrescu "Modern C++ Design": klasyczna praca o Policy Based Design.