পাঠ ১৮ · ৫৮-এর মধ্যে · মডিউল ৪

SOLID প্রিন্সিপল — DIP

SOLID principles — DIP
৮ মিনিট পড়া মাঝারি · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • DIP-এর সুনির্দিষ্ট সংজ্ঞা — কেন "নির্ভরতার দিক উল্টে দেওয়া" শব্দগুচ্ছটি সঠিক
  • dependency injection টেকনিক — কনস্ট্রাক্টরের মাধ্যমে dependency ইনজেক্ট করা
  • কীভাবে DIP একটি ক্লাসকে টেস্টেবল ও ফ্লেক্সিবল দুটোই বানায়, সত্যিকারের কোড দিয়ে প্রমাণসহ
  • DIP কীভাবে L14-এর লো-কাপলিং ও L16-এর OCP-র সাথে সরাসরি যুক্ত তা বোঝা

১ · Dependency Inversion Principle (DIP) কী

Dependency Inversion PrincipleDIPউচ্চ-স্তরের মডিউল নিম্ন-স্তরের মডিউলের উপর নির্ভর করা উচিত নয় — দুটোই অ্যাবস্ট্রাকশনের উপর নির্ভর করা উচিত; অ্যাবস্ট্রাকশন বিস্তারিত বাস্তবায়নের উপর নির্ভর করা উচিত নয়, বরং বিস্তারিত বাস্তবায়ন অ্যাবস্ট্রাকশনের উপর নির্ভর করা উচিত। (DIP, SOLID-এর শেষ অক্ষর) বলে — উচ্চ-স্তরের মডিউল (যেমন বিজনেস-লজিক ক্লাস) নিম্ন-স্তরের মডিউলের (যেমন একটি নির্দিষ্ট ডেটাবেস ক্লাস) উপর সরাসরি নির্ভর করা উচিত নয়। বদলে, দুটোই একটি অ্যাবস্ট্র্যাক্ট ইন্টারফেসের উপর নির্ভর করা উচিত — কংক্রিট বাস্তবায়ন তখন বাইরে থেকে ইনজেক্ট করা হয়, ক্লাসের ভেতরে হার্ডকোড করা নয়।

"ইনভার্শন" (উল্টানো) শব্দটি এখানে সুনির্দিষ্ট কারণেই ব্যবহৃত — সাধারণত উচ্চ-স্তরের কোড সরাসরি নিম্ন-স্তরের বিস্তারিত ক্লাস তৈরি করে ও তার উপর নির্ভর করে (নির্ভরতার দিক উপর থেকে নিচে)। DIP এই দিক উল্টে দেয় — এখন উভয় পক্ষই একটি ভাগাভাগি করা অ্যাবস্ট্রাকশনের উপর নির্ভর করে, এবং নিম্ন-স্তরের কংক্রিট ক্লাস সেই অ্যাবস্ট্রাকশন মেনে চলে, উচ্চ-স্তরের কোড তার নিজের প্রয়োজনে (L14-এর লো-কাপলিং লক্ষ্যের সরাসরি পেওফ, এবং L16-এর OCP-র সাথেও সরাসরি যুক্ত — নতুন database ক্লাস যোগ করা মানে existing OrderProcessor-এ কোনো পরিবর্তন লাগে না)।

সাধারণ (DIP ছাড়া) OrderProcessor (উচ্চ-স্তর) MySQLDatabaseConcrete (নিম্ন-স্তর) সরাসরি হার্ড dependency DIP-এর পরে OrderProcessor (উচ্চ-স্তর) Database (অ্যাবস্ট্রাকশন) MySQLDatabase / InMemoryFakeDatabase
বামে: উচ্চ-স্তরের কোড সরাসরি একটি কংক্রিট ক্লাসের উপর আটকে। ডানে: উভয় পক্ষই একটি অ্যাবস্ট্রাকশনের উপর নির্ভরশীল — কংক্রিট বাস্তবায়ন বাইরে থেকে সহজে পাল্টানো যায়।

২ · Dependency Injection — DIP প্রয়োগের প্র্যাকটিক্যাল কৌশল

Dependency InjectionDIএকটি ক্লাসের প্রয়োজনীয় dependency ক্লাসের ভেতরে নিজে তৈরি না করে, বাইরে থেকে (সাধারণত কনস্ট্রাক্টর প্যারামিটার হিসেবে) পাস করে দেওয়ার টেকনিক। হলো DIP বাস্তবে প্রয়োগ করার স্ট্যান্ডার্ড টেকনিক — একটি ক্লাসের dependency নিজে ভেতরে তৈরি না করে, বাইরে থেকে (সাধারণত কনস্ট্রাক্টর প্যারামিটার হিসেবে) পাস করা। এর সরাসরি, বাস্তব সুবিধা দুটো: ক্লাসটি সহজেই টেস্টেবল হয়ে যায় (টেস্টে একটি real dependency-র বদলে একটি ফেক/মক dependency ইনজেক্ট করা যায় — M10/L47-এর মকিং পাঠের সরাসরি ভিত্তি), এবং ক্লাসটি ফ্লেক্সিবল হয় (একটি বাস্তবায়ন থেকে আরেকটিতে পাল্টাতে হাই-লেভেল ক্লাসের কোনো পরিবর্তন লাগে না, যতক্ষণ দুটোই একই অ্যাবস্ট্রাক্ট ইন্টারফেস মেনে চলে)।

