عنوان:

‫چه زمان‌هایی بهتر است از new و یا تزریق وابستگی‌ها استفاده کنیم؟


نویسنده: وحید نصیری
تاریخ: ۱۴۰۴/۰۱/۲۴ ۰۹:۲۳
آدرس: www.dntips.ir
مقدمه
در دنیای توسعه نرم‌افزار، طراحی کدهای تمیز، قابل نگهداری و تست‌پذیر از اهمیت بالایی برخوردار است. یکی از مفاهیم کلیدی در این حوزه، مدیریت وابستگی‌ها (Dependencies) بین کلاس‌ها و اجزای مختلف برنامه است. دو رویکرد اصلی برای این منظور وجود دارد: استفاده از سازنده‌ها (Constructors) برای ایجاد اشیا و تزریق وابستگی (Dependency Injection یا DI). انتخاب بین این دو رویکرد می‌تواند تأثیر عمیقی بر کیفیت کد، قابلیت تست و نگهداری پروژه داشته باشد.

سازنده‌ها: روش سنتی ایجاد اشیا
استفاده از سازنده‌ها روشی کلاسیک برای ایجاد نمونه‌های (Instances) یک کلاس است. در این رویکرد، کلاس، خود مسئول ایجاد وابستگی‌هایش است. به مثال زیر در #C توجه کنید:
public class OrderService
{
    private readonly Database _database;

    public OrderService()
    {
        _database = new Database();
    }

    public void ProcessOrder(Order order)
    {
        _database.SaveOrder(order);
    }
}
در این کد، کلاس OrderService وابستگی خود به Database را مستقیماً در سازنده ایجاد می‌کند. این روش ساده و قابل فهم است، اما معایبی دارد:
  1. پیوند محکم (Tight Coupling): کلاس به یک پیاده‌سازی خاص از Database وابسته است، که تغییر آن (مثلاً به یک پایگاه داده دیگر) نیازمند تغییر کد کلاس است.
  2. مشکل در تست‌پذیری: چون وابستگی درون کلاس ایجاد می‌شود، نمی‌توان به‌راحتی در تست‌های واحد (Unit Tests) از نسخه‌های جعلی (Mock) استفاده کرد.
  3. عدم انعطاف‌پذیری: افزودن یا تغییر وابستگی‌ها معمولاً نیازمند بازنویسی بخش‌هایی از کد است.

این روش برای پروژه‌های کوچک یا زمانی که وابستگی‌ها ثابت و ساده هستند، ممکن است کافی باشد، اما در برنامه‌های پیچیده‌تر، محدودیت‌های آن آشکار می‌شود.

تزریق وابستگی: رویکردی مدرن
تزریق وابستگی یک الگوی طراحی است که وابستگی‌ها را از خارج کلاس به آن ارائه می‌دهد، به‌جای اینکه کلاس خود آن‌ها را ایجاد کند. این کار معمولاً از طریق سازنده، متد یا ویژگی‌ها انجام می‌شود، اما تزریق از طریق سازنده (Constructor Injection) رایج‌ترین و توصیه‌شده‌ترین روش در NET. است. به مثال زیر نگاه کنید:
public class OrderService
{
    private readonly IDatabase _database;

    public OrderService(IDatabase database)
    {
        _database = database ?? throw new ArgumentNullException(nameof(database));
    }

    public void ProcessOrder(Order order)
    {
        _database.SaveOrder(order);
    }
}
در این نمونه، وابستگی IDatabase از طریق سازنده تزریق می‌شود. مزایای این رویکرد عبارت‌اند از:
  1. پیوند شل (Loose Coupling): با استفاده از رابط (Interface) به‌جای پیاده‌سازی خاص، کلاس از جزئیات پیاده‌سازی مستقل می‌شود.
  2. تست‌پذیری بهتر: می‌توان به‌راحتی در تست‌ها از نسخه‌های جعلی استفاده کرد، مثلاً با ابزارهایی مثل Moq یا NSubstitute.
  3. شفافیت وابستگی‌ها: سازنده به‌وضوح نشان می‌دهد که کلاس به چه وابستگی‌هایی نیاز دارد.
  4. تضمین وجود وابستگی: با بررسی null در سازنده، از دسترسی به وابستگی‌های نامعتبر جلوگیری می‌شود.

مقایسه سازنده‌های سنتی و تزریق وابستگی
برای درک بهتر تفاوت‌ها، بیایید این دو رویکرد را از چند منظر مقایسه کنیم:
  • مدیریت وابستگی‌ها: در روش سنتی، کلاس کنترل کاملی بر ایجاد وابستگی‌ها دارد، اما این کنترل در پروژه‌های بزرگ می‌تواند به پیچیدگی منجر شود. تزریق وابستگی این مسئولیت را به یک ظرف (Container) مانند ASP.NET Core DI واگذار می‌کند، که مدیریت چرخه حیات اشیا را ساده‌تر می‌کند.
  • انعطاف‌پذیری: تغییر یک وابستگی در روش سنتی نیازمند تغییر کد کلاس است، درحالی‌که در تزریق وابستگی، کافی است پیاده‌سازی جدیدی در ظرف (DI Container) ثبت شود.
  • خوانایی کد: تزریق وابستگی وابستگی‌ها را صراحتاً در امضای سازنده نشان می‌دهد، که خواندن و درک کد را برای توسعه‌دهندگان دیگر آسان‌تر می‌کند.
  • کارایی: ایجاد دستی اشیا در سازنده ممکن است در پروژه‌های کوچک سریع‌تر باشد، اما در پروژه‌های بزرگ، مدیریت خودکار وابستگی‌ها توسط ظرف DI کارایی بیشتری دارد.

چه زمانی از کدام استفاده کنیم؟
تصمیم‌گیری بین استفاده از سازنده‌های سنتی و تزریق وابستگی به عوامل مختلفی بستگی دارد:
  1. پیچیدگی پروژه: در پروژه‌های کوچک با تعداد محدودی وابستگی ثابت، استفاده از سازنده‌های سنتی می‌تواند ساده‌تر باشد. اما در پروژه‌های بزرگ NET.، تزریق وابستگی استانداردی برای مدیریت پیچیدگی است.
  2. نیاز به تست‌پذیری: اگر تست‌های واحد بخش مهمی از فرآیند توسعه شما هستند، تزریق وابستگی به دلیل امکان استفاده از نسخه‌های جعلی، انتخاب بهتری است.
  3. نیاز به تغییرپذیری: اگر احتمال تغییر پیاده‌سازی وابستگی‌ها (مثلاً تغییر از SQL Server به MongoDB) وجود دارد، تزریق وابستگی انعطاف بیشتری فراهم می‌کند.
  4. وضوح طراحی: تزریق وابستگی به طراحی‌هایی منجر می‌شود که اصول SOLID (به‌ویژه اصل وارونگی وابستگی یا Dependency Inversion) را بهتر رعایت می‌کنند.

نکات عملی در استفاده از تزریق وابستگی در NET.
برای استفاده مؤثر از تزریق وابستگی در NET.، رعایت چند نکته ضروری است:
  • استفاده از رابط‌ها: همیشه از رابط‌ها به‌جای کلاس‌های مشخص برای تعریف وابستگی‌ها استفاده کنید. این کار پیوند شل را تضمین می‌کند.
  • مدیریت چرخه حیات: در ASP.NET Core، وابستگی‌ها می‌توانند به‌صورت موقتی (Transient)، محدود به حوزه (Scoped) یا تک‌نمونه (Singleton) ثبت شوند. انتخاب درست چرخه حیات از مشکلات عملکردی جلوگیری می‌کند.
  • بررسی null: در سازنده، همیشه ورودی‌ها را برای null بررسی کنید تا از خطاهای زمان اجرا جلوگیری شود.
  • اجتناب از تزریق بیش‌ازحد: اگر سازنده شما تعداد زیادی پارامتر دارد، ممکن است نشانه‌ای از نقض اصل مسئولیت واحد (Single Responsibility Principle) باشد. در این موارد، بازطراحی کلاس یا استفاده از الگوهایی مثل Facade می‌تواند مفید باشد.

مثال زیر نحوه ثبت و استفاده از تزریق وابستگی در ASP.NET Core را نشان می‌دهد:
public interface IDatabase
{
    void SaveOrder(Order order);
}

public class SqlDatabase : IDatabase
{
    public void SaveOrder(Order order)
    {
        // منطق ذخیره در SQL Server
    }
}

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddScoped<IDatabase, SqlDatabase>();
    }
}
در این کد، SqlDatabase به‌عنوان پیاده‌سازی IDatabase در ظرف DI ثبت شده و به‌صورت خودکار به کلاس‌هایی که به آن وابسته‌اند تزریق می‌شود.

چالش‌ها و معایب
با وجود مزایای فراوان، تزریق وابستگی بدون چالش نیست:
  • پیچیدگی اولیه: برای توسعه‌دهندگان تازه‌کار، درک مفهوم ظرف DI و تنظیمات آن ممکن است دشوار باشد.
  • وابستگی به فریم‌ورک: در NET.، استفاده از DI معمولاً با فریم‌ورک‌هایی مثل ASP.NET Core گره خورده است، که ممکن است در پروژه‌های غیروب محدودیت ایجاد کند.
  • خطاهای زمان اجرا: اگر وابستگی‌ها به‌درستی ثبت نشوند، خطاها ممکن است تا زمان اجرا ظاهر نشوند.
در مقابل، سازنده‌های سنتی نیز در پروژه‌های بزرگ می‌توانند به کدهایی غیرقابل نگهداری منجر شوند، اما برای سناریوهای ساده، پیچیدگی کمتری دارند.

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


مشاهده مطلب اصلی