Přeskočit na obsah

Složení, rozhraní a design

Ovládněte kompozici nad dědičností, čistě abstraktní rozhraní, principy návrhu (ISP, DIP), std::variant jako alternativu k dědičnosti a vymazání typu.

Proč složení přes dědičnost?

"Upřednostňujte složení před dědičností" je jedním z nejdůležitějších principů v návrhu OOP. Dědičnost vytváří těsné spojení mezi základními a odvozenými třídami – změny základny se prolínají celou hierarchií. Složení (s členem jiného typu) je flexibilnější: můžete změnit obsažený typ za běhu, volně kombinovat chování a vyhnout se problému s křehkou základní třídou.

Dědičnost je vhodná, když potřebujete polymorfismus (jednotné zacházení s různými typy prostřednictvím společného rozhraní). Pouze pro opětovné použití kódu je složení téměř vždy lepší.

Složení v praxi

Místo dědění chování jej obsahujte.CarneníEngine– má**Engine. To vám dává svobodu vyměňovat motory, testovat komponenty nezávisle a vyvíjet každou třídu, aniž byste porušili druhou.

composition.cpp
#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;
}

Čistá abstraktní rozhraní

Čisté abstraktní rozhraní je třída pouze s čistě virtuálními funkcemi a virtuálním destruktorem – žádná data, žádná implementace. To definuje smlouvu, kterou musí odvozené třídy splnit. Podporuje Princip inverze závislosti (DIP): závisí na abstrakcích, nikoli na konkrétních třídách.

interface.cpp
#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 — Alternativa dědičnosti

std::variant je typově bezpečný spoj, který pojme jeden z několika typů. V kombinaci s std::visit poskytuje polymorfismus v době kompilace — alternativu k virtuálním funkcím, pokud je v době kompilace známa sada typů. Vyhne se alokaci haldy a režii virtuálního odeslání.

variant_poly.cpp
#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;
}

Typ Vymazání s funkcí std::

Type erasure vám umožňuje uložit jakoukoli volatelnou funkci – funkci, lambda, funktor – s kompatibilní signaturou v jediném objektustd::function. Toto je forma runtime polymorfismu bez explicitní dědičnosti.

type_erasure.cpp
#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;
}
Osvědčený postup

Princip segregace rozhraní (ISP): preferujte malá, soustředěná rozhraní před velkými, monolitickými. Třída by neměla být nucena implementovat metody, které nepoužívá. RozdělteIAnimalnaIFlyableaISwimmable, pokud všechna zvířata nelétají a plavou.

Princip inverze závislosti (DIP): moduly na vysoké úrovni by měly záviset na abstrakcích (rozhraní), nikoli na konkrétních implementacích. PředejteIRepository&místoMySQLDatabase&– to vám umožní zaměnit implementace pro testování (falešné repozitáře) a různá prostředí.

Úskalí

Hluboké hierarchie dědičnosti (4+ úrovně) se stávají křehkými a je těžké o nich uvažovat. Změna základní třídy může mít neočekávané účinky na vzdálené potomky. Problém s křehkou základní třídou dělá z údržby noční můru. Preferujte ploché hierarchie (1-2 úrovně), používejte rozhraní pro polymorfismus a sestavujte chování s členskými objekty nebostd::variant. Pokud hloubka vaší hierarchie přesahuje 3 úrovně, je to téměř jistě designový zápach.

Klíčové poznatky
  • Složení je flexibilnější než dědičnost – použijte jej pro opětovné použití kódu (má vztahy)
  • Čistě abstraktní rozhraní definují smlouvy a umožňují princip inverze závislosti
  • std::variant sstd::visitposkytuje polymorfismus v době kompilace pro sady uzavřeného typu
  • std::function poskytuje mazání typů — uložte všechny volatelné s kompatibilním podpisem
  • Udržujte hierarchie dědičnosti ploché (1-2 úrovně) a upřednostňujte rozhraní před konkrétními základními třídami
  • Použijte ISP (malá rozhraní) a DIP (závisí na abstrakcích) pro udržovatelné, testovatelné návrhy

Kvíz — Otestujte své znalosti

(15 XP)

1. Kdy byste měli upřednostnit složení před dědictvím?

2. Jakou výhodu má`std::variant`oproti tradiční hierarchii dědičnosti?

3. Co říká Princip inverze závislosti (DIP)?