← بازگشت

سوالات مصاحبه Dependency Injection در .NET

مقدمه

Dependency Injection (DI) یا تزریق وابستگی، یک الگوی طراحی حیاتی در توسعه نرم‌افزار مدرن، به ویژه در اکوسیستم .NET است. این الگو به شما کمک می‌کند تا کدی با قابلیت تست‌پذیری بالا، نگهداری آسان‌تر و انعطاف‌پذیری بیشتر بنویسید. درک عمیق DI برای هر توسعه‌دهنده .NET ضروری است.

سوال 1: Dependency Injection (DI) چیست و چرا از آن استفاده می‌کنیم؟

پاسخ: Dependency Injection (DI) یک الگوی طراحی است که به شما امکان می‌دهد وابستگی‌های یک شیء را به جای اینکه شیء خودش آن‌ها را ایجاد کند، از خارج به آن “تزریق” کنید. این الگو یکی از پیاده‌سازی‌های اصلی اصل Inversion of Control (IoC) است.

وابستگی (Dependency): یک وابستگی، شیئی است که یک شیء دیگر برای انجام کار خود به آن نیاز دارد. به عنوان مثال، یک کلاس OrderService ممکن است برای ذخیره سفارشات به یک IOrderRepository نیاز داشته باشد.

بدون DI:

public class OrderService
{
    private OrderRepository _orderRepository;

    public OrderService()
    {
        // OrderService خودش وابستگی خود را ایجاد می‌کند
        _orderRepository = new OrderRepository(); 
    }

    public void PlaceOrder(Order order)
    {
        _orderRepository.Save(order);
    }
}

در این مثال، OrderService به OrderRepository وابسته است و خودش آن را ایجاد می‌کند. این باعث می‌شود:

با DI:

public class OrderService
{
    private IOrderRepository _orderRepository;

    // وابستگی از طریق Constructor تزریق می‌شود
    public OrderService(IOrderRepository orderRepository)
    {
        _orderRepository = orderRepository;
    }

    public void PlaceOrder(Order order)
    {
        _orderRepository.Save(order);
    }
}

// نحوه استفاده
// در یک برنامه واقعی، این کار توسط یک IoC Container انجام می‌شود
// var repository = new OrderRepository(); 
// var service = new OrderService(repository);

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

چرا از DI استفاده می‌کنیم؟

  1. افزایش تست‌پذیری (Testability): با تزریق وابستگی‌ها، می‌توانید به راحتی Mock یا Stub برای وابستگی‌ها ایجاد کنید و کلاس‌ها را به صورت ایزوله تست کنید.
  2. کاهش کوپلینگ (Loose Coupling): کلاس‌ها به جای وابندگی به پیاده‌سازی‌های خاص، به انتزاع‌ها (Interfaces) وابسته می‌شوند، که باعث کاهش وابستگی بین اجزا می‌شود.
  3. افزایش قابلیت نگهداری (Maintainability): تغییر در یک وابستگی کمتر احتمال دارد که بر سایر بخش‌های کد تأثیر بگذارد.
  4. افزایش قابلیت استفاده مجدد (Reusability): اجزا کمتر به context خاصی وابسته هستند و می‌توانند در سناریوهای مختلف استفاده شوند.
  5. مدیریت آسان‌تر وابستگی‌ها: با استفاده از یک IoC Container، مدیریت چرخه حیات (Lifetime) و ایجاد وابستگی‌ها به صورت خودکار انجام می‌شود.

نتیجه‌گیری: DI یک الگوی قدرتمند است که به شما کمک می‌کند تا کدی تمیزتر، قابل تست‌تر و انعطاف‌پذیرتر بنویسید، که برای توسعه نرم‌افزار مدرن ضروری است.

سوال 2: انواع مختلف Dependency Injection (Constructor Injection, Setter Injection, Interface Injection) را توضیح دهید.

پاسخ: سه نوع اصلی تزریق وابستگی وجود دارد:

  1. Constructor Injection (تزریق از طریق سازنده):
    • توضیح: وابستگی‌ها از طریق پارامترهای سازنده کلاس ارائه می‌شوند. این رایج‌ترین و بهترین روش تزریق وابستگی است.
    • مزایا:
      • وابستگی‌های ضروری: تضمین می‌کند که کلاس همیشه در یک وضعیت معتبر قرار دارد، زیرا تمام وابستگی‌های ضروری در زمان ساخت شیء فراهم می‌شوند.
      • تغییرناپذیری (Immutability): وابستگی‌ها می‌توانند به صورت readonly تعریف شوند، که تغییرناپذیری را تضمین می‌کند.
      • تست‌پذیری آسان: تست کردن کلاس آسان است، زیرا می‌توانید به راحتی Mock یا Stub برای وابستگی‌ها در سازنده ارائه دهید.
    • معایب:
      • اگر یک کلاس وابستگی‌های زیادی داشته باشد، سازنده می‌تواند بسیار طولانی شود (Constructor Over-injection)، که نشانه‌ای از نقض اصل Single Responsibility Principle (SRP) است.
    • مثال:
public class ProductService
{
    private readonly IProductRepository _productRepository;

    public ProductService(IProductRepository productRepository)
    {
        _productRepository = productRepository;
    }

    public Product GetProductById(int id)
    {
        return _productRepository.GetById(id);
    }
}
  1. Setter Injection (تزریق از طریق Setter):
    • توضیح: وابستگی‌ها از طریق متدهای public (معمولاً Property Setter) پس از ایجاد شیء تزریق می‌شوند.
    • مزایا:
      • اختیاری بودن وابستگی‌ها: برای وابستگی‌های اختیاری که همیشه مورد نیاز نیستند، مناسب است.
      • انعطاف‌پذیری: می‌توانید وابستگی‌ها را در طول عمر شیء تغییر دهید.
    • معایب:
      • عدم تضمین وضعیت معتبر: کلاس ممکن است بدون تزریق تمام وابستگی‌های لازم، در یک وضعیت نامعتبر قرار گیرد.
      • تغییرپذیری (Mutability): وابستگی‌ها می‌توانند در هر زمانی تغییر کنند، که مدیریت وضعیت شیء را دشوارتر می‌کند.
    • مثال:
public class ReportGenerator
{
    private ILogger _logger;

    public ILogger Logger
    {
        set { _logger = value; }
    }

    public void GenerateReport()
    {
        _logger?.Log("Generating report...");
        // ... logic to generate report
    }
}
  1. Interface Injection (تزریق از طریق رابط):
    • توضیح: یک کلاس رابطی را پیاده‌سازی می‌کند که شامل یک متد برای تزریق وابستگی است. این متد توسط تزریق‌کننده فراخوانی می‌شود.
    • مزایا:
      • کاهش کوپلینگ: کلاس فقط به رابطی که پیاده‌سازی می‌کند وابسته است.
    • معایب:
      • پیچیدگی بیشتر: نیاز به ایجاد رابط‌های اضافی و پیاده‌سازی آن‌ها در کلاس‌ها دارد.
      • کمتر رایج: در .NET کمتر از Constructor Injection استفاده می‌شود.
    • مثال:
