التكوين والواجهات والتصميم
تكوين رئيسي على الميراث، واجهات مجردة خالصة، مبادئ التصميم (ISP، DIP)، std::variant كبديل للميراث، ومحو الكتابة.
لماذا التكوين على الميراث؟
يعد "تفضيل التكوين على الميراث" أحد أهم المبادئ في تصميم OOP. ينشئ الوراثة اقترانًا محكمًا بين الفئات الأساسية والفئات المشتقة - التغييرات في التموج الأساسي عبر التسلسل الهرمي بأكمله. التركيب (وجود عضو من نوع آخر) أكثر مرونة: يمكنك تغيير النوع المتضمن في وقت التشغيل، ودمج السلوكيات بحرية، وتجنب مشكلة الفئة الأساسية الهشة.
يكون الوراثة مناسبًا عندما تحتاج إلى تعدد الأشكال (معالجة الأنواع المختلفة بشكل موحد من خلال واجهة مشتركة). بالنسبة لإعادة استخدام التعليمات البرمجية وحدها، يكون التكوين دائمًا متفوقًا.
التكوين في الممارسة
بدلاً من وراثة السلوك، احتوائه.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
مسح النوع يتيح لك تخزين أي وظيفة قابلة للاستدعاء، أو وظيفة lambda، أو functor - بتوقيع متوافق في كائن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&- يتيح لك ذلك تبديل التطبيقات للاختبار (المستودعات الوهمية) وبيئات مختلفة.
تصبح التسلسلات الهرمية العميقة للميراث (أكثر من 4 مستويات) هشة ويصعب التفكير فيها. يمكن أن يكون للتغيير إلى الفئة الأساسية تأثيرات غير متوقعة على الأحفاد البعيدين. مشكلة الطبقة الأساسية الهشة تجعل الصيانة كابوسًا. تفضل التسلسلات الهرمية المسطحة (1-2 مستويات)، واستخدم واجهات لتعدد الأشكال، وأنشئ سلوكيات باستخدام الكائنات الأعضاء أوstd::variant. إذا تجاوز عمق التسلسل الهرمي لديك 3 مستويات، فمن المؤكد تقريبًا أنها رائحة تصميم.
- التركيب أكثر مرونة من الوراثة — استخدمه لإعادة استخدام التعليمات البرمجية (علاقات has-a)
- واجهات مجردة خالصة تحدد العقود وتمكن مبدأ عكس التبعية
- يوفر
std::variantمعstd::visitتعدد الأشكال في وقت الترجمة لمجموعات الكتابة المغلقة std::functionيوفر مسح الكتابة - قم بتخزين أي شيء قابل للاستدعاء بتوقيع متوافق- حافظ على التسلسل الهرمي للميراث ثابتًا (المستويات 1-2) وفضل الواجهات على الطبقات الأساسية الملموسة
- تطبيق ISP (الواجهات الصغيرة) وDIP (يعتمد على التجريدات) لتصميمات قابلة للصيانة وقابلة للاختبار
اختبار — اختبر معرفتك
(15 نقطة خبرة)1. متى يجب تفضيل التأليف على الميراث؟
2. ما هي الميزة التي يتمتع بها`std::variant`على التسلسل الهرمي التقليدي للميراث؟
3. ماذا ينص مبدأ انعكاس التبعية (DIP)؟