Μετάβαση στο περιεχόμενο

Σύνθεση, Διεπαφές & Σχεδίαση

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?

Η "σύνθεση εύνοιας έναντι της κληρονομικότητας" είναι μια από τις πιο σημαντικές αρχές στο σχεδιασμό OOP. Η κληρονομικότητα δημιουργεί μια στενή σύζευξη μεταξύ βασικών και παράγωγων κλάσεων — αλλαγές στον κυματισμό βάσης σε ολόκληρη την ιεραρχία. Η Σύνθεση (έχοντας ένα μέλος άλλου τύπου) είναι πιο ευέλικτη: μπορείτε να αλλάξετε τον περιεχόμενο τύπο κατά την εκτέλεση, να συνδυάσετε ελεύθερα συμπεριφορές και να αποφύγετε το πρόβλημα της εύθραυστης κλάσης βάσης.

Η κληρονομικότητα είναι κατάλληλη όταν χρειάζεστε πολυμορφισμό (την αντιμετώπιση διαφορετικών τύπων ομοιόμορφα μέσω μιας κοινής διεπαφής). Μόνο για την επαναχρησιμοποίηση κώδικα, η σύνθεση είναι σχεδόν πάντα ανώτερη.

Σύνθεση στην πράξη

Αντί να κληρονομήσετε συμπεριφορά, περιλάβετε την. ΈναCarδεν είναιEngine— έχει έναEngine. Αυτό σας δίνει την ελευθερία να ανταλλάσσετε κινητήρες, να δοκιμάζετε εξαρτήματα ανεξάρτητα και να εξελίσσετε κάθε κατηγορία χωρίς να σπάζετε την άλλη.

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;
}

Καθαρές αφηρημένες διεπαφές

Μια καθαρή αφηρημένη διεπαφή είναι μια κλάση με μόνο καθαρές εικονικές συναρτήσεις και έναν εικονικό καταστροφέα — χωρίς δεδομένα, χωρίς υλοποίηση. Αυτό ορίζει μια σύμβαση που πρέπει να εκπληρώσουν οι παραγόμενες κλάσεις. Υποστηρίζει την Αρχή Αναστροφής Εξάρτησης (DIP): εξαρτάται από αφαιρέσεις, όχι συγκεκριμένες κλάσεις.

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 — Κληρονομική εναλλακτική

Το std::variant είναι μια ένωση ασφαλή για τον τύπο που μπορεί να χωρέσει έναν από πολλούς τύπους. Σε συνδυασμό με το std::visit, παρέχει πολυμορφισμό χρόνου μεταγλώττισης — μια εναλλακτική στις εικονικές συναρτήσεις όταν το σύνολο των τύπων είναι γνωστό κατά το χρόνο μεταγλώττισης. Αποφεύγει την κατανομή σωρού και τα γενικά έξοδα εικονικής αποστολής.

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;
}

Πληκτρολογήστε Erasure με συνάρτηση std::

Το Type erasure σάς επιτρέπει να αποθηκεύετε οποιαδήποτε δυνατότητα κλήσης — συνάρτηση, λάμδα, συνάρτηση — με συμβατή υπογραφή σε ένα μεμονωμένο αντικείμενοstd::function. Αυτή είναι μια μορφή πολυμορφισμού χρόνου εκτέλεσης χωρίς ρητή κληρονομικότητα.

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;
}
Βέλτιστη πρακτική

Αρχή διαχωρισμού διεπαφής (ISP): προτιμήστε μικρές, εστιασμένες διεπαφές έναντι μεγάλων, μονολιθικών. Μια κλάση δεν πρέπει να αναγκαστεί να εφαρμόσει μεθόδους που δεν χρησιμοποιεί. Διαχωρίστε τοIAnimalσεIFlyableκαιISwimmableαν δεν πετούν και κολυμπούν όλα τα ζώα.

Αρχή αντιστροφής εξάρτησης (DIP): οι ενότητες υψηλού επιπέδου πρέπει να εξαρτώνται από αφαιρέσεις (διεπαφές) και όχι από συγκεκριμένες υλοποιήσεις. Περάστε τοIRepository&αντί για τοMySQLDatabase&— αυτό σας επιτρέπει να ανταλλάξετε υλοποιήσεις για δοκιμές (ψευδείς αποθήκες) και διαφορετικά περιβάλλοντα.

Παγίδα

Οι βαθιές ιεραρχίες κληρονομικότητας (4+ επίπεδα) γίνονται εύθραυστες και δύσκολα αιτιολογημένες. Μια αλλαγή σε μια βασική κλάση μπορεί να έχει απροσδόκητα αποτελέσματα σε μακρινούς απογόνους. Το εύθραυστο πρόβλημα της βασικής κατηγορίας κάνει τη συντήρηση εφιάλτη. Προτιμήστε επίπεδες ιεραρχίες (1-2 επίπεδα), χρησιμοποιήστε διεπαφές για πολυμορφισμό και συνθέστε συμπεριφορές με αντικείμενα μελών ήstd::variant. Εάν το βάθος της ιεραρχίας σας υπερβαίνει τα 3 επίπεδα, είναι σχεδόν σίγουρα μια μυρωδιά σχεδιασμού.

Βασικά συμπεράσματα
  • Η Σύνθεση είναι πιο ευέλικτη από την κληρονομικότητα — χρησιμοποιήστε την για επαναχρησιμοποίηση κώδικα (έχει σχέσεις)
  • Οι καθαρές αφηρημένες διεπαφές ορίζουν συμβόλαια και ενεργοποιούν την αρχή της αντιστροφής εξάρτησης
  • Το std::variant με τοstd::visitπαρέχει πολυμορφισμό χρόνου μεταγλώττισης για σύνολα κλειστού τύπου
  • Το std::function παρέχει διαγραφή τύπου — αποθηκεύστε οποιονδήποτε καλούμενο με συμβατή υπογραφή
  • Διατηρήστε τις ιεραρχίες κληρονομικότητας επίπεδες (1-2 επίπεδα) και προτιμήστε τις διεπαφές σε σχέση με τις συγκεκριμένες βασικές κατηγορίες
  • Εφαρμόστε ISP (μικρές διεπαφές) και DIP (εξαρτάται από αφαιρέσεις) για συντηρήσιμα, ελεγχόμενα σχέδια

Κουίζ — Δοκιμάστε τις γνώσεις σας

(15 XP)

1. Πότε πρέπει να προτιμάτε τη σύνθεση από την κληρονομικότητα;

2. Τι πλεονέκτημα έχει το`std::variant`σε σχέση με μια παραδοσιακή ιεραρχία κληρονομικότητας;

3. Τι δηλώνει η Αρχή Αντιστροφής Εξάρτησης (DIP);