public interface IInjectLogger
{
    void InjectLogger(ILogger logger);
}

public class DataProcessor : IInjectLogger
{
    private ILogger _logger;

    public void InjectLogger(ILogger logger)
    {
        _logger = logger;
    }

    public void ProcessData()
    {
        _logger?.Log("Processing data...");
        // ... logic to process data
    }
}

انتخاب نوع تزریق: Constructor Injection معمولاً بهترین گزینه است، زیرا وابستگی‌های ضروری را تضمین می‌کند و تست‌پذیری را بهبود می‌بخشد. Setter Injection برای وابستگی‌های اختیاری مناسب است، در حالی که Interface Injection کمتر رایج است و معمولاً فقط در سناریوهای خاصی استفاده می‌شود.

سوال 3: یک کلاس ساده در زبان مورد علاقه خود بنویسید که Constructor Injection را نشان دهد.

// تعریف یک رابط برای وابستگی
public interface IMessageSender
{
    void SendMessage(string message);
}

// پیاده‌سازی وابستگی
public class EmailSender : IMessageSender
{
    public void SendMessage(string message)
    {
        Console.WriteLine($"Sending email: {message}");
    }
}

// کلاسی که از Constructor Injection استفاده می‌کند
public class NotificationService
{
    private readonly IMessageSender _messageSender;

    // وابستگی IMessageSender از طریق سازنده تزریق می‌شود
    public NotificationService(IMessageSender messageSender)
    {
        _messageSender = messageSender;
    }

    public void SendNotification(string notificationText)
    {
        _messageSender.SendMessage($"Notification: {notificationText}");
    }
}

// نحوه استفاده (معمولاً توسط یک IoC Container مدیریت می‌شود)
// var emailSender = new EmailSender();
// var notificationService = new NotificationService(emailSender);
// notificationService.SendNotification("Your order has been shipped!");

در این مثال، NotificationService به IMessageSender وابسته است. به جای اینکه NotificationService خودش یک IMessageSender ایجاد کند، آن را از طریق سازنده دریافت می‌کند. این تضمین می‌کند که NotificationService همیشه یک IMessageSender معتبر برای ارسال پیام‌ها دارد و می‌توانیم به راحتی پیاده‌سازی‌های مختلف IMessageSender (مانند SmsSender یا PushNotificationSender) را بدون تغییر NotificationService جایگزین کنیم.

سوال 4: کلاسی بنویسید که از Setter Injection استفاده کند. تفاوت آن را با Constructor Injection توضیح دهید.

// تعریف یک رابط برای وابستگی
public interface ILogger
{
    void Log(string message);
}

// پیاده‌سازی وابستگی
public class ConsoleLogger : ILogger
{
    public void Log(string message)
    {
        Console.WriteLine($"[LOG]: {message}");
    }
}

// کلاسی که از Setter Injection استفاده می‌کند
public class DataProcessor
{
    private ILogger _logger;

    // Property Setter برای تزریق ILogger
    public ILogger Logger
    {
        set { _logger = value; }
    }

    public void ProcessData(string data)
    {
        _logger?.Log($"Processing data: {data}");
        // ... logic to process data
    }
}

// نحوه استفاده
// var processor = new DataProcessor();
// processor.Logger = new ConsoleLogger(); // تزریق وابستگی از طریق Setter
// processor.ProcessData("Some important data");

// اگر Logger تزریق نشود، _logger null خواهد بود و Log فراخوانی نمی‌شود.
// var anotherProcessor = new DataProcessor();
// anotherProcessor.ProcessData("Data without logging");

تفاوت با Constructor Injection:

ویژگی Constructor Injection Setter Injection
زمان تزریق در زمان ساخت شیء (از طریق سازنده) پس از ساخت شیء (از طریق Property Setter)
وابستگی‌های ضروری برای وابستگی‌های ضروری که کلاس بدون آن‌ها کار نمی‌کند، مناسب است. برای وابستگی‌های اختیاری که همیشه مورد نیاز نیستند، مناسب است.
وضعیت معتبر تضمین می‌کند که شیء همیشه در یک وضعیت معتبر است. تضمین نمی‌کند که شیء همیشه در یک وضعیت معتبر است (ممکن است وابستگی تزریق نشود).
تغییرناپذیری وابستگی‌ها می‌توانند readonly باشند و تغییرناپذیری را تضمین می‌کنند. وابستگی‌ها می‌توانند در طول عمر شیء تغییر کنند.
تست‌پذیری بسیار آسان، زیرا وابستگی‌ها در سازنده Mock می‌شوند. آسان، اما نیاز به تنظیم وابستگی پس از ساخت شیء دارد.
پیچیدگی ساده‌تر و رایج‌تر کمی پیچیده‌تر، کمتر رایج و معمولاً برای موارد خاص.

نتیجه‌گیری: Constructor Injection برای وابستگی‌های حیاتی که کلاس بدون آن‌ها نمی‌تواند عملکرد صحیح داشته باشد، ارجحیت دارد. Setter Injection برای وابستگی‌های اختیاری که ممکن است در زمان‌های مختلفی در طول عمر شیء تنظیم شوند، مفید است.

سوال 5: مزایای استفاده از Dependency Injection در توسعه نرم‌افزار چیست؟

پاسخ: مزایای اصلی استفاده از Dependency Injection عبارتند از:

  1. افزایش تست‌پذیری (Improved Testability):
    • DI امکان تست واحد (Unit Testing) را به شدت تسهیل می‌کند. با تزریق وابستگی‌ها، می‌توانید به راحتی Mock یا Stub برای وابستگی‌های یک کلاس ایجاد کنید. این به شما اجازه می‌دهد تا کلاس مورد نظر را به صورت ایزوله تست کنید، بدون اینکه نگران رفتار وابستگی‌های واقعی باشید. این امر باعث می‌شود تست‌ها سریع‌تر، قابل اعتمادتر و مستقل‌تر باشند.
    • مثال: برای تست یک UserService که به IUserRepository وابسته است، می‌توانید یک MockUserRepository تزریق کنید که داده‌های ساختگی را برمی‌گرداند، به جای اینکه به یک دیتابیس واقعی متصل شوید.
  2. کاهش کوپلینگ (Reduced Coupling):
    • DI باعث می‌شود کلاس‌ها به جای وابستگی به پیاده‌سازی‌های خاص (Concrete Implementations)، به انتزاع‌ها (Interfaces یا Abstract Classes) وابسته شوند. این امر وابستگی بین اجزا را کاهش می‌دهد و آن‌ها را مستقل‌تر می‌کند. کوپلینگ پایین‌تر به معنای این است که تغییر در یک جزء کمتر احتمال دارد که بر سایر اجزا تأثیر بگذارد.
    • مثال: OrderService به IOrderRepository وابسته است، نه به SqlOrderRepository یا MongoOrderRepository. این بدان معناست که می‌توانید نوع دیتابیس را بدون تغییر OrderService تغییر دهید.
  3. افزایش قابلیت نگهداری (Enhanced Maintainability):
    • با کاهش کوپلینگ، تغییرات در کد آسان‌تر می‌شوند. اگر نیاز به تغییر پیاده‌سازی یک وابستگی باشد، می‌توانید این کار را بدون نیاز به تغییر کلاسی که از آن استفاده می‌کند، انجام دهید. این امر فرآیند رفع اشکال و افزودن ویژگی‌های جدید را ساده‌تر می‌کند.
  4. افزایش قابلیت استفاده مجدد (Increased Reusability):
    • اجزایی که به صورت loosely coupled طراحی شده‌اند، کمتر به context خاصی وابسته هستند و می‌توانند در سناریوهای مختلف و در بخش‌های مختلف یک برنامه یا حتی در برنامه‌های دیگر مورد استفاده مجدد قرار گیرند.
    • مثال: یک ILogger می‌تواند در هر کلاسی که نیاز به لاگ کردن دارد، بدون توجه به اینکه آن کلاس چه کاری انجام می‌دهد، استفاده شود.
  5. مدیریت آسان‌تر وابستگی‌ها و چرخه حیات (Easier Dependency and Lifetime Management):
    • با استفاده از یک IoC Container (مانند Microsoft.Extensions.DependencyInjection در .NET Core)، فرآیند ایجاد و مدیریت وابستگی‌ها و همچنین مدیریت چرخه حیات (Lifetime) و ایجاد وابستگی‌ها به صورت خودکار و متمرکز انجام می‌شود. این امر پیچیدگی مدیریت وابستگی‌ها را به شدت کاهش می‌دهد.
  6. پشتیبانی از توسعه موازی (Supports Parallel Development):
    • تیم‌های مختلف می‌توانند به صورت موازی روی اجزای مختلف کار کنند، زیرا هر جزء به انتزاع‌ها وابسته است و نه به پیاده‌سازی‌های کامل شده سایر تیم‌ها.

