Chuyển đến nội dung

Thành phần, Giao diện & Thiết kế

Thành phần tổng thể về kế thừa, giao diện trừu tượng thuần túy, nguyên tắc thiết kế (ISP, DIP), std::variant thay thế cho kế thừa và xóa kiểu.

Tại sao thành phần lại được kế thừa?

"Thành phần ưu tiên hơn kế thừa" là một trong những nguyên tắc quan trọng nhất trong thiết kế OOP. Tính kế thừa tạo ra sự liên kết chặt chẽ giữa các lớp cơ sở và lớp dẫn xuất — những thay đổi đối với gợn sóng cơ sở thông qua toàn bộ hệ thống phân cấp. Thành phần (có thành viên thuộc loại khác) linh hoạt hơn: bạn có thể thay đổi loại được chứa trong thời gian chạy, kết hợp các hành vi một cách tự do và tránh vấn đề lớp cơ sở dễ hỏng.

Kế thừa phù hợp khi bạn cần đa hình (xử lý thống nhất các loại khác nhau thông qua một giao diện chung). Riêng đối với việc tái sử dụng mã, bố cục hầu như luôn vượt trội.

Thành phần trong thực tế

Thay vì kế thừa hành vi, hãy chứa nó.Carkhông phải làEngine- nó cóEngine. Điều này cho phép bạn tự do trao đổi công cụ, kiểm tra các thành phần một cách độc lập và phát triển từng lớp mà không phá vỡ lớp kia.

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

Giao diện trừu tượng thuần túy

Giao diện trừu tượng thuần túy là một lớp chỉ có các hàm ảo thuần túy và hàm hủy ảo — không có dữ liệu, không có triển khai. Điều này xác định một hợp đồng mà các lớp dẫn xuất phải thực hiện. Nó hỗ trợ Nguyên tắc đảo ngược phụ thuộc (DIP): phụ thuộc vào sự trừu tượng hóa, không phải các lớp cụ thể.

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 — Thay thế kế thừa

std::variant là liên kết loại an toàn có thể chứa một trong nhiều loại. Kết hợp với std::visit, nó cung cấp tính đa hình trong thời gian biên dịch — một giải pháp thay thế cho các hàm ảo khi biết tập hợp các loại tại thời điểm biên dịch. Nó tránh được việc phân bổ heap và chi phí điều phối ảo.

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

Nhập Xóa bằng std::function

Xóa kiểu cho phép bạn lưu trữ bất kỳ lệnh gọi nào — hàm, lambda, functor — bằng chữ ký tương thích trong một đối tượngstd::functionduy nhất. Đây là một dạng đa hình thời gian chạy mà không có sự kế thừa rõ ràng.

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;
}
Thực hành tốt

Nguyên tắc phân chia giao diện (ISP): thích các giao diện nhỏ, tập trung hơn các giao diện lớn, nguyên khối. Một lớp không nên bị buộc phải thực hiện các phương thức mà nó không sử dụng. TáchIAnimalthànhIFlyablevàISwimmablenếu không phải tất cả động vật đều bay và bơi.

Nguyên tắc đảo ngược phụ thuộc (DIP): các mô-đun cấp cao phải phụ thuộc vào sự trừu tượng hóa (giao diện), chứ không phải việc triển khai cụ thể. ChuyểnIRepository&thay vìMySQLDatabase&— điều này cho phép bạn hoán đổi các triển khai để thử nghiệm (kho lưu trữ mô phỏng) và các môi trường khác nhau.

Lỗi thường gặp

Hệ thống phân cấp thừa kế sâu (4 cấp trở lên) trở nên mong manh và khó giải thích. Việc thay đổi lớp cơ sở có thể gây ra những tác động không mong muốn đối với con cháu ở xa. Vấn đề về lớp cơ sở dễ vỡ khiến việc bảo trì trở thành một cơn ác mộng. Ưu tiên các hệ thống phân cấp phẳng (1-2 cấp độ), sử dụng giao diện cho tính đa hình và soạn thảo các hành vi với các đối tượng thành viên hoặcstd::variant. Nếu độ sâu phân cấp của bạn vượt quá 3 cấp thì gần như chắc chắn đó là mùi thiết kế.

Điểm chính
  • Thành phần linh hoạt hơn tính kế thừa — sử dụng nó để tái sử dụng mã (mối quan hệ has-a)
  • Giao diện trừu tượng thuần túy xác định hợp đồng và kích hoạt Nguyên tắc đảo ngược phụ thuộc
  • std::variant vớistd::visitcung cấp tính đa hình tại thời điểm biên dịch cho các tập hợp kiểu đóng
  • std::function cung cấp khả năng xóa kiểu — lưu trữ mọi lệnh gọi có thể bằng chữ ký tương thích
  • Giữ hệ thống phân cấp kế thừa bằng phẳng (1-2 cấp) và ưu tiên giao diện hơn các lớp cơ sở cụ thể
  • Áp dụng ISP (giao diện nhỏ) và DIP (phụ thuộc vào tính trừu tượng) cho các thiết kế có thể kiểm tra và bảo trì

Câu đố — Kiểm tra kiến thức

(15 XP)

1. Khi nào bạn nên thích thành phần hơn kế thừa?

2. What advantage does`std::variant`have over a traditional inheritance hierarchy?

3. What does the Dependency Inversion Principle (DIP) state?