এই পর্বে যা শিখবেন
- একটি class-এর লুকানো dependency খুঁজে বের করে constructor injection দিয়ে প্রকাশ্যে আনা, আর fake দিয়ে test করা
- Composition root-এ পুরো object graph জোড়া, আর production বনাম test-এর আলাদা জোড়া বানানো
- একটি ছোট DI container কীভাবে dependency graph resolve করে, lifetime মানে, আর cycle ধরে — তা নিজে লিখে দেখা
- Service locator আর constructor over-injection চিনে সমালোচনা করা
- Fragile base class-এর সমস্যা ধাপে ধাপে বুঝে composition আর delegation দিয়ে সারানো
১ · সমস্যা — class-এর ভেতরে লুকানো new
পর্ব ২৮-এ তিনটি নাম আলাদা করেছিলেন: dependency inversion একটি নীতি (নির্ভরতার তীর কোন দিকে, abstraction-এর মালিক কে), dependency injection একটি কৌশল (object তার dependency কোথা থেকে পায়), আর inversion of control একটি বড় ধারণা (প্রবাহ কে চালায়)। সেখানে কথা দেওয়া ছিল, injection-এর কৌশলগুলো আর DI container-এর ভেতরের কাজ এই পর্বে আসবে। মডিউল ৬-এর pattern-গুলো (পর্ব ৩২ থেকে পর্ব ৩৬) দেখলে লক্ষ করবেন, প্রায় প্রতিটির ভেতরে একটি object আরেকটি object-কে “হাতে পায়” — strategy পায় তার algorithm, decorator পায় যাকে সে মোড়ায়, observer-এর subject পায় তার শ্রোতাদের। হাতে পাওয়াটা কে, কোথায়, কীভাবে করে — এই পর্বের বিষয় সেটিই।
একটি চায়ের দোকানের কথা ভাবুন। এক দোকানদার নিজের বাড়ির গরু থেকে দুধ, নিজের বাগান থেকে পাতা আর নিজের কুয়া থেকে পানি আনেন — সব দোকানের ভেতরে “বাঁধা”। গরু অসুস্থ হলে দোকান বন্ধ; অন্য দুধে চা কেমন হয় তা পরখ করতেও গরু বদলাতে হবে! আরেক দোকানদার বলেন, “দুধ, পাতা, পানি — যে সরবরাহকারী দেবেন, নিয়ম মেনে দিলেই হলো।” সকালে গোয়ালা দুধ দিয়ে যান, পাতা আসে পাইকারের কাছ থেকে। দ্বিতীয় দোকানদারের চা বানানোর দক্ষতা একই, কিন্তু তাঁর উপকরণ বাইরে থেকে আসে — তাই সরবরাহকারী বদলানো যায়, নতুন দুধ চেখে দেখা যায়, দোকান কারও ওপর জিম্মি নয়। Dependency injection হলো দ্বিতীয় দোকানদারের পদ্ধতি।
কোডে প্রথম দোকানদারকে চেনা যায় class-এর ভেতরের new (Python-এ সরাসরি class ডেকে object বানানো) দেখে। একটি কাল্পনিক মুদি-দোকানের হিসাব app-এর ReportService প্রতি মাসে দোকান-মালিককে বিক্রির রিপোর্ট email করে। প্রথম সংস্করণে সে নিজের constructor-এ SmtpMailer() বানায়। এতে তিনটি সমস্যা:
- লুকানো নির্ভরতা (hidden dependency):
ReportService()-এর signature দেখে বোঝার উপায় নেই যে এটি চালাতে mail server লাগবে। Caller জানতে পারেন কেবল চালানোর পর, error পেয়ে। - Test করা যায় না: “ঋণাত্মক মোট হলে mail যাবে না” — এই নিয়ম যাচাই করতে গেলেও আসল SMTP server-এ সংযোগের চেষ্টা হয়। Test ধীর, পরিবেশ-নির্ভর, আর কখনো কখনো অকারণে ব্যর্থ।
- অনমনীয়: SMS বা অন্য কোনো email সেবায় যেতে হলে
ReportService-এর কোড খুলতে হবে, যদিও রিপোর্টের নিয়মে কোনো বদল নেই — পর্ব ২৭-এর open-closed-এর উল্টো।
নিচের কোডে আগে আর পরে পাশাপাশি। SmtpMailer এখানে ইচ্ছা করে ConnectionError ছোড়ে, যেন test চালানোর যন্ত্রে mail server নেই। দ্বিতীয় সংস্করণে mailer আসে constructor-এর parameter হয়ে; test-এ বসে FakeMailer, যা mail পাঠায় না, শুধু কে কী পাঠাতে চাইল তা লিখে রাখে।
class SmtpMailer: # the real detail
def send(self, to, subject, body):
raise ConnectionError("cannot reach smtp.example.com:587")
class ReportServiceV1: # BEFORE: hard-wired new
def __init__(self):
self.mailer = SmtpMailer() # hidden dependency
def send_monthly(self, shop, total):
self.mailer.send(f"{shop}@example.com", "Monthly report", f"total={total}")
class ReportService: # AFTER: constructor injection
def __init__(self, mailer):
self.mailer = mailer # any object with send(to, subject, body)
def send_monthly(self, shop, total):
if total < 0: raise ValueError("total cannot be negative")
self.mailer.send(f"{shop}@example.com", "Monthly report", f"total={total}")
class FakeMailer: # records calls instead of sending
def __init__(self): self.sent = []
def send(self, to, subject, body): self.sent.append((to, subject, body))
def check(name, fn):
try: fn(); print("PASS", name)
except Exception as e: print("FAIL", name, "->", type(e).__name__, e)
def test_v1(): ReportServiceV1().send_monthly("rahim-store", 5200)
def test_sends_one_mail():
fake = FakeMailer()
ReportService(fake).send_monthly("rahim-store", 5200)
assert fake.sent == [("rahim-store@example.com", "Monthly report", "total=5200")]
def test_negative_sends_nothing():
fake = FakeMailer()
try: ReportService(fake).send_monthly("rahim-store", -1); assert False
except ValueError: assert fake.sent == []
for t in (test_v1, test_sends_one_mail, test_negative_sends_nothing): check(t.__name__, t)আউটপুট:
FAIL test_v1 -> ConnectionError cannot reach smtp.example.com:587
PASS test_sends_one_mail
PASS test_negative_sends_nothing
test_v1 ব্যর্থ, কিন্তু রিপোর্টের নিয়মে কোনো ভুলের জন্য নয় — mail server না থাকায়। দ্বিতীয় সংস্করণে ReportService(fake) লিখেই নির্ভরতা চোখের সামনে: “আমাকে একটি mailer দিন।” test_sends_one_mail দেখল ঠিক একটি mail, ঠিক ঠিকানায়, ঠিক বিষয়ে পাঠানোর চেষ্টা হয়েছে; test_negative_sends_nothing দেখল ঋণাত্মক মোটে ValueError এসেছে এবং কোনো mail যায়নি। FakeMailer হলো একটি fake — যা ডাক এসেছে তা রেখে দেয় বলে কেউ কেউ একে spy-ও বলেন; নামের এই পার্থক্য আর কোন test double কখন, তা পর্ব ৪৯-এ। লক্ষ করুন, ReportService কোনো interface বা base class চায়নি; Python-এ send(to, subject, body) method থাকলেই চলে (duck typing).
Dependency injection-এর মূল কথা এক লাইনের: যে object কোনো কাজ করে, সে তার সহযোগী বানায় না; সহযোগী তাকে দেওয়া হয়। “Injection” শব্দটি ভয় দেখায়, কিন্তু কৌশলটি আসলে একটি constructor parameter. জটিলতা আসে পরে — কে দেবে, কোথায় বানানো হবে, কতদিন বাঁচবে — আর বাকি পর্ব সেই প্রশ্নগুলোর উত্তর।
২ · Injection-এর চার রূপ
Martin Fowler-এর ২০০৪ সালের প্রবন্ধে (বিস্তারিত অংশ ৫-এ) তিনটি রূপের নাম আসে — constructor, setter আর interface injection; চতুর্থটি, method বা parameter injection, পরে অনেক লেখক আলাদা করে দেখান। চারটির একটিই প্রশ্ন: dependency কোন দরজা দিয়ে ঢোকে?
| রূপ | কীভাবে | কখন ঠিক | ঝুঁকি |
|---|---|---|---|
| Constructor injection | ReportService(mailer) | যে dependency ছাড়া object কাজই করতে পারে না — প্রায় সব ক্ষেত্রে প্রথম পছন্দ | Parameter অনেক হয়ে গেলে চোখে পড়ে — এটি আসলে সুবিধা (অংশ ৫) |
| Setter / property injection | svc.logger = FileLogger() | ঐচ্ছিক dependency, যার একটি যুক্তিসঙ্গত default আছে; framework-এর কিছু ক্ষেত্রে | Set করতে ভুললে আধা-তৈরি object; পরে বদলালে আচরণ হঠাৎ বদলায় |
| Method / parameter injection | report.render(formatter) | প্রতিবার ডাকার সময় আলাদা জিনিস লাগে — যেমন এই অনুরোধের ভাষা বা এই মুহূর্তের সময় | প্রতিটি caller-কে জিনিসটি জোগাড় করতে হয়; বেশি হলে parameter-এর মিছিল |
| Interface injection | class একটি বিশেষ interface বাস্তবায়ন করে, যেমন inject_mailer(m); container সেটি ডাকে | কিছু পুরোনো framework-এ ব্যবহার হতো; আজ বিরল | প্রতিটি dependency-র জন্য framework-নির্দিষ্ট interface — coupling বাড়ে |
class ReportService:
def __init__(self, mailer, repo): # constructor: required
self.mailer, self.repo = mailer, repo
self.logger = NullLogger() # setter target: optional, safe default
def render(self, month, formatter): # method: varies per call
return formatter.format(self.repo.sales(month))
svc = ReportService(SmtpMailer(cfg), SqlSalesRepo(cfg))
svc.logger = FileLogger("report.log") # setter / property injection
print(svc.render("2026-03", BanglaFormatter()))এই উদাহরণ (চালানোর জন্য নয়, শুধু আকার দেখানোর জন্য) একটি প্রচলিত সুপারিশ মানে: যা বাধ্যতামূলক, তা constructor-এ; যা ঐচ্ছিক, তার জন্য নিরাপদ default আর setter; যা প্রতিবার বদলায়, তা method-এর parameter-এ। Constructor injection-এর প্রধান সুবিধা হলো, object তৈরি হওয়ার মুহূর্তেই সম্পূর্ণ — কোনো “আধা-তৈরি” অবস্থা নেই, আর field-গুলো চাইলে অপরিবর্তনীয় রাখা যায় (পর্ব ৩১-এর immutability). NullLogger হলো এমন একটি object যা কিছুই করে না — “logger নেই” বোঝাতে None পরীক্ষার বদলে এটি বসালে কোডে if self.logger: ছড়াতে হয় না।
Default হিসেবে আসল object বানানো একটি ফাঁদ হতে পারে। def __init__(self, mailer=None): self.mailer = mailer or SmtpMailer() দেখতে সুবিধাজনক, আর পুরোনো কোড ধীরে ধীরে বদলানোর সময় সাময়িকভাবে কাজেরও। কিন্তু থেকে গেলে SmtpMailer-এর import আর নির্ভরতা class-এর ভেতরেই রয়ে যায়, আর কেউ parameter দিতে ভুললে test চুপচাপ আসল server-এ যায়। Seemann তাঁর ২০১১ সালের বইয়ে এই রূপকে “Bastard Injection” নামে একটি anti-pattern বলেছিলেন বলে জানা যায় — নামটি কঠোর, কিন্তু সতর্কতাটি বাস্তব।
৩ · Composition root — সব জোড়া এক জায়গায়
প্রতিটি class যদি বলে “আমাকে দিন, আমি বানাব না”, তাহলে প্রশ্ন জাগে: শেষ পর্যন্ত বানাবে কে? উত্তর: একটি জায়গা, program-এর একেবারে শুরুর কাছে। Mark Seemann এই জায়গার নাম দেন composition root — তাঁর Dependency Injection in .NET (২০১১) বইয়ে এই ধারণাটি কেন্দ্রীয়, বলে সাধারণত উল্লেখ করা হয়। Console app-এ এটি main(), web app-এ server চালু হওয়ার কোড, test-এ প্রতিটি test-এর শুরুর অংশ। নিয়মটি সরল: object বানানো আর জোড়া হবে composition root-এ; বাকি সব কোড কেবল যা পেয়েছে তা ব্যবহার করবে।
বিয়েবাড়ির আয়োজনের তালিকা মনে করুন। বাবুর্চি, ডেকোরেটর, সাউন্ড সিস্টেম — কে আসবেন, কে কার সাথে সমন্বয় করবেন, তা ঠিক হয় একটি তালিকায়, একজন আয়োজকের হাতে। বাবুর্চি নিজে ডেকোরেটর খুঁজতে বের হন না; তিনি রান্না করেন, আর তাঁর দরকারি জিনিস (চুলা, হাঁড়ি, সহকারী) আয়োজক জোগাড় করে দেন। আয়োজকের তালিকাটিই composition root — পুরো অনুষ্ঠানের জোড়া এক পাতায় দেখা যায়, আর একজন বাবুর্চি বদলাতে চাইলে শুধু সেই তালিকা বদলায়।
Composition root-এ তৈরি object graph
ড্যাশ তীর = composition root বানায় (new); টানা তীর = object অন্যটিকে ধরে রাখে ও ব্যবহার করে
main() object বানায়। ডান দিকের কোনো class-এর ভেতরে new নেই; প্রত্যেকে constructor-এ যা পেয়েছে, তা ধরে রাখে। Config একটিই, দুজন ভাগ করে — এটিও composition root-এর সিদ্ধান্ত, Config-এর নিজের নয়।নিচের stepper-এ composition root লাইন ধরে চলছে। খেয়াল করুন, ক্রমটি dependency graph-এর নিচ থেকে ওপরে: যার কোনো নির্ভরতা নেই (Config) সে আগে, যে সবার ওপর নির্ভর করে (Scheduler) সে শেষে।
- main()
Config.load()শুরু - SmtpMailer
(config)detail - SqlSalesRepo
(config)detail - ReportService
(repo, mailer)policy - Scheduler
.start()চালু
| এই ধাপে বানানো | |
| মোট object | |
| Config ভাগ করে | |
| বাকি কোডে new |
- শুরু: configuration.
main()প্রথমে settings পড়ে একটিConfigবানায় — server-এর ঠিকানা, database-এর নাম। এর নিজের কোনো dependency নেই, তাই সবার আগে। - প্রথম detail:
SmtpMailer(config). Mailer config পেল constructor-এ। সে জানে না config কোথা থেকে এসেছে, জানার দরকারও নেই। - দ্বিতীয় detail:
SqlSalesRepo(config). একইConfigobject এখানেও গেল। “Config একটিই হবে” — এই সিদ্ধান্ত Config class-এর ভেতরে কোনো singleton কৌশলে নয়, composition root-এ একবার বানিয়ে দুজনকে দেওয়ার মাধ্যমে। - Policy:
ReportService(repo, mailer). দুটি detail তৈরি, তাই এখন policy বানানো যায়। Constructor-এর দুটি parameter-ই পুরো নির্ভরতার তালিকা — কিছু লুকানো নেই। - চালু:
Scheduler(report).start(). Graph সম্পূর্ণ, program চলতে শুরু করল। এখন থেকে কোনো class নতুন সহযোগী বানায় না; সবাই যা পেয়েছে তা দিয়ে কাজ করে। পুরো জোড়া এক screen-এ দেখা যায়। - Test-এর নিজের composition root. একই
ReportService, কিন্তু test-এর শুরুতে জোড়া হলোInMemorySalesRepoআরFakeMailerদিয়ে। Config, SMTP বা database কিছুই লাগল না — মাত্র ৩টি object.
এই জোড়া হাতে লেখা — কোনো library ছাড়া, সাধারণ constructor ডেকে। Seemann একে প্রথমে “Poor Man's DI” বলতেন, পরে নাম বদলে pure DI রাখেন, কারণ এটি দুর্বল বিকল্প নয় — অনেক সময় সবচেয়ে ভালো বিকল্প। Python-এ এর আকার:
def main(): # the composition root
config = Config.load("shop.toml")
mailer = SmtpMailer(config)
repo = SqlSalesRepo(config)
report = ReportService(repo, mailer)
Scheduler(report).start()
if __name__ == "__main__":
main()Composition root কোথায় থাকবে না: library-র ভেতরে। আপনি যদি অন্যদের ব্যবহারের জন্য একটি package লেখেন, তার ভেতরে নিজস্ব container বা global জোড়া রাখবেন না — package শুধু constructor-এ dependency চাইবে, আর জোড়া দেবে যে app package-টি ব্যবহার করে। একটি app-এ composition root একটিই (test-গুলোর নিজস্ব আলাদা), আর সেটি app-এর সবচেয়ে বাইরের স্তরে থাকে — architecture-এর স্তরের ভাষায় পর্ব ৪০-এ এই কথাটি আবার আসবে।
৪ · Pure DI বনাম DI container
পাঁচটি object হাতে জোড়া সহজ। কিন্তু একটি বড় web app-এ দুই-তিনশো class থাকতে পারে, আর প্রতিটি web request-এর জন্য কিছু object নতুন করে বানাতে হয়। তখন অনেক দল একটি DI container ব্যবহার করে — একটি library, যা “কোন নামের জিনিস কীভাবে বানাতে হয়” তার তালিকা রাখে আর চাইলে পুরো graph নিজেই বানিয়ে দেয়। Container তিনটি কাজ করে:
- Registration (নিবন্ধন): composition root-এ বলে দেওয়া — “
mailerচাইলেSmtpMailerদেবে, তার লাগবেconfig.” - Resolution (সমাধান): কেউ
ordersচাইলে container তার dependency-গুলো খোঁজে, সেগুলোর dependency খোঁজে — graph-এর নিচ পর্যন্ত — তারপর নিচ থেকে ওপরে বানায়, ঠিক stepper ১-এর ক্রমে। - Lifetime (আয়ুষ্কাল) ব্যবস্থাপনা: প্রতিবার নতুন object, না একটিই সবার জন্য? এর উত্তর service lifetime.
| Lifetime | মানে | উপযুক্ত জিনিস |
|---|---|---|
| Transient | যতবার চাওয়া, ততবার নতুন object | ছোট, অবস্থাহীন বা সস্তা জিনিস; যার অবস্থা কারও সাথে ভাগ করা উচিত নয় |
| Scoped | একটি scope-এর (সাধারণত একটি web request) ভেতরে একটি; নতুন scope-এ নতুন | একটি request-এর কাজের একক — database transaction, এই ক্রেতার cart |
| Singleton | পুরো container-এ (সাধারণত পুরো app-এ) একটিই | দামি বা ভাগ করা জিনিস — config, connection pool, cache; thread-safe হতে হবে |
Container-এর “singleton lifetime” আর পর্ব ৩৩-এর Singleton pattern এক জিনিস নয়। Pattern-এ class নিজেই নিশ্চিত করে যে তার একটিই object হবে, আর সেটি global ভাবে যে কেউ নিতে পারে — ফলে লুকানো নির্ভরতা আর test-এ বদলানোর কষ্ট। Container-এর singleton-এ class একেবারে সাধারণ; “একটিই হবে” সিদ্ধান্তটি composition root-এর, আর object পৌঁছায় constructor দিয়ে, global দিয়ে নয়। Test-এ আপনি নির্দ্বিধায় নতুন একটি বানাতে পারেন।
অনেক container আরও একটি সুবিধা দেয় — auto-wiring: constructor-এর parameter-এর type বা নাম দেখে container নিজেই বোঝে কী দিতে হবে, প্রতিটি জোড়া হাতে লিখতে হয় না। সুবিধা হলো কম কোড; মূল্য হলো জোড়া চোখে দেখা যায় না, আর ভুল অনেক সময় ধরা পড়ে compile-এর সময় নয়, app চালু হওয়ার বা প্রথম ব্যবহারের সময়।
নিচের stepper আর তারপরের কোডে ৩০ লাইনের একটি খেলনা container — বাস্তব container-এর মূল কৌশল দেখানোর জন্য, ব্যবহারের জন্য নয়। এতে আছে registration, recursion দিয়ে resolution, transient আর singleton lifetime, আর একটি dependency cycle ধরার উপায়: resolve করার সময় “এখন কোন পথে আছি” তার একটি তালিকা (path) রাখা; একই নাম পথে দ্বিতীয়বার এলেই বৃত্ত।
- orders
repo, smstransient - repo
—transient - sms → config
configদুটিই singleton - billing → invoice → tax
→ billingবৃত্ত!
| resolve-এর পথ | |
| এই ধাপে বানানো | |
| singleton cache | |
| made | |
| ফল |
resolve("orders"). Container নিবন্ধন দেখল: orders-এর লাগবেrepoআরsms. আগে সেগুলো, তাই এখনো কিছু বানানো হয়নি। পথে এখন শুধু orders.repo— transient, কোনো dependency নেই। সাথে সাথে বানানো:repo#1. Transient বলে cache-এ রাখা হলো না।sms-এর লাগবেconfig. পথ লম্বা হলো: orders → sms → config. Config-এর কোনো dependency নেই, বানানো হলোconfig#2; singleton, তাই cache-এ রাখা।sms#3তৈরি। Config হাতে পেয়ে sms বানানো গেল; এটিও singleton, cache-এ গেল। পথ আবার ছোট হয়ে orders → sms.orders#4তৈরি। দুটি dependency হাতে, এখন orders বানানো যায় — তৈরির ক্রম ১, ২, ৩, ৪: নিচ থেকে ওপরে। এটিইa.- আবার
resolve("orders"). repo transient, তাই নতুনrepo#5; sms singleton, তাই cache থেকে সেইsms#3— নতুন কিছু বানাতে হলো না, config-ও নয়। নতুনorders#6হলোb. মোট বানানো ৬। - এবার একটি ভুল নিবন্ধন। billing চায় invoice, invoice চায় tax, আর tax চায় billing. Resolve করতে করতে পথে billing দ্বিতীয়বার এল — বৃত্ত। কিছু না বানিয়ে container পুরো পথসহ error দিল; recursion অনন্তকাল চলল না।
// A tiny DI container: register, resolve, lifetimes, cycle detection.
class Container {
constructor() { this.regs = new Map(); this.single = new Map(); this.made = 0; }
register(name, deps, factory, life = "transient") {
this.regs.set(name, { deps, factory, life });
}
resolve(name, path = []) {
if (path.includes(name)) throw new Error("cycle: " + [...path, name].join(" -> "));
const r = this.regs.get(name);
if (!r) throw new Error("not registered: " + name);
if (r.life === "singleton" && this.single.has(name)) return this.single.get(name);
const args = r.deps.map(d => this.resolve(d, [...path, name]));
const obj = r.factory(...args); obj.id = name + "#" + (++this.made);
if (r.life === "singleton") this.single.set(name, obj);
return obj;
}
}
const c = new Container();
c.register("config", [], () => ({ smsSender: "SHOP" }), "singleton");
c.register("sms", ["config"], cfg => ({ cfg }), "singleton");
c.register("repo", [], () => ({ rows: [] })); // transient
c.register("orders", ["repo", "sms"], (repo, sms) => ({ repo, sms }));
const a = c.resolve("orders"), b = c.resolve("orders");
console.log("a:", a.id, "repo", a.repo.id, "sms", a.sms.id, "cfg", a.sms.cfg.id);
console.log("b:", b.id, "repo", b.repo.id, "sms", b.sms.id);
console.log("same sms?", a.sms === b.sms, "| same repo?", a.repo === b.repo);
console.log("objects made:", c.made);
c.register("billing", ["invoice"], inv => ({ inv }));
c.register("invoice", ["tax"], tax => ({ tax }));
c.register("tax", ["billing"], bill => ({ bill }));
try { c.resolve("billing"); } catch (e) { console.log("error ->", e.message); }আউটপুট:
a: orders#4 repo repo#1 sms sms#3 cfg config#2
b: orders#6 repo repo#5 sms sms#3
same sms? true | same repo? false
objects made: 6
error -> cycle: billing -> invoice -> tax -> billing
প্রথম লাইনের নম্বরগুলো তৈরির ক্রম বলে: repo#1, config#2, sms#3, তারপর orders#4 — dependency আগে, যে চায় সে পরে। দ্বিতীয় resolve-এ repo নতুন (#5), কিন্তু sms সেই #3: singleton-এর মানে ঠিক এটুকুই। দুই resolve-এ মোট ৬টি object; সব transient হলে হতো ৮টি (প্রতিবার orders, repo, sms, config). শেষ লাইনে path-এর কৌশল কাজ করল: billing পথে থাকা অবস্থায় আবার চাওয়া হলো, তাই container পুরো বৃত্তটি নাম ধরে দেখাল। বাস্তব container-গুলোও সাধারণত এমন বার্তা দেয় — আর সমাধান প্রায় সবসময় নকশায়: বৃত্তের কোনো এক class দুটি দায়িত্ব নিয়েছে, অথবা একটি নির্ভরতা event বা ছোট interface দিয়ে ভাঙা দরকার (পর্ব ২৫-এর acyclic dependency-র আলোচনা)। এই খেলনায় scoped lifetime নেই; তার আচরণ অনুশীলন ১-এ।
Lifetime মেলানোর ফাঁদ — captive dependency: একটি singleton যদি তার constructor-এ একটি scoped বা transient object পায়, তবে সেই object singleton-এর ভেতরে “বন্দি” হয়ে পুরো app-এর আয়ু পর্যন্ত বাঁচে। প্রথম ক্রেতার cart বা প্রথম request-এর database transaction পরের সব request-এ ভাগ হয়ে যেতে পারে — ভয়ংকর আর খুঁজে পাওয়া কঠিন bug. নিয়ম: কোনো object নিজের চেয়ে কম আয়ুর object ধরে রাখবে না। কিছু container (যেমন .NET-এর built-in container, development mode-এ) এটি পরীক্ষা করে error দেয় বলে documentation-এ বলা আছে; সব container করে না।
বিভিন্ন ecosystem-এ container — সাধারণ পরিচয়
লেখার সময়ে প্রায় প্রতিটি বড় ecosystem-এ DI container বা DI-সমর্থিত framework আছে। নিচের বর্ণনা সাধারণ; খুঁটিনাটি version-ভেদে বদলায়, তাই ব্যবহারের আগে সংশ্লিষ্ট documentation দেখুন। এখানে কোনোটিকে সুপারিশ করা হচ্ছে না।
| Ecosystem | সাধারণ রূপ (লেখার সময়ে) |
|---|---|
| Java — Spring Framework | Container “bean” বানায় ও জোড়ে; ডিফল্ট scope singleton, প্রতিবার নতুনের জন্য “prototype” scope, web-এ request-এর মতো scope; constructor injection-কে সাধারণত উৎসাহ দেওয়া হয় |
| .NET — built-in container | Framework-এর সাথে আসা container-এ registration-এর সময় সরাসরি transient, scoped আর singleton বেছে নেওয়া হয়; web app-এ প্রতিটি request একটি scope |
| TypeScript — Angular | Framework-এর মধ্যেই DI; injector-গুলো একটি স্তরবিন্যাস (hierarchy) গড়ে, কোথায় provide করা হলো তার ওপর নির্ভর করে object কতজন ভাগ করবে |
| Node.js — NestJS | Module-ভিত্তিক DI; provider ডিফল্টভাবে singleton, request আর transient scope-ও আছে |
Python — dependency-injector library | Container class-এ provider ঘোষণা — যেমন প্রতিবার নতুনের জন্য factory provider, একটির জন্য singleton provider; Python-এ অনেক দল container ছাড়াই pure DI-তে কাজ চালায় |
কোনটি বেছে নেবেন — pure DI না container? Pure DI-তে জোড়া সাধারণ কোড: IDE-তে “definition-এ যান” চাপলেই দেখা যায় কী কোথায় গেল, ভুল type দিলে type checker বা compiler ধরে, শেখার কিছু নেই। Container-এ লাভ আসে বড় graph-এ, অনেক scoped object-এ, আর framework যখন নিজেই container দেয় (তখন তার বিপরীতে যাওয়া অর্থহীন)। পর্ব ২৬-এর YAGNI এখানে প্রযোজ্য: ছোট বা মাঝারি app-এ pure DI দিয়ে শুরু করুন; জোড়ার কোড সত্যিই অসহনীয় হলে container আনুন — তখন composition root আগে থেকেই এক জায়গায় থাকায় বদলটিও এক জায়গায় হবে।
৫ · Inversion of control, service locator আর over-injection
“Inversion of control” কথাটি framework নিয়ে লেখায় আগে থেকেই ছিল — সাধারণত Ralph Johnson ও Brian Foote-এর ১৯৮৮ সালের “Designing Reusable Classes” প্রবন্ধের নাম করা হয়; ধারণাটি হলো, library-তে আপনার কোড library-কে ডাকে, কিন্তু framework-এ framework আপনার কোড ডাকে। ২০০০-এর দশকের শুরুতে Java জগতে কয়েকটি “IoC container” জনপ্রিয় হয় — PicoContainer, Spring ইত্যাদি। জানুয়ারি ২০০৪-এ Martin Fowler “Inversion of Control Containers and the Dependency Injection pattern” প্রবন্ধে যুক্তি দেন যে “inversion of control” খুব সাধারণ কথা — প্রতিটি framework-ই তো control উল্টায় — তাই এই container-গুলোর আসল কাজের জন্য তিনি (কয়েকজন সহকর্মীর সাথে আলোচনা করে) নতুন নাম বেছে নেন: dependency injection. একই প্রবন্ধে তিনি service locator-কে একটি বিকল্প হিসেবে তুলনা করেন, আর দুটিকেই গ্রহণযোগ্য বলেন। পরে ২০১০ সালে Mark Seemann “Service Locator is an Anti-Pattern” নামে একটি বহুল আলোচিত লেখা লেখেন। এই তারিখ আর নামগুলো সাধারণভাবে স্বীকৃত; প্রতিটি নামের প্রথম ব্যবহার কে করেছেন, তা নিয়ে উৎসভেদে কিছু ভিন্নতা আছে।
Service locator — নির্ভরতা আবার লুকানো
Service locator হলো একটি global রেজিস্টার, যার কাছে class নিজেই চেয়ে নেয়: Locator.get("mailer"). দেখতে DI-এর মতো — class তো SmtpMailer() বানাচ্ছে না! কিন্তু dependency-র দিক আবার উল্টে গেছে: class নিজে টেনে আনছে (pull), বাইরে থেকে ঠেলে দেওয়া হচ্ছে না (push).
class ReportService: # service locator: dependencies hidden
def __init__(self):
pass # signature says: "I need nothing"
def send_monthly(self, shop, total):
mailer = Locator.get("mailer") # discovered only when this line runs
repo = Locator.get("repo")
...
class ReportService: # DI: dependencies in the signature
def __init__(self, repo, mailer):
self.repo, self.mailer = repo, mailerSeemann-এর যুক্তির সারকথা: (১) signature মিথ্যা বলে — ReportService() খালি constructor দেখায়, অথচ ভেতরে দুটি জিনিস লাগে; (২) ভুল ধরা পড়ে দেরিতে — কেউ “repo” নিবন্ধন করতে ভুললে error আসে মাসের শেষে, যেদিন send_monthly প্রথম চলে; (৩) test-এ অনুমান — কোন কোন নাম locator-এ বসাতে হবে, তা জানতে class-এর ভেতরের কোড পড়তে হয়, আর এক test-এর বসানো জিনিস global বলে আরেক test-এ চুইয়ে পড়তে পারে; (৪) সবাই locator-এর ওপর নির্ভরশীল — প্রতিটি class একটি নির্দিষ্ট infrastructure-কে চেনে। ন্যায্যতার খাতিরে: Fowler নিজে service locator-কে গ্রহণযোগ্য বিকল্প বলেছিলেন, আর কিছু জায়গায় এটি প্রায় অনিবার্য — যেমন framework যখন আপনার class নিজে বানায় আর constructor-এ কিছু দিতে দেয় না, বা পুরোনো কোডে ধাপে ধাপে DI আনার মাঝপথে। তখনও নিয়ম হলো: locator ডাকার কাজটি composition root-এর কাছে, সীমানায় সীমিত রাখুন, business কোডের ভেতরে ছড়িয়ে নয়।
Constructor over-injection — parameter গুনেই সন্দেহ
Constructor injection-এর একটি “অসুবিধা” প্রায়ই বলা হয়: কিছু class-এর constructor-এ আট-দশটি parameter হয়ে যায়। কিন্তু এটি injection-এর দোষ নয়, বরং তার উপহার — দোষটি আগেও ছিল, শুধু লুকানো ছিল। Seemann এই লক্ষণকে constructor over-injection নামে একটি code smell বলেন: এতগুলো সহযোগী লাগছে মানে class-টি সম্ভবত অনেক দায়িত্ব নিয়েছে — পর্ব ২৭-এর single responsibility principle-এর লঙ্ঘন, হয়তো একটি উঠতি god class. কোনো সংখ্যার নিয়ম নেই; অনেকে চার-পাঁচটির বেশি হলে থেমে ভাবেন।
যা করবেন না: setter injection-এ সরিয়ে parameter লুকানো, বা সব dependency একটি “context” বস্তায় ভরে একটি parameter বানানো — এতে সংখ্যা কমে, সমস্যা কমে না। যা করতে পারেন: দেখুন কোন dependency-গুলো সবসময় একসাথে ব্যবহার হয়। ধরুন একটি কাল্পনিক অনলাইন বাজারের CheckoutService নেয় cart_repo, price_rules, coupon_repo, tax_table, payment_gateway, fraud_check, sms, email, audit_log — নয়টি। প্রথম চারটি মিলে আসলে একটি কাজ: “এই cart-এর চূড়ান্ত দাম বের করা।” সেগুলো একটি নতুন PricingService-এ যায়; sms আর email একটি Notifier-এ। এখন CheckoutService(pricing, payment_gateway, fraud_check, notifier, audit_log) — পাঁচটি, আর প্রতিটি একটি স্পষ্ট ভূমিকা। Seemann এই কৌশলকে facade service-এ refactor করা বলেন; এটি পর্ব ৩৪-এর facade-এর আত্মীয়, তবে লক্ষ্য এখানে দায়িত্ব ভাগ।
৬ · Functional ধাঁচে DI — function, closure আর partial application
Class না থাকলেও dependency থাকে। Functional ধাঁচের কোডে DI-এর সবচেয়ে সরল রূপ: dependency হলো function-এর argument. বারবার একই জিনিস দিতে না চাইলে দুটি কৌশল আছে। একটি closure: একটি function অন্য একটি function ফেরত দেয়, আর ভেতরের function বাইরের function-এর variable মনে রাখে — dependency একবার দেওয়া হয়, ভেতরের function যতবার ডাকা হোক, সেগুলো হাতের কাছে থাকে। অন্যটি partial application: অনেক argument-এর একটি function-এর প্রথম কয়েকটি argument আগেই বসিয়ে দিয়ে ছোট একটি function বানানো। JavaScript-এর closure নতুন মনে হলে JavaScript কোর্সের closure-এর পর্বটি পাশে রাখতে পারেন; এগুলো higher-order function-এর ব্যবহার।
// Functional DI: dependencies are just function arguments, fixed by closures.
const makePlaceOrder = ({ save, notify, now }) => // inject once...
(item, qty) => { // ...use many times
if (qty <= 0) return "rejected";
const order = { item, qty, at: now() };
save(order);
notify(`order ${item} x${qty} at ${order.at}`);
return "placed";
};
// Partial application: fix the first argument, get a smaller function.
const partial = (fn, ...first) => (...rest) => fn(...first, ...rest);
const discount = (percent, price) => price - (price * percent) / 100;
const eidDiscount = partial(discount, 10);
// Production-like wiring (composition root) vs test wiring.
const rows = [], log = [];
const placeTest = makePlaceOrder({
save: o => rows.push(o),
notify: m => log.push(m),
now: () => "2026-03-30T10:00", // a fixed clock for the test
});
console.log(placeTest("tea", 2), placeTest("sugar", 0));
console.log("saved:", JSON.stringify(rows));
console.log("notified:", JSON.stringify(log));
console.log("eid price of 450:", eidDiscount(450), "| of 1200:", eidDiscount(1200));
// A closure can also hide state: n is reachable only through nextInvoice.
const nextInvoice = (() => { let n = 100; return () => "INV-" + (++n); })();
console.log(nextInvoice(), nextInvoice(), nextInvoice());আউটপুট:
placed rejected
saved: [{"item":"tea","qty":2,"at":"2026-03-30T10:00"}]
notified: ["order tea x2 at 2026-03-30T10:00"]
eid price of 450: 405 | of 1200: 1080
INV-101 INV-102 INV-103
makePlaceOrder একটি object নেয় — তিনটি dependency: save, notify, now — আর একটি function ফেরত দেয়। এই বাইরের ডাকটিই composition root-এর কাজ; ফেরত পাওয়া placeTest closure-এর মাধ্যমে তিনটিকে মনে রাখে। প্রথম ডাক ("tea", 2) অর্ডার জমা রাখল আর বার্তা পাঠাল; দ্বিতীয় ("sugar", 0) নিয়মে আটকে rejected, তাই saved-এ একটিই সারি। ঘড়িটিও dependency: test-এ now সবসময় 2026-03-30T10:00 দেয়, তাই ফল প্রতিবার একই — আসল ঘড়ি দিলে test-এর ফল সময়ভেদে বদলাত। partial(discount, 10) শতাংশটি আগে বসিয়ে eidDiscount বানাল: ৪৫০-এ ১০% ছাড়ে ৪০৫, ১২০০-তে ১০৮০। শেষ লাইনে closure লুকানো অবস্থা রাখে: n বাইরে থেকে দেখা বা বদলানো যায় না, শুধু nextInvoice() দিয়ে বাড়ে।
Class-ভিত্তিক DI-এর সাথে মিলিয়ে দেখুন: constructor-এর parameter ↔ বাইরের function-এর parameter; object-এর field ↔ closure-এর মনে রাখা variable; new ReportService(...) ↔ makePlaceOrder({...}). একই ধারণা, দুই রূপ। Haskell-এর মতো ভাষায় এর আরও আনুষ্ঠানিক রূপ আছে (যেমন Reader monad নামের একটি কৌশল, যা পরিবেশকে প্রতিটি function-এ নিঃশব্দে পৌঁছে দেয়); সেটি এই কোর্সের বিষয়ের বাইরে। একটি সতর্কতা: Python বা JavaScript-এ test-এর সময় module-এর ভেতরের function জোর করে বদলে দেওয়া যায় (Python-এ unittest.mock.patch, JavaScript test runner-এর module mocking) — এটি জরুরি অবস্থায় কাজের, কিন্তু DI-এর বিকল্প হিসেবে নিয়মিত ব্যবহার করলে নির্ভরতা আবার লুকানো থাকে, আর test ভাঙে কোডের ভেতরের নাম বদলালেই।
৭ · Inheritance-এর বদলে composition — গভীরে
পর্ব ২৬-এ composition over inheritance-এর নাম শুনেছেন — Gang of Four-এর ১৯৯৪ সালের বইয়ের পরামর্শ: “class inheritance-এর চেয়ে object composition-কে অগ্রাধিকার দিন।” DI আর composition একই মুদ্রার দুই পিঠ: composition মানে একটি object আরেকটিকে ধরে রেখে তাকে দিয়ে কাজ করায়; DI ঠিক করে সেই ধরে-রাখা object কোথা থেকে আসে। কিন্তু inheritance-এর বিরুদ্ধে আপত্তিটা আসলে কী? সবচেয়ে বিখ্যাত উত্তর একটি সমস্যার নাম — fragile base class.
একটি ভাড়াবাড়ির কথা ভাবুন। ভাড়াটে দেয়ালের একটি বিশেষ জায়গায় পেরেক মেরে ভারী তাক লাগিয়েছেন — ধরে নিয়েছেন দেয়ালের ভেতরে কিছু নেই। পরের বছর বাড়িওয়ালা “ভেতরের কাজ” করলেন: সেই দেয়ালের ভেতর দিয়ে নতুন পানির পাইপ গেল। বাড়ির বাইরে থেকে কিছুই বদলায়নি, ভাড়ার চুক্তিও একই — কিন্তু পরের পেরেকে পাইপ ফুটো। ভাড়াটে বাড়ির ভেতরের গঠনের ওপর নির্ভর করেছিলেন, যা চুক্তির অংশ ছিল না। Subclass-ও base class-এর ভেতরের গঠনের ওপর এমনভাবে নির্ভর করে।
ক্লাসিক উদাহরণটি Joshua Bloch-এর Effective Java বই থেকে জনপ্রিয় — সেখানে একটি HashSet-এর subclass কতগুলো উপাদান যোগ হলো তা গুনতে গিয়ে ভুল সংখ্যা পায়। নিচে একই ধারণা একটি কাল্পনিক মুদি-দোকানের বাজারের ঝুড়ি দিয়ে। Basket-এর দুটি method: add একটি জিনিস যোগ করে, add_all অনেকগুলো। একজন developer CountingBasket নামে subclass লিখলেন, যা গোনে কতগুলো জিনিস যোগ হয়েছে — দুটি method-ই override করে, দুই জায়গাতেই গুনে। Base-এর প্রথম সংস্করণে সব ঠিক। তারপর base-এর লেখক একটি “নিরীহ” পরিষ্কারের কাজ করলেন: add_all এখন নিজে list-এ না বসিয়ে self.add ডাকে — পুনরাবৃত্তি কমল, বাইরের আচরণ একই। নিচের stepper-এ দেখুন subclass-এ কী ঘটে।
1class CountingBasket(BasketV2):2 def add(self, item):3 self.count += 1; super().add(item)4 def add_all(self, items):5 self.count += len(items); super().add_all(items)6# BasketV2.add_all (base, new version):7 for it in items: self.add(it)8b.add("rice"); b.add_all(["dal", "oil", "salt"])
| এখন চলছে | |
| self.add কোনটি? | |
| len(items) | |
| count | |
| count ঠিক? |
b.add("rice"). Subclass-এরaddচলল: count ১, তারপরsuper().addচাল ঝুড়িতে রাখল। ঝুড়িতে ১টি, count ১ — ঠিক।b.add_allতিনটি জিনিস নিয়ে। Subclass-এরadd_allএকবারে ৩ যোগ করল: count ৪। এরপর সেsuper().add_all-কে বাকি কাজ দিল। এ পর্যন্ত সব ঠিক।- Base-এর নতুন
add_allডাকলself.add("dal"). এখানেই ফাঁদ।selfআসলে একটিCountingBasket, তাই Python subclass-এরaddচালায় (dynamic dispatch). সেইaddআবার গুনল: count ৫, অথচ ঝুড়িতে মাত্র ২টি। self.add("oil"). আবার subclass-এরadd: count ৬, ঝুড়িতে ৩টি। একই জিনিস দুবার গোনা হচ্ছে — একবারadd_all-এ, একবারadd-এ।self.add("salt"). count ৭, ঝুড়িতে ৪টি। Subclass-এর একটি লাইনও বদলায়নি, base-এর বাইরের আচরণও বদলায়নি (চারটি জিনিসই ঝুড়িতে)।- ফল: ঝুড়িতে ৪টি, count ৭। Base-এর প্রথম সংস্করণে count ছিল ৪। Subclass base-এর ভেতরের একটি খুঁটিনাটির ওপর নির্ভর করছিল — “
add_alladdডাকে না” — যা কোনো চুক্তিতে লেখা ছিল না। এটিই fragile base class.
এবার আসল কোডে, আর তার সমাধান: CountingBasketC কোনো কিছু inherit করে না; একটি ঝুড়ি ধরে রাখে (composition) আর আসল কাজ তাকে দিয়ে করায় (delegation). ভেতরের ঝুড়ি তার নিজের add ডাকলেও সেটি ভেতরের ঝুড়ির add, গোনার মোড়কের নয় — তাই দুবার গোনার পথই নেই।
class Basket: # base class, version 1
def __init__(self): self.items = []
def add(self, item): self.items.append(item)
def add_all(self, items):
for it in items: self.items.append(it) # v1: does NOT call add()
class BasketV2(Basket): # same base after a "harmless" cleanup
def add_all(self, items):
for it in items: self.add(it) # v2: now reuses add()
def make_counting(base): # subclass written against the base
class CountingBasket(base):
def __init__(self): super().__init__(); self.count = 0
def add(self, item): self.count += 1; super().add(item)
def add_all(self, items): self.count += len(items); super().add_all(items)
return CountingBasket
for base in (Basket, BasketV2):
b = make_counting(base)()
b.add("rice"); b.add_all(["dal", "oil", "salt"])
print(f"{base.__name__:8} items={len(b.items)} count={b.count}")
class CountingBasketC: # the fix: composition + delegation
def __init__(self, inner): self.inner = inner; self.count = 0
def add(self, item): self.count += 1; self.inner.add(item)
def add_all(self, items): self.count += len(items); self.inner.add_all(items)
@property
def items(self): return self.inner.items
for inner in (Basket(), BasketV2()):
c = CountingBasketC(inner)
c.add("rice"); c.add_all(["dal", "oil", "salt"])
print(f"composed {type(inner).__name__:8} items={len(c.items)} count={c.count}")আউটপুট:
Basket items=4 count=4
BasketV2 items=4 count=7
composed Basket items=4 count=4
composed BasketV2 items=4 count=4
প্রথম দুই লাইন stepper-এর গল্প: একই subclass, base-এর দুই সংস্করণে দুই ফল — ৪ আর ৭। make_counting(base) একটি ছোট কৌশল, যা একই subclass-কে দুই base-এর ওপর বানায়, যাতে তুলনাটি ন্যায্য হয়। শেষ দুই লাইনে composition-এর সংস্করণ দুই ঝুড়িতেই ঠিক ৪ গোনে। কারণ CountingBasketC কেবল ঝুড়ির প্রকাশ্য চুক্তির ওপর নির্ভর করে — add, add_all, items — ভেতরে কে কাকে ডাকে তার ওপর নয়। এর মূল্যও আছে: প্রতিটি method হাতে forward করতে হয় (এখানে তিনটি), আর ঝুড়িতে নতুন method এলে মোড়কে না যোগ করা পর্যন্ত সেটি পাওয়া যাবে না। এই গঠনটি আসলে পর্ব ৩৪-এর decorator-এর একটি রূপ — একই চুক্তির মোড়ক, যা কাজের আগে-পরে কিছু যোগ করে।
Inheritance কখন ঠিক
Composition-এর পক্ষে বলা মানে inheritance নিষিদ্ধ নয়। প্রশ্নটি হলো সম্পর্কের ধরন:
| Inheritance | Composition + delegation | |
|---|---|---|
| সম্পর্ক | “is-a” — এবং আচরণেও: subtype base-এর সব প্রতিশ্রুতি রাখে (পর্ব ২৮-এর LSP) | “has-a” বা “uses” — object অন্যটিকে ধরে রাখে বা ব্যবহার করে |
| কখন জোড়া হয় | কোড লেখার সময়, স্থির; চালানোর সময় বদলানো যায় না | চালানোর সময়, composition root-এ; চাইলে বদলানো যায় |
| কী জানে | Base-এর protected অংশ আর ভেতরের ডাকার ক্রম — এনক্যাপসুলেশন দুর্বল | শুধু প্রকাশ্য চুক্তি |
| খরচ | কম কোড; কিন্তু base বদলালে subclass ভাঙতে পারে | Forward করার কোড; কিন্তু পরিবর্তনের প্রভাব সীমিত |
| ভালো উদাহরণ | একই প্যাকেজের ভেতরে, extension-এর জন্য নকশা ও documentation করা base; framework-এর বিশেষ hook | Strategy, decorator, repository, mailer — প্রায় সব “সহযোগী” |
Bloch-এর পরামর্শটি মনে রাখার মতো: base class হয় inheritance-এর জন্য নকশা করে documentation লিখুন (কোন method কোন method-কে ডাকে, তা চুক্তির অংশ করে), নয়তো inheritance নিষেধ করুন (Java-তে final, Kotlin-এ class ডিফল্টভাবেই final). পর্ব ৩৫-এর template method এই প্রথম পথের উদাহরণ: base জেনেশুনে নির্দিষ্ট hook দেয় আর ডাকার ক্রম ঘোষণা করে। একই সমস্যার composition-রূপ হলো strategy — hook-গুলো আলাদা object হয়ে constructor-এ আসে।
Mixin, trait আর delegation — মাঝামাঝি পথ
Mixin হলো একটি ছোট class, যা একা ব্যবহারের জন্য নয়, অন্য class-এ একটি ক্ষমতা “মিশিয়ে” দেওয়ার জন্য — যেমন Python-এ class Invoice(JsonMixin, Base), যেখানে JsonMixin শুধু to_json() দেয়। Python-এ এটি multiple inheritance দিয়ে হয়, তাই fragile base class আর method-এর নাম-সংঘাতের ঝুঁকি থেকেই যায়; ভালো mixin ছোট, অবস্থাহীন আর অন্য method-এর ভেতরের ক্রমের ওপর নির্ভর করে না। Trait একই উদ্দেশ্যের আরও নিয়ন্ত্রিত রূপ — Schärli ও সহকর্মীদের ২০০৩ সালের ECOOP গবেষণাপত্র থেকে প্রচলিত বলে উল্লেখ করা হয় — যেখানে নাম-সংঘাত হলে ভাষা চুপচাপ একটি বেছে নেয় না, আপনাকে স্পষ্ট সমাধান দিতে হয়। লক্ষ করুন, “trait” শব্দটি ভাষাভেদে ভিন্ন জিনিস বোঝায়: PHP আর Scala-র trait কোড মেশায়, আর Rust-এর trait অনেকটা interface-এর মতো — Rust-এ class inheritance-ই নেই, Go-তেও নেই (Go-তে struct embedding দিয়ে delegation-এর মতো ব্যবস্থা)। কিছু ভাষা delegation-কে সহজ করে — যেমন Kotlin-এ by শব্দ দিয়ে একটি interface-এর সব method আরেকটি object-এ forward করা যায়, যা CountingBasketC-এর হাতে লেখা forward-এর কাজ বাঁচায়।
আরও গভীরে: “Fragile base class problem” নিয়ে আনুষ্ঠানিক বিশ্লেষণ পাবেন Mikhajlov ও Sekerinski-এর ১৯৯৮ সালের ECOOP গবেষণাপত্রে (“A Study of the Fragile Base Class Problem”), আর inheritance যে encapsulation ভাঙে, সেই যুক্তি Alan Snyder-এর ১৯৮৬ সালের OOPSLA প্রবন্ধে বলে সাধারণত উল্লেখ করা হয়। DI-এর সবচেয়ে বিস্তারিত বই Mark Seemann ও Steven van Deursen-এর Dependency Injection: Principles, Practices, and Patterns (২০১৯) — Seemann-এর ২০১১ সালের বইয়ের নতুন রূপ; উদাহরণ .NET-এ হলেও ধারণাগুলো ভাষা-নিরপেক্ষ। Fowler-এর ২০০৪ সালের প্রবন্ধটি তাঁর website-এ বিনামূল্যে পড়া যায়।
পরীক্ষা ও ইন্টারভিউতে — বিশ্ববিদ্যালয়, চাকরি বা ইন্টারভিউর পরীক্ষায় এই ধরনের প্রশ্ন আসতে পারে:
- Dependency injection কী, আর কেন? — object তার সহযোগী বানায় না, বাইরে থেকে পায়; নির্ভরতা প্রকাশ্য হয়, test-এ fake বসানো যায়, বাস্তবায়ন বদলাতে class খুলতে হয় না।
- Constructor না setter injection? — বাধ্যতামূলক dependency constructor-এ (object শুরু থেকেই সম্পূর্ণ); ঐচ্ছিক আর নিরাপদ default থাকলে setter.
- Transient, scoped, singleton-এর পার্থক্য? — প্রতিবার নতুন; প্রতি scope (request) একটি; পুরো container-এ একটি। Singleton-এ scoped ঢোকানো captive dependency.
- Service locator কেন সমালোচিত? — নির্ভরতা signature-এ দেখা যায় না, ভুল ধরা পড়ে চালানোর সময়, সব class locator-এর ওপর নির্ভরশীল।
- Composition over inheritance কেন? — inheritance base-এর ভেতরের গঠনে বাঁধে (fragile base class) আর স্থির; composition শুধু প্রকাশ্য চুক্তিতে নির্ভর করে আর চালানোর সময় বদলানো যায়।
৮ · AI-এর যুগে DI আর composition
AI-কে “এই কোডে dependency injection যোগ করো” বললে সে প্রায়ই অনেক বেশি করে — অথবা ভুল জিনিসটি করে। কিছু সাধারণ ভুল আর ধরার উপায়:
| AI-এর সাধারণ ভুল | কীভাবে ধরবেন |
|---|---|
| ৫০ লাইনের script-এ একটি DI container library আর প্রতিটি class-এর interface | জিজ্ঞেস করুন: composition root-এ হাতে জোড়া কত লাইন হতো? দ্বিতীয় বাস্তবায়ন (test fake-সহ) কোথায়? |
“DI”-এর নামে global Locator.get() বা module-level singleton | Constructor-এর signature দেখুন — সব নির্ভরতা কি সেখানে দেখা যায়? |
| সবকিছু singleton lifetime — mutable অবস্থাসহ, বা scoped জিনিস ভেতরে নিয়ে | প্রতিটি singleton-এর জন্য: এর অবস্থা কি request-এর মধ্যে ভাগ হওয়া নিরাপদ? এর dependency-গুলোর আয়ু কি সমান বা বেশি? |
| Container-এর এমন annotation, decorator বা method, যা সেই library-তে নেই বা অন্য version-এর | Library-র আপনার version-এর official documentation মিলিয়ে দেখুন; একটি throwaway branch-এ চালিয়ে দেখুন |
| সব dependency mock করে এমন test, যা শুধু call-ক্রম যাচাই করে — implementation-এর হুবহু প্রতিচ্ছবি | কোডে ইচ্ছা করে ভুল ঢুকিয়ে test চালান (mutation testing, পর্ব ৫০); ফেল না করলে test অকেজো |
| পুনর্ব্যবহারের জন্য inheritance-এর গভীর hierarchy প্রস্তাব | প্রতিটি subclass-এ base-এর contract test চালান (পর্ব ২৮); “is-a” সত্যিই আচরণে খাটে কি না দেখুন |
| ভুল ইতিহাস: “Fowler IoC আবিষ্কার করেছেন”, “Spring DI-এর জনক” | মূল উৎস দেখুন: IoC ধারণা পুরোনো, DI নামটি Fowler-এর ২০০৪ সালের প্রবন্ধ থেকে প্রচলিত |
AI-কে নকশার সমালোচক হিসেবে ব্যবহার করুন, আর কোড দেওয়ার আগে গোপন key, password আর গ্রাহকের তথ্য মুছে নিন:
AI-এর প্রস্তাব হাতে পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১“Inversion of control” নাম তো আগেই ছিল। Fowler ২০০৪ সালে “dependency injection” নামে নতুন শব্দ আনলেন কেন? একটি নামের কী এমন গুরুত্ব?
২০০০-এর দশকের শুরুতে Java-র enterprise জগতে একটি ক্লান্তি ছিল। তখনকার প্রচলিত enterprise কাঠামোয় (বিশেষ করে EJB-র পুরোনো version-এ) একটি business object লিখতে অনেক framework-নির্দিষ্ট interface আর configuration লাগত, আর object তার সহযোগী খুঁজত JNDI-র মতো lookup ব্যবস্থায় — মূলত একটি service locator. Test করা কঠিন ছিল, কারণ object চালাতে পুরো server লাগত। এর প্রতিক্রিয়ায় কয়েকটি হালকা container আসে — PicoContainer, Spring (Rod Johnson-এর ২০০২ সালের বই থেকে জন্ম বলে সাধারণত বলা হয়) ইত্যাদি — যারা সাধারণ Java object নিয়ে কাজ করত আর সহযোগী নিজেরাই জুগিয়ে দিত। এদের ডাকা হতো “IoC container”.
Fowler-এর আপত্তি ছিল শব্দের নির্দিষ্টতা নিয়ে। Framework মাত্রই control উল্টায় — GUI-র event loop আপনার button handler ডাকে, web framework আপনার request handler ডাকে, unit test framework আপনার test ডাকে। “আমাদের container IoC করে” বললে তাই প্রায় কিছুই বলা হয় না; আসল নতুনত্ব ছিল নির্দিষ্ট একটি দিকে control উল্টানো — dependency জোগাড়ের control. তাই তিনি সহকর্মীদের সাথে আলোচনা করে “dependency injection” নামটি বেছে নেন, আর একই লেখায় constructor, setter আর interface injection-এর তুলনা করেন, service locator-কে বিকল্প হিসেবে রাখেন।
নামের গুরুত্ব বাস্তব। একটি সুনির্দিষ্ট নাম আলোচনা সম্ভব করে — “এখানে constructor injection নাও” বললে একটি দল এক মুহূর্তে একই জিনিস বোঝে। Gang of Four-এর pattern-এর বইয়ের বড় অবদানও ছিল এটিই: একটি শব্দভাণ্ডার (পর্ব ৩২)। আবার নামের ঝুঁকিও আছে: “injection” শুনলে অনেকে ধরে নেন একটি জটিল যন্ত্র লাগবে, অথচ কৌশলটি constructor parameter মাত্র; container তার একটি সুবিধাজনক সরঞ্জাম। আজ অনেক ইন্টারভিউতে DI আর DI container গুলিয়ে যাওয়া এই নামেরই উত্তরাধিকার।
মূল উপলব্ধি: DI নামটি এসেছিল একটি বড়, অস্পষ্ট ধারণা (IoC) থেকে একটি নির্দিষ্ট কৌশল আলাদা করতে; কৌশলটি সরল, container তার ঐচ্ছিক সরঞ্জাম।
প্র ০২Fowler service locator-কে গ্রহণযোগ্য বললেন, Seemann বললেন anti-pattern. কে ঠিক? কোনো অবস্থায় কি locator-ই ভালো?
দুজনের মতভেদ আসলে প্রেক্ষাপটের। Fowler ২০০৪ সালে লিখছিলেন এমন সময়ে, যখন locator ছিল প্রচলিত অভ্যাস আর DI container নতুন। তাঁর তুলনায় locator-এর একটি সুবিধা ছিল সরলতা — একটি পরিচিত জিনিস, কোনো নতুন container বা জোড়ার “জাদু” লাগে না — আর অসুবিধা ছিল প্রতিটি class-এর locator-এর ওপর নির্ভরতা। তিনি বলেছিলেন, যে কোড অনেক অজানা app-এ ব্যবহার হবে (যেমন অন্যদের জন্য লেখা component), সেখানে locator-এর নির্ভরতা বেশি ক্ষতিকর; নিজের app-এর ভেতরে কম।
Seemann-এর ২০১০ সালের যুক্তি API-র দৃষ্টিকোণ থেকে। একটি class-এর constructor তার চুক্তির অংশ — “আমাকে বানাতে এগুলো লাগবে।” Locator-এ সেই চুক্তি মিথ্যা হয়ে যায়: constructor খালি, কিন্তু চালাতে গেলে কিছু নিবন্ধন লাগে, যা জানতে ভেতরের কোড পড়তে হয়। Compiler, type checker বা IDE কেউ সাহায্য করতে পারে না; ভুল ধরা পড়ে চালানোর সময়, হয়তো production-এ, হয়তো মাসে একবার চলা কোনো পথে। বড় দলে, যেখানে class-এর লেখক আর ব্যবহারকারী আলাদা মানুষ, এই খরচ দ্রুত বাড়ে। তিনি পরে যোগ করেন, সমস্যাটি locator যন্ত্রে নয়, তার ভূমিকায়: একই container composition root-এ ব্যবহার হলে ঠিক, business class-এর ভেতর থেকে ডাকলে locator.
তাহলে কখন locator প্রায় অনিবার্য? (১) Framework যখন আপনার class নিজে বানায় আর constructor-এ parameter দিতে দেয় না — যেমন পুরোনো কিছু UI বা plugin ব্যবস্থা; তখন framework-এর সীমানায় একটি পাতলা স্তর locator থেকে নিয়ে বাকি কোডে constructor দিয়ে দেয়। (২) বিশাল legacy কোডে ধাপে ধাপে DI আনার মাঝপথে, যেখানে সব caller একসাথে বদলানো সম্ভব নয়। (৩) সত্যিকারের cross-cutting কিছু জিনিস — যেমন logging — নিয়ে অনেক দল ব্যবহারিক আপস করে, যদিও সেখানেও বিকল্প আছে।
দুজনের অবস্থান মেলালে একটি ব্যবহারিক নিয়ম দাঁড়ায়: business কোডে locator নয়; সীমানায়, ব্যাখ্যাসহ, সাময়িক — হলে সহনীয়। আর locator রাখলেও তাকে একটি জায়গায় সীমিত রাখুন, যাতে পরে সরানো সহজ হয়।
মূল উপলব্ধি: Service locator-এর আসল খরচ লুকানো চুক্তি; Fowler আর Seemann-এর পার্থক্য মূলত প্রেক্ষাপট আর দলের আকারের — business কোডের ভেতরে locator এড়ানোই নিরাপদ নিয়ম।
প্র ০৩DI container কি সময় বাঁচায়, না জটিলতা বাড়ায়? কোন প্রশ্নগুলো করে একটি দল সিদ্ধান্ত নেবে?
Container-এর পক্ষে সবচেয়ে বড় যুক্তি হলো জোড়ার কোডের আকার। দুইশো class-এর একটি web app-এ হাতে লেখা composition root কয়েকশো লাইন হতে পারে, আর প্রতিটি নতুন constructor parameter যোগ করলে সেখানে বদল লাগে। Auto-wiring-ওয়ালা container এই কাজের বেশিরভাগ নিজে করে। দ্বিতীয় যুক্তি lifetime: প্রতিটি web request-এর জন্য scoped object বানানো আর request শেষে পরিষ্কার করা (যেমন database connection ফেরত দেওয়া) হাতে লেখা কঠিন আর ভুলপ্রবণ। তৃতীয় যুক্তি ecosystem: Spring, ASP.NET Core, Angular বা NestJS-এর মতো framework-এ container framework-এরই অংশ — তার বিপরীতে কাজ করা অর্থহীন।
বিপক্ষের যুক্তিও শক্ত। প্রথমত, দৃশ্যমানতা: auto-wiring-এ “কোন object কোথায় গেল” প্রশ্নের উত্তর কোডে লেখা নেই, container-এর নিয়মে আছে। নতুন সদস্যের কাছে এটি জাদুর মতো লাগে। দ্বিতীয়ত, ভুল ধরার সময়: pure DI-তে একটি parameter বাদ পড়লে type checker বা compiler সাথে সাথে বলে; container-এ অনেক ভুল ধরা পড়ে app চালু হওয়ার সময় বা প্রথম সেই পথে যাওয়ার সময়। কিছু container চালু হওয়ার সময় পুরো graph যাচাই করার ব্যবস্থা দেয়, যা এই ঝুঁকি কমায়। তৃতীয়ত, নির্ভরতা: container একটি বাইরের library, তার version, তার আচরণ, তার শেখার খরচ — সবই দলের ঘাড়ে। চতুর্থত, অনেক container reflection ব্যবহার করে, যা চালু হওয়ার সময় কিছুটা খরচ যোগ করতে পারে; ছোট command-line tool বা serverless function-এ এটি চোখে পড়তে পারে।
সিদ্ধান্তের জন্য কয়েকটি প্রশ্ন: আমাদের framework কি নিজেই container দেয়? কতগুলো object-এর request-ভিত্তিক lifetime লাগে? হাতে লেখা composition root কত লাইন হবে, আর কতবার বদলায়? দলের সবাই কি container-এর নিয়ম জানেন? ভুল জোড়া কি app চালু হওয়ার সময়ই ধরা পড়বে? একটি সতর্ক নিয়ম: container ব্যবহার করলেও তাকে composition root-এ সীমিত রাখুন — business class-এ container-এর annotation বা type যত কম, পরে বদলানো তত সহজ।
মূল উপলব্ধি: Container একটি সরঞ্জাম, DI-এর শর্ত নয়; বড় graph আর অনেক scoped object-এ এটি সময় বাঁচায়, ছোট app-এ প্রায়ই শুধু দৃশ্যমানতা কমায়।
প্র ০৪Java-র নিজের library-তেও inheritance-এর ভুল আছে বলে শোনা যায়। Go আর Rust class inheritance রাখেইনি। এটা কি inheritance-এর পরাজয়?
Java-র ঐতিহাসিক উদাহরণগুলো বহুল উদ্ধৃত। java.util.Stack হলো Vector-এর subclass, ফলে একটি stack-এ মাঝখানে উপাদান ঢোকানোর method-ও পাওয়া যায় — stack-এর ধারণা ভাঙে। Properties হলো Hashtable-এর subclass, তাই string-ছাড়া key-ও বসানো যায়, যদিও Properties-এর অনেক method শুধু string আশা করে। Bloch তাঁর Effective Java-য় এগুলোকে inheritance-এর ভুল ব্যবহারের উদাহরণ হিসেবে দেখান, আর একই বইয়ে counting set-এর সেই উদাহরণ, যা এই পর্বের ঝুড়ির গল্পের উৎস। শিক্ষাটি হলো: “is-a” ভাষায় মনে হলেই হয় না; base-এর সব প্রকাশ্য method subclass-এর চুক্তিতে চলে আসে, চাক বা না চাক।
Go (২০০৯) আর Rust (১.০ সংস্করণ ২০১৫) class inheritance রাখেনি। Go-তে interface আছে, আর একটি struct আরেকটিকে “embed” করে তার method সরাসরি পেতে পারে — কিন্তু এটি delegation: embed করা অংশ জানে না সে কার ভেতরে আছে, তাই base-এর method subclass-এর override ডাকবে, এমন dynamic dispatch-এর ফাঁদ নেই। Rust-এ trait আচরণের চুক্তি দেয়, default method-ও দিতে পারে, কিন্তু data-র inheritance নেই। এই ভাষাগুলো দেখিয়েছে যে বড় system inheritance ছাড়াও বেশ লেখা যায়।
তবু একে “পরাজয়” বলা বাড়াবাড়ি। Inheritance-এর দুটি কাজ আলাদা করলে ছবি পরিষ্কার হয়: subtyping (একটি type আরেকটির জায়গায় বসতে পারে) আর implementation-এর ভাগাভাগি (কোড পুনর্ব্যবহার)। Interface বা trait দিয়ে প্রথমটি নিরাপদে পাওয়া যায়; দ্বিতীয়টিই fragile base class-এর উৎস। অনেক আধুনিক নকশা-রীতি তাই বলে: interface থেকে inherit করুন, concrete class থেকে সাবধানে। আর সত্যিকারের ভালো ক্ষেত্রও আছে — একই দলের লেখা, extension-এর জন্য নকশা করা base (template method), GUI toolkit-এর widget hierarchy, বা exception-এর hierarchy, যেখানে LSP সহজে মানা যায়।
মূল উপলব্ধি: সমস্যা inheritance নিজে নয়, implementation inheritance-এর অবাধ ব্যবহার; subtyping-এর জন্য interface আর পুনর্ব্যবহারের জন্য composition — নতুন ভাষাগুলো এই বিভাজনই ভাষার গঠনে বসিয়েছে।
প্র ০৫Python বা JavaScript-এ তো test-এর সময় যেকোনো function বা module জোর করে বদলে দেওয়া যায়। তাহলে dynamic ভাষায় DI-এর দরকার আছে কি?
প্রশ্নটি যুক্তিসঙ্গত। Python-এর unittest.mock.patch দিয়ে test-এর সময় reports.SmtpMailer-কে একটি নকল দিয়ে বদলে দেওয়া যায়, আর ReportServiceV1-এর কোডে একটি লাইনও বদলাতে হয় না। JavaScript-এর অনেক test runner module-এর import-ও নকল দিয়ে বদলাতে পারে। Java বা C#-এর মতো static ভাষায় এমন কৌশল কঠিন বা অসম্ভব ছিল বলেই সেখানে DI এত জরুরি হয়ে ওঠে — এই ঐতিহাসিক যুক্তি অনেকে দেন।
কিন্তু patching-এর কয়েকটি খরচ আছে। প্রথমত, test ভেতরের গঠনের সাথে বাঁধা: patch লক্ষ্য করে একটি নাম — “reports module-এর ভেতরের SmtpMailer” — তাই কেউ import-এর ধরন বদলালে বা class সরালে test ভাঙে, যদিও আচরণ একই। প্রথমবার যাঁরা patch লেখেন, তাঁরা প্রায়ই ভুল জায়গায় patch করেন (যেখানে সংজ্ঞা আছে সেখানে, যেখানে ব্যবহার হয় সেখানে নয়) — Python-এর documentation-এ এ নিয়ে আলাদা সতর্কতা আছে। দ্বিতীয়ত, নির্ভরতা লুকানোই থাকে: test পাস করে, কিন্তু class-এর signature এখনো বলে না তার কী লাগে। তৃতীয়ত, production-এ নমনীয়তা আসে না: SMS-এ যেতে হলে এখনো class খুলতে হবে; patching শুধু test-এর সমস্যা মেটায়।
অন্যদিকে dynamic ভাষায় DI অনেক হালকা: interface ঘোষণা লাগে না, duck typing-ই যথেষ্ট; function-কে সরাসরি argument হিসেবে দেওয়া যায় (অংশ ৬); container প্রায় কখনো লাগে না। তাই অনেক Python আর JavaScript দলের ব্যবহারিক রীতি: boundary-র জিনিস — database, network, ঘড়ি, random, file — constructor বা function-এর parameter হিসেবে নিন; patching রাখুন পুরোনো কোডের জন্য, বা এমন জায়গার জন্য যেখানে নিয়ন্ত্রণ আপনার হাতে নেই (তৃতীয় পক্ষের library-র ভেতরে)।
মূল উপলব্ধি: Patching test-এর তাৎক্ষণিক সমাধান, নকশার নয়; dynamic ভাষায় DI সস্তা, তাই boundary-তে সেটিই প্রথম পছন্দ আর patching শেষ উপায়।
অনুশীলন
আউটপুট অনুমান, object গোনা, refactoring আর সমালোচনা — চারটিই পরীক্ষা ও ইন্টারভিউর ধরনের।
-
আউটপুট অনুমান করুন (container-এর lifetime): নিচের ছোট resolver-এ
clocksingleton,cartscoped (প্রতিটি web request-এর জন্য একটি scope),logtransient. তিনটি লাইনের আউটপুট লিখুন, আর বলুন কোন জিনিসটি দুই request-এ ভাগ হলো।let made = 0; const life = { clock: "singleton", cart: "scoped", log: "transient" }; const single = {}; function resolve(name, scope) { const l = life[name]; if (l === "singleton") return single[name] ??= name + (++made); if (l === "scoped") return scope[name] ??= name + (++made); return name + (++made); } for (const req of ["req1", "req2"]) { const scope = {}; // one scope per web request console.log(req, resolve("cart", scope), resolve("clock", scope), resolve("cart", scope), resolve("log", scope), resolve("log", scope)); } console.log("made:", made);আসল আউটপুট (চালিয়ে মেলানো):
req1 cart1 clock2 cart1 log3 log4 req2 cart5 clock2 cart5 log6 log7 made: 7- req1: প্রথম
cart— scope খালি, তাই নতুন cart1 (made ১)।clock— singleton cache খালি, তাই clock2. দ্বিতীয়cart— একই scope-এ আগেরটি আছে, তাই আবার cart1. দুটিlog— transient, প্রতিবার নতুন: log3, log4. - req2: নতুন request, নতুন খালি scope — তাই নতুন cart5, আর এই request-এর ভেতরে দুবারই cart5.
clocksingleton, তাই সেই clock2 — দুই request-এ ভাগ হলো শুধু এটিই। log আবার দুটি নতুন: log6, log7. - মোট ৭: cart ২টি (প্রতি request একটি), clock ১টি, log ৪টি।
খেয়াল করুন
??=-এর কাজ: বাঁ দিকে মান না থাকলে (undefined) তবেই ডান দিক চলে আর++madeহয়; থাকলে পুরোনো মান ফেরত আসে, made বাড়ে না। যদিclockতার ভেতরে একটিcartধরে রাখত, তবে req1-এর cart1 req2-তেও পৌঁছে যেত — captive dependency. - req1: প্রথম
-
হিসাব (object graph): একটি কাল্পনিক অনলাইন বাজারে নিবন্ধন:
checkoutচায়cartআরpayment;paymentচায়gatewayআরlog;cartচায়repoআরlog;gatewayআরrepoদুটিই চায়config;logআরconfig-এর কোনো dependency নেই। Container dependency-গুলো তালিকার ক্রমে (বাঁ থেকে ডানে) resolve করে। (ক) সব transient হলে একবারcheckoutresolve করতে কতটি object তৈরি হয়, আর তিনবার করলে? (খ) প্রথম resolve-এ তৈরির ক্রম কী? (গ) শুধুconfigআরgatewaysingleton হলে একবারে আর তিনবারে কত?(ক) সব transient: প্রতিটি পথ আলাদা গোনা হয়। checkout → cart → (repo → config) আর log; checkout → payment → (gateway → config) আর log. গুনলে: config ২, repo ১, log ২, cart ১, gateway ১, payment ১, checkout ১ = ৯টি। তিনবারে ৯ × ৩ = ২৭টি। লক্ষ করুন, transient হলে config আর log দুবার করে তৈরি হয় — একটি graph-এ একই জিনিসের দুটি আলাদা object.
(খ) তৈরির ক্রম (নিচ থেকে ওপরে, বাঁ থেকে ডানে): config, repo, log, cart, config, gateway, log, payment, checkout.
(গ) config আর gateway singleton: প্রথম resolve-এ config একবারই (দ্বিতীয়বার cache থেকে): config, repo, log, cart, gateway, log, payment, checkout = ৮টি। পরের প্রতিটি resolve-এ config আর gateway cache থেকে, নতুন হয় repo, log, cart, log, payment, checkout = ৬টি। তিনবারে ৮ + ৬ + ৬ = ২০টি। (একটি ছোট script-এ graph চালিয়ে সংখ্যাগুলো মেলানো হয়েছে।) এখানে কোনো captive dependency নেই, কারণ singleton দুটির (gateway, config) dependency-ও singleton বা নেই।
-
এই class refactor করুন: একটি কাল্পনিক কোচিং সেন্টারের হাজিরা app-এ নিচের class. লুকানো dependency-গুলো চিহ্নিত করুন, injection-এর রূপ বেছে নিন, একটি composition root আর একটি test-এর রূপরেখা দিন।
class AttendanceAlert: def run(self): today = datetime.date.today() rows = csv.reader(open("/srv/coaching/att.csv")) absent = [r[0] for r in rows if r[1] == str(today) and r[2] == "A"] sms = SmsClient(api_key=os.environ["SMS_KEY"]) for phone in absent: sms.send(phone, f"Absent on {today}") return len(absent)লুকানো dependency চারটি: (১) ঘড়ি —
date.today(); (২) file — নির্দিষ্ট path-এর CSV; (৩) environment variable —SMS_KEY; (৪) SMS client — ভেতরে বানানো। ফলে test চালাতে আসল file, আসল key আর আসল SMS লাগে, আর ফল দিনের ওপর নির্ভর করে।রূপ বাছাই: হাজিরার উৎস আর SMS sender ছাড়া class কাজই করতে পারে না — constructor injection. তারিখ প্রতিবার আলাদা হতে পারে (কাল রাতের জন্য আবার চালানো) — method parameter হিসেবে
run(day); অথবা একটিclockconstructor-এ। API key class-এর জানার দরকারই নেই — সেটি composition root-এSmsClientবানানোর সময় লাগে।class AttendanceAlert: def __init__(self, attendance, sms): # attendance.absent_on(day) -> phones self.attendance, self.sms = attendance, sms def run(self, day): phones = self.attendance.absent_on(day) for p in phones: self.sms.send(p, f"Absent on {day}") return len(phones) def main(): # composition root alert = AttendanceAlert(CsvAttendance("/srv/coaching/att.csv"), SmsClient(api_key=os.environ["SMS_KEY"])) alert.run(datetime.date.today()) def test_two_absent(): sms = FakeSms() att = InMemoryAttendance({"2026-03-30": ["017xxxxxxx1", "018xxxxxxx2"]}) assert AttendanceAlert(att, sms).run("2026-03-30") == 2 assert len(sms.sent) == 2CSV পড়ার কাজটি আলাদা class-এ গেল (
CsvAttendance), তাইAttendanceAlert-এর কাজ এখন শুধু নিয়ম: “যারা অনুপস্থিত, তাদের বার্তা।” আরabsent_on(day)চুক্তিটি policy-র ভাষায় লেখা — পর্ব ২৮-এর DIP. ধাপে ধাপে করলে: আগে একটি characterization test (বর্তমান আচরণ ধরে রাখা) লিখুন, তারপর একেকটি dependency একেক commit-এ বের করুন। -
এই নকশার সমালোচনা করুন: একটি কাল্পনিক বিশ্ববিদ্যালয়ের ফলাফল পোর্টালে একজন developer লিখেছেন: “DI চালু করেছি। (১) একটি global
Servicesclass বানিয়েছি; ৪০টি class এখনServices.get("db"),Services.get("mailer")দিয়ে যা লাগে নেয় — কারও constructor-এ কিছু দিতে হয় না। (২)ResultPublisher-এর constructor-এ ১১টি parameter ছিল, তাই সবগুলো একটিdepsdict-এ ভরেছি। (৩)GradeCachesingleton, আর তার constructor-এ request-ভিত্তিকCurrentStudentদেওয়া হয়।” Review-তে কী বলবেন?- (১) এটি DI নয়, service locator: ৪০টি class-এর constructor এখন বলে “কিছু লাগে না”, অথচ প্রত্যেকে ভেতরে global থেকে টেনে নেয়। নির্ভরতা লুকানো, নিবন্ধন ভুললে error চালানোর সময়, test-এ কী বসাতে হবে জানতে কোড পড়তে হয়, আর এক test-এর বসানো জিনিস global বলে অন্য test-এ চুইয়ে যেতে পারে। প্রস্তাব: প্রতিটি class যা লাগে তা constructor-এ চাক;
Services-এর ব্যবহার শুধু composition root-এ থাকুক, পরে সেটিও হাতে লেখা জোড়ায় বদলানো যায়। - (২) সংখ্যা লুকানো, সমস্যা নয়: ১১টি dependency একটি dict-এ ভরলে constructor over-injection-এর সতর্কসংকেত চাপা পড়ে, আর type checker-ও আর সাহায্য করতে পারে না (
deps["mailr"]-এর মতো বানান ভুল ধরা পড়ে চালানোর সময়)। প্রস্তাব: কোন dependency-গুলো একসাথে ব্যবহার হয় দেখুন — যেমন grade-এর নিয়ম, GPA-র হিসাব আর transcript format মিলে একটিGradingService; email, SMS আর notice board মিলে একটিNotifier.ResultPublisher-এ তখন হয়তো চার-পাঁচটি স্পষ্ট ভূমিকা থাকবে (SRP). - (৩) Captive dependency: singleton
GradeCache-এর ভেতরে request-ভিত্তিকCurrentStudentবন্দি হবে — প্রথম যে ছাত্রের request-এ cache তৈরি হলো, পরের সবাই হয়তো তার তথ্য দেখবে। এটি গোপনীয়তার গুরুতর ঝুঁকিও। প্রস্তাব:GradeCachestudent-নিরপেক্ষ রাখুন আর student id method parameter হিসেবে নিন (cache.grades_for(student_id)) — method injection. - প্রক্রিয়া: তিনটি বদল আলাদা pull request-এ, প্রথমে (৩), কারণ এটি সরাসরি ভুল ফল আর তথ্য ফাঁসের ঝুঁকি।
মন্তব্যের সুর: “DI ভুল হয়েছে” না বলে প্রতিটি বদলের ফল জিজ্ঞেস করুন — “নতুন সদস্য কীভাবে জানবেন এই class চালাতে কী লাগে?”, “দুজন ছাত্র একসাথে login করলে cache কী দেখাবে?”
- (১) এটি DI নয়, service locator: ৪০টি class-এর constructor এখন বলে “কিছু লাগে না”, অথচ প্রত্যেকে ভেতরে global থেকে টেনে নেয়। নির্ভরতা লুকানো, নিবন্ধন ভুললে error চালানোর সময়, test-এ কী বসাতে হবে জানতে কোড পড়তে হয়, আর এক test-এর বসানো জিনিস global বলে অন্য test-এ চুইয়ে যেতে পারে। প্রস্তাব: প্রতিটি class যা লাগে তা constructor-এ চাক;
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকটি পুরোনো class-এ ধাপে ধাপে, কিছু না ভেঙে dependency injection আনার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- 🔍 Class-এর ভেতরের লুকানো new, global আর locator-এর ডাক খুঁজে তালিকা করা
- 🧪 বর্তমান আচরণ ধরে রাখতে একটি characterization test লেখা
- 🚪 Constructor-এ নতুন parameter যোগ করা, সাময়িকভাবে পুরোনো object-কে default রেখে
- 🧷 সব caller-কে composition root থেকে dependency দেওয়ানো
- ✂️ Default সরিয়ে parameter বাধ্যতামূলক করা, আর fake দিয়ে নতুন test চালানো
একটি PaymentService payment gateway ছাড়া কোনো কাজই করতে পারে না, আর gateway object তৈরির পর বদলায় না। Gateway দেওয়ার সবচেয়ে উপযুক্ত উপায় কোনটি?
- Setter injection — তৈরির পর
service.gateway = ... - Service locator থেকে method-এর ভেতরে
Locator.get("gateway") - Constructor injection —
PaymentService(gateway) - প্রতিটি
pay()ডাকে method parameter হিসেবে
একটি web app-এর DI container-এ CartService scoped (প্রতি request-এ একটি) আর PriceCache singleton. PriceCache-এর constructor একটি CartService নেয়। কী সমস্যা হতে পারে?
- প্রতিটি request-এ নতুন PriceCache তৈরি হবে, তাই cache কাজ করবে না
- প্রথম request-এর CartService singleton-এর ভেতরে আটকে থাকবে, আর পরের request-গুলো সেটি ভাগ করবে — captive dependency
- Container একটি dependency cycle-এর error দেবে
- কোনো সমস্যা নেই, কারণ দুটিই container তৈরি করে
একটি OrderService-এর constructor কোনো parameter নেয় না, কিন্তু তার ship() method-এর ভেতরে Locator.get("courier") ডাকা হয়। এই নকশার প্রধান আপত্তি কোনটি?
- Locator থেকে object নেওয়া constructor-এর চেয়ে অনেক ধীর
- Class নিজের ভেতরে new দিয়ে courier বানাচ্ছে
- শুধু static ভাষায় এটি সমস্যা, dynamic ভাষায় নয়
- Courier-এর নির্ভরতা signature-এ লুকানো, আর ভুল নিবন্ধন ধরা পড়ে কেবল ship() চলার সময়
Base class-এ add(x) একটি উপাদান যোগ করে, আর add_all(xs) প্রতিটির জন্য self.add(x) ডাকে। Subclass দুটিই override করে: add-এ count += 1, add_all-এ count += len(xs), তারপর super()-এর method ডাকে। নতুন object-এ add("a") আর add_all(["b", "c"]) ডাকার পর count কত?
- ৩
- ৪
- ৫
- ৬
একটি CheckoutService-এর constructor-এ ৯টি dependency. সবচেয়ে উপযুক্ত পদক্ষেপ কোনটি?
- দেখা, কোন dependency-গুলো একসাথে একটি কাজ করে, আর সেগুলো নতুন service-এ সরিয়ে class-এর দায়িত্ব ভাগ করা
- সবগুলো setter injection-এ সরানো, যাতে constructor ছোট দেখায়
- একটি service locator ব্যবহার করা
- সব dependency একটি dict বা context object-এ ভরে একটি parameter বানানো
Inversion of control আর dependency injection সম্পর্কে কোন বক্তব্যটি সঠিক?
- Martin Fowler ২০০৪ সালে inversion of control ধারণাটি আবিষ্কার করেন
- Dependency injection হলো inversion of control-এর একটি নির্দিষ্ট রূপ, আর নামটি Fowler-এর ২০০৪ সালের প্রবন্ধ থেকে প্রচলিত
- DI container ছাড়া dependency injection করা যায় না
- Dependency injection আর dependency inversion principle একই জিনিসের দুই নাম
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- লুকানো
newনির্ভরতা লুকায়, test আটকায় আর বদল কঠিন করে। DI মানে সহযোগী বাইরে থেকে দেওয়া — বাধ্যতামূলক হলে constructor-এ, ঐচ্ছিক হলে setter-এ নিরাপদ default-সহ, প্রতিবার ভিন্ন হলে method parameter-এ। - Composition root: app-এর শুরুতে একটি জায়গায় সব object তৈরি ও জোড়া, নিচ থেকে ওপরে; বাকি কোডে কোনো new নেই। Test-এর নিজস্ব ছোট root.
- Pure DI বনাম container: হাতে লেখা জোড়া স্পষ্ট আর type-নিরাপদ; container বড় graph-এ registration, resolution, auto-wiring আর lifetime (transient, scoped, singleton) সামলায়। কোনো object নিজের চেয়ে কম আয়ুর object ধরে রাখবে না।
- সতর্কসংকেত: service locator (নির্ভরতা আবার লুকানো), constructor over-injection (SRP-র লঙ্ঘন — dict-এ ভরা নয়, দায়িত্ব ভাগ), আর dependency cycle (নকশা ভাঙা)। IoC বড় ধারণা; DI নামটি Fowler-এর ২০০৪ সালের প্রবন্ধ থেকে।
- Composition over inheritance: base-এর ভেতরের বদল subclass ভাঙতে পারে (fragile base class). Inheritance রাখুন আচরণেও “is-a” আর LSP-মানা সম্পর্কে, extension-এর জন্য নকশা করা base-এ; বাকি সব “has-a/uses” — composition আর delegation. Functional ধাঁচে একই কাজ করে closure আর partial application.
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৩৬ · Behavioral pattern ২ — state, iterator, chain of responsibility, mediator আর visitorআগের পর্বমডিউল ৬-এর শেষ pattern-গুলো।
- পর্ব ৩৮ · Software architecture — layered, client-server, pipe-filter, event-driven আর ADRপরবর্তী পর্বClass ছাড়িয়ে পুরো system-এর গঠন।
- পর্ব ২৮ · SOLID ২ — Liskov substitution, interface segregation আর dependency inversionভিত্তিDIP, DI আর IoC-এর পার্থক্য, আর LSP — inheritance কখন নিরাপদ।
- পর্ব ৩৩ · Creational pattern — singleton, factory method, abstract factory, builder আর prototypeএকই মডিউলSingleton pattern-এর সমস্যা আর factory দিয়ে object তৈরি।
- সফটওয়্যার ইঞ্জিনিয়ারিং ও Git · কোর্স হোম৭০টি পর্বপুরো কোর্সের রোডম্যাপ।
- Python প্রোগ্রামিং · Inheritance আর compositionপাশের ট্র্যাকPython-এ inheritance, MRO আর composition-এর খুঁটিনাটি।
- JavaScript প্রোগ্রামিং · Closureপাশের ট্র্যাকFunctional DI-এর ভিত্তি — function কীভাবে variable মনে রাখে।