به طور خلاصه، DI به توسعه‌دهندگان کمک می‌کند تا کدی تمیزتر، ماژولارتر، قابل تست‌تر و انعطاف‌پذیرتر بنویسند که برای پروژه‌های بزرگ و پیچیده بسیار حیاتی است.

سوال 6: آیا می‌توانید مثالی از موقعیتی ارائه دهید که Dependency Injection ممکن است بهترین انتخاب نباشد؟

پاسخ: در حالی که DI مزایای زیادی دارد، اما در برخی سناریوها ممکن است بهترین انتخاب نباشد یا پیچیدگی غیرضروری ایجاد کند:

  1. کلاس‌های ساده و بدون وابستگی (Simple Classes with No Dependencies):
    • برای کلاس‌های بسیار ساده که هیچ وابستگی خارجی ندارند یا وابستگی‌های آن‌ها بسیار ابتدایی و ثابت هستند (مانند کلاس‌های Utility یا Value Objects)، استفاده از DI می‌تواند بیش از حد باشد. در این موارد، ایجاد مستقیم شیء (new MySimpleClass()) کاملاً قابل قبول است.
  2. وابستگی‌های داخلی و خصوصی (Internal/Private Dependencies):
    • اگر یک کلاس دارای وابستگی‌هایی است که کاملاً داخلی و خصوصی به آن کلاس هستند و هرگز قرار نیست تغییر کنند یا Mock شوند، ممکن است DI ضروری نباشد. با این حال، این سناریو کمتر رایج است و معمولاً نشانه‌ای از طراحی نامناسب است.
  3. برنامه‌های کوچک و یکپارچه (Small, Monolithic Applications):
    • در برنامه‌های بسیار کوچک و یکپارچه که پیچیدگی کمی دارند و نیاز به تست‌پذیری یا انعطاف‌پذیری بالا ندارند، سربار اضافه کردن یک IoC Container و پیاده‌سازی DI ممکن است بیشتر از مزایای آن باشد. با این حال، حتی در این موارد هم، رعایت اصول DI (حتی بدون Container) می‌تواند به بهبود کیفیت کد کمک کند.
  4. پروژه‌های قدیمی (Legacy Projects):
    • اعمال DI در یک پروژه قدیمی و بزرگ که از ابتدا با این الگو طراحی نشده است، می‌تواند بسیار چالش‌برانگیز و پرهزینه باشد. در این موارد، ممکن است نیاز به بازسازی گسترده کد باشد که همیشه توجیه اقتصادی ندارد.
  5. عملکرد بحرانی (Performance-Critical Scenarios):
    • در سناریوهای بسیار حساس به عملکرد که هر میلی‌ثانیه اهمیت دارد، سربار جزئی ایجاد شده توسط DI Container (به دلیل Reflection و مدیریت Lifetime) ممکن است نگران‌کننده باشد. با این حال، این موارد بسیار نادر هستند و در اکثر برنامه‌ها، مزایای DI به مراتب بیشتر از سربار عملکردی آن است.

نتیجه‌گیری: DI یک ابزار قدرتمند است، اما مانند هر ابزار دیگری، باید با درایت و در جای مناسب خود استفاده شود. در اکثر برنامه‌های مدرن، مزایای آن به مراتب بیشتر از معایب آن است.

سوال 7: Inversion of Control (IoC) چیست و چه ارتباطی با Dependency Injection دارد؟

پاسخ: Inversion of Control (IoC) یک اصل طراحی نرم‌افزار است که در آن جریان کنترل یک برنامه معکوس می‌شود. به جای اینکه یک شیء خودش وابستگی‌هایش را ایجاد یا جستجو کند، این وابستگی‌ها توسط یک فریم‌ورک یا Container به آن ارائه می‌شوند.

توضیح IoC:

ارتباط با Dependency Injection:

مثال:

// بدون IoC/DI
public class ReportService
{
    private DatabaseReporter _reporter = new DatabaseReporter(); // کنترل ایجاد وابستگی در دست ReportService

    public void GenerateDailyReport()
    {
        _reporter.GenerateReport();
    }
}

// با IoC/DI
public class ReportService
{
    private IReporter _reporter; // وابستگی به انتزاع

    public ReportService(IReporter reporter) // کنترل ایجاد وابستگی معکوس شده و از خارج تزریق می‌شود
    {
        _reporter = reporter;
    }

    public void GenerateDailyReport()
    {
        _reporter.GenerateReport();
    }
}

در مثال دوم، ReportService دیگر مسئول ایجاد IReporter نیست. این مسئولیت به یک نهاد خارجی (که IoC Container آن را مدیریت می‌کند) واگذار شده است. این معکوس شدن کنترل، همان Inversion of Control است که از طریق Dependency Injection پیاده‌سازی شده است.

نتیجه‌گیری: IoC یک اصل است و DI یک الگو برای پیاده‌سازی آن اصل. DI Container ابزاری است که این پیاده‌سازی را خودکار و مدیریت می‌کند.

سوال 8: IoC Container (یا DI Container) چیست و چه نقشی در Dependency Injection دارد؟

پاسخ: IoC Container (که اغلب به آن DI Container نیز گفته می‌شود) یک فریم‌ورک نرم‌افزاری است که مسئول مدیریت وابستگی‌ها و چرخه حیات اشیاء در یک برنامه است. این Container اصل Inversion of Control (IoC) را پیاده‌سازی می‌کند و فرآیند Dependency Injection را خودکار می‌سازد.