Python
from abc import ABC, abstractmethod

# --- DIP লঙ্ঘন: OrderProcessor সরাসরি কংক্রিট MySQLDatabase-এর উপর নির্ভরশীল ---
class MySQLDatabaseConcrete:
    def save(self, record):
        return f"[MySQL] সংরক্ষিত: {record}"

class OrderProcessorBad:
    def __init__(self):
        # কনস্ট্রাক্টরের ভেতরেই কংক্রিট ক্লাস তৈরি করে ফেলা -- hard dependency
        self.database = MySQLDatabaseConcrete()

    def process(self, order):
        return self.database.save(order)


# --- DIP-কমপ্লায়েন্ট: একটি অ্যাবস্ট্র্যাকশনের উপর নির্ভরশীল, dependency injection দিয়ে ---
class Database(ABC):
    @abstractmethod
    def save(self, record):
        ...

class MySQLDatabase(Database):
    def save(self, record):
        return f"[MySQL] সংরক্ষিত: {record}"

class InMemoryFakeDatabase(Database):
    def __init__(self):
        self.records = []
    def save(self, record):
        self.records.append(record)
        return f"[InMemory] সংরক্ষিত: {record} (মোট {len(self.records)}টি)"

class OrderProcessor:
    def __init__(self, database: Database):
        # dependency ইনজেক্ট করা হচ্ছে -- OrderProcessor জানেই না কোন কংক্রিট ক্লাস এটা
        self.database = database

    def process(self, order):
        return self.database.save(order)


print("-- DIP লঙ্ঘন: OrderProcessorBad সবসময় MySQLDatabaseConcrete-এই আটকে --")
bad = OrderProcessorBad()
print(bad.process("অর্ডার #101"))

print("\n-- DIP-কমপ্লায়েন্ট: একই OrderProcessor ক্লাস, দুটি ভিন্ন database দিয়ে --")
real_db_processor = OrderProcessor(MySQLDatabase())
print(real_db_processor.process("অর্ডার #201"))

fake_db_processor = OrderProcessor(InMemoryFakeDatabase())
print(fake_db_processor.process("অর্ডার #202"))
print(fake_db_processor.process("অর্ডার #203"))

print("\nOrderProcessor ক্লাসের নিজের কোডে কোনো পরিবর্তন ছাড়াই দুই ধরনের database কাজ করল।")
print(f"real_db_processor.database আসলে MySQLDatabase? {isinstance(real_db_processor.database, MySQLDatabase)}")
print(f"fake_db_processor.database আসলে InMemoryFakeDatabase? {isinstance(fake_db_processor.database, InMemoryFakeDatabase)}")

    
লক্ষ্য করুন — OrderProcessor ক্লাসের সংজ্ঞায় কোথাও MySQLDatabase বা InMemoryFakeDatabase নাম লেখা নেই, শুধু Database অ্যাবস্ট্রাকশন। তাই একই OrderProcessor কোড দুই সম্পূর্ণ ভিন্ন বাস্তবায়নের সাথে কাজ করলো — এটাই DIP-এর ফ্লেক্সিবিলিটি সুবিধার সরাসরি, চালানো প্রমাণ। OrderProcessorBad এই সুবিধা পায় না — তার কোড পরিবর্তন না করে কখনো অন্য database ব্যবহার করা যাবে না।
মূল কথা · Key takeaway

SOLID-এর পাঁচটি অক্ষরই একটি সাধারণ লক্ষ্যের দিকে যায় — পরিবর্তন সহজ, পরীক্ষা সহজ, বোঝা সহজ এমন কোড। DIP এই লক্ষ্যের সবচেয়ে সরাসরি বাস্তবায়ন — নির্দিষ্ট বাস্তবায়নের বদলে অ্যাবস্ট্রাকশনের উপর নির্ভর করে, dependency injection দিয়ে। পরের পাঠে (L19) আমরা UML-এর মাধ্যমে এই ধরনের ক্লাস-সম্পর্ক ভিজ্যুয়ালি দেখানোর মান নোটেশন শিখব।

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।

প্র ০১ "Dependency Inversion" নামের "Inversion" (উল্টানো) শব্দটা ঠিক কী উল্টায় — সুনির্দিষ্টভাবে ব্যাখ্যা করুন।

