ব্যাক-এন্ড ফ্রেমওয়ার্কে ডিপেন্ডেন্সি ইনজেকশন
এই পাঠে যা শিখবেন
- ডিপেন্ডেন্সি ইনজেকশনের মূল ধারণা — হার্ডকোডেড ডিপেন্ডেন্সির বদলে ইনজেক্টেড ডিপেন্ডেন্সি
- একটি real
DIContainerক্লাস —register()ওresolve()সহ - কীভাবে একই হ্যান্ডলার কোড একটি ভিন্ন কন্টেইনার-কনফিগারেশনের মাধ্যমে সম্পূর্ণ ভিন্ন ডিপেন্ডেন্সি দিয়ে চলতে পারে (প্রোডাকশন বনাম টেস্টিং)
- এটি কেন টেস্টেবল ও মেইনটেইনেবল কোড লেখার একটি কেন্দ্রীয় কৌশল
১ · সমস্যা: হার্ডকোডেড ডিপেন্ডেন্সি
যদি একটি হ্যান্ডলার ফাংশন সরাসরি RealDatabase() তৈরি করে ভেতরে ব্যবহার করে, তাহলে সেই
হ্যান্ডলারকে টেস্ট করতে হলে প্রতিবার একটি আসল ডেটাবেস কানেকশন লাগবে — এবং হ্যান্ডলারের কোড না পাল্টে
অন্য কোনো ইমপ্লিমেন্টেশনে সুইচ করা অসম্ভব। ডিপেন্ডেন্সি ইনজেকশন এই সমস্যাটির সমাধান করে — হ্যান্ডলার
শুধু একটি "ইন্টারফেস নাম" জানে (যেমন "database"), কনক্রিট ইমপ্লিমেন্টেশন কোনটা তা বাইরে
থেকে সরবরাহ করা হয়।
হ্যান্ডলার নিজেই
RealDatabase() তৈরি করে -- কোড পাল্টানো ছাড়া অন্য কিছু দিয়ে সোয়াপ করা যায় না।হ্যান্ডলার একটি কন্টেইনার থেকে
resolve("database") কল করে -- কন্টেইনার কনফিগার করলেই ভিন্ন ইমপ্লিমেন্টেশন সোয়াপ হয়ে যায়, হ্যান্ডলার অপরিবর্তিত থাকে।২ · একটি real DIContainer — কোড দিয়ে
নিচের কোডে DIContainer একটি dict-ভিত্তিক রেজিস্ট্রি রাখে (নাম → ফ্যাক্টরি ফাংশন)।
get_user_handler() ফাংশনটি তার ডিপেন্ডেন্সি সরাসরি তৈরি না করে container.resolve(...)
দিয়ে পায়। একই হ্যান্ডলার প্রথমে একটি প্রোডাকশন কন্টেইনার দিয়ে, তারপর একটি টেস্ট কন্টেইনার দিয়ে চালানো
হয়েছে — কোড কোনো জায়গায় পাল্টানো হয়নি।
class DIContainer:
def __init__(self):
self._factories = {}
def register(self, name, factory_fn):
self._factories[name] = factory_fn
def resolve(self, name):
factory_fn = self._factories.get(name)
if factory_fn is None:
raise KeyError(f"'{name}' কন্টেইনারে নিবন্ধিত (registered) নয়")
return factory_fn()
# --- আসল (production) ডিপেন্ডেন্সি ---
class RealDatabase:
def __init__(self):
self._rows = {1: "প্রথম ইউজার", 2: "দ্বিতীয় ইউজার"}
def get(self, user_id):
return self._rows.get(user_id, "পাওয়া যায়নি")
class RealLogger:
def log(self, message):
print(f"[REAL LOG] {message}")
# --- ফেক/টেস্ট ডিপেন্ডেন্সি -- একই "ইন্টারফেস" (get/log মেথড), ভিন্ন ইমপ্লিমেন্টেশন ---
class FakeDatabase:
def __init__(self):
self._rows = {1: "টেস্ট ইউজার"}
def get(self, user_id):
return self._rows.get(user_id, "পাওয়া যায়নি (fake)")
class FakeLogger:
def __init__(self):
self.messages = []
def log(self, message):
self.messages.append(message)
# --- হ্যান্ডলার -- কোথাও RealDatabase/RealLogger/FakeDatabase সরাসরি লেখা নেই ---
def get_user_handler(container, user_id):
db = container.resolve("database")
logger = container.resolve("logger")
logger.log(f"user_id={user_id} খোঁজা হচ্ছে")
result = db.get(user_id)
logger.log(f"ফলাফল: {result}")
return result
# --- প্রোডাকশন কন্টেইনার ---
prod_container = DIContainer()
prod_container.register("database", lambda: RealDatabase())
prod_container.register("logger", lambda: RealLogger())
print("=== প্রোডাকশন কন্টেইনার দিয়ে (একই হ্যান্ডলার) ===")
prod_result = get_user_handler(prod_container, 1)
print("রিটার্ন মান:", prod_result)
# --- টেস্ট কন্টেইনার -- একই হ্যান্ডলার কোড, ভিন্ন কনক্রিট ইমপ্লিমেন্টেশন ---
fake_logger = FakeLogger()
test_container = DIContainer()
test_container.register("database", lambda: FakeDatabase())
test_container.register("logger", lambda: fake_logger)
print("\n=== টেস্ট কন্টেইনার দিয়ে (হুবহু একই get_user_handler ফাংশন) ===")
test_result = get_user_handler(test_container, 1)
print("রিটার্ন মান:", test_result)
print("ফেক লগারে সংরক্ষিত বার্তা:", fake_logger.messages)
print("\nহ্যান্ডলার ফাংশন কি পাল্টানো হয়েছিল?", "না -- একই get_user_handler দুইবার কল হয়েছে")
prod_result হবে "প্রথম ইউজার" (RealDatabase থেকে), কিন্তু test_result
হবে "টেস্ট ইউজার" (FakeDatabase থেকে) — একই user_id=1 দিয়েও ফলাফল ভিন্ন, কারণ
কন্টেইনার ভিন্ন ইমপ্লিমেন্টেশন রেজল্ভ করেছে। fake_logger.messages-এ দেখা যাবে দুটো বার্তা
সংরক্ষিত হয়েছে ("খোঁজা হচ্ছে" ও "ফলাফল") — এটি প্রমাণ করে logger.log() কলগুলো সত্যিই ফেক
লগারে গিয়েছে, শুধু print() হয়নি।
৩ · কেন এটি Spring-এর মতো ফ্রেমওয়ার্কে কেন্দ্রীয় ধারণা
L20-এ দেখা হয়েছিল Spring "মডুলার/কনফিগারযোগ্য" — এর একটি বড় কারণ হলো Spring-এর কোর ধারণাগুলোর একটি হলো ডিপেন্ডেন্সি ইনজেকশন (সেখানে "IoC কন্টেইনার" নামে পরিচিত)। বড় এন্টারপ্রাইজ অ্যাপ্লিকেশনে হাজারো ক্লাস একে অপরের উপর নির্ভরশীল হতে পারে — DI ছাড়া প্রতিটি ক্লাসকে ম্যানুয়ালি তার সব ডিপেন্ডেন্সি তৈরি করে "ওয়্যার" করতে হতো। DI কন্টেইনার এই ওয়্যারিং কেন্দ্রীভূতভাবে সামলায়, এবং পরীক্ষার সময় সহজেই একটি ইমপ্লিমেন্টেশনকে ফেক দিয়ে সোয়াপ করার সুযোগ দেয় — ঠিক যেমন উপরের কোডে দেখানো হয়েছে।
ডিপেন্ডেন্সি ইনজেকশনে একটি হ্যান্ডলার তার ডিপেন্ডেন্সি নিজে তৈরি করে না — একটি DI কন্টেইনারের
resolve() মেথড থেকে পায়। যেহেতু কন্টেইনারের রেজিস্ট্রেশন হ্যান্ডলারের কোড থেকে সম্পূর্ণ
আলাদা, তাই কন্টেইনার পাল্টে দিলেই (প্রোডাকশন বনাম টেস্টিং) একই হ্যান্ডলার সম্পূর্ণ ভিন্ন কনক্রিট
ইমপ্লিমেন্টেশনের সাথে কাজ করে -- এটিই DI-এর মূল মূল্য: টেস্টেবিলিটি ও নমনীয়তা।
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।
প্র ০১
register()-এ সরাসরি একটি ইনস্ট্যান্স না দিয়ে একটি ফ্যাক্টরি ফাংশন
(lambda: RealDatabase()) কেন দেওয়া হয়েছে?
ফ্যাক্টরি ফাংশন দিলে ইনস্ট্যান্স তৈরি হওয়া বিলম্বিত (lazy) হয় -- resolve() আসলে কল না
হওয়া পর্যন্ত RealDatabase() তৈরিই হয় না। এটি গুরুত্বপূর্ণ যদি ইনস্ট্যান্স তৈরি করা ব্যয়বহুল
হয় (যেমন একটি প্রকৃত ডেটাবেস কানেকশন খোলা) -- অপ্রয়োজনে আগেভাগে তৈরি করার দরকার নেই। এছাড়া এটি প্রতিবার
resolve() কল হলে একটি নতুন ইনস্ট্যান্স দেওয়ারও সুযোগ রাখে, প্রয়োজনে।
প্র ০২
টেস্ট কন্টেইনারে fake_logger-কে lambda: fake_logger দিয়ে রেজিস্টার করা
হয়েছে (একটি প্রি-তৈরি ভ্যারিয়েবল), কিন্তু ডেটাবেসের জন্য lambda: FakeDatabase() ব্যবহার
করা হয়েছে -- পার্থক্যটা কেন?
fake_logger-কে আগে থেকে তৈরি রাখা হয়েছে যাতে get_user_handler() কল করার পরও
কোড থেকে সরাসরি fake_logger.messages পরীক্ষা করা যায় -- যদি প্রতিবার
resolve("logger")-এ নতুন FakeLogger() তৈরি হতো, তাহলে বাইরের কোড কোন
instance-টিকে রেফার করছে তা নিশ্চিত হওয়া যেত না। এটি দেখায় ফ্যাক্টরি ফাংশন ক্লোজারের মাধ্যমে একটি
নির্দিষ্ট, পূর্ব-তৈরি instance-ও ফেরত দিতে পারে -- শুধু নতুন instance তৈরি করাই বাধ্যতামূলক নয়।
প্র ০৩
যদি get_user_handler(prod_container, 1)-এর বদলে
get_user_handler(prod_container, 999) কল করা হতো, ফলাফল কী হতো?
RealDatabase._rows-এ কী 999 নেই, তাই db.get(999)
self._rows.get(999, "পাওয়া যায়নি")-এর ডিফল্ট মান "পাওয়া যায়নি" রিটার্ন করত।
logger.log() তখনও দুইবার কল হতো (খোঁজার শুরুতে ও ফলাফলসহ), কিন্তু দ্বিতীয় বার্তাটি হতো
"ফলাফল: পাওয়া যায়নি" -- হ্যান্ডলারের লজিক অপরিবর্তিত থাকে, শুধু ডেটার ফলাফল পাল্টায়।
অনুশীলন
-
চিন্তা করুন: যদি
DIContainer.resolve()-এ কোনো নিবন্ধিত নাম না পাওয়া গেলেNoneরিটার্ন করত (এক্সেপশন না তুলে), তাহলেget_user_handler()-এ কী সমস্যা দেখা দিতে পারত?db = container.resolve("database")যদিNoneহয়, তাহলে পরের লাইনেdb.get(user_id)কল করলে একটিAttributeError(কারণNone-এর কোনোgetমেথড নেই) নিক্ষিপ্ত হতো -- কিন্তু এরর মেসেজটি বিভ্রান্তিকর হতো, কারণ প্রকৃত সমস্যা ("database" নিবন্ধিত হয়নি) স্পষ্টভাবে বলা হতো না। তাইKeyErrorসাথে সাথে, স্পষ্ট বার্তাসহ তোলা ভালো অনুশীলন -- সমস্যাটি যেখানে ঘটেছে সেখানেই দ্রুত ধরা পড়ে। -
পরীক্ষা করুন: উপরের কোড সেলে একটি তৃতীয় কন্টেইনার
broken_containerতৈরি করুন যেখানে শুধু"database"রেজিস্টার করা আছে,"logger"রেজিস্টার করা নেই। তারপরget_user_handler(broken_container, 1)কল করে Run চেপে দেখুন কী এরর আসে।যেহেতু
get_user_handler()-এ প্রথমেcontainer.resolve("database")সফল হয়, কিন্তু পরের লাইনেcontainer.resolve("logger")চলার সময়"logger"নিবন্ধিত না থাকায়self._factories.get("logger")Noneরিটার্ন করবে, এবংif factory_fn is None:শর্তেKeyError("'logger' কন্টেইনারে নিবন্ধিত (registered) নয়")নিক্ষিপ্ত হবে -- অর্থাৎ প্রোগ্রামটি একটি স্পষ্ট এরর মেসেজ সহ থেমে যাবে।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পরবর্তী পাঠ: এরর হ্যান্ডলিং মিডলওয়্যার L22 L19-এর মিডলওয়্যার চেইন এখন try/except-ভিত্তিক সেন্ট্রালাইজড এরর হ্যান্ডলিং দিয়ে সম্প্রসারিত হবে।
- কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৮টি পাঠ আর্কিটেকচার প্যাটার্ন, ফ্রন্ট-এন্ড/ব্যাক-এন্ড ফ্রেমওয়ার্ক ফান্ডামেন্টাল, স্টেট ম্যানেজমেন্ট, REST API, ORM, অথেন্টিকেশন, রেন্ডারিং স্ট্র্যাটেজি ও ডিপ্লয়মেন্ট।
- Python Programming কোর্স সহোদর কোর্স ল্যাম্বডা ফাংশন, ক্লোজার ও ক্লাস সিনট্যাক্সের ভাষাগত ভিত্তি সেই কোর্সেই তৈরি হয়েছে।