نقش IoC Container:

  1. ثبت (Registration):
    • شما به Container می‌گویید که وقتی به یک انتزاع (مانند interface) نیاز است، کدام پیاده‌سازی (concrete class) را باید ارائه دهد. این کار معمولاً در زمان راه‌اندازی برنامه انجام می‌شود.
    • مثال: Container.Register<ILogger, ConsoleLogger>() به Container می‌گوید که هر زمان ILogger درخواست شد، یک نمونه از ConsoleLogger را برگرداند.
  2. حل (Resolution):
    • وقتی یک کلاس به وابستگی نیاز دارد، Container مسئول ایجاد نمونه‌های مناسب از آن وابستگی و تزریق آن‌ها به کلاس است. این فرآیند به صورت بازگشتی انجام می‌شود؛ اگر یک وابستگی خودش وابستگی‌های دیگری داشته باشد، Container ابتدا آن‌ها را حل می‌کند.
    • مثال: وقتی Container یک ProductService را ایجاد می‌کند که به IProductRepository نیاز دارد، Container ابتدا IProductRepository را حل کرده و سپس آن را به سازنده ProductService تزریق می‌کند.
  3. مدیریت چرخه حیات (Lifetime Management):
    • Container مسئول مدیریت چرخه حیات اشیاء است. شما می‌توانید تعیین کنید که یک وابستگی به صورت Singleton (یک نمونه برای کل برنامه)، Scoped (یک نمونه برای هر درخواست/اسکوپ) یا Transient (یک نمونه جدید در هر بار درخواست) ایجاد شود.
    • مثال: Container.RegisterSingleton<IDatabase, SqlDatabase>()

مزایای استفاده از IoC Container:

معروف‌ترین IoC Containerها در .NET:

نتیجه‌گیری: IoC Container ابزاری قدرتمند است که فرآیند Dependency Injection را خودکار و مدیریت می‌کند و به توسعه‌دهندگان کمک می‌کند تا برنامه‌هایی با طراحی بهتر و قابل نگهداری‌تر بسازند.

سوال 9: Service Locator چیست و چه تفاوتی با Dependency Injection دارد؟

پاسخ: Service Locator یک الگوی طراحی است که در آن یک شیء مرکزی (Service Locator) مسئول پیدا کردن و ارائه سرویس‌ها (وابستگی‌ها) به کلاسی است که به آن‌ها نیاز دارد. این الگو نیز به نوعی Inversion of Control را پیاده‌سازی می‌کند، اما با Dependency Injection تفاوت‌های کلیدی دارد.

Service Locator:

مثال Service Locator:

public class MyServiceLocator
{
    private static readonly Dictionary<Type, object> _services = new Dictionary<Type, object>();

    public static void RegisterService<TInterface, TImplementation>()
        where TImplementation : TInterface, new()
    {
        _services[typeof(TInterface)] = new TImplementation();
    }

    public static TInterface GetService<TInterface>()
    {
        return (TInterface)_services[typeof(TInterface)];
    }
}

public class OrderProcessor
{
    public void ProcessOrder(Order order)
    {
        // کلاس خودش وابستگی را از Service Locator درخواست می‌کند
        ILogger logger = MyServiceLocator.GetService<ILogger>(); 
        logger.Log("Order processed.");
        // ...
    }
}

تفاوت‌های کلیدی با Dependency Injection:

ویژگی Dependency Injection (DI) Service Locator
نحوه دریافت وابستگی وابستگی‌ها به کلاس “تزریق” می‌شوند (معمولاً از طریق سازنده). کلاس خودش وابستگی‌ها را از Service Locator “درخواست” می‌کند.
وابستگی به Container کلاس به Container وابسته نیست؛ فقط به انتزاع‌ها وابسته است. کلاس به Service Locator (که یک Container است) وابسته است.
تست‌پذیری بسیار آسان، زیرا وابستگی‌ها به راحتی Mock می‌شوند. دشوارتر، زیرا کلاس به Service Locator وابسته است و Mock کردن آن پیچیده‌تر است.
شفافیت وابستگی وابستگی‌ها در امضای سازنده یا متدها مشخص هستند (وابستگی‌های آشکار). وابستگی‌ها پنهان هستند و باید کد را بررسی کنید تا آن‌ها را پیدا کنید (وابستگی‌های پنهان).
نقض SRP کمتر احتمال دارد SRP را نقض کند. ممکن است SRP را نقض کند، زیرا کلاس مسئول پیدا کردن وابستگی‌های خود نیز می‌شود.

چرا DI معمولاً ارجحیت دارد؟

نتیجه‌گیری: هرچند Service Locator می‌تواند IoC را پیاده‌سازی کند، اما Dependency Injection به دلیل مزایای بیشتر در Loose Coupling، تست‌پذیری و شفافیت، الگوی ارجح در توسعه نرم‌افزار مدرن است.

سوال 10: Lifetime (چرخه حیات) سرویس‌ها در DI Container چیست؟ انواع آن را توضیح دهید.

پاسخ: Lifetime (چرخه حیات) یک سرویس در DI Container تعیین می‌کند که چه زمانی و چگونه نمونه‌ای از آن سرویس ایجاد و مدیریت شود. این مفهوم برای بهینه‌سازی منابع و اطمینان از رفتار صحیح برنامه بسیار مهم است. سه نوع اصلی Lifetime وجود دارد:

  1. Transient (گذرا):
    • توضیح: هر بار که سرویس درخواست می‌شود، یک نمونه جدید از آن ایجاد می‌شود.
    • کاربرد: برای سرویس‌های سبک، بدون حالت (stateless) یا سرویس‌هایی که نیاز به داده‌های خاص هر عملیات دارند.
    • مثال: ILogger (اگرچه اغلب به صورت Singleton یا Scoped ثبت می‌شود)، EmailSender.
    • ثبت در .NET Core DI: services.AddTransient<IMyService, MyService>();
  2. Scoped (محدود به اسکوپ):
    • توضیح: یک نمونه از سرویس برای هر اسکوپ (محدوده) ایجاد می‌شود. در برنامه‌های وب، یک اسکوپ معمولاً معادل یک درخواست HTTP است. در طول یک درخواست HTTP، هر بار که سرویس درخواست شود، همان نمونه قبلی برگردانده می‌شود. با پایان یافتن درخواست، نمونه نیز از بین می‌رود.
    • کاربرد: برای سرویس‌هایی که نیاز به حفظ حالت در طول یک درخواست خاص دارند، مانند DbContext در Entity Framework Core.
    • مثال: DbContext، UnitOfWork.
    • ثبت در .NET Core DI: services.AddScoped<IMyService, MyService>();
  3. Singleton (تک نمونه):
    • توضیح: تنها یک نمونه از سرویس در طول عمر کل برنامه ایجاد می‌شود و هر بار که سرویس درخواست شود، همان نمونه برگردانده می‌شود.
    • کاربرد: برای سرویس‌های بدون حالت، سرویس‌هایی که منابع سنگین را مدیریت می‌کنند (مانند Cache) یا سرویس‌هایی که نیاز به اشتراک‌گذاری یک نمونه در سراسر برنامه دارند.
    • مثال: ConfigurationService، CachingService.
    • ثبت در .NET Core DI: services.AddSingleton<IMyService, MyService>();

