مفهوم Dependency Injection چیست

خلاصه
1404/09/21

Dependency Injection (DI) یا تزریق وابستگی یک الگوی طراحی (Design Pattern) در برنامه نویسی است که هدف آن کاهش وابستگی (Coupling) بین کلاس‌ها و افزایش قابلیت تست‌پذیری، نگهداری

مفهوم Dependency Injection چیست

Dependency Injection (DI) یا تزریق وابستگی یک الگوی طراحی (Design Pattern) در برنامه نویسی است که هدف آن کاهش وابستگی (Coupling) بین کلاس‌ها و افزایش قابلیت تست‌پذیری، نگهداری و استفاده مجدد از کد است.

**درک مفهوم وابستگی (Dependency):**

قبل از اینکه به DI بپردازیم، باید درک کنیم منظور از "وابستگی" در این زمینه چیست. یک کلاس A وقتی به کلاس B وابسته است که:

* کلاس A از کلاس B برای انجام کاری استفاده کند.
* کلاس A برای درست کار کردن، به وجود کلاس B نیاز داشته باشد.
* کلاس A یک شی (Instance) از کلاس B ایجاد کند.

**مشکلات وابستگی بالا:**

ایجاد مستقیم وابستگی‌ها در داخل کلاس‌ها (مانند ایجاد یک شی از کلاس دیگر در سازنده) باعث مشکلات زیر می‌شود:

* **Coupling بالا:** تغییر در کلاس وابسته (B) ممکن است نیازمند تغییر در کلاس استفاده کننده (A) باشد.
* **تست‌پذیری دشوار:** تست کردن کلاس A به طور مجزا دشوار است، زیرا نیاز به فراهم کردن کلاس B (و احتمالاً وابستگی‌های آن) دارد.
* **استفاده مجدد محدود:** کلاس A نمی‌تواند به راحتی با پیاده‌سازی‌های مختلف کلاس B استفاده شود.
* **نگهداری دشوار:** با افزایش پیچیدگی برنامه، مدیریت و نگهداری وابستگی‌ها دشوار می‌شود.

**Dependency Injection چگونه مشکل را حل می‌کند؟**

DI راه حل این مشکلات را با **جداسازی (Decoupling)** فرآیند ایجاد وابستگی‌ها از کلاس استفاده کننده ارائه می‌دهد. به جای اینکه کلاس مسئول ایجاد وابستگی‌های خود باشد، وابستگی‌ها به کلاس **تزریق** می‌شوند.

**به عبارت ساده‌تر:**

به جای اینکه کلاس "بپرسد" چه وابستگی‌هایی نیاز دارد و خودش آن‌ها را ایجاد کند، وابستگی‌ها به کلاس "داده می‌شوند".

**روش‌های تزریق وابستگی:**

سه روش اصلی برای تزریق وابستگی وجود دارد:

1. **Constructor Injection (تزریق از طریق سازنده):** وابستگی‌ها از طریق سازنده کلاس به آن تزریق می‌شوند. این روش رایج‌ترین و توصیه شده‌ترین روش است، زیرا وابستگی‌ها را در زمان ایجاد شی مشخص می‌کند و کلاس را به یک حالت معتبر می‌رساند.

```java
public class EmailService {
private final Logger logger;

public EmailService(Logger logger) {
this.logger = logger;
}

public void sendEmail(String message) {
logger.log("Sending email: " + message);
// ... کد ارسال ایمیل ...
}
}

// استفاده:
Logger myLogger = new MyLogger(); // یک پیاده‌سازی از Logger
EmailService emailService = new EmailService(myLogger);
emailService.sendEmail("Hello!");
```

در این مثال، `EmailService` به یک `Logger` وابسته است. به جای اینکه `EmailService` خودش یک `Logger` ایجاد کند، یک `Logger` از طریق سازنده‌اش به آن تزریق می‌شود.

2. **Setter Injection (تزریق از طریق متد Set):** وابستگی‌ها از طریق متدهای Set یا Properties بعد از ایجاد شی، تزریق می‌شوند.

```java
public class EmailService {
private Logger logger;

public void setLogger(Logger logger) {
this.logger = logger;
}

public void sendEmail(String message) {
logger.log("Sending email: " + message);
// ... کد ارسال ایمیل ...
}
}

// استفاده:
EmailService emailService = new EmailService();
Logger myLogger = new MyLogger();
emailService.setLogger(myLogger);
emailService.sendEmail("Hello!");
```

این روش انعطاف‌پذیرتر است، اما ممکن است کلاس در ابتدا در یک حالت نامعتبر قرار داشته باشد (یعنی `logger` برابر `null` باشد) تا زمانی که متد `setLogger` فراخوانی شود.

3. **Interface Injection (تزریق از طریق Interface):** کلاس یک رابط (Interface) را پیاده‌سازی می‌کند که متدی برای تزریق وابستگی دارد.

```java
public interface LoggerAware {
void setLogger(Logger logger);
}

public class EmailService implements LoggerAware {
private Logger logger;

@Override
public void setLogger(Logger logger) {
this.logger = logger;
}

public void sendEmail(String message) {
logger.log("Sending email: " + message);
// ... کد ارسال ایمیل ...
}
}

// استفاده:
EmailService emailService = new EmailService();
Logger myLogger = new MyLogger();
emailService.setLogger(myLogger);
emailService.sendEmail("Hello!");
```

