DIP (Dependency Inversion Principle) — SOLID এর পঞ্চম নীতি যা মডিউলগুলির মধ্যে নির্ভরতা তৈরির নিয়ম নির্ধারণ করে: উচ্চ-স্তরের মডিউল নিম্ন-স্তরের মডিউলের উপর নির্ভর করবে না, উভয়েই অ্যাবস্ট্রাকশনের উপর নির্ভর করবে। অ্যাবস্ট্রাকশন বিস্তারিত বিবরণের উপর নির্ভর করবে না — বিস্তারিত বিবরণ অ্যাবস্ট্রাকশনের উপর নির্ভর করবে। এই নীতিটি, রবার্ট মার্টিনের Clean Architecture (2017)-এ বর্ণিত, শিথিল-যুগ্ম আর্কিটেকচারের ভিত্তি। এই বই অনুসারে, নির্ভরতা বিপর্যাসের নীতি অ্যাপ্লিকেশনের স্তরগুলির মধ্যে কঠিন সংযোগ দূর করে।
মূল বিষয়
DIP (Dependency Inversion Principle) হলো নির্ভরতা বিপর্যাসের নীতি যা মডিউলগুলির মধ্যে নির্ভরতার দিক সম্পর্কে ঐতিহ্যবাহী ধারণাকে উল্টে দেয়। উচ্চ-স্তরের মডিউল (ব্যবসায়িক যুক্তি) সরাসরি নিম্ন-স্তরের মডিউল (ডাটাবেস, নেটওয়ার্ক, UI) এর উপর নির্ভর করবে না। পরিবর্তে, উভয় স্তরই উচ্চ-স্তরের মডিউলে সংজ্ঞায়িত অ্যাবস্ট্রাকশনের উপর নির্ভর করে।
DIP-এর আনুষ্ঠানিক গঠনে দুটি নিয়ম রয়েছে: A — উচ্চ-স্তরের মডিউল নিম্ন-স্তরের মডিউলের উপর নির্ভর করবে না, উভয়েই অ্যাবস্ট্রাকশনের উপর নির্ভর করবে। B — অ্যাবস্ট্রাকশন বিস্তারিত বিবরণের উপর নির্ভর করবে না, বিস্তারিত বিবরণ অ্যাবস্ট্রাকশনের উপর নির্ভর করবে। দ্বিতীয় নিয়মটি প্রথমটি থেকে অনুসৃত: যদি কোনো অ্যাবস্ট্রাকশন বিস্তারিত বিবরণের উপর নির্ভর করে, তবে তা উচ্চ-স্তরের মডিউলের জন্য স্থিতিশীল ভিত্তি হতে পারে না।
DIP ছাড়া, একটি সাধারণ আর্কিটেকচার এরকম দেখায়: BusinessLogic → DatabaseRepository — ব্যবসায়িক যুক্তি সরাসরি একটি কংক্রিট রিপোজিটরির উপর নির্ভর করে। DIP-এর সাথে: BusinessLogic → DatabaseServiceInterface ← DatabaseRepository। BusinessLogic DatabaseRepository-এর অস্তিত্ব জানে না, এটি শুধু DatabaseService ইন্টারফেস জানে, যা ব্যবসায়িক যুক্তির বাইরে বাস্তবায়িত হয়।
বিপর্যাস মানে হলো নিয়ন্ত্রণ প্রবাহ এবং নির্ভরতা প্রবাহ বিপরীত দিকে যায়। নিয়ন্ত্রণ প্রবাহ উপর থেকে নিচে যায়: UI → ViewModel → UseCase → Repository। নির্ভরতা প্রবাহ নিচ থেকে উপরে যায়: Repository, UseCase-এ সংজ্ঞায়িত ইন্টারফেস বাস্তবায়ন করে। Repository (নিম্ন-স্তর) UseCase (উচ্চ-স্তর) এর উপর নির্ভর করে।
এই বিপর্যাস হলো DIP এবং সাধারণ স্তর বিভাজনের মধ্যে মূল পার্থক্য। ঐতিহ্যবাহী স্তরীয় আর্কিটেকচারে, প্রতিটি স্তর নিচের স্তরের উপর নির্ভর করে। DIP-ভিত্তিক আর্কিটেকচারে, সব স্তর অ্যাবস্ট্রাকশনের উপর নির্ভর করে, যখন এই অ্যাবস্ট্রাকশনের বাস্তবায়ন ইনফ্রাস্ট্রাকচার স্তরে থাকে, যা DI প্রক্রিয়ার মাধ্যমে উপরের স্তরের সাথে “সংযুক্ত” হয়।
DIP প্রক্রিয়া উচ্চ-স্তরের মডিউলে অ্যাবস্ট্রাকশন সংজ্ঞায়িত করে এবং নিম্ন-স্তরের মডিউলে সেগুলি বাস্তবায়নের মাধ্যমে কার্যকর করা হয়। উচ্চ-স্তরের মডিউল তার প্রয়োজনীয় কার্যকারিতার জন্য একটি ইন্টারফেস ঘোষণা করে। নিম্ন-স্তরের মডিউল এই ইন্টারফেস বাস্তবায়ন করে। ওয়্যারিং অ্যাপ্লিকেশনের কম্পোজিশন রুটে ঘটে।
বিদ্যমান কোডে DIP প্রবর্তনের প্রক্রিয়া: নিম্ন-স্তরের মডিউলের জন্য একটি ইন্টারফেস নিষ্কাশন করুন, এই ইন্টারফেসটি উচ্চ-স্তরের মডিউলে (বা একটি পৃথক অ্যাবস্ট্রাকশন স্তরে) স্থানান্তর করুন, উচ্চ-স্তরের মডিউলের নির্ভরতা ইন্টারফেস ব্যবহার করার জন্য পুনরায় লিখুন, নিম্ন-স্তরের মডিউলকে এই ইন্টারফেস বাস্তবায়ন করতে দিন। এই ধাপগুলির পরে, নির্ভরতার দিক উল্টে গেছে।
DIP-এর একটি কম্পোজিশন রুট প্রক্রিয়া প্রয়োজন — অ্যাপ্লিকেশনে একটি বিন্দু যেখানে সব নির্ভরতা তৈরি এবং একসাথে যুক্ত করা হয়। Android-এ এটি Application.get() বা Hilt কম্পোনেন্ট, iOS-এ — AppDelegate বা SceneDelegate। কম্পোজিশন রুট হল একমাত্র স্থান যেখানে কোড কংক্রিট বাস্তবায়ন সম্পর্কে জানে।
DIP অ্যাপ্লিকেশন স্তরগুলির মধ্যে আর্কিটেকচারাল সীমানা তৈরি করে। যখন ViewModel, UserRepository ইন্টারফেসের উপর নির্ভর করে, প্রেজেন্টেশন এবং ডোমেন স্তরের মধ্যে একটি সীমানা তৈরি হয়: ViewModel (প্রেজেন্টেশন) জানে না ডেটা কোথা থেকে আসে। এই সীমানা ViewModel-কে প্রভাবিত না করেই UserRepository-এর বাস্তবায়ন (Room → REST → Mock) পরিবর্তন করতে দেয়। যত বেশি এই ধরনের সীমানা, অ্যাপ্লিকেশন ফ্রেমওয়ার্ক এবং লাইব্রেরি পরিবর্তনের প্রতি তত বেশি প্রতিরোধী।
Google-প্রস্তাবিত Android আর্কিটেকচারে, DIP UseCases-এর মাধ্যমে বাস্তবায়িত হয় যা ডোমেন স্তরে থাকে এবং Repository ইন্টারফেসের উপর নির্ভর করে। RepositoryImpl ডেটা স্তরে থাকে এবং এই ইন্টারফেসগুলি বাস্তবায়ন করে। প্রেজেন্টেশন স্তর (ViewModel) UseCases-এর উপর নির্ভর করে। নির্ভরতার দিক প্রেজেন্টেশন থেকে ডোমেন, ডোমেন থেকে ডেটাতে যায় — কিন্তু কোনো স্তরই অন্য স্তরের কংক্রিট বাস্তবায়ন জানে না।
DIP এবং DI প্রায়ই গুলিয়ে ফেলা হয়, কিন্তু এগুলি ভিন্ন ধারণা। DIP একটি আর্কিটেকচারাল নীতি (কী করতে হবে: অ্যাবস্ট্রাকশনের উপর নির্ভর করা)। DI একটি বাস্তবায়ন প্যাটার্ন (কীভাবে করতে হবে: কনস্ট্রাক্টরের মাধ্যমে নির্ভরতা পাঠানো)। DIP প্রশ্নের উত্তর দেয় “মডিউল কিসের উপর ভিত্তি করে হওয়া উচিত?”, DI উত্তর দেয় “অবজেক্ট কীভাবে তার নির্ভরতা পায়?”।
ডিপেন্ডেন্সি ইনজেকশন হলো কনস্ট্রাক্টর, মেথড বা প্রপার্টির মাধ্যমে একটি অবজেক্টে নির্ভরতা ইনজেক্ট করার উপায়। যখন একটি Kotlin ক্লাস তার কনস্ট্রাক্টরের মাধ্যমে Repository ইন্টারফেস গ্রহণ করে — সেটি DI। আর ViewModel ক্লাসটি একটি কংক্রিট RoomRepository বাস্তবায়নের পরিবর্তে Repository ইন্টারফেসের উপর নির্ভর করে — সেটি DIP। DI হলো টুল, DIP হলো লক্ষ্য।
আপনি DI ফ্রেমওয়ার্ক ছাড়াই DIP অনুসরণ করতে পারেন: কম্পোজিশন রুটে নির্ভরতার ম্যানুয়াল ওয়্যারিংও DI (ম্যানুয়াল DI)। আপনি DIP লঙ্ঘন করে DI ফ্রেমওয়ার্ক (Dagger, Hilt, Koin) ব্যবহার করতে পারেন: যদি ViewModel সরাসরি new() এর মাধ্যমে Repository অবজেক্ট তৈরি করে — DIP লঙ্ঘিত হয়েছে, এমনকি ফ্রেমওয়ার্ক ইনস্টল থাকলেও। DIP একটি আর্কিটেকচারাল সিদ্ধান্ত, DI একটি প্রযুক্তিগত বিবরণ।
ডেটা স্তরে DIP প্রয়োগের একটি Android উদাহরণ দেখি। DIP ছাড়া, ViewModel সরাসরি RoomDatabase এবং DAO তৈরি করে। DIP-এর সাথে — ViewModel, UserRepository ইন্টারফেসের উপর নির্ভর করে, এবং কংক্রিট RoomUserRepository বাস্তবায়ন বাইরে থেকে সরবরাহ করা হয়।
// অ্যাবস্ট্রাকশন ডোমেন স্তরের (উচ্চ-স্তর)
interface UserRepository {
fun getUser(id: Int): User
}
// ডোমেন স্তর শুধু অ্যাবস্ট্রাকশনের উপর নির্ভর করে
class GetUserUseCase(
private val repo: UserRepository
) {
fun execute(id: Int): User = repo.getUser(id)
}
// ডেটা স্তরে বাস্তবায়ন ডোমেন স্তরের অ্যাবস্ট্রাকশনের উপর নির্ভর করে
class RoomUserRepository(
private val dao: UserDao
) : UserRepository {
override fun getUser(id: Int): User {
return dao.getById(id)
}
}
// কম্পোজিশন রুট
class AppModule {
fun provideUserRepository(dao: UserDao): UserRepository {
return RoomUserRepository(dao)
}
}
একটি iOS উদাহরণ Application Coordinator এবং নেভিগেশন প্রোটোকল সহ:
// ডোমেন স্তরে নেভিগেশন অ্যাবস্ট্রাকশন
protocol AuthNavigation {
func navigateToHome()
func navigateToLogin()
}
// ViewModel অ্যাবস্ট্রাকশনের উপর নির্ভর করে, UIKit-এর উপর নয়
final class AuthViewModel {
private let navigation: AuthNavigation
init(navigation: AuthNavigation) {
self.navigation = navigation
}
func onLoginSuccess() {
navigation.navigateToHome()
}
}
// Coordinator (UIKit স্তর) ডোমেন স্তরের প্রোটোকল বাস্তবায়ন করে
final class AppCoordinator: AuthNavigation {
func navigateToHome() {
// UIKit নেভিগেশন কোড
}
func navigateToLogin() {
// UIKit নেভিগেশন কোড
}
}
মূল পয়েন্ট: AuthViewModel (ডোমেন) AppCoordinator (UIKit)-এর অস্তিত্ব জানে না। এটি শুধু AuthNavigation প্রোটোকল জানে। যদি আগামীকাল UIKit-কে SwiftUI দিয়ে প্রতিস্থাপন করা হয় — AuthViewModel-এ কোনো পরিবর্তনের প্রয়োজন নেই। DIP ডোমেন স্তরকে UI ফ্রেমওয়ার্ক এবং লাইব্রেরি থেকে স্বাধীন করে।
Hilt Android-এর জন্য মানক DI টুল, Google-প্রস্তাবিত। এটি Jetpack-এ নির্মিত, ViewModel, Fragment, Service এবং অন্যান্য Android কম্পোনেন্ট সমর্থন করে। Hilt @Module, @Provides, @Inject অ্যানোটেশনের মাধ্যমে কম্পোজিশন রুট তৈরি স্বয়ংক্রিয় করে। Hilt ব্যবহার DIP-এর আনুগত্য নিশ্চিত করে না — UserRepository ইন্টারফেস ডোমেন স্তরে সংজ্ঞায়িত হতে হবে, ডেটা স্তরে নয়।
Koin Kotlin-এর জন্য একটি লাইটওয়েট DI ফ্রেমওয়ার্ক যেখানে কোড জেনারেশন বা অ্যানোটেশন প্রসেসিং নেই। Koin-এর DSL (module, single, factory) শেখা সহজ, কিন্তু নির্ভরতা যাচাই কম্পাইল টাইমের পরিবর্তে রানটাইমে ঘটে। Koin iOS সমর্থনের কারণে মাল্টিপ্ল্যাটফর্ম প্রকল্পে (KMP) জনপ্রিয়।
Dagger 2 Hilt-এর পূর্বসূরি, এখনও বড় প্রকল্পে ব্যবহৃত হয়। Dagger কম্পাইল টাইমে DI কোড জেনারেট করে, যা বিল্ড টাইমে সর্বোচ্চ কর্মক্ষমতা এবং ত্রুটি নির্ণয় প্রদান করে। Hilt Dagger-এর উপরে নির্মিত এবং একটি সরলীকৃত API প্রদান করে। নতুন প্রকল্পের জন্য, Google Hilt-কে প্রাথমিক DI ফ্রেমওয়ার্ক হিসাবে সুপারিশ করে।
DI মডিউল আর্কিটেকচারাল স্তরগুলির সাথে সঙ্গতিপূর্ণ হওয়া উচিত এবং DomainModule, DataModule, PresentationModule-এ বিভক্ত হওয়া উচিত। DomainModule শুধু অ্যাবস্ট্রাকশন এবং UseCases সরবরাহ করে। DataModule অ্যাবস্ট্রাকশনের জন্য বাস্তবায়ন সরবরাহ করে। PresentationModule ViewModels-কে UseCases-এর সাথে সংযুক্ত করে। এই সংগঠন নিশ্চিত করে যে ডোমেন স্তর ইনফ্রাস্ট্রাকচার লাইব্রেরি থেকে স্বাধীন থাকে।
DI ফ্রেমওয়ার্কের মধ্যে মাইগ্রেট করার সময় (যেমন, Koin থেকে Hilt-এ), DomainModule-এর কাঠামো পরিবর্তন হয় না — শুধু DataModule এবং PresentationModule-এ ওয়্যারিং পদ্ধতি পরিবর্তন হয়। DIP ডোমেন যুক্তির বিচ্ছিন্নতা নিশ্চিত করে, যখন DI ফ্রেমওয়ার্ক একটি প্রযুক্তিগত ওয়্যারিং প্রক্রিয়া।
প্রায়শই জিজ্ঞাসিত প্রশ্ন
DIP আর্কিটেকচারাল সীমানায় প্রয়োজন — অ্যাপ্লিকেশন স্তরের মধ্যে (ডোমেন → ডেটা, প্রেজেন্টেশন → ডোমেন)। একই স্তরের ভিতরে, DIP অতিরিক্ত হতে পারে। উদাহরণস্বরূপ, ডোমেন স্তরের ভিতরে একটি ইউটিলিটি ক্লাস StringFormatter-এর ইন্টারফেস প্রয়োজন হয় না — যদি এটি প্রতিস্থাপনের কোনো কারণ না থাকে।
না। DIP একটি নীতি: মডিউল অ্যাবস্ট্রাকশনের উপর নির্ভর করবে। DI একটি প্যাটার্ন: একটি অবজেক্ট বাইরে থেকে নির্ভরতা গ্রহণ করে, নিজে তৈরি করার পরিবর্তে। DI হলো DIP বাস্তবায়নের একটি উপায়, কিন্তু DIP DI ছাড়াও অনুসরণ করা যেতে পারে (ফ্যাক্টরি বা সার্ভিস লোকেটরের মাধ্যমে)। DI ছাড়া DIP সম্ভব কিন্তু এর কোনো আর্কিটেকচারাল মূল্য নেই।
ইন্টারফেস সেই মডিউলের হয় যা সেগুলি ব্যবহার করে, যে মডিউল সেগুলি বাস্তবায়ন করে তার নয়। UserRepository ডোমেন স্তরে ঘোষিত হয় এবং ডেটা স্তরে বাস্তবায়িত হয়। এটি DIP-এর মূল নিয়ম: অ্যাবস্ট্রাকশনের মালিক ভোক্তা, বাস্তবায়নের সরবরাহকারী নয়।
DIP বিচ্ছিন্ন স্তরে টেস্টিং সম্ভব করে। একটি ViewModel যা UserRepository (ইন্টারফেস) এর উপর নির্ভর করে, ডাটাবেস ছাড়াই মক বাস্তবায়নের সাথে পরীক্ষা করা যেতে পারে। DIP ছাড়া, ViewModel RoomUserRepository-এর উপর নির্ভর করবে এবং প্রতিটি পরীক্ষার জন্য ডাটাবেস সেটআপ প্রয়োজন হবে। DIP + DI টেস্টিংয়ের সময় সম্পূর্ণ মডিউল বিচ্ছিন্নতা প্রদান করে।
Hilt Android প্রকল্পের জন্য মানক পছন্দ, Google-প্রস্তাবিত। Koin Kotlin Multiplatform প্রকল্পের জন্য একটি বিকল্প। Dagger 2 বিদ্যমান প্রকল্পের জন্য যেখানে Hilt-এ মাইগ্রেশন যুক্তিসঙ্গত নয়। ফ্রেমওয়ার্ক নির্বাচন আর্কিটেকচার স্তরে DIP অনুসরণের প্রয়োজনীয়তা দূর করে না।
সারসংক্ষেপ
আমরা একটি মোবাইল অ্যাপ্লিকেশন টার্নকি তৈরি করব
IT Sectr 2017 সাল থেকে স্টার্টআপ এবং ব্যবসার জন্য iOS এবং Android অ্যাপ্লিকেশন তৈরি করে। আমরা আপনাকে পরামর্শ দেব এবং সেরা সমাধান প্রস্তাব করব।
আরও পড়ুন