جدول مقایسه Lifetimeها:

Lifetime توضیح زمان ایجاد نمونه تعداد نمونه کاربرد
Transient هر بار یک نمونه جدید هر بار درخواست نامحدود سرویس‌های سبک، بدون حالت
Scoped یک نمونه برای هر اسکوپ یک بار در هر اسکوپ (مثلاً درخواست HTTP) یک نمونه در هر اسکوپ سرویس‌های با حالت در طول یک درخواست
Singleton یک نمونه برای کل برنامه اولین بار درخواست یا در زمان راه‌اندازی یک نمونه سرویس‌های بدون حالت، مدیریت منابع سنگین

نکته مهم: مراقب Captive Dependency باشید. این اتفاق زمانی می‌افتد که یک سرویس با Lifetime کوتاه‌تر (مثلاً Scoped) به یک سرویس با Lifetime طولانی‌تر (مثلاً Singleton) تزریق شود. این می‌تواند منجر به مشکلات پنهان در مدیریت حالت و منابع شود.

سوال 11: AutoFac چیست و چه تفاوتی با Microsoft.Extensions.DependencyInjection دارد؟

پاسخ: AutoFac و Microsoft.Extensions.DependencyInjection هر دو IoC Container (یا DI Container) هستند که برای مدیریت وابستگی‌ها در برنامه‌های .NET استفاده می‌شوند. با این حال، تفاوت‌هایی در قابلیت‌ها، فلسفه طراحی و نحوه استفاده دارند.

Microsoft.Extensions.DependencyInjection:

AutoFac:

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

نتیجه‌گیری: انتخاب بین این دو به نیازهای خاص پروژه شما بستگی دارد. برای سادگی و یکپارچگی، Microsoft.Extensions.DependencyInjection عالی است. برای قابلیت‌های پیشرفته و کنترل بیشتر، AutoFac گزینه بهتری است.

سوال 12: Poor Man’s DI چیست؟

پاسخ: Poor Man’s DI به رویکردی برای پیاده‌سازی Dependency Injection اشاره دارد که در آن از هیچ IoC Container یا فریم‌ورک خاصی استفاده نمی‌شود. در این روش، وابستگی‌ها به صورت دستی در نقطه ورود برنامه (Composition Root) ایجاد و به کلاس‌ها تزریق می‌شوند.

توضیح:

مثال:

public interface ILogger
{
    void Log(string message);
}

public class ConsoleLogger : ILogger
{
    public void Log(string message)
    {
        Console.WriteLine($"[LOG]: {message}");
    }
}

public interface IProductRepository
{
    Product GetById(int id);
}

public class SqlProductRepository : IProductRepository
{
    private readonly ILogger _logger;

    public SqlProductRepository(ILogger logger)
    {
        _logger = logger;
    }

    public Product GetById(int id)
    {
        _logger.Log($"Getting product {id} from SQL database.");
        return new Product { Id = id, Name = $"Product {id}" };
    }
}

public class ProductService
{
    private readonly IProductRepository _productRepository;
    private readonly ILogger _logger;

    public ProductService(IProductRepository productRepository, ILogger logger)
    {
        _productRepository = productRepository;
        _logger = logger;
    }

    public Product GetProductDetails(int id)
    {
        _logger.Log($"Getting details for product {id}.");
        return _productRepository.GetById(id);
    }
}

// Composition Root (نقطه ورود برنامه)
public class Program
{
    public static void Main(string[] args)
    {
        // ایجاد دستی وابستگی‌ها و تزریق آن‌ها
        ILogger logger = new ConsoleLogger();
        IProductRepository productRepository = new SqlProductRepository(logger);
        ProductService productService = new ProductService(productRepository, logger);

        Product product = productService.GetProductDetails(1);
        Console.WriteLine($"Product Name: {product.Name}");
    }
}

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
}

مزایا:

معایب:

نتیجه‌گیری: Poor Man’s DI یک راه حل عالی برای شروع با DI در پروژه‌های کوچک است. اما با رشد پروژه و افزایش پیچیدگی، استفاده از یک DI Container مناسب‌تر خواهد بود.

سوال 13: Composition Root چیست؟

پاسخ: Composition Root (ریشه ترکیب) یک مکان واحد و خاص در برنامه است که در آن تمام وابستگی‌های برنامه ایجاد و به کلاس‌های مربوطه تزریق می‌شوند. این نقطه، جایی است که گراف کامل اشیاء برنامه ساخته می‌شود.

توضیح:

چرا Composition Root مهم است؟

  1. جداسازی مسئولیت‌ها: کلاس‌های برنامه از مسئولیت ایجاد وابستگی‌های خود رها می‌شوند و فقط بر روی منطق کسب و کار خود تمرکز می‌کنند. این امر باعث رعایت اصل Single Responsibility Principle (SRP) می‌شود.
  2. مدیریت متمرکز: تمام منطق مربوط به ایجاد و سیم‌کشی وابستگی‌ها در یک مکان متمرکز می‌شود، که نگهداری و تغییر آن را آسان‌تر می‌کند.
  3. تست‌پذیری: از آنجایی که کلاس‌ها وابستگی‌های خود را از خارج دریافت می‌کنند، تست واحد آن‌ها بسیار آسان‌تر می‌شود. Composition Root نقطه ایده‌آلی برای تنظیم محیط تست با Mock یا Stub است.
  4. انعطاف‌پذیری: تغییر پیاده‌سازی یک وابستگی (مثلاً تغییر از یک Logger فایل به یک Logger دیتابیس) فقط نیاز به تغییر در Composition Root دارد و بر روی سایر بخش‌های برنامه تأثیری نمی‌گذارد.

مثال در ASP.NET Core:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // این متد Composition Root برای ASP.NET Core است
        services.AddSingleton<ILogger, ConsoleLogger>();
        services.AddScoped<IProductRepository, SqlProductRepository>();
        services.AddTransient<ProductService>();

        services.AddControllersWithViews();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        // ...
    }
}

در این مثال، متد ConfigureServices در کلاس Startup به عنوان Composition Root عمل می‌کند. در اینجا، تمام سرویس‌ها ثبت می‌شوند و DI Container داخلی .NET Core مسئول ایجاد و تزریق آن‌ها در طول عمر برنامه خواهد بود.

نتیجه‌گیری: Composition Root یک مفهوم کلیدی در طراحی برنامه‌هایی با Dependency Injection است که به جداسازی مسئولیت‌ها، افزایش تست‌پذیری و نگهداری آسان‌تر کد کمک می‌کند.

سوال 14: Service Locator و Composition Root چه تفاوتی دارند؟

پاسخ: Service Locator و Composition Root هر دو با مدیریت وابستگی‌ها در یک برنامه مرتبط هستند، اما مفاهیم و نقش‌های متفاوتی دارند:

Service Locator:

Composition Root:

جدول مقایسه:

