এই পর্বে যা শিখবেন
- “ভালো ডিজাইন” মাপার একটি বাস্তব মাপকাঠি — বদলের খরচ, coupling আর cohesion
- কখন inheritance, কখন composition; একটি inheritance-এর জট কীভাবে composition দিয়ে খোলা যায়
- SOLID-এর পাঁচটি নীতি, প্রতিটি ছোট C++ আগে-পরে উদাহরণে, বাংলাদেশি অনলাইন দোকান আর রাইড-শেয়ারিংয়ের প্রসঙ্গে
- DRY, KISS, YAGNI, Law of Demeter, আর value টাইপ বনাম entity টাইপ
- একটি god class ধাপে ধাপে ভাঙা, কখন নীতি প্রয়োগ করবেন না, আর AI-এর নকশা যাচাইয়ের চেকলিস্ট
১ · ভালো ডিজাইন মানে কী — বদলানো সহজ, ভাঙা কঠিন
মডিউল ৪-এ আপনি অনেক যন্ত্র হাতে পেয়েছেন। পর্ব ২০-এ class আর object, পর্ব ২১-এ constructor ও destructor, পর্ব ২২-এ copy ও move, পর্ব ২৩-এ operator overloading, পর্ব ২৪-এ inheritance, পর্ব ২৫-এ virtual ফাংশন আর পর্ব ২৬-এ abstract class ও interface. কিন্তু হাতুড়ি, করাত আর পেরেক থাকলেই ভালো আসবাব বানানো যায় না। কোন যন্ত্র কোথায়, কতটুকু — সেই বিচারবুদ্ধির নাম ডিজাইন। এই পর্ব সেই বিচারবুদ্ধি নিয়ে।
প্রথমে একটি সৎ প্রশ্ন: “ভালো ডিজাইন” কীভাবে মাপবেন? কোড সুন্দর দেখায় কি না, class কয়টি, pattern কয়টি — এগুলো মাপকাঠি নয়। বাস্তব মাপকাঠি একটাই: একটি নতুন চাহিদা এলে কোড বদলাতে কত খরচ, আর বদলাতে গিয়ে অন্য কিছু ভাঙার সম্ভাবনা কত। সফটওয়্যার লেখা হয় একবার, কিন্তু বদলানো হয় শতবার। অনলাইন দোকানে আজ bKash, কাল Nagad, পরশু কিস্তিতে কেনার সুবিধা; আজ ঢাকার ভেতরে ডেলিভারি, কাল সারা দেশে। যে কোড এই বদলগুলো সহজে নিতে পারে, সেটিই ভালো ডিজাইন।
পুরান ঢাকার একটি পুরোনো বাড়ির কথা ভাবুন, যেখানে বিদ্যুতের তার দেয়ালের ভেতরে প্লাস্টারে গেঁথে দেওয়া, আর পানির পাইপ সেই একই দেয়ালে। একটি সুইচ বদলাতে গেলে দেয়াল ভাঙতে হয়, আর দেয়াল ভাঙতে গেলে পাইপ ফেটে যায়। এবার একটি আধুনিক অফিস ভাবুন — তার চলে আলাদা নালিতে, প্রতিটি ঘরের আলাদা ব্রেকার, পাইপ আলাদা পথে। একটি ঘরের বাতি বদলালে পাশের ঘর টেরও পায় না। দুটি বাড়িতেই আলো জ্বলে, পানি আসে — পার্থক্য শুধু বদলানোর সময়। সফটওয়্যারের ডিজাইনও ঠিক তাই।
পর্ব ২-এ আপনি দুটি শব্দ শিখেছিলেন, যা এই পর্বের প্রতিটি নীতির পেছনে কাজ করে। coupling — দুটি অংশ একে অপরের ওপর কতটা নির্ভর করে; একটিকে বদলালে অন্যটিকেও বদলাতে হয় কি না। cohesion — একটি অংশের ভেতরের জিনিসগুলো একই উদ্দেশ্যে কতটা একসাথে বাঁধা। লক্ষ্য সবসময় একই: ভেতরে উচ্চ cohesion, বাইরে কম coupling. এই পর্বের প্রতিটি নীতি আসলে এই একটি লক্ষ্যে পৌঁছানোর একেকটি কৌশল।
ডিজাইন নীতিগুলো আইন নয়, অভিজ্ঞতা থেকে আসা পরামর্শ (rules of thumb). প্রতিটি নীতি একটি নির্দিষ্ট ধরনের বদলকে সস্তা করে, আর বিনিময়ে একটু জটিলতা যোগ করে। যেখানে সেই বদল আসার সম্ভাবনা আছে, সেখানে নীতিটি লাভজনক; যেখানে নেই, সেখানে শুধু বোঝা। তাই প্রতিটি নীতির সাথে প্রশ্ন করবেন: “এখানে কোন বদল আসতে পারে?”
২ · composition বনাম inheritance — “has-a” আর “is-a”
দুটি class-কে জোড়ার প্রধান দুটি উপায় আছে। inheritance (পর্ব ২৪) বলে is-a সম্পর্ক: “একটি SavingsAccount একটি Account”. composition বলে has-a সম্পর্ক: “একটি Car-এর একটি Engine আছে” — অর্থাৎ Engine হলো Car-এর একটি সদস্য।
class Engine { public: void start(); };
class Car : public Engine { }; // is-a: “গাড়ি একটি ইঞ্জিন” — অর্থহীন
class Car2 { // has-a: “গাড়ির একটি ইঞ্জিন আছে” — সত্যি
Engine engine; // composition: সদস্য হিসেবে রাখা
public:
void start() { engine.start(); } // কাজটা ইঞ্জিনকে দিয়ে দেওয়া (delegation)
};প্রথম রূপটিও compile হয়, আর Car থেকে start() ডাকাও যায়। কিন্তু এতে Car-এর বাইরের জগৎ জেনে যায় যে Car “আসলে” একটি Engine — যেকোনো ফাংশন, যে Engine& চায়, সে একটি পুরো Car নিয়ে নেবে। Engine-এর প্রতিটি public ফাংশন Car-এর ঘাড়েও চাপে। আর ইঞ্জিন বদলানো? inheritance compile-এর সময়ে স্থির; একটি Car সারাজীবন একই base class নিয়ে থাকে।
নিয়মটা তাই এমন: inheritance ব্যবহার করুন শুধু তখন, যখন derived class সত্যিই base class-এর জায়গায় সব জায়গায় চলতে পারে — যখন আপনি polymorphism চান (পর্ব ২৫), আর “is-a” কথাটি আচরণের দিক থেকে সত্য। শুধু কোড পুনঃব্যবহারের জন্য inheritance নেবেন না; সেই কাজটি composition আরও পরিষ্কারভাবে করে। ১৯৯৪ সালের বিখ্যাত বই Design Patterns-এর একটি মূল উপদেশ ঠিক এটাই: “class inheritance-এর চেয়ে object composition-কে প্রাধান্য দিন।”
| প্রশ্ন | inheritance (is-a) | composition (has-a) |
|---|---|---|
| সম্পর্ক কখন ঠিক হয়? | compile-এর সময়, স্থায়ী | চলার সময়েও বদলানো যায় |
| coupling | বেশি — derived class base-এর ভেতরের খুঁটিনাটির ওপর নির্ভর করতে পারে | কম — শুধু সদস্যের public interface জানে |
| বাইরে কী দেখা যায়? | base-এর সব public ফাংশন | শুধু যা আপনি নিজে দেখাতে চান |
| কয়েকটি দিক একসাথে বদলালে | class-এর সংখ্যা গুণ হয়ে বাড়ে | অংশের সংখ্যা যোগ হয়ে বাড়ে |
| সবচেয়ে ভালো কাজে লাগে | polymorphism, যেখানে “is-a” আচরণে সত্য | কোড পুনঃব্যবহার, নমনীয় আচরণ, অংশ বদলানো |
inheritance-এর জট — একটি রাইড-শেয়ারিং অ্যাপ
ধরুন আপনি ঢাকার একটি রাইড-শেয়ারিং অ্যাপের ভাড়ার হিসাব লিখছেন। শুরুতে শুধু দুই ধরনের বাহন — বাইক আর গাড়ি। inheritance দিয়ে শুরু করা স্বাভাবিক মনে হয়। তারপর নতুন চাহিদা আসতে থাকে। নিচের ডায়াগ্রামে দেখুন, প্রতিটি নতুন চাহিদায় class-এর গাছ কীভাবে বাড়ে, আর composition কীভাবে সেই জট খোলে:
Vehicle
/ \
Bike Car
// fare() প্রতিটিতে আলাদা — সব ঠিকঠাক মনে হচ্ছে
Vehicle
/ | | \ ...
PetrolBike ElectricBike PetrolCar ElectricCar
GasCNG ElectricCNG
// প্রতিটি নতুন বাহনে ২টি নতুন class
PetrolBikeNormal PetrolBikeSurge
ElectricBikeNormal ElectricBikeSurge
PetrolCarNormal PetrolCarSurge
ElectricCarNormal ElectricCarSurge
GasCNGNormal GasCNGSurge
ElectricCNGNormal ElectricCNGSurge
// আর বৃষ্টি শুরু হলে চলমান রাইডের class বদলানো যায় না!
Ride has-a VehicleType { Bike | Car | CNG }
has-a Engine { Petrol | Electric }
has-a FareRule { Normal | Surge }
// যেকোনো জোড়া চলার সময় বানানো যায়, আর
// ride.setRule(Surge) — চলমান রাইডেও নিয়ম বদলায়
FareRule { Normal | Surge | StudentDiscount }
// inheritance-এ: আরও ৬টি class (প্রতিটি বাহন × জ্বালানি)
// composition-এ: ১টি নতুন FareRule — অন্য কিছু ছোঁয়া হয়নি
- সরল শুরু।
Vehiclebase class, তার দুটি derived class —BikeআরCar. প্রতিটি নিজেরfare()লেখে। এখানে inheritance যুক্তিসঙ্গত মনে হয়, আর আসলেও দোষের কিছু নেই। - দ্বিতীয় দিক যোগ হলো। কোম্পানি CNG অটোরিকশা আর ইলেকট্রিক বাহন যোগ করল। এখন “কোন বাহন” আর “কোন জ্বালানি” — দুটি আলাদা দিক। inheritance একটিমাত্র গাছ, তাই দুটি দিক মেলাতে প্রতিটি জোড়ার জন্য আলাদা class লাগে।
- তৃতীয় দিক — বিস্ফোরণ। বৃষ্টি বা অফিস-ছুটির ভিড়ে বাড়তি ভাড়া (surge) এল। ৩ × ২ × ২ = ১২টি class, আর প্রতিটির ভেতরে প্রায় একই কোডের পুনরাবৃত্তি। সবচেয়ে বড় সমস্যা: একটি রাইড চলার মাঝে বৃষ্টি শুরু হলে তার class বদলানো অসম্ভব — object-এর টাইপ জন্ম থেকেই স্থির।
- composition দিয়ে জট খোলা। প্রতিটি দিককে আলাদা ছোট অংশ বানান, আর
Rideসেগুলো সদস্য হিসেবে রাখুক। এখন class-এর সংখ্যা গুণ নয়, যোগ হয়ে বাড়ে: ৩ + ২ + ২ = ৭. যেকোনো জোড়া চলার সময়ে বানানো যায়, আর ভাড়ার নিয়ম চলমান রাইডেও বদলানো যায়। - নতুন চাহিদার খরচ। স্টুডেন্টদের জন্য ছাড় এল। inheritance-এর গাছে এর মানে আরও ছয়টি class; composition-এ একটিমাত্র নতুন
FareRule. পুরোনো কোনো class খুলতেই হয়নি। এই “শুধু যোগ, কোনো সংশোধন নয়” ধারণাটিই একটু পরে Open/Closed নীতি হিসেবে দেখবেন।
এবার এই নকশাটি সত্যিকারের, চালানো যায় এমন কোডে দেখুন। FareRule একটি ছোট interface (পর্ব ২৬), আর Ride তার একটি std::unique_ptr সদস্য হিসেবে রাখে (পর্ব ১৭) — অর্থাৎ Ride-এর একটি ভাড়ার নিয়ম আছে:
#include <memory>
#include <print>
#include <utility>
struct FareRule { // interface: "kivabe vara"
virtual ~FareRule() = default;
virtual int fare(int km) const = 0;
};
struct Normal : FareRule { int fare(int km) const override { return 30 + 25 * km; } };
struct Surge : FareRule { int fare(int km) const override { return (30 + 25 * km) * 3 / 2; } };
class Ride { // Ride HAS-A FareRule
std::unique_ptr<FareRule> rule;
public:
explicit Ride(std::unique_ptr<FareRule> r) : rule(std::move(r)) {}
void setRule(std::unique_ptr<FareRule> r) { rule = std::move(r); }
void quote(int km) const { std::println("{} km -> {} taka", km, rule->fare(km)); }
};
int main() {
Ride car(std::make_unique<Normal>());
car.quote(8);
car.setRule(std::make_unique<Surge>()); // rule bodlalo, Ride class noy
car.quote(8);
}আউটপুট দুই লাইন: 8 km -> 230 taka, আর বৃষ্টি শুরুর পর 8 km -> 345 taka. হিসাবটা সহজ — ভিত্তি ৩০ টাকা, তার সাথে কিলোমিটারপ্রতি ২৫ টাকা; surge-এ পুরোটার দেড় গুণ। মূল জিনিস: একই car object, চলার মাঝে তার ভাড়ার নিয়ম বদলে গেল, অথচ Ride class-এর একটি লাইনও বদলাতে হয়নি। inheritance দিয়ে এটি অসম্ভব ছিল। আর Ride জানেই না কোন নিয়ম তার হাতে — সে শুধু FareRule-এর প্রতিশ্রুতি (fare(km)) জানে। এই ধরনের নকশাকে বলে Strategy pattern.
C-তে class বা inheritance নেই, কিন্তু composition আছে — একটি struct-এর ভেতরে আরেকটি struct. আর বদলযোগ্য আচরণ (FareRule) C-তে লেখা হয় ফাংশন পয়েন্টার দিয়ে: int (*fare)(int km);. Linux kernel-এর struct file_operations ঠিক এভাবে ফাংশন পয়েন্টারের একটি “interface” বানায়। C++-এর virtual ফাংশন সেই একই ধারণার নিরাপদ, compiler-যাচাই করা রূপ। গভীরে জানতে C কোর্সের ফাংশন পয়েন্টার ও callback পর্বটি দেখুন; C না জানলে এই বাক্সটি বাদ দিতে পারেন।
৩ · S — Single Responsibility: বদলানোর কারণ একটাই
এবার পাঁচটি নীতির একটি পরিবার, যাদের নামের প্রথম অক্ষর মিলে হয় SOLID. প্রথমটি Single Responsibility Principle (SRP): একটি class-এর বদলানোর কারণ থাকা উচিত মাত্র একটি। খেয়াল করুন, নিয়মটা “একটিই কাজ করবে” নয় — “একটিই কারণে বদলাবে”. প্রশ্নটা হলো, কোন কোন মানুষ বা কোন কোন সিদ্ধান্ত এই class-কে বদলাতে বাধ্য করতে পারে।
একটি অনলাইন দোকানের বিক্রির রিপোর্ট দেখুন:
// আগে: এক class, তিনটি বদলের কারণ
class SalesReport {
public:
int total() const; // হিসাব — হিসাব বিভাগ নিয়ম বদলালে বদলায়
void printAsTable() const; // দেখানো — ডিজাইনার নকশা বদলালে বদলায়
void saveToFile(const std::string& path) const; // সংরক্ষণ — CSV থেকে ডেটাবেসে গেলে বদলায়
};তিনটি আলাদা দল তিনটি আলাদা কারণে এই একটি class খুলবে। হিসাব বিভাগ VAT-এর নিয়ম বদলাল — আপনি total() ঠিক করতে গিয়ে ভুলে printAsTable()-এর একটি জিনিস ভাঙলেন। দুজন প্রোগ্রামার একই ফাইলে একসাথে কাজ করছেন, git-এ সংঘর্ষ। আর রিপোর্টটি ফাইলে না রেখে ইমেইলে পাঠাতে চাইলে? সেই একই class আবার খুলতে হবে। সমাধান — প্রতিটি কারণকে নিজের ঘর দিন:
// পরে: প্রতিটি class-এর একটি করে বদলের কারণ
struct SalesReport { std::vector<Sale> sales; int total() const; }; // শুধু ডেটা আর হিসাব
class TablePrinter { public: void print(const SalesReport& r) const; }; // শুধু দেখানো
class CsvSaver { public: void save(const SalesReport& r, const std::string& path) const; };এখন VAT-এর নিয়ম বদলালে শুধু SalesReport, টেবিলের নকশা বদলালে শুধু TablePrinter. প্রতিটি অংশ আলাদাভাবে পরীক্ষাও করা যায় — হিসাব যাচাই করতে ফাইল তৈরি করতে হয় না। কোন অংশ কোন বিভাগের, তা দেখে বোঝা যায় — এটাই উচ্চ cohesion. পর্ব ১৯-এর মাঝপথের প্রজেক্টে আপনি separation of concerns নামে যা দেখেছিলেন, SRP তারই class-স্তরের রূপ।
SRP-কে আক্ষরিক অর্থে নিলে উল্টো বিপদ: প্রতিটি ফাংশনের জন্য একটি করে class, আর পঞ্চাশটি ফাইল ঘুরে একটি কাজ বুঝতে হয়। “একটি কারণ” মানে একটি দায়িত্বের এলাকা, একটি ফাংশন নয়। SalesReport-এ total(), average(), bestSeller() একসাথে থাকতে পারে — তিনটিই হিসাবের নিয়মের কারণে বদলায়। আর C++-এ সবকিছু class হতেও হয় না: একটি সরল free function প্রায়ই সবচেয়ে ভালো “একক দায়িত্ব”।
৪ · O — Open/Closed: নতুন payment method, পুরোনো কোড অক্ষত
Open/Closed Principle (OCP): কোড নতুন আচরণ যোগের জন্য খোলা (open), কিন্তু সংশোধনের জন্য বন্ধ (closed) থাকবে। শুনতে স্ববিরোধী — খোলা আবার বন্ধ? অর্থটা হলো: নতুন ধরনের জিনিস যোগ করতে আপনি নতুন কোড লিখবেন, কিন্তু যে কোড পরীক্ষিত আর চলছে, তা খুলে বদলাবেন না।
একটি বাংলাদেশি অনলাইন দোকানের checkout ভাবুন। প্রথম সংস্করণে পেমেন্ট নেওয়া হয় একটি switch দিয়ে:
// আগে: প্রতিটি নতুন পদ্ধতিতে এই ফাংশন খুলতে হয়
enum class PayKind { BKash, Nagad, Card, CashOnDelivery };
bool pay(PayKind k, int taka) {
switch (k) {
case PayKind::BKash: /* bKash API ডাকো */ return true;
case PayKind::Nagad: /* Nagad API ডাকো */ return true;
case PayKind::Card: /* ব্যাংকের gateway */ return true;
case PayKind::CashOnDelivery: return true;
}
return false;
}চারটি পদ্ধতি থাকলে এটি একদম ঠিক আছে। সমস্যা শুরু হয় যখন এমন switch কোডের পাঁচ জায়গায় ছড়িয়ে থাকে — পেমেন্ট নেওয়া, রসিদ ছাপা, টাকা ফেরত (refund), ফি হিসাব। নতুন একটি পদ্ধতি যোগ করতে হলে পাঁচটি জায়গাই খুঁজে খুঁজে বদলাতে হয়, আর একটি ভুলে গেলে সেটি চলার সময়ের বাগ। এবার interface-এর পথ:
// পরে: নতুন পদ্ধতি = নতুন class; পুরোনো কোড অক্ষত
struct PaymentMethod { // পর্ব ২৬-এর interface
virtual ~PaymentMethod() = default;
virtual bool pay(int taka) = 0;
virtual int fee(int taka) const = 0;
virtual std::string name() const = 0;
};
class BKashPay : public PaymentMethod { /* ... */ };
class NagadPay : public PaymentMethod { /* ... */ };
void checkout(PaymentMethod& m, int taka) { // এই ফাংশন আর কখনো বদলাবে না
if (m.pay(taka + m.fee(taka))) std::println("paid via {}", m.name());
}এখন নতুন একটি পদ্ধতি — ধরুন Rocket বা Upay — যোগ করতে কী কী ছুঁতে হয়, ধাপে ধাপে দেখুন। লাল দাগ মানে “এই ফাইল খুলে বদলাতে হবে”:
- switch-এর পথ। পেমেন্টের ধরন একটি
enum, আর চারটি ফাইলে সেই enum নিয়েswitch. ছোট প্রজেক্টে এতে কোনো দোষ নেই — সব এক নজরে দেখা যায়। - Rocket যোগ করতে হবে। চারটি ফাইলই লাল: enum-এ নতুন মান, আর তিনটি
switch-এ নতুনcase. প্রতিটি পরীক্ষিত ফাইল আবার খোলা মানে আবার পরীক্ষা। একটিcaseভুলে গেলে?-Wswitchসতর্কতা হয়তো ধরবে — যদিdefaultনা থাকে আর আপনি সতর্কতা পড়েন। - interface-এর পথ। একটি
PaymentMethodinterface, আর প্রতিটি পদ্ধতি নিজের ফাইলে নিজের class.checkout.cppশুধু interface জানে — bKash না Nagad, তা তার জানার দরকারই নেই। পরিবর্তনশীল অংশ (কোন পদ্ধতি) আলাদা করা হলো স্থির অংশ (checkout-এর ধাপ) থেকে। - শুধু নতুন ফাইল। Rocket যোগ করতে একটি নতুন ফাইল
rocket.cpp, যেখানেRocketPayclass interface-টি পূরণ করে। checkout, receipt, refund — কোনো পুরোনো ফাইল খোলা হয়নি, তাই সেগুলোর পুরোনো পরীক্ষা এখনো বৈধ। - এক জায়গায় জোড়া। কোথাও না কোথাও কাউকে বলতে হবে “RocketPay-ও আছে” — সাধারণত
main-এ বা প্রোগ্রাম শুরুর জায়গায়, যেখানে সব অংশ জোড়া দেওয়া হয়। একে বলে composition root. সৎভাবে বললে OCP-তে শূন্য লাইন বদলায় না; বদল সীমাবদ্ধ থাকে একটি নির্দিষ্ট, অনুমেয় জায়গায়।
একটি গুরুত্বপূর্ণ সতর্কতা: OCP সব দিকে একসাথে “খোলা” রাখা যায় না। interface-এর নকশা ঠিক করে দেয় কোন দিকের বদল সস্তা। এখানে নতুন পেমেন্ট পদ্ধতি যোগ সস্তা, কিন্তু সব পদ্ধতিতে একটি নতুন কাজ (ধরুন installment()) যোগ করতে এখন প্রতিটি class খুলতে হবে — ঠিক যেটা switch-এর পথে সহজ ছিল। তাই প্রশ্ন করুন: আমার প্রজেক্টে কোনটা বেশি আসে — নতুন ধরন, নাকি নতুন কাজ? নতুন ধরন বেশি এলে interface; নির্দিষ্ট কয়েকটি ধরন আর নতুন কাজ বেশি এলে enum + switch বা পর্ব ৩৮-এর std::variant.
৫ · L — Liskov Substitution: Square কি সত্যিই একটি Rectangle?
Liskov Substitution Principle (LSP): যেখানে base class চলে, সেখানে যেকোনো derived class বসালেও প্রোগ্রাম ঠিকঠাক চলবে। derived class base class-এর প্রতিশ্রুতি ভাঙতে পারবে না — base যা নিশ্চিত করে, derived-কেও তা নিশ্চিত করতে হবে। পর্ব ২৪-এ আপনি এই প্রশ্নের প্রথম ঝলক দেখেছেন; এবার পুরো গল্প।
গণিতে বর্গক্ষেত্র একটি আয়তক্ষেত্র — চার কোণ সমকোণ, শুধু চার বাহু সমান। তাই প্রথম ভাবনা:
class Rectangle {
protected:
int w = 0, h = 0;
public:
virtual ~Rectangle() = default;
virtual void setWidth(int x) { w = x; }
virtual void setHeight(int y) { h = y; }
int area() const { return w * h; }
};
class Square : public Rectangle { // “বর্গ একটি আয়ত” — গণিতে সত্য
public:
void setWidth(int x) override { w = h = x; } // বাহু সমান রাখতে দুটিই বদলায়
void setHeight(int y) override { w = h = y; }
};
void stretch(Rectangle& r) { // Rectangle-এর প্রতিশ্রুতির ওপর ভরসা
r.setWidth(5);
r.setHeight(4);
assert(r.area() == 20); // Square পেলে area 16 — ভেঙে গেল!
}stretch ফাংশনটি একটি যুক্তিসঙ্গত ধারণা করে: Rectangle-এর প্রস্থ বদলালে উচ্চতা বদলায় না। এটি Rectangle-এর একটি অলিখিত প্রতিশ্রুতি, একটি invariant-এর মতো। Square সেই প্রতিশ্রুতি ভাঙে — setHeight(4) চুপচাপ প্রস্থও বদলে দেয়। compiler কিছু বলে না, কোড compile হয়, কিন্তু Rectangle-এর জন্য লেখা প্রতিটি ফাংশন এখন Square পেলে ভুল ফল দিতে পারে।
শিক্ষাটা গভীর: “is-a” কথাটির বিচার হয় আচরণ দিয়ে, সংজ্ঞা দিয়ে নয়। গণিতের বর্গ কখনো “বদলায়” না, তাই সেখানে সম্পর্কটি ঠিক। কিন্তু বদলানো যায় এমন Square বদলানো যায় এমন Rectangle-এর মতো আচরণ করে না। সমাধান কয়েকটি:
অপরিবর্তনীয় বানান
setter না রেখে constructor-এ মাপ নিন। বদলানো না গেলে প্রতিশ্রুতি ভাঙারও সুযোগ নেই — এখন Square সত্যিই একটি Rectangle হতে পারে।
immutableভাইবোন বানান
দুটিই একটি Shape interface থেকে আসুক, যার শুধু area() আছে। কেউ কারও derived নয়, তাই প্রতিশ্রুতির সংঘর্ষও নেই।
inheritance বাদ দিন
অনেক সময় সবচেয়ে সৎ উত্তর — এদের মধ্যে inheritance-ই লাগে না। দুটি আলাদা value টাইপ (অংশ ৮-এ দেখবেন) যথেষ্ট।
no hierarchyবাস্তব জীবনে LSP ভাঙার চিহ্নগুলো চিনে রাখুন: derived class-এ কোনো virtual ফাংশন throw std::logic_error("not supported") করে; derived class base-এর চেয়ে কড়া শর্ত চায় (base যেকোনো পরিমাণ নেয়, derived শুধু ১০০-এর গুণিতক); বা ব্যবহারকারী কোডে dynamic_cast দিয়ে “এটা কি আসলে Square?” পরীক্ষা করতে হয়। এগুলো দেখলে বুঝবেন — inheritance-এর গাছটি মিথ্যা বলছে।
৬ · I আর D — ছোট interface, আর abstraction-এর ওপর নির্ভরতা
I — Interface Segregation: মোটা interface ভেঙে ছোট করুন
Interface Segregation Principle (ISP): কাউকে এমন ফাংশনের ওপর নির্ভর করতে বাধ্য করবেন না, যা সে ব্যবহারই করে না। অফিসের একটি “সব-এক-সাথে” মেশিনের interface দেখুন:
// আগে: একটি মোটা interface
struct Machine {
virtual ~Machine() = default;
virtual void print(const Doc& d) = 0;
virtual void scan(Doc& d) = 0;
virtual void fax(const Doc& d) = 0;
};
class CheapPrinter : public Machine {
void print(const Doc& d) override { /* ঠিক আছে */ }
void scan(Doc&) override { throw std::logic_error("no scanner"); } // LSP-ও ভাঙল
void fax(const Doc&) override { throw std::logic_error("no fax"); }
};
// পরে: ছোট, নির্দিষ্ট interface
struct Printer { virtual ~Printer() = default; virtual void print(const Doc& d) = 0; };
struct Scanner { virtual ~Scanner() = default; virtual void scan(Doc& d) = 0; };
class CheapPrinter2 : public Printer { /* শুধু print */ };
class OfficeCombo : public Printer, public Scanner { /* দুটিই পারে */ };খেয়াল করুন, মোটা interface সরাসরি LSP ভাঙার দিকে ঠেলে দেয়: যে class একটি কাজ পারে না, তাকে সেই কাজের ভান করতে হয়। আর যে ফাংশন শুধু প্রিন্ট করে, সে এখন Printer& চায় — তার কাছে স্ক্যানার আছে কি না, তা অপ্রাসঙ্গিক। C++-এ একটি class একাধিক interface থেকে আসতে পারে (multiple inheritance), আর শুধু pure virtual ফাংশনের interface থেকে নিলে এতে বিপদ কম। ISP-এর সারকথা: interface বানান ব্যবহারকারীর প্রয়োজন দেখে, বাস্তবায়নকারীর সব ক্ষমতা দেখে নয়।
D — Dependency Inversion: খুঁটিনাটির ওপর নয়, abstraction-এর ওপর নির্ভর করুন
Dependency Inversion Principle (DIP)-এর দুটি অংশ। এক, উঁচু স্তরের কোড (ব্যবসার নিয়ম — “অর্ডার হলে গ্রাহককে জানাও”) নিচু স্তরের কোডের (খুঁটিনাটি — “এই কোম্পানির SMS gateway-তে এই URL-এ request পাঠাও”) ওপর নির্ভর করবে না; দুটিই নির্ভর করবে একটি abstraction-এর ওপর। দুই, সেই abstraction-এর মালিক উঁচু স্তর — interface-টি লেখা হয় ব্যবসার ভাষায়, gateway-এর ভাষায় নয়।
পর্ব ২৬-এ আপনি Notifier-এর মতো interface দেখেছেন। এবার দেখুন, নির্ভরতার তীরগুলো কীভাবে ঘুরে যায় — নামটির “inversion” এখান থেকেই:
[ OrderService ] উঁচু স্তর: ব্যবসার নিয়ম | | #include "ssl_sms.h" v [ SslWirelessSms ] নিচু স্তর: নির্দিষ্ট কোম্পানির API
[ OrderService ] ← খুলে বদলাতে হবে | v [ SslWirelessSms ] [ EmailSender ]? [ টেস্টের নকল ]? // প্রতিটি টেস্ট চালালে সত্যি SMS যায়, টাকা কাটে
[ OrderService ] ───uses───> [ Notifier ] (interface, OrderService-এর পাশে) // OrderService এখন কোনো কোম্পানির নাম জানে না
[ OrderService ] ───uses───> [ Notifier ]
^ ^ ^
implements | | | implements
[ SmsNotifier ] [ EmailNotifier ] [ FakeNotifier ]
// নিচু স্তর এখন উঁচু স্তরের interface-এর দিকে তাকায়
main():
SmsNotifier sms; OrderService shop(sms); // আসল দোকান
FakeNotifier fake; OrderService test(fake); // টেস্ট
// OrderService-এর কোড দুই ক্ষেত্রে হুবহু একই
- সরাসরি নির্ভরতা।
OrderServiceনিজের ভেতরে একটি নির্দিষ্ট SMS কোম্পানির class সদস্য হিসেবে রাখে। নির্ভরতার তীর ওপর থেকে নিচে — ব্যবসার নিয়ম একটি নির্দিষ্ট কোম্পানির API-তে বাঁধা। - বদলের চাপ। কোম্পানি বদলাতে চাইলে, বা ইমেইল যোগ করতে চাইলে, ব্যবসার নিয়মের class খুলতে হয়। আরও খারাপ — টেস্ট চালালেই সত্যিকারের SMS যায়, গ্রাহক ভুল বার্তা পান, আর ইন্টারনেট না থাকলে টেস্টই চলে না।
- interface উঁচু স্তরের।
OrderService-এর পাশে একটি ছোট interface — “আমার কাউকে একটি বার্তা পাঠানো দরকার”. এটি ব্যবসার ভাষায় লেখা; কোনো কোম্পানির নাম, URL বা API key এতে নেই। - তীর উল্টে গেল। এখন SMS, ইমেইল আর টেস্টের নকল class — সবাই
Notifier-এর দিকে তাকায় আর তা পূরণ করে। নিচু স্তর উঁচু স্তরের তৈরি নিয়মের ওপর নির্ভর করছে, উল্টোটা নয়। এটাই “inversion”. - বাইরে থেকে জোগান। কোন Notifier ব্যবহার হবে, তা
OrderServiceনিজে ঠিক করে না — constructor-এ বাইরে থেকে দেওয়া হয়। এই কৌশলের নাম dependency injection. DIP হলো নীতি (কার ওপর নির্ভর করবে), dependency injection হলো কৌশল (নির্ভরতা কীভাবে হাতে পৌঁছাবে)।
C++-এ abstraction বানানোর একমাত্র উপায় virtual interface নয়। যদি কোন Notifier ব্যবহার হবে তা compile-এর সময়েই জানা থাকে, তাহলে template দিয়েও একই নির্ভরতা-বিচ্ছেদ পাওয়া যায়, virtual ডাকের খরচ ছাড়াই — এটি পর্ব ২৮-২৯-এ দেখবেন, আর পর্ব ৪০-এর concepts দিয়ে সেই “interface”-এর শর্তও লিখে দেওয়া যায়। ছোট কাজের জন্য একটি std::function বা lambda-ও যথেষ্ট। নীতিটা একই — কোন রূপে লিখবেন, তা প্রয়োজন দেখে।
৭ · DRY, KISS, YAGNI আর Law of Demeter — আর কখন থামবেন
SOLID-এর বাইরেও চারটি ছোট নীতি প্রায় প্রতিদিন কাজে লাগে। এগুলো মুখস্থের জিনিস নয় — কোড রিভিউর সময় মনে মনে করার চারটি প্রশ্ন।
DRY — একই জ্ঞান এক জায়গায়
DRY (Don't Repeat Yourself): ডেলিভারি চার্জ ৬০ টাকা — এই তথ্য কোডের তিন জায়গায় লেখা থাকলে একদিন দুই জায়গায় বদলাবে, এক জায়গায় ভুলবেন। একটি named constant বা ফাংশনে রাখুন।
Don't Repeat YourselfKISS — সরল রাখুন
KISS (Keep It Simple): দুটি সমাধান একই কাজ করলে যেটি নতুন একজন সহকর্মী দ্রুত বোঝেন, সেটি নিন। চতুর কোড লেখার আনন্দ একবার, বোঝার কষ্ট বারবার।
Keep It SimpleYAGNI — আজ না লাগলে বানাবেন না
YAGNI (You Aren't Gonna Need It): “হয়তো একদিন বিটকয়েনে পেমেন্ট লাগবে” ভেবে আজ plugin-ব্যবস্থা বানাবেন না। প্রয়োজন এলে তখন বানান — তখন আপনি জানবেন আসলে কী লাগবে।
You Aren't Gonna Need ItLaw of Demeter — শুধু কাছের বন্ধুর সাথে কথা
Law of Demeter: order.customer().address().city().name() — এই শিকল মানে আপনার কোড চারটি class-এর ভেতরের গঠন জানে। একটি বদলালেই এটি ভাঙে। বরং order.deliveryCity().
DRY-এর একটি সূক্ষ্মতা জানা জরুরি: DRY মানে একই জ্ঞানের পুনরাবৃত্তি এড়ানো, দেখতে একই রকম কোডের নয়। ধরুন রাইড-শেয়ারিং অ্যাপে বাইকের ন্যূনতম ভাড়া আর পার্সেল ডেলিভারির ন্যূনতম ফি — দুটিই আজ ৩০ টাকা। একটি constant-এ জুড়ে দিলে কাল পার্সেলের ফি বাড়ালে বাইকের ভাড়াও বেড়ে যাবে। দুটি আলাদা ব্যবসায়িক সিদ্ধান্ত, তাই আলাদা থাকাই ঠিক। ভুল জায়গায় DRY coupling তৈরি করে, যা কমানোই ছিল লক্ষ্য।
কখন নীতি প্রয়োগ করবেন না — অতিরিক্ত ডিজাইন
নীতিগুলোর একটি অন্ধকার দিক আছে, যার নাম over-engineering. একজন নতুন উৎসাহী প্রোগ্রামার SOLID শিখে একটি ৩০০ লাইনের টিউশন-ফি হিসাবের প্রোগ্রামে বানিয়ে ফেলেন IFeeCalculatorFactory, AbstractStudentRepository, FeeCalculationStrategyManager — প্রতিটির একটিমাত্র বাস্তবায়ন। কোড এখন বারোটি ফাইলে ছড়ানো, আর একটি ছোট বদলের জন্য পাঁচটি ফাইল খুলতে হয়। নীতি মানতে গিয়ে ঠিক সেই সমস্যাটি তৈরি হলো, যা নীতিগুলো সমাধান করার কথা ছিল: বদল কঠিন।
| চিহ্ন | সম্ভবত অতিরিক্ত | সম্ভবত দরকারি |
|---|---|---|
| interface-এর বাস্তবায়ন কয়টি? | একটি, আর দ্বিতীয়টির কোনো বাস্তব সম্ভাবনা নেই | দুই বা তার বেশি — অথবা টেস্টের জন্য একটি নকল লাগবেই |
| বদলের ইতিহাস | “একদিন হয়তো” — অনুমান | এই অংশটি আগেও বারবার বদলেছে |
| নামের ধরন | Manager, Helper, Factory, AbstractBase — যা কিছুই বলে না | ব্যবসার ভাষার নাম — PaymentMethod, FareRule |
| প্রোগ্রামের আয়ু | একবার চালিয়ে ফেলে দেওয়ার স্ক্রিপ্ট, কনটেস্টের সমাধান | বছরের পর বছর অনেক মানুষ যে কোডে কাজ করবেন |
একটি কার্যকর অভ্যাস হলো “তিনের নিয়ম” (rule of three): প্রথমবার সরাসরি লিখুন; দ্বিতীয়বার একই রকম কিছু লাগলে একটু অস্বস্তি নিয়ে আবার লিখুন; তৃতীয়বার এলে তখন abstraction বানান — কারণ ততক্ষণে আপনি তিনটি বাস্তব উদাহরণ দেখেছেন, আর জানেন আসলে কোনটা বদলায়। ভুল abstraction-এর দাম প্রায়ই পুনরাবৃত্তির দামের চেয়ে বেশি।
- SOLID-এর পূর্ণরূপ কী? — Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion.
- composition আর inheritance-এর পার্থক্য? — inheritance “is-a”, compile-এর সময়ে স্থির, coupling বেশি; composition “has-a”, অংশটি সদস্য, চলার সময়েও বদলানো যায়, coupling কম।
- Square-Rectangle সমস্যা কোন নীতি ভাঙে? — Liskov Substitution: বদলানো যায় এমন Square, Rectangle-এর “প্রস্থ বদলালে উচ্চতা বদলায় না” প্রতিশ্রুতি রাখে না।
- Dependency Inversion আর dependency injection কি একই? — না; প্রথমটি নীতি (abstraction-এর ওপর নির্ভর করা), দ্বিতীয়টি কৌশল (নির্ভরতা বাইরে থেকে, যেমন constructor দিয়ে, দেওয়া)।
- god class কী, কীভাবে ঠিক করবেন? — যে class অনেক অসম্পর্কিত দায়িত্ব নেয়; প্রতিটি বদলের কারণ ধরে আলাদা class-এ বের করে আনুন, মূল class শুধু সমন্বয় করুক।
৮ · value টাইপ বনাম entity টাইপ — C++-এর নিজস্ব প্রশ্ন
Java বা C#-এ প্রায় সব object একটি reference দিয়ে ধরা হয়, তাই “কপি করলে কী হবে” প্রশ্নটি কম ওঠে। C++-এ ডিফল্ট হলো value semantics — auto b = a; একটি সত্যিকারের কপি বানায় (পর্ব ২২)। তাই C++-এ প্রতিটি class ডিজাইনের শুরুতেই একটি প্রশ্নের উত্তর দিতে হয়: এই টাইপটি কি একটি মান, নাকি একটি পরিচয়?
| দিক | value টাইপ | entity টাইপ |
|---|---|---|
| উদাহরণ | Money, Date, Address, Point, std::string | Customer, Order, Driver, BankAccount |
| দুটি “সমান” কখন? | সব মান মিললে — ৫০০ টাকা = ৫০০ টাকা | পরিচয় (id) মিললে — একই নামের দুজন গ্রাহক আলাদা মানুষ |
| কপি | স্বাভাবিক আর নিরাপদ — যত খুশি কপি | সাধারণত অর্থহীন — একটি অর্ডারের দুটি “আসল” কপি বিভ্রান্তিকর; প্রায়ই কপি নিষেধ করা হয় |
| বদলানো | প্রায়ই অপরিবর্তনীয়, বা ছোট আর সরল | সময়ের সাথে অবস্থা বদলায় (অর্ডার: placed → shipped → delivered) |
| ফাংশনে পাঠানো | by value বা const T& | T& বা const T&; মালিকানা দরকার হলে std::unique_ptr |
| সাধারণ ডিজাইন | Rule of Zero, operator== (পর্ব ২৩), virtual নয় | id সদস্য, polymorphic হলে virtual destructor, কপি = delete |
এই বিভাজন ডিজাইনে বড় সাহায্য করে। Money-কে একটি সাধারণ int-এর বদলে value টাইপ বানালে (টাকা আর পয়সা আলাদা, যোগ-বিয়োগের operator সহ) “টাকার সাথে কিলোমিটার যোগ” ধরনের ভুল compile-এর সময়ে ধরা পড়ে। আর polymorphic entity — যেমন PaymentMethod — কপি করতে দিলে পর্ব ২৫-এর object slicing-এর বিপদ: derived অংশ কেটে বাদ পড়ে। তাই একটি ভালো অভ্যাস: polymorphic base class-এ কপি নিষেধ করুন, আর value টাইপে virtual ফাংশন রাখবেন না। দুটি ভূমিকা এক টাইপে মেশালে প্রায়ই সমস্যা হয়।
struct Money { // value: মান দিয়ে চেনা, কপি নিরাপদ
long long paisa = 0;
friend Money operator+(Money a, Money b) { return {a.paisa + b.paisa}; }
friend bool operator==(Money, Money) = default;
};
class Order { // entity: পরিচয় দিয়ে চেনা
int id;
Money total;
public:
explicit Order(int i) : id(i) {}
Order(const Order&) = delete; // একই অর্ডারের দুটি “আসল” কপি নয়
Order& operator=(const Order&) = delete;
int orderId() const { return id; }
};৯ · কেস স্টাডি — একটি OrderProcessor god class ধাপে ধাপে ভাঙা
এবার সব নীতি এক জায়গায়। ধরুন “দেশি বাজার” নামের একটি ছোট অনলাইন দোকানের প্রথম প্রোগ্রামার তাড়াহুড়োয় সব কাজ একটি class-এ লিখেছিলেন। দুই বছর পর সেটি ১,৫০০ লাইনের। এমন class-কে বলে god class — সে সব জানে, সব করে, আর সবাই তার ওপর নির্ভর করে।
class OrderProcessor { // সবকিছু এক জায়গায়
public:
void process(Order& o) {
// ১. কার্টের দাম, ছাড়ের কুপন, ডেলিভারি চার্জ হিসাব (৩০০ লাইন)
// ২. স্টকে আছে কি না দেখা, স্টক কমানো (২০০ লাইন)
// ৩. switch দিয়ে bKash / Nagad / Card / COD পেমেন্ট (৪০০ লাইন)
// ৪. সরাসরি MySQL-এ অর্ডার সংরক্ষণ (২০০ লাইন)
// ৫. SSL Wireless-এর API দিয়ে SMS পাঠানো (১৫০ লাইন)
// ৬. PDF রসিদ তৈরি (২৫০ লাইন)
}
};লক্ষণগুলো চেনা: কুপনের নিয়ম বদলালে পেমেন্টের কোড ভাঙে; কোনো টেস্ট চালাতে আসল ডেটাবেস আর আসল SMS লাগে; দুজন প্রোগ্রামার একসাথে এই ফাইলে কাজ করতে পারেন না। পুরোটা একবারে নতুন করে লেখা ঝুঁকিপূর্ণ — তাই refactoring করবেন ছোট ছোট নিরাপদ ধাপে, প্রতি ধাপের পর প্রোগ্রাম চালিয়ে আর টেস্ট করে। নিচে লাল দাগ দেখাবে, কোন দায়িত্বটি এই ধাপে বের করে আনা হচ্ছে:
- শুরুর অবস্থা। একটি class, ছয়টি অসম্পর্কিত দায়িত্ব, ছয়টি আলাদা বদলের কারণ — মার্কেটিং (কুপন), ফিনান্স (পেমেন্ট), IT (ডেটাবেস), টেলিকম চুক্তি (SMS), গুদাম (স্টক), আইনি নিয়ম (রসিদ)। প্রথম কাজ: কিছু বদলানোর আগে বর্তমান আচরণ ধরে রাখার কয়েকটি টেস্ট লিখুন।
- সবচেয়ে সহজটি আগে — দামের হিসাব। দাম হিসাব শুধু ডেটা নিয়ে কাজ করে, ফাইল বা নেটওয়ার্ক ছোঁয় না। তাই এটি একটি সরল ফাংশন বা value টাইপে বের করে আনা সবচেয়ে নিরাপদ, আর সঙ্গে সঙ্গে টেস্ট করা যায়। সব নীতির মধ্যে এই ধাপটি সবচেয়ে বেশি লাভ দেয়।
- পেমেন্টের switch → interface. পেমেন্ট পদ্ধতি এই দোকানে সবচেয়ে ঘন ঘন বদলায় (নতুন ওয়ালেট, নতুন ব্যাংক)। তাই এখানে OCP লাভজনক:
PaymentMethodinterface, প্রতিটি পদ্ধতি আলাদা class. - বাইরের জগৎ — ডেটাবেস আর SMS. এগুলো নিচু স্তরের খুঁটিনাটি, আর টেস্টের পথে সবচেয়ে বড় বাধা। দুটির জন্য ব্যবসার ভাষায় ছোট interface (
OrderRepository,Notifier), আর আসল MySQL বা SMS class সেগুলো পূরণ করে — DIP. এখন টেস্টে নকল বসানো যায়। - বাকি দায়িত্ব নিজের ঘরে। স্টক আর রসিদ — প্রত্যেকে নিজের class-এ, কারণ তাদের বদলের কারণ আলাদা (গুদাম আর আইনি নিয়ম)। খেয়াল করুন, এখানে interface বানানো হয়নি — এগুলোর এখন একটিই বাস্তবায়ন, আর অন্যটির সম্ভাবনা নেই (YAGNI).
- যা বাকি থাকল — সমন্বয়কারী। পুরোনো
OrderProcessorএখন ছোট্ট একটিOrderService: সে শুধু জানে কোন ধাপের পর কোন ধাপ — দাম, স্টক, পেমেন্ট, সংরক্ষণ, জানানো। কে আসল কাজটি করবে, তাmain()ঠিক করে দেয়। ১,৫০০ লাইন কমেনি, কিন্তু এখন প্রতিটি টুকরো ছোট, নিজের নামে, আর আলাদাভাবে বদলানো যায়।
ফলাফলের একটি ছোট, চালানো যায় এমন সংস্করণ দেখুন। এখানে দামের হিসাব একটি free function, জানানোর কাজ Notifier interface-এর পেছনে, আর OrderService শুধু দুটিকে জোড়া দেয়। একই OrderService একবার আসল SMS দিয়ে, একবার টেস্টের নকল দিয়ে চলছে:
#include <format>
#include <print>
#include <string>
#include <vector>
int orderTotal(const std::vector<int>& prices) { // job 1: price only, no I/O
int t = 0;
for (int p : prices) t += p;
return t >= 1000 ? t : t + 60; // delivery 60 below 1000
}
struct Notifier { // job 2: tell (interface)
virtual ~Notifier() = default;
virtual void send(const std::string& msg) = 0;
};
struct SmsNotifier : Notifier {
void send(const std::string& msg) override { std::println("SMS: {}", msg); }
};
struct FakeNotifier : Notifier { // for tests
std::vector<std::string> sent;
void send(const std::string& msg) override { sent.push_back(msg); }
};
class OrderService { // only coordinates
Notifier& notifier;
public:
explicit OrderService(Notifier& n) : notifier(n) {}
void place(const std::vector<int>& prices) {
notifier.send(std::format("confirmed, total {} taka", orderTotal(prices)));
}
};
int main() {
SmsNotifier sms; OrderService shop(sms); shop.place({850, 120, 120});
FakeNotifier fake; OrderService test(fake); test.place({10, 10, 10});
std::println("fake saw {} message: {}", fake.sent.size(), fake.sent.at(0));
}আউটপুট দুই লাইন: SMS: confirmed, total 1090 taka আর fake saw 1 message: confirmed, total 90 taka. প্রথম অর্ডারে একটি শাড়ি ৮৫০ আর দুই প্যাকেট চা ১২০ করে — মোট ১,০৯০, তাই ডেলিভারি ফ্রি। দ্বিতীয়টিতে তিনটি কলম মিলে ৩০ টাকা, সাথে ৬০ টাকা ডেলিভারি — ৯০. মূল বিষয়: OrderService-এর কোড একটুও না বদলে দ্বিতীয়বার কোনো SMS যায়নি — নকল FakeNotifier শুধু বার্তাটি একটি vector-এ রেখে দিয়েছে, যা টেস্ট পরে পরীক্ষা করতে পারে। আর orderTotal একটি সাধারণ ফাংশন — interface বা class বানানো হয়নি, কারণ দরকার ছিল না। কোথায় abstraction আর কোথায় নয় — এই বাছাইটাই ডিজাইন। (পর্ব ৪৮-এ এমন নকল দিয়ে আসল টেস্ট লেখা দেখবেন।)
SOLID-এর পাঁচটি ধারণা একসাথে জনপ্রিয় করেন Robert C. Martin (“Uncle Bob”), বিশেষ করে ২০০০ সালের তাঁর লেখা Design Principles and Design Patterns-এ। তবে প্রতিটি ধারণার শিকড় আরও পুরোনো: Open/Closed কথাটি Bertrand Meyer-এর ১৯৮৮ সালের বই Object-Oriented Software Construction থেকে, আর Liskov নীতি Barbara Liskov-এর ১৯৮৭ সালের একটি বক্তৃতা থেকে, যা পরে Jeannette Wing-এর সাথে ১৯৯৪ সালে আনুষ্ঠানিক রূপ পায়। “SOLID” — এই পাঁচ অক্ষরের সংক্ষিপ্ত নামটি দেন Michael Feathers, ২০০০-এর দশকের শুরুতে। আর Barbara Liskov ২০০৮ সালে কম্পিউটার বিজ্ঞানের সর্বোচ্চ সম্মান Turing Award পান — ডেটা abstraction-এ তাঁর কাজের জন্য।
১০ · AI-এর যুগে — অতিরিক্ত ডিজাইন আর god class চেনা
ডিজাইনের প্রশ্নে AI টুলগুলো সাধারণত দুই চরমে যায়। “একটি enterprise-grade, SOLID-মেনে চলা সিস্টেম লেখো” বললে আসে interface-এর পাহাড়: প্রতিটি class-এর জন্য একটি IThing, একটি ThingFactory, একটি ThingManager — যার বেশিরভাগের একটিমাত্র বাস্তবায়ন। আর কিছু না বলে “একটি অর্ডার প্রসেসিং প্রোগ্রাম লেখো” বললে প্রায়ই আসে একটি বিশাল main বা একটি god class, যেখানে হিসাব, প্রিন্ট আর ফাইল লেখা মেশানো। দুটিই compile হয়, চলেও — সমস্যা দেখা দেয় প্রথম বদলের সময়।
| AI যা করে | সমস্যা | যা চাইবেন |
|---|---|---|
| একটিমাত্র বাস্তবায়নের জন্য interface + factory | পড়তে বেশি ফাইল, বদলে কোনো লাভ নেই | সরাসরি class; দ্বিতীয় বাস্তবায়ন বা টেস্টের নকল লাগলে তখন interface |
Manager, Helper, Util নামের class | নাম কোনো দায়িত্ব বলে না — ধীরে ধীরে god class হয় | ব্যবসার ভাষার নাম, প্রতিটির একটি বদলের কারণ |
| সব ডেটার জন্য getter/setter | encapsulation-এর ভান; invariant কেউ রক্ষা করে না | সরল ডেটা হলে struct; নিয়ম থাকলে অর্থবহ ফাংশন |
| কোড পুনঃব্যবহারের জন্য inheritance | ভুল “is-a”, LSP ভাঙে | composition; inheritance শুধু polymorphism-এর জন্য |
সবকিছু heap-এ, std::shared_ptr দিয়ে | মালিকানা অস্পষ্ট, entity আর value মিশে যায় | value টাইপ সরাসরি; entity-র মালিক একজন |
| এক ফাংশনে হিসাব + প্রিন্ট + ফাইল | পরীক্ষা করা যায় না, প্রতিটি বদলে সব ঝুঁকিতে | হিসাব আলাদা free function, I/O আলাদা |
AI-কে নকশা চাইলে আগে বদলের সম্ভাবনা বলে দিন, আর সবচেয়ে সরল ঠিক সমাধানটি চান। নিচের prompt-টি কপি করে নিজের প্রজেক্ট অনুযায়ী বদলে নিন:
AI-এর নকশা যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।
প্র ০১SOLID-এর পাঁচটি নীতি কি একজন মানুষ একদিনে আবিষ্কার করেছেন? এই নামগুলোর ইতিহাস কী, আর ইতিহাসটা জানা কেন কাজের?
না — SOLID আসলে কয়েক দশকের অভিজ্ঞতার একটি সংকলন, যাকে একটি মনে রাখার মতো নাম দেওয়া হয়েছে। ১৯৯০-এর দশকে Robert C. Martin C++-এ বড় object-oriented সিস্টেমের পরামর্শক হিসেবে কাজ করতেন, আর comp.object-এর মতো অনলাইন আলোচনা-ফোরামে ও পত্রিকায় নকশার নীতি নিয়ে লিখতেন। ২০০০ সালে তাঁর লেখা Design Principles and Design Patterns-এ এই নীতিগুলো একসাথে আসে। তাঁর প্রথম দিকের উদাহরণের বড় অংশই ছিল C++-এ — header ফাইলের নির্ভরতা, recompile-এর সময়, virtual interface. অর্থাৎ SOLID জন্মেছিল C++-এর বাস্তব সমস্যা থেকেই।
প্রতিটি নীতির উৎস আলাদা। Open/Closed কথাটি Bertrand Meyer-এর ১৯৮৮ সালের বই Object-Oriented Software Construction-এর; Meyer অবশ্য inheritance দিয়ে “বন্ধ” class বাড়ানোর কথা ভেবেছিলেন, আর Martin পরে একে abstract interface-এর ভাষায় নতুন করে বলেন। Liskov নীতি Barbara Liskov-এর ১৯৮৭ সালের OOPSLA সম্মেলনের বক্তৃতা “Data Abstraction and Hierarchy” থেকে, যা তিনি Jeannette Wing-এর সাথে ১৯৯৪ সালে একটি আনুষ্ঠানিক সংজ্ঞায় রূপ দেন। Interface Segregation-এর পেছনে Martin একটি পরামর্শ-কাজের গল্প বলেন: Xerox-এর একটি প্রিন্টার সিস্টেমে একটি বিশাল Job class সব ধরনের কাজের জন্য ব্যবহার হতো, আর ছোট বদলেও পুরো সিস্টেম rebuild করতে অনেক সময় লাগত। Single Responsibility-র শিকড় আরও পুরোনো — ১৯৭০-এর দশকে David Parnas-এর মডিউল ভাগের ধারণা, আর cohesion নিয়ে Larry Constantine-দের কাজ। “SOLID” — এই সংক্ষিপ্ত নামটি দেন Michael Feathers, ২০০০-এর দশকের শুরুতে, Martin-এর নীতিগুলোর প্রথম অক্ষর সাজিয়ে।
ইতিহাসটা জানা কেন কাজের? কারণ এটি বোঝায় নীতিগুলো কোন সমস্যার উত্তর ছিল। ১৯৯০-এর C++ প্রজেক্টে একটি header বদলালে কয়েক ঘণ্টার recompile হতে পারত — তাই নির্ভরতা কমানো, ছোট interface আর abstraction-এর ওপর নির্ভরতা সরাসরি সময় বাঁচাত। আজ compiler অনেক দ্রুত, আর C++20-এর modules-ও আছে, তাই সেই চাপ কিছুটা কম; কিন্তু “একটি বদল কতদূর ছড়ায়” প্রশ্নটি আগের মতোই জরুরি। Martin নিজেও পরে SRP-কে আরও স্পষ্ট করেছেন: “একটি module একজন actor-এর — অর্থাৎ একদল মানুষের — কাছে দায়বদ্ধ থাকবে।”
মূল উপলব্ধি: SOLID কোনো পবিত্র গ্রন্থ নয়; এটি বাস্তব প্রজেক্টের ব্যথা থেকে আসা পাঁচটি শিক্ষা, যার প্রতিটির নির্দিষ্ট প্রেক্ষাপট আছে। নীতিটির পেছনের সমস্যাটি বুঝলে বুঝবেন কখন সেটি প্রয়োগ করতে হবে, আর কখন নয়।
প্র ০২১৯৯৪ সালের “Gang of Four”-এর Design Patterns বইয়ের সাথে C++-এর সম্পর্ক কী? আধুনিক C++-এ কি সেই pattern-গুলো এখনো একই রূপে লাগে?
১৯৯৪ সালে Erich Gamma, Richard Helm, Ralph Johnson আর John Vlissides — যাঁদের একসাথে “Gang of Four” (GoF) বলা হয় — প্রকাশ করেন Design Patterns: Elements of Reusable Object-Oriented Software. বইটিতে ২৩টি design pattern আছে — বারবার দেখা দেওয়া নকশা-সমস্যার পরীক্ষিত সমাধান, প্রতিটির একটি নাম: Strategy, Observer, Factory Method, Visitor, Adapter ইত্যাদি। “pattern” ধারণাটি তাঁরা নিয়েছিলেন স্থপতি Christopher Alexander-এর A Pattern Language (১৯৭৭) থেকে, যিনি বাড়ি আর শহরের নকশায় বারবার আসা সমাধানগুলোর তালিকা করেছিলেন।
বইয়ের উদাহরণ ছিল প্রধানত C++ আর Smalltalk-এ। তখন C++-এ template ছিল নতুন আর অপরিণত, lambda ছিল না, standard library-ও আজকের মতো ছিল না। তাই অনেক pattern-এর রূপ ছিল ভারী: Strategy মানে একটি abstract class, কয়েকটি derived class, আর heap-এ object. বইটির দুটি বড় উপদেশ আজও প্রাসঙ্গিক — “বাস্তবায়নের নয়, interface-এর ওপর প্রোগ্রাম লিখুন” আর “class inheritance-এর চেয়ে object composition-কে প্রাধান্য দিন” — যা আপনি এই পর্বে দেখলেন।
আধুনিক C++-এ অনেক pattern ভাষার যন্ত্রে মিশে গেছে। একটিমাত্র ফাংশন বদলানোর Strategy আজ প্রায়ই শুধু একটি lambda বা std::function — std::sort-এর তুলনা-ফাংশন নিজেই একটি Strategy. Iterator pattern STL-এর ভিত্তি (পর্ব ৩২)। Visitor-এর অনেক কাজ std::variant আর std::visit করে (পর্ব ৩৮)। RAII নিজেই এমন একটি C++-ধরনের pattern, যা GoF-এ নেই। ১৯৯৬ সালে Peter Norvig একটি উপস্থাপনায় দেখান, ২৩টির মধ্যে ১৬টি pattern Lisp বা Dylan-এর মতো dynamic ভাষায় অনেক সরল বা প্রায় অদৃশ্য হয়ে যায় — অর্থাৎ অনেক pattern আসলে ভাষার কোনো অভাব পূরণের কৌশল।
তার মানে GoF পুরোনো নয় — pattern-এর নাম আজও দারুণ কাজের, কারণ “এখানে একটি Observer লাগবে” বললে পুরো দল এক মুহূর্তে বোঝে। কিন্তু pattern-কে লক্ষ্য বানানো ভুল। “আমার কোডে কয়টি pattern আছে” একটি খারাপ মাপকাঠি; ভালো প্রশ্ন হলো “এই সমস্যার সবচেয়ে সরল সমাধান কী, আর তার কোনো পরিচিত নাম আছে কি?”
মূল উপলব্ধি: pattern হলো সমস্যা-সমাধানের একটি শব্দভাণ্ডার, রেসিপি নয়। আধুনিক C++-এ একই ধারণা প্রায়ই অনেক হালকা রূপে লেখা যায় — template, lambda, variant দিয়ে — আর ভালো C++ প্রোগ্রামার আগে সেই হালকা রূপটি খোঁজেন।
প্র ০৩SOLID-এর সমালোচনা কী কী? Dan North-এর CUPID কী বলতে চায়, আর “Clean Code” ধরনের নকশা নিয়ে পারফরম্যান্সের বিতর্কটা কী?
SOLID যত জনপ্রিয় হয়েছে, তার সমালোচনাও তত বেড়েছে। প্রথম সমালোচনা — নীতিগুলো অস্পষ্ট। “একটি দায়িত্ব” কতটা বড়? একজন বলেন একটি class-এর একটি কাজ, আরেকজন বলেন একটি পুরো মডিউল। এই অস্পষ্টতার কারণে একই কোড দেখে দুজন অভিজ্ঞ প্রোগ্রামার বিপরীত সিদ্ধান্তে পৌঁছান। দ্বিতীয় — নীতিগুলো object-oriented ভাষা, বিশেষ করে inheritance-ভিত্তিক ডিজাইন ধরে নিয়ে লেখা; functional বা data-ভিত্তিক কোডে এগুলোর অনেকটা খাপ খায় না। তৃতীয় — অন্ধভাবে মানলে interface-এর পাহাড় আর অপ্রয়োজনীয় পরোক্ষতা তৈরি হয়, যা আপনি অংশ ৭-এ দেখেছেন।
Behaviour-Driven Development (BDD)-এর জনক হিসেবে পরিচিত Dan North ২০১০-এর দশকের শেষ দিকে একটি বক্তৃতায় SOLID-এর প্রতিটি নীতিকে প্রশ্ন করেন, আর ২০২২ সালে একটি লেখায় বিকল্প হিসেবে প্রস্তাব করেন CUPID. তাঁর মূল কথা: নীতি (principle) নিয়ম হিসেবে হয় মানা হয়, নয় ভাঙা হয়; তার বদলে তিনি কিছু বৈশিষ্ট্য (property) বলেন, যা কোডে কম বা বেশি থাকতে পারে, আর যেদিকে এগোনো যায়। CUPID-এর পাঁচটি অক্ষর: Composable (সহজে অন্য কোডের সাথে জোড়া যায়), Unix philosophy (একটি কাজ ভালোভাবে করে), Predictable (যা আশা করেন, তাই করে), Idiomatic (ভাষার স্বাভাবিক রীতিতে লেখা), Domain-based (সমস্যার জগতের ভাষায় নাম ও গঠন)। খেয়াল করুন, “Idiomatic” C++-এর জন্য বিশেষ অর্থবহ — C++-এ ভালো কোড প্রায়ই Java-ধরনের interface-এর পাহাড় নয়, বরং value টাইপ, RAII আর algorithm.
আরেকটি বিতর্ক পারফরম্যান্স নিয়ে। ২০২৩ সালে গেম-প্রোগ্রামার Casey Muratori “Clean Code, Horrible Performance” নামে একটি ভিডিওতে দেখান, আকৃতির ক্ষেত্রফল হিসাবের একটি সরল উদাহরণে virtual ফাংশনের class-গাছের বদলে switch বা টেবিল ব্যবহার করলে কোড অনেক গুণ দ্রুত চলে। এর উত্তরে অনেকে বলেন, বেশিরভাগ ব্যবসায়িক সফটওয়্যারে সময় যায় নেটওয়ার্ক আর ডেটাবেসে, virtual ডাকে নয়; আর পারফরম্যান্স মাপা উচিত আসল প্রোফাইলিং দিয়ে (পর্ব ৪৯)। দুই পক্ষেরই আংশিক সত্য আছে: hot loop-এ লাখবার ডাকা কোডে পরোক্ষতার দাম বাস্তব, আর checkout-এর মতো কোডে পরিষ্কার নকশার লাভ বাস্তব।
মূল উপলব্ধি: SOLID একটি চিন্তার কাঠামো, একমাত্র কাঠামো নয়। সমালোচনাগুলো জানলে আপনি নীতিগুলোকে আরও সৎভাবে ব্যবহার করতে পারবেন — প্রতিটি নীতির দাম আর লাভ মেপে, প্রেক্ষাপট দেখে।
প্র ০৪গেম ইঞ্জিনের অনেক প্রোগ্রামার classic OOP ছেড়ে “data-oriented design” আর ECS-এর দিকে গেছেন। কেন? এটি কি এই পর্বের নীতিগুলোর বিরোধী?
Classic OOP-তে একটি গেমের প্রতিটি শত্রু একটি object: তার অবস্থান, গতি, স্বাস্থ্য, ছবি — সব একসাথে, আর একটি virtual update(). হাজার হাজার শত্রু থাকলে প্রতিটি frame-এ (সেকেন্ডে ৬০ বার) প্রতিটির update() ডাকা হয়। সমস্যা হলো আধুনিক CPU-এর গতি নির্ভর করে মেমরি থেকে ডেটা কত দ্রুত আসে তার ওপর — cache locality (পর্ব ১৩)। heap-এ ছড়ানো object, প্রতিটিতে অনেক অপ্রয়োজনীয় ডেটা, আর প্রতিটি ডাকে একটি virtual লাফ — এতে CPU-এর cache বারবার খালি হয়, আর CPU বেশিরভাগ সময় অপেক্ষায় কাটায়।
data-oriented design প্রশ্নটা উল্টে দেয়: “আমার object কী?” নয়, “আমার ডেটা কী, আর CPU সেটা কীভাবে পড়বে?” সব শত্রুর অবস্থান একটি std::vector-এ পাশাপাশি, সব গতি আরেকটিতে — একে বলে Structure of Arrays. অবস্থান হালনাগাদের লুপ শুধু এই দুটি vector পড়ে, একটানা, কোনো virtual ডাক ছাড়া। ২০১৪ সালের CppCon-এ Insomniac Games-এর Mike Acton-এর বক্তৃতা “Data-Oriented Design and C++” এই ভাবনাকে C++ সম্প্রদায়ে ব্যাপক পরিচিতি দেয়; তাঁর একটি বিখ্যাত কথা ছিল, সফটওয়্যারের আসল কাজ হলো ডেটাকে এক রূপ থেকে আরেক রূপে বদলানো। এর একটি জনপ্রিয় কাঠামো ECS (Entity-Component-System): entity শুধু একটি id, component শুধু ডেটা (Position, Health), আর system হলো ফাংশন, যা নির্দিষ্ট component-এর সব ডেটার ওপর একসাথে চলে। Unity-র DOTS আর অনেক ইঞ্জিন এই পথে কাজ করে।
এটি কি এই পর্বের নীতির বিরোধী? আংশিক। inheritance-এর গাছের প্রতি অবিশ্বাস দুই পক্ষেরই আছে — ECS আসলে composition-এর একটি চরম রূপ: একটি entity-র “has-a” Position, Health, Sprite; inheritance একেবারেই নেই। Separation of concerns-ও আছে: প্রতিটি system একটি কাজ করে। পার্থক্য হলো “object নিজের ডেটা আর আচরণ একসাথে লুকিয়ে রাখবে” — OOP-এর এই মূল ধারণা data-oriented design-এ ইচ্ছা করে ভাঙা হয়, কারণ সেখানে প্রধান বাধা বদলের খরচ নয়, মেমরির গতি।
মূল উপলব্ধি: ডিজাইন নীতি সবসময় একটি নির্দিষ্ট চাপের উত্তর। ব্যবসায়িক সফটওয়্যারে চাপ বদলের হার, তাই SOLID; গেম ইঞ্জিনের ভেতরের লুপে চাপ মেমরির গতি, তাই data-oriented design. C++ দুটিই সমানভাবে সমর্থন করে, আর একই প্রোগ্রামে দুটি একসাথেও থাকতে পারে।
প্র ০৫Bjarne Stroustrup বারবার বলেছেন C++ “শুধু একটি object-oriented ভাষা” নয়। তাহলে C++-এ ভালো ডিজাইন কি Java-র ভালো ডিজাইনের মতোই, নাকি আলাদা?
Stroustrup C++-কে বর্ণনা করেন একটি সাধারণ-উদ্দেশ্যের ভাষা হিসেবে, যা কয়েকটি প্রোগ্রামিং ধারাকে (paradigm) সমর্থন করে — C-ধরনের procedural প্রোগ্রামিং, data abstraction (নিজের টাইপ বানানো), object-oriented প্রোগ্রামিং আর generic প্রোগ্রামিং (template). এজন্যই C++-কে বলা হয় multi-paradigm ভাষা (পর্ব ১-এ এই শব্দটি প্রথম দেখেছেন)। তিনি একাধিকবার সতর্ক করেছেন, সব সমস্যাকে class-এর গাছ দিয়ে সমাধানের চেষ্টা — “সবকিছু একটি Object থেকে আসবে” — ভালো C++ নয়। তাঁর পরামর্শ: যেখানে সত্যিকারের hierarchy আছে আর চলার সময়ে টাইপ বাছাই দরকার, সেখানে inheritance ও virtual; বাকি জায়গায় concrete টাইপ, template আর সরল ফাংশন।
সবচেয়ে স্পষ্ট উদাহরণ C++-এর নিজের standard library. STL নকশা করেছিলেন Alexander Stepanov, আর তাতে std::vector, std::map বা std::sort-এ কোনো virtual ফাংশন নেই, কোনো সাধারণ base class নেই। container আর algorithm জোড়া লাগে iterator আর template দিয়ে — compile-এর সময়ের polymorphism. Stepanov নিজে classic OOP-এর কড়া সমালোচক ছিলেন বলে পরিচিত। অথচ STL এই পর্বের নীতিগুলো দারুণভাবে মানে: প্রতিটি algorithm একটি কাজ করে (SRP), নতুন container বানালে পুরোনো algorithm বদলাতে হয় না (OCP), আর algorithm নির্ভর করে iterator-এর ধারণার ওপর, নির্দিষ্ট container-এর ওপর নয় (DIP-এর মতো)। অর্থাৎ নীতিগুলো inheritance ছাড়াও মানা যায়।
তাই C++-এর ভালো ডিজাইন Java-র থেকে কয়েক জায়গায় আলাদা। (১) value টাইপ প্রথম শ্রেণির নাগরিক — Money, Date সরাসরি stack-এ, কপিযোগ্য, heap ছাড়া। (২) সবকিছু class-এর ভেতরে হতে হয় না — namespace-এর free function প্রায়ই সবচেয়ে পরিষ্কার নকশা; Scott Meyers এমনকি যুক্তি দিয়েছেন যে non-member ফাংশন encapsulation বাড়াতে পারে। (৩) abstraction-এর তিনটি যন্ত্র: virtual (চলার সময়), template ও concepts (compile-এর সময়), আর std::variant (নির্দিষ্ট কয়েকটি টাইপের মধ্যে)। (৪) মালিকানা আর lifetime ডিজাইনের অংশ — RAII আর smart pointer (পর্ব ১৭) প্রতিটি নকশায় প্রশ্ন তোলে: কে কার মালিক।
মূল উপলব্ধি: SOLID-এর পেছনের লক্ষ্য — কম coupling, উচ্চ cohesion, সস্তা বদল — সব ভাষায় একই। কিন্তু C++-এ সেই লক্ষ্যে পৌঁছানোর পথ অনেক: inheritance তার একটি মাত্র। পরের মডিউলে template দিয়ে আপনি আরেকটি শক্তিশালী পথ শিখবেন।
অনুশীলন
বেশিরভাগ কাজে কোড লিখতে হবে না — আগে নিজে ভাবুন, তারপর উত্তর মিলিয়ে নিন।
-
has-a না is-a? প্রতিটি জোড়ার জন্য বলুন composition না inheritance, কারণসহ — (ক)
StudentআরPerson(খ)BusআরSeat(গ)Stackআরstd::vector(ঘ)BKashPayআরPaymentMethod.(ক) সাধারণত inheritance চলে — একজন ছাত্র সব জায়গায় একজন মানুষের মতো আচরণ করে; তবে একজন মানুষ পরে শিক্ষকও হতে পারেন, তাই অনেক সিস্টেমে “Person has-a Role” composition আরও নমনীয়। (খ) composition — বাসের অনেক সিট আছে, বাস কোনো সিট নয়। (গ) composition — Stack-এর ভেতরে একটি vector রাখুন; inheritance নিলে vector-এর
insert,operator[]বাইরে চলে আসে আর Stack-এর নিয়ম (শুধু ওপর থেকে) ভাঙা যায়। standard library-রstd::stackঠিক এভাবে একটি container সদস্য রাখে। (ঘ) inheritance — এটি interface পূরণ, আর polymorphism-ই উদ্দেশ্য। -
কোন নীতি ভাঙছে? (ক) একটি
PenguinclassBirdথেকে এসেছে, আর তারfly()exception ছোড়ে। (খ)Invoiceclass নিজেই PDF বানায়, ইমেইল পাঠায় আর ট্যাক্স হিসাব করে। (গ)ReportService-এর constructor-এর ভেতরেMySqlDatabase db("10.0.0.5");লেখা। (ঘ) নতুন কুরিয়ার কোম্পানি যোগ করতে পাঁচটি ফাইলেরswitchবদলাতে হয়।(ক) Liskov Substitution — Bird-এর জন্য লেখা কোড Penguin পেলে ভেঙে যায়; সম্ভবত
fly()Bird-এর অংশই হওয়া উচিত নয় (একটি ছোটFlyerinterface — ISP-ও)। (খ) Single Responsibility — তিনটি আলাদা বদলের কারণ। (গ) Dependency Inversion — উঁচু স্তর একটি নির্দিষ্ট ডেটাবেস আর ঠিকানায় বাঁধা; একটিReportStoreinterface constructor-এ বাইরে থেকে নিন। (ঘ) Open/Closed — যদি নতুন কুরিয়ার ঘন ঘন আসে, একটিCourierinterface লাভজনক। -
বেশি না কম? একটি AI একটি ২০০ লাইনের কনসোল ক্যালকুলেটরের জন্য দিয়েছে:
IOperation,OperationFactory,AddOperation,SubOperation,MulOperation,DivOperation,CalculatorManager,InputHelper. আপনি কী রাখবেন, কী বদলাবেন?চারটি স্থির অপারেশনের জন্য এটি অতিরিক্ত ডিজাইন — নতুন অপারেশন আসার বাস্তব সম্ভাবনা কম, আর এলে একটি
switch-এ এক লাইন যোগই যথেষ্ট। সরল সমাধান: একটিdouble apply(char op, double a, double b)ফাংশনswitchদিয়ে, ইনপুট পড়ার একটি ফাংশন, আরmain.ManagerআরHelperনাম কোনো দায়িত্ব বলে না — বাদ। যদি সত্যিই প্রত্যাশা থাকে যে ব্যবহারকারী নতুন অপারেশন যোগ করবেন, তাহলেstd::map<char, std::function<double(double, double)>>— তবু কোনো factory বা class-গাছ লাগে না। (KISS, YAGNI.) -
নিজে লিখুন (কোড): উপরের
ride_fare.cpp-এ একটিStudentDiscountনিয়ম যোগ করুন, যা সাধারণ ভাড়া থেকে ২০% কমায়। আগে অনুমান করুন —Rideবা পুরোনো কোনোFareRuleclass কি খুলতে হবে? তারপরcar-এ নিয়মটি বসিয়ে ৮ কিলোমিটারের ভাড়া বের করুন।কোনো পুরোনো class খুলতে হয় না — এটাই Open/Closed. যোগ করুন:
struct StudentDiscount : FareRule { int fare(int km) const override { return (30 + 25 * km) * 4 / 5; } };আরmain-এর শেষেcar.setRule(std::make_unique<StudentDiscount>()); car.quote(8);. ফল: ২৩০ × ৪ ÷ ৫ = ১৮৪ — আউটপুট8 km -> 184 taka. আরও ভালো নকশা চাইলে ভাবুন: ছাড়টি কি অন্য যেকোনো নিয়মের ওপর বসানো যায়? (একটিDiscountনিয়ম, যার ভেতরে আরেকটিFareRuleআছে — composition-এর ভেতরে composition; এর নাম Decorator pattern.)
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকটি বড় god class নিরাপদে refactor করার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- কিছু বদলানোর আগে বর্তমান আচরণ ধরে রাখার কয়েকটি টেস্ট লেখা
- I/O ছাড়া হিসাবের অংশ আলাদা ফাংশনে বের করা
- বাইরের জগতের (ডেটাবেস, SMS) জন্য interface বানিয়ে নির্ভরতা উল্টানো
- মূল class-কে শুধু সমন্বয়কারী হিসেবে রেখে টেস্ট আবার চালানো
C++-এ একটি Car class-এর একটি Engine দরকার। কোন নকশাটি সাধারণত সবচেয়ে উপযুক্ত?
- class Car : public Engine
- Engine-কে Car-এর একটি সদস্য হিসেবে রাখা (composition)
- class Car : private Engine
- একটি global Engine ভ্যারিয়েবল, যা সব Car ব্যবহার করে
একটি C++ Square class বদলানো যায় এমন Rectangle থেকে এসেছে, আর setWidth উচ্চতাও বদলে দেয়। এটি কোন নীতি ভাঙে?
- Single Responsibility
- Open/Closed
- Liskov Substitution
- Interface Segregation
একটি অনলাইন দোকানে নতুন পেমেন্ট পদ্ধতি যোগ করতে শুধু একটি নতুন class লিখতে হয়, যা PaymentMethod interface পূরণ করে, আর checkout-এর কোড একটুও বদলায় না। এটি কোন নীতির উদাহরণ?
- Open/Closed
- Liskov Substitution
- DRY
- YAGNI
C++-এ একটি OrderService constructor-এ একটি Notifier& নেয়, যা একটি interface; আসল দোকানে SMS আর টেস্টে নকল Notifier দেওয়া হয়। নির্ভরতা এভাবে বাইরে থেকে দেওয়ার কৌশলের নাম কী?
- Inheritance
- Singleton
- Law of Demeter
- Dependency injection
C++-এ ডিজাইনের ক্ষেত্রে নিচের কোনটি একটি value টাইপের সবচেয়ে ভালো উদাহরণ?
- একটি Customer, যার একটি id আছে
- একটি Money, যা টাকা ও পয়সা রাখে
- একটি Ride, যা শুরু থেকে শেষ পর্যন্ত অবস্থা বদলায়
- একটি BankAccount, যার ব্যালান্স বদলায়
একটি C++ প্রজেক্টে একটি interface-এর মাত্র একটি বাস্তবায়ন আছে, আর দ্বিতীয়টির কোনো বাস্তব পরিকল্পনা বা টেস্টের প্রয়োজন নেই। কোন নীতি অনুযায়ী interface-টি সম্ভবত অপ্রয়োজনীয়?
- Interface Segregation
- Dependency Inversion
- YAGNI
- Liskov Substitution
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- ভালো ডিজাইন = সস্তা বদল। লক্ষ্য ভেতরে উচ্চ cohesion, বাইরে কম coupling; নীতিগুলো আইন নয়, এই লক্ষ্যের কৌশল।
- আগে composition ভাবুন। inheritance শুধু সেখানে, যেখানে “is-a” আচরণে সত্য আর polymorphism দরকার; কয়েকটি দিক একসাথে বদলালে composition class-এর বিস্ফোরণ থামায়।
- SOLID: একটি বদলের কারণ; নতুন যোগে পুরোনো অক্ষত; derived base-এর প্রতিশ্রুতি রাখে; interface ছোট; নির্ভরতা abstraction-এর ওপর, বাইরে থেকে দেওয়া।
- DRY, KISS, YAGNI, Law of Demeter — একই জ্ঞান এক জায়গায়, সরল সমাধান, আজ যা লাগে তাই, আর কাছের object-এর সাথেই কথা।
- C++-এ value বনাম entity আগে ঠিক করুন, আর অতিরিক্ত ডিজাইনকেও বাগ ভাবুন — AI-এর নকশায় interface গুনুন, প্রতিটির কারণ চান।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ২৮ · function templateপরবর্তী পর্বমডিউল ৫-এর শুরু — একবার লিখে অনেক টাইপে ব্যবহার।
- C++ প্রোগ্রামিং · কোর্স হোম৫৫টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- পর্ব ২৬ · abstract class ও interfaceআবার দেখুনPaymentMethod আর Notifier-এর মতো interface কীভাবে লেখা হয় — এই পর্বের প্রতিটি নীতির ভিত্তি।