این روش کمتر رایج است و بیشتر در مواردی استفاده می‌شود که یک کلاس نیاز به پشتیبانی از تزریق چند وابستگی مختلف دارد.

**مزایای Dependency Injection:**

* **Coupling کمتر:** وابستگی بین کلاس‌ها کاهش می‌یابد و تغییرات در یک کلاس کمتر احتمال دارد بر سایر کلاس‌ها تأثیر بگذارد.
* **تست‌پذیری بهتر:** تست کردن کلاس‌ها به طور مجزا آسان‌تر است، زیرا می‌توان وابستگی‌ها را با Mock یا Stub جایگزین کرد.
* **قابلیت استفاده مجدد بیشتر:** کلاس‌ها می‌توانند به راحتی با پیاده‌سازی‌های مختلف وابستگی‌ها استفاده شوند.
* **نگهداری آسان‌تر:** مدیریت و نگهداری وابستگی‌ها آسان‌تر می‌شود، به خصوص در برنامه‌های بزرگ و پیچیده.
* **انعطاف‌پذیری بیشتر:** به راحتی می‌توان وابستگی‌ها را در زمان اجرا تغییر داد.
* **پیروی از اصول SOLID:** DI به پیروی از اصل *Dependency Inversion* در SOLID کمک می‌کند.

**معایب Dependency Injection:**

* **پیچیدگی بیشتر کد:** کد می‌تواند کمی پیچیده‌تر شود، به خصوص در ابتدا.
* **نیاز به Container (اختیاری):** برای مدیریت وابستگی‌ها، ممکن است نیاز به استفاده از یک Dependency Injection Container باشد. (در ادامه توضیح داده خواهد شد)

**Dependency Injection Container (DI Container):**

یک DI Container (یا IoC Container) یک فریم‌ورک یا کتابخانه است که وظیفه مدیریت و تزریق وابستگی‌ها را به عهده دارد. به جای اینکه شما دستی وابستگی‌ها را ایجاد و تزریق کنید، DI Container این کار را به صورت خودکار انجام می‌دهد.

DI Containerها معمولاً از فایل‌های پیکربندی (XML، YAML، JSON و غیره) یا Annotationها برای مشخص کردن وابستگی‌ها استفاده می‌کنند.

**برخی از DI Containerهای معروف:**

* **Java:** Spring Framework, Guice, Dagger
* **.NET:** Autofac, Ninject, Microsoft.Extensions.DependencyInjection
* **PHP:** Symfony Dependency Injection, Laravel Service Container
* **Python:** Injector, Dependency Injector

**مثال استفاده از DI Container (Spring Framework در Java):**

```java
// EmailService.java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class EmailService {
private final Logger logger;

@Autowired
public EmailService(Logger logger) {
this.logger = logger;
}

public void sendEmail(String message) {
logger.log("Sending email: " + message);
// ... کد ارسال ایمیل ...
}
}

// Logger.java
import org.springframework.stereotype.Component;

@Component
public class MyLogger implements Logger {
@Override
public void log(String message) {
System.out.println("Log: " + message);
}
}

// Application.java
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan
public class Application {

public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(Application.class);
EmailService emailService = context.getBean(EmailService.class);
emailService.sendEmail("Hello from Spring!");
}
}
```

در این مثال:

* `@Component` و `@Autowired` Annotationها از Spring Framework استفاده می‌شوند.
* `@Component` به Spring می‌گوید که کلاس `EmailService` و `MyLogger` را به عنوان Bean (شیء مدیریت شده توسط Container) در نظر بگیرد.
* `@Autowired` به Spring می‌گوید که یک `Logger` را به سازنده `EmailService` تزریق کند.
* `ApplicationContext` یک DI Container از Spring است. `getBean(EmailService.class)` یک شیء از `EmailService` را از Container دریافت می‌کند. Spring به صورت خودکار وابستگی‌های `EmailService` را مدیریت می‌کند (در این مورد، `MyLogger`).

**چه زمانی از Dependency Injection استفاده کنیم؟**

* هنگامی که می‌خواهید Coupling بین کلاس‌ها را کاهش دهید.
* هنگامی که می‌خواهید تست‌پذیری کد خود را بهبود بخشید.
* هنگامی که می‌خواهید قابلیت استفاده مجدد از کد خود را افزایش دهید.
* در برنامه‌های بزرگ و پیچیده که مدیریت وابستگی‌ها اهمیت دارد.
* در پروژه‌هایی که از اصول SOLID پیروی می‌کنند.

**خلاصه:**

Dependency Injection یک الگوی طراحی قدرتمند است که به شما کمک می‌کند کد بهتری بنویسید: کد با Coupling کمتر، تست‌پذیری بیشتر، قابلیت استفاده مجدد بیشتر و نگهداری آسان‌تر. در حالی که ممکن است در ابتدا کمی پیچیده به نظر برسد، با درک اصول و استفاده از DI Containerها، می‌توانید از مزایای آن در پروژه‌های خود بهره‌مند شوید.