ویژگی Service Locator Composition Root
نوع الگوی طراحی مفهوم/مکان در برنامه
مسئولیت پیدا کردن و ارائه سرویس‌ها ایجاد و سیم‌کشی تمام وابستگی‌ها
نحوه تعامل کلاس‌ها کلاس‌ها از آن درخواست می‌کنند وابستگی‌ها به کلاس‌ها تزریق می‌شوند
وابستگی به Container کلاس‌ها به Service Locator وابسته هستند کلاس‌ها به Container وابسته نیستند (فقط Composition Root)
شفافیت وابستگی پنهان آشکار
تست‌پذیری دشوارتر آسان‌تر

نتیجه‌گیری: Composition Root یک مکان مناسب برای استفاده از DI Container و مدیریت تمام وابستگی‌های برنامه است. در مقابل، Service Locator یک الگوی ضد (anti-pattern) در نظر گرفته می‌شود، زیرا باعث ایجاد وابستگی‌های پنهان و کاهش تست‌پذیری می‌شود. هدف Composition Root این است که اطمینان حاصل کند DI به درستی در برنامه پیاده‌سازی شده است.

سوال 15: DI Container چگونه Circular Dependencies (وابستگی‌های دایره‌ای) را مدیریت می‌کند؟

پاسخ: Circular Dependency (وابستگی دایره‌ای) زمانی اتفاق می‌افتد که دو یا چند کلاس به صورت متقابل به یکدیگر وابسته باشند. به عنوان مثال، کلاس A به کلاس B وابسته است و کلاس B نیز به کلاس A وابسته است. این وضعیت می‌تواند باعث مشکلاتی در زمان ایجاد اشیاء توسط DI Container شود، زیرا Container نمی‌تواند تعیین کند که کدام شیء را ابتدا ایجاد کند.

مشکل Circular Dependency:

چگونه DI Containerها Circular Dependencies را مدیریت می‌کنند؟

رفتار DI Containerها در مواجهه با Circular Dependency متفاوت است:

  1. تشخیص و خطا (Detection and Error):
    • بسیاری از DI Containerهای مدرن (از جمله Microsoft.Extensions.DependencyInjection) به صورت پیش‌فرض Circular Dependency را تشخیص می‌دهند و یک خطا (exception) پرتاب می‌کنند. این بهترین رفتار است، زیرا Circular Dependency معمولاً نشانه‌ای از طراحی نامناسب است و باید برطرف شود.
  2. پشتیبانی از Property Injection (برای حل برخی موارد):
    • برخی Containerها (مانند Autofac) می‌توانند Circular Dependency را در صورتی که یکی از وابستگی‌ها از طریق Property Injection باشد، مدیریت کنند. در این حالت، Container ابتدا یکی از اشیاء را ایجاد می‌کند و سپس وابستگی دایره‌ای را از طریق Property Setter تزریق می‌کند.
    • مثال:
      public class A
      {
          public B BInstance { get; set; } // Property Injection
      }
      
      public class B
      {
          private A _aInstance;
      
          public B(A aInstance) // Constructor Injection
          {
              _aInstance = aInstance;
          }
      }

      در این سناریو، Container می‌تواند ابتدا A را ایجاد کند (با BInstance null)، سپس B را ایجاد کند (با تزریق A)، و در نهایت BInstance را در A تنظیم کند.

  3. استفاده از Lazy Loading (برای حل برخی موارد):
    • برخی Containerها امکان تزریق Lazy<T> را فراهم می‌کنند. این بدان معناست که نمونه واقعی وابستگی تا زمانی که برای اولین بار به آن دسترسی پیدا شود، ایجاد نمی‌شود. این می‌تواند Circular Dependency را به تعویق بیندازد یا حل کند.
    • مثال:
      public class A
      {
          private Lazy<B> _bInstance;
      
          public A(Lazy<B> bInstance)
          {
              _bInstance = bInstance;
          }
      
          public void DoSomething()
          {
              _bInstance.Value.DoAnotherThing(); // B فقط در اینجا ایجاد می‌شود
          }
      }
      
      public class B
      {
          private A _aInstance;
      
          public B(A aInstance)
          {
              _aInstance = aInstance;
          }
      }

چگونه Circular Dependency را برطرف کنیم؟

بهترین راه حل برای Circular Dependency، بازنگری طراحی و حذف آن است. این معمولاً نشانه‌ای از نقض اصل Single Responsibility Principle (SRP) یا Open/Closed Principle (OCP) است. راه‌های حل عبارتند از:

نتیجه‌گیری: در حالی که برخی DI Containerها راه‌هایی برای مدیریت Circular Dependency ارائه می‌دهند، بهترین رویکرد همیشه بازطراحی کد برای حذف آن‌هاست، زیرا این کار به بهبود کیفیت و نگهداری کد کمک می‌کند.

سوال 16: Method Injection چیست؟

پاسخ: Method Injection (تزریق از طریق متد) یکی از انواع کمتر رایج Dependency Injection است که در آن وابستگی‌ها به جای سازنده یا Property Setter، از طریق پارامترهای یک متد تزریق می‌شوند. این روش معمولاً برای وابستگی‌هایی استفاده می‌شود که فقط برای یک عملیات خاص در یک متد مورد نیاز هستند و نه برای کل عمر شیء.

توضیح:

مثال:

public interface ILogger
{
    void Log(string message);
}

public class ConsoleLogger : ILogger
{
    public void Log(string message)
    {
        Console.WriteLine($"[LOG]: {message}");
    }
}

public class ReportGenerator
{
    public void GenerateReport(string reportName, ILogger logger) // Method Injection
    {
        logger.Log($"Generating report: {reportName}");
        // ... logic to generate report
    }

    public void GenerateSummary(string summaryName)
    {
        // این متد نیازی به ILogger ندارد
        Console.WriteLine($"Generating summary: {summaryName}");
    }
}

مزایا:

معایب:

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

نتیجه‌گیری: Method Injection یک گزینه معتبر برای تزریق وابستگی است، اما باید با دقت و فقط در سناریوهای مناسب استفاده شود. Constructor Injection همچنان روش ارجح برای وابستگی‌های ضروری و دائمی یک کلاس است.

سوال 17: Property Injection چیست؟

پاسخ: Property Injection (تزریق از طریق ویژگی/پراپرتی) یکی از انواع Dependency Injection است که در آن وابستگی‌ها از طریق public propertyهای قابل نوشتن (public settable properties) به یک کلاس تزریق می‌شوند. این روش معمولاً برای وابستگی‌های اختیاری یا زمانی که Constructor Injection به دلیل Circular Dependency یا Constructor Over-injection مناسب نیست، استفاده می‌شود.

توضیح:

مثال:

public interface ILogger
{
    void Log(string message);
}

public class ConsoleLogger : ILogger
{
    public void Log(string message)
    {
        Console.WriteLine($"[LOG]: {message}");
    }
}

public class DataProcessor
{
    public ILogger Logger { get; set; } // Property Injection

    public void ProcessData(string data)
    {
        // بررسی null بودن ضروری است، زیرا Logger اختیاری است
        Logger?.Log($"Processing data: {data}"); 
        // ... logic to process data
    }
}