স্বাভাবিক নির্ভরতার দিক উচ্চ-স্তর থেকে নিম্ন-স্তরের কংক্রিট বিস্তারিত ক্লাসের দিকে যায় (উচ্চ-স্তরের কোড নিম্ন-স্তরের ক্লাস সরাসরি জানে ও তৈরি করে)। DIP এই দিক উল্টে দেয় — এখন নিম্ন-স্তরের কংক্রিট ক্লাস একটি অ্যাবস্ট্রাকশনের (ইন্টারফেস) দিকে "নির্ভর করে" (অর্থাৎ সেই ইন্টারফেস মেনে চলে), এবং উচ্চ-স্তরের কোডও একই অ্যাবস্ট্রাকশনের দিকে নির্ভর করে — কেউই কারো কংক্রিট বিস্তারিত রূপ সরাসরি জানে না। নির্ভরতার তীর এখন দুই দিক থেকেই অ্যাবস্ট্রাকশনের দিকে, নিম্ন থেকে উচ্চের দিকে নয়।

প্র ০২ উপরের কোড সেলে OrderProcessorBad-কে টেস্ট করা কেন কঠিন, অথচ OrderProcessor-কে সহজ?

OrderProcessorBad-এর কনস্ট্রাক্টর নিজেই MySQLDatabaseConcrete() তৈরি করে ফেলে — তাই একে টেস্ট করতে হলে হয় একটি সত্যিকারের MySQL সংযোগ লাগবে (ধীর, ভঙ্গুর, টেস্ট পরিবেশে অবাস্তব), নয়তো কোনো উপায়ই নেই আলাদা করে যাচাই করার যে OrderProcessorBad-এর নিজস্ব লজিক সঠিক কিনা। OrderProcessor-এ dependency ইনজেক্ট করা যায় — টেস্টে একটি সরল InMemoryFakeDatabase (বা আরও ছোট একটি মক) পাস করে দিলেই, কোনো real database ছাড়াই, দ্রুত ও নির্ভরযোগ্যভাবে যাচাই করা যায় OrderProcessor.process() ঠিকমতো save() ডাকছে কিনা।

প্র ০৩ DIP আর OCP (L16) — দুটোই "existing কোড পরিবর্তন না করে নতুন কিছু যোগ করা" নিয়ে কথা বলে। এই দুটোর সম্পর্ক কী?

DIP আসলে OCP অর্জনের একটি প্র্যাকটিক্যাল টুল। L16-এর OCP বলে entities "extension-এর জন্য open, modification-এর জন্য closed" হওয়া উচিত। DIP এটি সম্ভব করে — OrderProcessor একটি অ্যাবস্ট্রাকশনের উপর নির্ভর করার কারণেই, নতুন একটি PostgresDatabase ক্লাস যোগ করা যায় (extension) কোনো existing OrderProcessor কোড পরিবর্তন (modification) ছাড়াই। DIP ছাড়া, নতুন database সাপোর্ট করতে হলে OrderProcessorBad-এর ভেতরের কোডই পরিবর্তন করতে হতো — সরাসরি OCP লঙ্ঘন।

অনুশীলন

  1. চিন্তা করুন: একটি NotificationService ক্লাস আছে যার কনস্ট্রাক্টরের ভেতরে সরাসরি EmailSender() তৈরি হয়। ভবিষ্যতে SMS ও push notification সাপোর্ট করতে হবে। DIP প্রয়োগ করে এই ক্লাসের ডিজাইন কীভাবে বদলাবেন তা লিখুন।

    একটি অ্যাবস্ট্র্যাক্ট NotificationChannel ইন্টারফেস তৈরি করতে হবে (যেমন একটি send(message) মেথডসহ), তারপর EmailSender, SMSSender, PushSender — প্রতিটি এই ইন্টারফেস ইমপ্লিমেন্ট করবে। NotificationService-এর কনস্ট্রাক্টর একটি channel: NotificationChannel প্যারামিটার নেবে (dependency injection), নিজে কোনো কংক্রিট sender তৈরি করবে না। ফলে নতুন চ্যানেল যোগ করতে NotificationService-এর কোনো কোড পরিবর্তনের দরকার হবে না — ঠিক এই পাঠের OrderProcessor-এর মতোই।

  2. পরীক্ষা করুন: উপরের কোড সেলে একটি তৃতীয় Database বাস্তবায়ন যোগ করুন — class LoggingDatabase(Database), যার save() শুধু একটি প্রিন্ট স্টেটমেন্ট দেয় ("[LOG] would save: ...") — এবং সেটি OrderProcessor-এ ইনজেক্ট করে চালিয়ে দেখুন।

    নতুন LoggingDatabase ক্লাসটি Database-এর save() কনট্র্যাক্ট মেনে চললেই OrderProcessor(LoggingDatabase()) সরাসরি কাজ করবে — OrderProcessor-এর একটি লাইনও পরিবর্তন করতে হবে না। এটিই DIP-এর মূল সুবিধা বাস্তবে দেখা — নতুন বাস্তবায়ন যোগ করা মানে শুধু একটি নতুন ক্লাস লেখা, existing, ইতিমধ্যে-টেস্ট-করা কোড স্পর্শ করা নয়।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

আগের পাঠ
SOLID প্রিন্সিপল — LSP ও ISP