कंपोज़िशन, इंटरफ़ेस और डिज़ाइन
इनहेरिटेंस पर कंपोज़िशन को प्राथमिकता, प्योर ऐब्स्ट्रैक्ट इंटरफ़ेस, डिज़ाइन सिद्धांत (ISP, DIP), इनहेरिटेंस के विकल्प के रूप में std::variant, और टाइप इरेज़र में महारत हासिल करें।
इनहेरिटेंस की बजाय कंपोज़िशन क्यों?
“इनहेरिटेंस की बजाय कंपोज़िशन चुनें” OOP डिज़ाइन के सबसे महत्वपूर्ण सिद्धांतों में से एक है। इनहेरिटेंस बेस और डिराइव्ड क्लास के बीच कसा हुआ कपलिंग बनाता है — बेस में बदलाव पूरी हाइरार्की में फैल जाते हैं। कंपोज़िशन (किसी दूसरे टाइप का मेंबर रखना) अधिक लचीला है: आप रनटाइम पर शामिल टाइप बदल सकते हैं, व्यवहारों को खुलकर जोड़ सकते हैं, और fragile base class समस्या से बच सकते हैं।
इनहेरिटेंस तब उचित है जब आपको पॉलीमॉर्फ़िज़्म चाहिए (अलग-अलग टाइप को एक साझा इंटरफ़ेस के ज़रिए एक समान बरतना)। केवल कोड रीयूज़ के लिए, कंपोज़िशन लगभग हमेशा बेहतर है।
व्यवहार में कंपोज़िशन
व्यवहार को इनहेरिट करने के बजाय उसे रखें। 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;
}प्योर ऐब्स्ट्रैक्ट इंटरफ़ेस
प्योर ऐब्स्ट्रैक्ट इंटरफ़ेस (pure abstract interface) ऐसी क्लास है जिसमें केवल प्योर वर्चुअल फ़ंक्शन और एक वर्चुअल डिस्ट्रक्टर होता है — कोई डेटा नहीं, कोई इम्प्लीमेंटेशन नहीं। यह एक अनुबंध (contract) तय करता है जिसे डिराइव्ड क्लास को पूरा करना होता है। यह डिपेंडेंसी इन्वर्शन प्रिंसिपल (Dependency Inversion Principle) (DIP) का समर्थन करता है: अमूर्तताओं (abstractions) पर निर्भर रहें, कंक्रीट क्लास पर नहीं।
#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 के साथ मिलाकर यह vtable-रहित पॉलीमॉर्फ़िज़्म देता है — जब टाइप का समूह कंपाइल टाइम पर ज्ञात हो, तब वर्चुअल फ़ंक्शन का एक विकल्प। विज़िट रनटाइम पर संग्रहीत टाइप के आधार पर सही कोड चुनता है, लेकिन इसके लिए हीप आवंटन या वर्चुअल डिस्पैच (vtable) का ओवरहेड नहीं लगता।
#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;
}इंटरफ़ेस सेग्रेगेशन प्रिंसिपल (Interface Segregation Principle, ISP): बड़े, भारी-भरकम (monolithic) इंटरफ़ेस की जगह छोटे, केंद्रित इंटरफ़ेस को प्राथमिकता दें। किसी क्लास को ऐसे मेथड लागू करने के लिए बाध्य नहीं होना चाहिए जिनका वह उपयोग नहीं करती। अगर सभी जानवर उड़ते और तैरते नहीं हैं, तो IAnimal को IFlyable और ISwimmable में बाँट दें।
डिपेंडेंसी इन्वर्शन प्रिंसिपल (Dependency Inversion Principle, DIP): उच्च-स्तरीय मॉड्यूल को अमूर्तताओं (इंटरफ़ेस) पर निर्भर होना चाहिए, कंक्रीट इम्प्लीमेंटेशन पर नहीं। MySQLDatabase& की जगह IRepository& पास करें — इससे आप टेस्टिंग (मॉक रिपॉज़िटरी) और अलग-अलग परिवेशों के लिए इम्प्लीमेंटेशन बदल सकते हैं।
गहरी इनहेरिटेंस हाइरार्की (4+ स्तर) नाज़ुक और समझने में कठिन हो जाती हैं। बेस क्लास में बदलाव का दूर के वंशजों पर अप्रत्याशित प्रभाव पड़ सकता है। fragile base class समस्या रखरखाव को दुःस्वप्न बना देती है। सपाट हाइरार्की (आमतौर पर 2-3 स्तर से अधिक नहीं) को प्राथमिकता दें, पॉलीमॉर्फ़िज़्म के लिए इंटरफ़ेस इस्तेमाल करें, और व्यवहारों को मेंबर ऑब्जेक्ट या std::variant से जोड़ें। अगर आपकी हाइरार्की की गहराई 3 स्तर से अधिक हो जाए, तो यह लगभग निश्चित रूप से डिज़ाइन की खराबी (design smell) का संकेत है।
- कंपोज़िशन इनहेरिटेंस से अधिक लचीला है — इसे कोड रीयूज़ के लिए इस्तेमाल करें (has-a संबंध)
- प्योर ऐब्स्ट्रैक्ट इंटरफ़ेस अनुबंध तय करते हैं और डिपेंडेंसी इन्वर्शन प्रिंसिपल को संभव बनाते हैं
std::variantऔरstd::visitबंद टाइप-समूह के लिए vtable-रहित पॉलीमॉर्फ़िज़्म देते हैंstd::functionटाइप इरेज़र देता है — संगत सिग्नेचर वाले किसी भी कॉलेबल को रखें- इनहेरिटेंस हाइरार्की को सपाट रखें (आमतौर पर 2-3 स्तर से अधिक नहीं) और कंक्रीट बेस क्लास की जगह इंटरफ़ेस को प्राथमिकता दें
- रखरखाव-योग्य, टेस्ट-योग्य डिज़ाइन के लिए ISP (छोटे इंटरफ़ेस) और DIP (अमूर्तताओं पर निर्भरता) लागू करें
क्विज़ — अपना ज्ञान परखें
(15 XP)1. आपको इनहेरिटेंस की बजाय कंपोज़िशन को कब प्राथमिकता देनी चाहिए?
2. पारंपरिक इनहेरिटेंस हाइरार्की की तुलना में `std::variant` का क्या लाभ है?
3. डिपेंडेंसी इन्वर्शन प्रिंसिपल (DIP) क्या कहता है?