مزایا:

معایب:

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

نتیجه‌گیری: Property Injection یک ابزار مفید برای مدیریت وابستگی‌های اختیاری است، اما باید با احتیاط استفاده شود. Constructor Injection همچنان روش ارجح برای وابستگی‌های ضروری و دائمی یک کلاس است.

سوال 18: Decorator Pattern (الگوی دکوراتور) و DI چگونه با هم کار می‌کنند؟

پاسخ: Decorator Pattern (الگوی دکوراتور) یک الگوی طراحی ساختاری است که به شما امکان می‌دهد رفتار جدیدی را به صورت پویا به یک شیء موجود اضافه کنید، بدون اینکه ساختار اصلی آن شیء را تغییر دهید. Dependency Injection (DI) به خوبی با Decorator Pattern کار می‌کند و آن را به روشی قدرتمند برای افزودن قابلیت‌های متقاطع (cross-cutting concerns) مانند لاگینگ، کشینگ، اعتبارسنجی یا مدیریت خطا، بدون آلوده کردن منطق اصلی کسب و کار تبدیل می‌کند.

چگونه با هم کار می‌کنند؟

  1. تعریف Interface مشترک: هم سرویس اصلی و هم دکوراتورهای آن باید یک interface مشترک را پیاده‌سازی کنند. این امر به دکوراتور اجازه می‌دهد تا به عنوان جایگزینی برای سرویس اصلی عمل کند.
  2. تزریق سرویس اصلی به دکوراتور: دکوراتور، سرویس اصلی را از طریق Constructor Injection دریافت می‌کند. این به دکوراتور امکان می‌دهد تا قبل یا بعد از فراخوانی متدهای سرویس اصلی، منطق اضافی خود را اجرا کند.
  3. ثبت در DI Container: DI Container به شما امکان می‌دهد تا به راحتی دکوراتورها را ثبت کنید. شما به Container می‌گویید که وقتی یک interface خاص درخواست می‌شود، ابتدا دکوراتور را برگرداند و دکوراتور نیز سرویس اصلی را از Container دریافت کند.

مثال:

// 1. تعریف Interface مشترک
public interface IProductService
{
    Product GetProductById(int id);
    void AddProduct(Product product);
}

// 2. پیاده‌سازی سرویس اصلی
public class ProductService : IProductService
{
    public Product GetProductById(int id)
    {
        Console.WriteLine($"Getting product {id} from database.");
        return new Product { Id = id, Name = $"Product {id}" };
    }

    public void AddProduct(Product product)
    {
        Console.WriteLine($"Adding product {product.Name} to database.");
    }
}

// 3. پیاده‌سازی دکوراتور (مثلاً برای لاگینگ)
public class LoggingProductServiceDecorator : IProductService
{
    private readonly IProductService _decoratedProductService;
    private readonly ILogger _logger;

    public LoggingProductServiceDecorator(IProductService decoratedProductService, ILogger logger)
    {
        _decoratedProductService = decoratedProductService;
        _logger = logger;
    }

    public Product GetProductById(int id)
    {
        _logger.Log($"Logging: Attempting to get product by ID: {id}");
        var product = _decoratedProductService.GetProductById(id);
        _logger.Log($"Logging: Successfully retrieved product: {product.Name}");
        return product;
    }

    public void AddProduct(Product product)
    {
        _logger.Log($"Logging: Attempting to add product: {product.Name}");
        _decoratedProductService.AddProduct(product);
        _logger.Log($"Logging: Successfully added product: {product.Name}");
    }
}

// 4. ثبت در DI Container (مثلاً با Microsoft.Extensions.DependencyInjection)
// public void ConfigureServices(IServiceCollection services)
// {
//     services.AddSingleton();

//     // ثبت سرویس اصلی به عنوان یک پیاده‌سازی موقت
//     services.AddSingleton();

//     // ثبت دکوراتور و تزریق سرویس اصلی به آن
//     services.AddSingleton(provider =>
//     {
//         var actualService = provider.GetRequiredService();
//         var logger = provider.GetRequiredService();
//         return new LoggingProductServiceDecorator(actualService, logger);
//     });
// }

// نحوه استفاده:
// var productService = serviceProvider.GetRequiredService();
// productService.GetProductById(1); // لاگینگ انجام می‌شود
// productService.AddProduct(new Product { Id = 2, Name = "New Product" }); // لاگینگ انجام می‌شود

در این مثال، LoggingProductServiceDecorator رفتار لاگینگ را به ProductService اضافه می‌کند، بدون اینکه ProductService اصلی نیازی به دانستن در مورد لاگینگ داشته باشد. DI Container این فرآیند را به صورت شفاف مدیریت می‌کند.

مزایای ترکیب Decorator Pattern و DI:

نتیجه‌گیری: Decorator Pattern و DI یک ترکیب قدرتمند برای ساخت برنامه‌های ماژولار، قابل نگهداری و توسعه‌پذیر هستند. DI Container فرآیند سیم‌کشی دکوراتورها را ساده می‌کند و به شما امکان می‌دهد تا به راحتی رفتار جدیدی را به سرویس‌های موجود اضافه کنید.

سوال 19: Aspect-Oriented Programming (AOP) و DI چگونه با هم کار می‌کنند؟

پاسخ: Aspect-Oriented Programming (AOP) یک پارادایم برنامه‌نویسی است که هدف آن افزایش مدولار بودن با امکان جداسازی نگرانی‌های متقاطع (cross-cutting concerns) است. نگرانی‌های متقاطع، قابلیت‌هایی هستند که در چندین بخش از برنامه تکرار می‌شوند (مانند لاگینگ، کشینگ، اعتبارسنجی، مدیریت خطا). Dependency Injection (DI) و AOP می‌توانند به خوبی با هم کار کنند تا این نگرانی‌ها را به صورت تمیز و کارآمد مدیریت کنند.

چگونه با هم کار می‌کنند؟

مثال با Dynamic Proxy و DI:

// 1. تعریف Interface سرویس
public interface IProductService
{
    Product GetProductById(int id);
    void AddProduct(Product product);
}

// 2. پیاده‌سازی سرویس اصلی
public class ProductService : IProductService
{
    public Product GetProductById(int id)
    {
        Console.WriteLine($"Getting product {id} from database.");
        return new Product { Id = id, Name = $"Product {id}" };
    }

    public void AddProduct(Product product)
    {
        Console.WriteLine($"Adding product {product.Name} to database.");
    }
}

// 3. تعریف Aspect (مثلاً برای لاگینگ) با استفاده از یک Interceptor
// (این بخش معمولاً با فریم‌ورک‌های AOP مانند Castle.Core یا Autofac.Extras.DynamicProxy پیاده‌سازی می‌شود)
public class LoggingInterceptor : IInterceptor
{
    private readonly ILogger _logger;

    public LoggingInterceptor(ILogger logger)
    {
        _logger = logger;
    }

    public void Intercept(IInvocation invocation)
    {
        _logger.Log($"Before {invocation.Method.Name} with args: {string.Join(", ", invocation.Arguments)}");
        invocation.Proceed(); // فراخوانی متد اصلی
        _logger.Log($"After {invocation.Method.Name} returned: {invocation.ReturnValue}");
    }
}

