ترکیب، رابطها و طراحی
ترکیب بر وراثت، رابطهای انتزاعی محض، اصول طراحی (ISP، DIP)، std::variant بهعنوان جایگزین وراثت و پاکسازی نوع را تسلط یابید.
چرا ترکیب بر وراثت؟
«ترکیب را بر وراثت ترجیح دهید» یکی از مهمترین اصول در طراحی شیءگرا است. وراثت یک وابستگی محکم بین کلاسهای پایه و مشتقشده ایجاد میکند — تغییرات در پایه در کل سلسلهمراتب منتشر میشود. ترکیب (Composition) (داشتن یک عضو از نوع دیگر) انعطافپذیرتر است: میتوانید نوع شاملشده را در زمان اجرا تغییر دهید، رفتارها را آزادانه ترکیب کنید و از مشکل کلاس پایه شکننده اجتناب کنید.
وراثت زمانی مناسب است که به چندریختی (رفتار یکسان با انواع مختلف از طریق یک رابط مشترک) نیاز دارید. برای استفاده مجدد از کد تنها، ترکیب تقریباً همیشه برتر است.
ترکیب در عمل
به جای ارثبری رفتار، آن را شامل کنید. یک Car یک Engine نیست — بلکه یک Engine دارد. این آزادی تعویض موتورها، تست مؤلفهها بهصورت مستقل و تکامل هر کلاس بدون شکستن دیگری را به شما میدهد.
#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) پشتیبانی میکند: به انتزاعات وابسته باشید، نه به کلاسهای مشخص.
#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، چندریختی زمان کامپایل فراهم میکند — جایگزینی برای توابع مجازی وقتی مجموعه انواع در زمان کامپایل مشخص است. از تخصیص هیپ و سربار ارسال مجازی اجتناب میکند.
#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;
}پاکسازی نوع با std::function
پاکسازی نوع (Type Erasure) به شما اجازه میدهد هر قابلفراخوانی — تابع، لامبدا، فانکتور — با امضای سازگار را در یک شیء std::function واحد ذخیره کنید. این شکلی از چندریختی زمان اجرا بدون وراثت صریح است.
#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& — این اجازه تعویض پیادهسازیها برای تست (مخازن شبیهسازی) و محیطهای مختلف را میدهد.
سلسلهمراتب وراثت عمیق (بیش از ۴ سطح) شکننده و دشوار در استدلال میشوند. تغییر در یک کلاس پایه میتواند اثرات غیرمنتظرهای بر فرزندان دور داشته باشد. مشکل کلاس پایه شکننده نگهداری را به کابوس تبدیل میکند. سلسلهمراتب مسطح (۱ تا ۲ سطح) را ترجیح دهید، از رابطها برای چندریختی استفاده کنید و رفتارها را با اشیاء عضو یا std::variant ترکیب کنید. اگر عمق سلسلهمراتب شما بیش از ۳ سطح باشد، تقریباً مطمئناً نشانهای از طراحی نامناسب است.
- ترکیب انعطافپذیرتر از وراثت است — برای استفاده مجدد از کد (روابط «دارد-یک») از آن استفاده کنید
- رابطهای انتزاعی محض قراردادها را تعریف میکنند و اصل وارونگی وابستگی را فعال میکنند
std::variantباstd::visitچندریختی زمان کامپایل برای مجموعههای نوع بسته فراهم میکندstd::functionپاکسازی نوع فراهم میکند — هر قابلفراخوانی با امضای سازگار را ذخیره کنید- سلسلهمراتب وراثت را مسطح نگه دارید (۱ تا ۲ سطح) و رابطها را بر کلاسهای پایه مشخص ترجیح دهید
- ISP (رابطهای کوچک) و DIP (وابستگی به انتزاعات) را برای طراحیهای قابل نگهداری و تستپذیر اعمال کنید
آزمون — دانش خود را بسنجید
(15 امتیاز)1. چه زمانی باید ترکیب را بر وراثت ترجیح دهید؟
2. `std::variant` چه مزیتی نسبت به سلسلهمراتب وراثت سنتی دارد؟
3. اصل وارونگی وابستگی (DIP) چه میگوید؟