Composition, Interfaces & Design
Master composition over inheritance, pure abstract interfaces, design principles (ISP, DIP), std::variant as an alternative to inheritance, and type erasure.
Why Composition Over Inheritance?
"Favor composition over inheritance" is one of the most important principles in OOP design. Inheritance creates a tight coupling between base and derived classes — changes to the base ripple through the entire hierarchy. Composition (having a member of another type) is more flexible: you can change the contained type at runtime, combine behaviors freely, and avoid the fragile base class problem.
Inheritance is appropriate when you need polymorphism (treating different types uniformly through a common interface). For code reuse alone, composition is almost always superior.
Composition in Practice
Instead of inheriting behavior, contain it. ACaris not anEngine— it has anEngine. This gives you freedom to swap engines, test components independently, and evolve each class without breaking the other.
#include <iostream>
#include <string>
#include <memory>
class Engine {
int horsepower_;
std::string type_;
public:
Engine(int hp, const std::string& type)
: horsepower_{hp}, type_{type} {}
void start() const {
std::cout << type_ << " engine (" << horsepower_ << "hp) started\n";
}
int horsepower() const { return horsepower_; }
};
class Transmission {
int gears_;
public:
explicit Transmission(int gears) : gears_{gears} {}
void shift(int gear) const {
if (gear >= 1 && gear <= gears_) {
std::cout << "Shifted to gear " << gear << '\n';
}
}
};
// Car COMPOSES Engine and Transmission (has-a, not is-a)
class Car {
std::string model_;
Engine engine_;
Transmission trans_;
public:
Car(const std::string& model, Engine engine, Transmission trans)
: model_{model}
, engine_{std::move(engine)}
, trans_{std::move(trans)} {}
void drive() const {
std::cout << model_ << ": ";
engine_.start();
trans_.shift(1);
}
};
int main() {
Engine v8{450, "V8"};
Transmission auto6{6};
Car mustang{"Mustang", std::move(v8), std::move(auto6)};
mustang.drive();
return 0;
}Pure Abstract Interfaces
A pure abstract interface is a class with only pure virtual functions and a virtual destructor — no data, no implementation. This defines a contract that derived classes must fulfill. It supports the Dependency Inversion Principle (DIP): depend on abstractions, not concrete classes.
#include <iostream>
#include <memory>
#include <string>
#include <vector>
// Pure abstract interface — the contract
class IRepository {
public:
virtual ~IRepository() = default;
virtual void save(const std::string& key, const std::string& value) = 0;
virtual std::string load(const std::string& key) const = 0;
virtual bool exists(const std::string& key) const = 0;
};
// Concrete implementation: in-memory storage
class MemoryRepository : public IRepository {
std::vector<std::pair<std::string, std::string>> data_;
public:
void save(const std::string& key, const std::string& value) override {
for (auto& [k, v] : data_) {
if (k == key) { v = value; return; }
}
data_.emplace_back(key, value);
}
std::string load(const std::string& key) const override {
for (const auto& [k, v] : data_) {
if (k == key) return v;
}
return "";
}
bool exists(const std::string& key) const override {
for (const auto& [k, v] : data_) {
if (k == key) return true;
}
return false;
}
};
// Service depends on the INTERFACE, not the concrete class (DIP)
class UserService {
std::unique_ptr<IRepository> repo_;
public:
explicit UserService(std::unique_ptr<IRepository> repo)
: repo_{std::move(repo)} {}
void register_user(const std::string& username) {
if (repo_->exists(username)) {
std::cout << username << " already exists\n";
return;
}
repo_->save(username, "active");
std::cout << username << " registered\n";
}
};
int main() {
auto repo = std::make_unique<MemoryRepository>();
UserService service{std::move(repo)};
service.register_user("alice"); // alice registered
service.register_user("bob"); // bob registered
service.register_user("alice"); // alice already exists
return 0;
}std::variant — Alternativă de moștenire
std::variant este o uniune sigură de tip care poate ține unul dintre mai multe tipuri. Combinat cu std::visit, oferă polimorfism în timp de compilare — o alternativă la funcțiile virtuale atunci când setul de tipuri este cunoscut în momentul compilării. Se evită alocarea heap-ului și supraîncărcarea virtuală de expediere.
#include <iostream>
#include <variant>
#include <vector>
#include <cmath>
#include <numbers>
struct Circle { double radius; };
struct Square { double side; };
struct Triangle { double base, height; };
// Type alias for the closed set of shapes
using Shape = std::variant<Circle, Square, Triangle>;
// Visitor: a callable that handles each type
struct AreaVisitor {
double operator()(const Circle& c) const {
return std::numbers::pi * c.radius * c.radius;
}
double operator()(const Square& s) const {
return s.side * s.side;
}
double operator()(const Triangle& t) const {
return 0.5 * t.base * t.height;
}
};
struct NameVisitor {
std::string operator()(const Circle&) const { return "Circle"; }
std::string operator()(const Square&) const { return "Square"; }
std::string operator()(const Triangle&) const { return "Triangle"; }
};
int main() {
std::vector<Shape> shapes{
Circle{5.0},
Square{4.0},
Triangle{3.0, 6.0}
};
for (const auto& shape : shapes) {
std::string name = std::visit(NameVisitor{}, shape);
double area = std::visit(AreaVisitor{}, shape);
std::cout << name << ": area = " << area << '\n';
}
return 0;
}Tastați ștergere cu funcția std::
Ștergerea tipului vă permite să stocați orice apelabil - funcție, lambda, functor - cu o semnătură compatibilă într-un singur obiectstd::function. Aceasta este o formă de polimorfism de rulare fără moștenire explicită.
#include <iostream>
#include <functional>
#include <vector>
#include <string>
// std::function erases the concrete callable type
using Callback = std::function<void(const std::string&)>;
class EventEmitter {
std::vector<Callback> listeners_;
public:
void on(Callback cb) {
listeners_.push_back(std::move(cb));
}
void emit(const std::string& event) const {
for (const auto& cb : listeners_) {
cb(event);
}
}
};
// A functor
struct FileLogger {
void operator()(const std::string& msg) const {
std::cout << "[File] " << msg << '\n';
}
};
void console_log(const std::string& msg) {
std::cout << "[Console] " << msg << '\n';
}
int main() {
EventEmitter emitter;
// Register different callable types — all stored as std::function
emitter.on(console_log); // function pointer
emitter.on(FileLogger{}); // functor
emitter.on([](const std::string& m) { // lambda
std::cout << "[Lambda] " << m << '\n';
});
emitter.emit("user_login");
return 0;
}Principiul de segregare a interfeței (ISP): preferați interfețele mici, concentrate față de cele mari, monolitice. O clasă nu ar trebui să fie forțată să implementeze metode pe care nu le folosește. ÎmpărțițiIAnimalînIFlyableșiISwimmabledacă nu toate animalele zboară și înoată.
Principiul inversării dependenței (DIP): modulele de nivel înalt ar trebui să depindă de abstracții (interfețe), nu de implementări concrete. TrecețiIRepository&în loc deMySQLDatabase&— acest lucru vă permite să schimbați implementările pentru testare (depozite simulate) și diferite medii.
Ierarhiile profunde de moștenire (4+ niveluri) devin fragile și greu de raționat. O schimbare la o clasă de bază poate avea efecte neașteptate asupra descendenților îndepărtați. Problema fragilă a clasei de bază face ca întreținerea să fie un coșmar. Preferați ierarhiile plate (1-2 nivele), utilizați interfețe pentru polimorfism și compuneți comportamente cu obiecte membre saustd::variant. Dacă adâncimea ierarhiei dvs. depășește 3 niveluri, este aproape sigur un miros de design.
- Compoziția este mai flexibilă decât moștenirea - folosește-o pentru reutilizarea codului (relații are-a)
- Interfețele pure abstracte definesc contractele și activează Principiul inversării dependenței
std::variantcustd::visitoferă polimorfism în timp de compilare pentru seturi de tip închisstd::functionoferă ștergerea tipului - stocați orice apelabil cu o semnătură compatibilă- Keep inheritance hierarchies flat (1-2 levels) and prefer interfaces over concrete base classes
- Apply ISP (small interfaces) and DIP (depend on abstractions) for maintainable, testable designs
Test — Verifică-ți cunoștințele
(15 XP)1. When should you prefer composition over inheritance?
2. Ce avantaj are`std::variant`față de o ierarhie tradițională de moștenire?
3. Ce afirmă principiul inversării dependenței (DIP)?