// 4. ثبت در DI Container (مثلاً با Autofac)
// public void ConfigureContainer(ContainerBuilder builder)
// {
//     builder.RegisterType().As().SingleInstance();
//     builder.RegisterType();

//     builder.RegisterType().As()
//         .EnableInterfaceInterceptors()
//         .InterceptedBy(typeof(LoggingInterceptor));
// }

// نحوه استفاده:
// var productService = container.Resolve();
// productService.GetProductById(1); // لاگینگ به صورت خودکار اعمال می‌شود

در این سناریو، DI Container (مانند Autofac) یک proxy از IProductService ایجاد می‌کند. وقتی متدهای IProductService فراخوانی می‌شوند، proxy ابتدا LoggingInterceptor را فراخوانی می‌کند که منطق لاگینگ را اجرا می‌کند و سپس متد اصلی را فراخوانی می‌کند. این کار بدون تغییر کد اصلی ProductService انجام می‌شود.

تفاوت با Decorator Pattern:

مزایای ترکیب AOP و DI:

نتیجه‌گیری: AOP و DI مکمل یکدیگر هستند. DI به مدیریت وابستگی‌ها کمک می‌کند، در حالی که AOP به تزریق رفتار متقاطع کمک می‌کند. ترکیب این دو، به ویژه با استفاده از DI Containerهایی که از AOP پشتیبانی می‌کنند، منجر به کدی بسیار تمیزتر، ماژولارتر و قابل نگهداری‌تر می‌شود.

سوال 20: Scratchpad چیست؟

پاسخ: Scratchpad در زمینه برنامه‌نویسی و توسعه نرم‌افزار به یک فضای کاری موقت و غیررسمی اشاره دارد که توسعه‌دهندگان از آن برای آزمایش سریع ایده‌ها، نوشتن قطعه کدهای کوچک، انجام محاسبات، یا نگهداری یادداشت‌های موقت استفاده می‌کنند. این فضا معمولاً برای کارهایی است که نیازی به ذخیره دائمی، کامپایل کامل پروژه، یا رعایت دقیق ساختار پروژه ندارند.

کاربردهای Scratchpad:

ویژگی‌های Scratchpad:

ابزارهایی که می‌توانند به عنوان Scratchpad استفاده شوند:

نتیجه‌گیری: Scratchpad یک ابزار ضروری برای هر توسعه‌دهنده است که به آن‌ها امکان می‌دهد ایده‌ها را به سرعت آزمایش کنند و بهره‌وری خود را افزایش دهند، بدون اینکه نگران تأثیر آن بر روی پروژه اصلی باشند.

1. Dependency Injection چیست؟

Dependency Injection (DI) یک الگوی طراحی است که به شما کمک می‌کند تا وابستگی‌های بین کلاس‌ها را مدیریت کنید. به جای اینکه یک کلاس خودش وابستگی‌هایش را ایجاد کند، این وابستگی‌ها از خارج به آن تزریق می‌شوند. این کار باعث می‌شود کد شما ماژولارتر، قابل تست‌تر و قابل نگهداری‌تر باشد.

2. چرا باید از Dependency Injection استفاده کنیم؟

استفاده از DI مزایای زیادی دارد:

3. Inversion of Control (IoC) چیست و چه ارتباطی با DI دارد؟

Inversion of Control (IoC) یک اصل طراحی است که در آن جریان کنترل برنامه از کد شما به یک فریم‌ورک یا کانتینر منتقل می‌شود. به جای اینکه کد شما به صورت فعال وابستگی‌هایش را فراخوانی کند، فریم‌ورک مسئول ایجاد و تزریق این وابستگی‌ها است. Dependency Injection یکی از پیاده‌سازی‌های رایج اصل IoC است.

4. IoC Container (DI Container) چیست؟

یک IoC Container (که به آن DI Container هم گفته می‌شود) یک فریم‌ورک نرم‌افزاری است که مسئول مدیریت وابستگی‌ها و چرخه حیات آبجکت‌ها در یک برنامه است. این کانتینر کلاس‌ها را ایجاد می‌کند، وابستگی‌های آن‌ها را تزریق می‌کند و آن‌ها را در صورت نیاز به شما ارائه می‌دهد. مثال‌هایی از IoC Containerها شامل Autofac، Ninject، Unity و Microsoft.Extensions.DependencyInjection هستند.

5. انواع Dependency Injection کدامند؟

سه نوع اصلی Dependency Injection وجود دارد:

6. Constructor Injection چیست؟

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

public class MyService
{
    private readonly IMyDependency _dependency;

    public MyService(IMyDependency dependency)
    {
        _dependency = dependency;
    }

    public void DoSomething()
    {
        _dependency.Execute();
    }
}

7. Property (Setter) Injection چیست؟

Property Injection (که به آن Setter Injection هم گفته می‌شود) روشی است که در آن وابستگی‌ها از طریق پراپرتی‌های عمومی (Public Properties) کلاس تزریق می‌شوند. این روش زمانی مفید است که وابستگی اختیاری باشد یا بعداً در چرخه حیات آبجکت تنظیم شود. برخلاف Constructor Injection، یک آبجکت می‌تواند بدون این وابستگی‌ها ایجاد شود.

public class MyService
{
    public IMyDependency Dependency { get; set; }

    public void DoSomething()
    {
        Dependency?.Execute();
    }
}

8. Method (Interface) Injection چیست؟

Method Injection (که به آن Interface Injection هم گفته می‌شود) روشی است که در آن وابستگی‌ها از طریق متدهای خاصی تزریق می‌شوند. این روش کمتر رایج است و معمولاً برای تزریق وابستگی‌های موقتی یا خاص به یک متد استفاده می‌شود، نه به کل کلاس. این روش انعطاف‌پذیری بالایی را برای تزریق وابستگی‌ها در زمان اجرا فراهم می‌کند.

public interface IExecutable
{
    void Execute(IMyDependency dependency);
}

public class MyService : IExecutable
{
    public void Execute(IMyDependency dependency)
    {
        dependency.DoSomething();
    }
}

9. Service Locator Pattern چیست و چه تفاوتی با DI دارد؟

Service Locator Pattern یک الگوی طراحی است که در آن یک آبجکت مرکزی (Service Locator) مسئول پیدا کردن و ارائه سرویس‌ها به درخواست‌کنندگان است. تفاوت اصلی با DI این است که در Service Locator، کلاس خودش به صورت فعال وابستگی‌هایش را از Service Locator درخواست می‌کند (Pull-based)، در حالی که در DI، وابستگی‌ها به صورت منفعل به کلاس تزریق می‌شوند (Push-based). DI به طور کلی به دلیل کاهش وابستگی و افزایش قابلیت تست، روش ارجح است.

10. Lifetime Management در DI Containerها به چه معناست؟

Lifetime Management (مدیریت طول عمر) در DI Containerها به نحوه ایجاد و مدیریت نمونه‌های (Instances) وابستگی‌ها اشاره دارد. سه نوع اصلی طول عمر وجود دارد: