برنامه نویسی

شما باید استفاده از Spring @Autowired را متوقف کنید

یا “چرا هنگام استفاده از فنر نباید از تزریق فیلد استفاده کنید”.


*TL;DR *
تزریق مستقیم لوبیا به مزارع با استفاده از @Autowired وابستگی های شما را “پنهان” می کند و طراحی بد را تشویق می کند. به جای آن از تزریق مبتنی بر سازنده استفاده کنید.


بسیار محتمل است که قبلاً یک @Autowired حاشیه نویسی هنگام کدنویسی در یک برنامه Java/Spring. با این حال، چیزی که بیشتر دوستان توسعه‌دهندگان من نمی‌دانند این است که سیم‌کشی خودکار یکی از رایج‌ترین ضدالگوهای مشاهده شده در پایگاه‌های کدی است که از چارچوب Spring IoC/DI استفاده می‌کنند.

فقط برای تازه کردن حافظه شما در اینجا یک مثال کوتاه از چیزی است که من در مورد آن صحبت می کنم.

@Service
public class UserService {

    @Autowired // Field Injection
    private UserRepository userRepository;

    // ...
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

ممکن است روشی ساده و عملی برای انجام Dependency Injection به نظر برسد، درست است؟ اما عواقب این امر می تواند پایگاه کد شما را به آرامی (یا سریع) به اسپاگتی تبدیل کند.

اما چرا توسعه دهندگان به انتخاب این روش ادامه می دهند؟ احتمالاً به این دلیل که استفاده از آن آسان است و زمانی که توسعه‌دهنده نیاز به تزریق وابستگی به کلاس دارد، تجربه‌ای خارج از جعبه ارائه می‌کند. با این حال، مشابه هر عمل بد دیگری، این کار در بقیه پایگاه کد بسیار آسان است و معمولاً مردم هرگز بحث نمی کنند که چرا آن طراحی کد انتخاب شده است. و نتیجه استفاده از @Autowired چیست؟ کد شما دچار مشکل خواهد شد جفت، آ تجربه آزمون بد، وابستگی های پنهان، و دیگران.

به نظر من، عدم درک مکانیک زیربنایی یک سیستم می تواند به طور بالقوه منجر به ایجاد پایگاه های کد فاجعه آمیز شود. این برای استفاده از «@Autowired» هنگام استفاده از Spring اعمال می شود. بدون درک کامل مفاهیم حیاتی مانند وارونگی کنترل و تزریق وابستگی، توسعه دهندگان به احتمال زیاد دچار خطا می شوند و در دام این دام گرفتار می شوند.

برای به دست آوردن درک بهتر از بهترین شیوه های طراحی کد، برای توسعه دهندگان و معماران مهم است که مفاهیم کلیدی را بررسی کرده و رویکردهای مختلف تزریق وابستگی با Spring را بررسی کنند. با انجام این کار، می توانیم ارزیابی کنیم که کدام روش برای نیازهای ما بهینه است و به طور بالقوه معایب استفاده از @Autowired را به عنوان استراتژی تزریق وابستگی کشف کنیم.

تزریق وابستگی با فنر

Dependency Injection یک الگوی حیاتی در توسعه نرم افزار است و در چارچوب های آماده تولید مانند Spring وجود دارد. این اتصال شل بین کلاس ها را ترویج می کند، تست پذیری را بهبود می بخشد و ماژولار بودن را تسهیل می کند.

توضیحات تصویر

اساساً سه راه برای انجام Dependency Injection با استفاده از Spring Framework وجود دارد:

  • تزریق میدانی از بازتاب برای تنظیم مقادیر خصوصیات/فیلدهای خصوصی استفاده می کند.
  • تزریق ستر از تنظیم کننده های عمومی برای تنظیم مقدار ویژگی ها/فیلدها استفاده می کند.
  • تزریق سازنده در زمان ایجاد خود شی اتفاق می افتد.

تزریق میدانی

تزریق فیلد را می توان با استفاده از Spring با افزودن حاشیه نویسی @Autowired به یک فیلد کلاس به دست آورد. و @Autowired چه کاری انجام می دهد؟ @Autowired یک حاشیه نویسی ارائه شده توسط Spring است که امکان سیم کشی خودکار وابستگی را فراهم می کند. هنگامی که به یک فیلد، متد یا سازنده اعمال می شود، Spring سعی می کند یک وابستگی مناسب از نوع مورد نیاز پیدا کند و آن را به کلاس هدف تزریق کند.

@Autowired به تنهایی ریشه همه بدی ها نیست. جنبه کلیدی جایی است که شما از آن استفاده می کنید @Autowired. بیشتر اوقات، توسعه دهندگان از آن در سطح فیلد/ویژگی استفاده می کنند. استفاده از آن در روش ستر می تواند آسیب را کاهش دهد، اما به هیچ وجه همه آن را از بین نمی برد.

اما چرا @Autowired اینقدر بد است؟ اساسا، @Autowired برخی از اصول طراحی کد خوب را نقض می کند.

وارونگی وابستگی (D از SOLID)

اگر می خواهید از کلاس خود خارج از کانتینر برنامه استفاده کنید، به عنوان مثال برای تست واحد، مجبور هستید از یک کانتینر Spring برای نمونه سازی کلاس خود استفاده کنید زیرا هیچ راه ممکن دیگری (به جز بازتاب) برای تنظیم فیلدهای @Autowired وجود ندارد.

هنگام استفاده از تزریق وابستگی مبتنی بر فیلد با @Autowired، کلاس ذاتاً این وابستگی ها را از دنیای خارج پنهان می کند. به عبارت دیگر، نقطه تزریق وجود ندارد.

در زیر نمونه ای از این سناریو را مشاهده می کنید.

@Component
public class Calculator {

   @Autowired 
   private Multiplier multiplier;

   public int add(int a, int b) {
      return a + b;
   }

   public int multiply(int a, int b) {
      return multiplier.multiply(a, b);
   }
}

@Component
public class Multiplier {
   public int multiply(int a, int b) {
      return a * b;
   }
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

با وجود این واقعیت که در زمان اجرا Spring وابستگی را تزریق می کند (نمونه ای از Multiplier)، هیچ راهی برای تزریق وابستگی به صورت دستی وجود ندارد، به عنوان مثال هنگام آزمایش واحد این کلاس.

کلاس تست واحد زیر کامپایل می شود، اما در زمان اجرا کاندیدای خوبی برای پرتاب NullPointerException است، چرا؟ زیرا وابستگی بین Calculator و Multiplier برآورده نشد.

public class CalculatorTest {

   @Test
   public void testAdd() {
      Calculator calculator = new Calculator();
      int result = calculator.add(2, 3);
      assertEquals(5, result);
   }

   @Test
   public void testMultiply() {
      Calculator calculator = new Calculator();
      int result = calculator.multiply(2, 3);
      assertEquals(6, result);
   }
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

کد پاک / معماری جیغ / اصل مسئولیت منفرد (S از SOLID)

کد باید طراحی خود را فریاد بزند. و اصل SRP بیان می کند که یک کلاس باید تنها یک دلیل برای تغییر داشته باشد. این بدان معناست که یک کلاس باید تنها یک مسئولیت یا دغدغه داشته باشد. @Autowired حاشیه نویسی، به خودی خود، این اصل را نقض نمی کند.

با این حال، اگر یک کلاس دارای مسئولیت‌ها و وابستگی‌های متعددی باشد که از طریق حاشیه‌نویسی @Autowired تزریق می‌شوند، می‌تواند نشانه واضحی از نقض SRP باشد. در این صورت شاید بهتر باشد که کلاس را مجدداً تغییر دهیم و مسئولیت ها را در کلاس های جداگانه استخراج کنیم.

در غیر این صورت، اگر به جای آن از تزریق وابستگی مبتنی بر سازنده استفاده شود، همانطور که وابستگی های بیشتری به کلاس شما اضافه می شود، سازنده بزرگتر و بزرگتر می شود و کد شروع به بوییدن می کند و سیگنال های واضحی ارسال می کند که چیزی اشتباه است. این موضوع در الگوی معماری جیغ / Clean Code / Screaming Architecture در کتاب نوشته شده توسط رابرت سی مارتین با نام مستعار عمو باب توضیح داده شده است.

پیچیدگی

@Autowired می‌تواند کد را پیچیده‌تر کند، مخصوصاً وقتی با وابستگی‌های دایره‌ای سروکار داریم. هنگامی که دو یا چند کلاس به یکدیگر وابسته هستند، تعیین ترتیبی که باید در آن نمونه سازی شوند دشوار می شود. این می تواند منجر به خطاهای زمان اجرا شود که اشکال زدایی آنها سخت است.


به احتمال زیاد پروژه شما اصلاً به @Autowired نیاز ندارد


در عوض از چه چیزی باید استفاده کنید؟

پاسخ کوتاه این است تزریق سازنده.

در OOP یک شی را با فراخوانی سازنده آن ایجاد می کنیم. اگر سازنده تمام وابستگی‌های مورد نیاز را به‌عنوان پارامتر انتظار داشته باشد، می‌توانیم 100% مطمئن باشیم که کلاس بدون تزریق وابستگی‌های آن هرگز نمونه‌سازی نمی‌شود.

در زیر یک مثال با استفاده از تزریق وابستگی مبتنی بر سازنده به جای @Autowired.

@Component
public class Calculator {

   private Multiplier multiplier;

   // Default constructor. It will be used by Spring to 
   // inject the dependencies declared here.
   public Calculator(Multiplier multiplier) {
      this.multiplier = multiplier;
   }

   public int add(int a, int b) {
      return a + b;
   }

   public int multiply(int a, int b) {
      return multiplier.multiply(a, b);
   }
}

@Component
public class Multiplier {
   public int multiply(int a, int b) {
      return a * b;
   }
}

public class CalculatorTest {

   @Test
   public void testAdd() {
      Calculator calculator = new Calculator(mock(Multiplier.class));
      int result = calculator.add(2, 3);
      assertEquals(5, result);
   }

   @Test
   public void testMultiply() {
      Multiplier mockMultiplier = mock(Multiplier.class);
      when(mockMultiplier.multiply(2, 3)).thenReturn(6);
      Calculator calculator = new Calculator(mockMultiplier);
      int result = calculator.multiply(2, 3);
      assertEquals(6, result);
   }
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اکنون وابستگی بین دو مؤلفه به صراحت اعلام شد و طراحی کد اجازه می دهد تا یک نمونه ساختگی از Multiplier در زمان اجرا تزریق شود.

همچنین، تزریق سازنده به ایجاد اشیاء تغییرناپذیر کمک می کند، زیرا سازنده تنها راه ممکن برای ایجاد اشیاء است. هنگامی که یک لوبیا ایجاد می کنیم، دیگر نمی توانیم وابستگی های آن را تغییر دهیم. از طرف دیگر، با استفاده از تزریق ستر، می توان وابستگی را پس از ایجاد یا تغییر وابستگی تزریق کرد، بنابراین به اشیاء قابل تغییر منجر می شود که از جمله موارد دیگر، ممکن است در یک محیط چند رشته ای ایمن نباشند و به سختی انجام شوند. اشکال زدایی به دلیل تغییرپذیری آنها

با تعریف صریح وابستگی های کلاس به سازنده، می توانید کد را بیشتر کنید قابل نگهداری، قابل آزمایش، و قابل انعطاف.

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

این رویکرد درک و پیگیری وابستگی های کلاس های شما را آسان تر می کند. علاوه بر این، با وادار کردن شما به ارائه صریح اشیاء ساختگی برای وابستگی‌های مورد نیاز در زمان کدنویسی، آزمایش کلاس را به صورت مجزا آسان‌تر می‌کند.

تزریق ستر

یکی دیگر از روش‌های انجام Dependency Injection با استفاده از Spring، استفاده از روش‌های setter برای تزریق نمونه‌ای از وابستگی است.

در زیر می توانید نمونه ای از نحوه ظاهر Setter Injection در هنگام استفاده از Spring را مشاهده کنید.

public class UserService {
    private UserRepository userRepository;        

    ...

    @Autowired // Setter Injection
    public void setUserRepository(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    ...
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

این رویکرد می تواند منجر به مشکلاتی مانند:

  • وابستگی های اختیاری. Setter Injection به وابستگی‌های اختیاری اجازه می‌دهد، که اگر وابستگی‌ها به درستی برای مقادیر تهی بررسی نشده باشند، می‌توانند منجر به استثناهای اشاره گر تهی شوند.
  • وضعیت شی ناقص. اگر یک شی تا حدی ساخته شود و یک تنظیم کننده فراخوانی شود، می تواند منجر به ناقص بودن شیء شود که می تواند منجر به رفتار غیرمنتظره یا استثناهای اشاره گر تهی شود.
  • وابستگی های پنهان. از آنجایی که وابستگی به صراحت در سازنده اعلام نشده است، درک کد و وابستگی های آن را دشوارتر می کند.

علیرغم ایرادات، Setter Injection جایی بین Field و Constructor Injection قرار دارد زیرا حداقل ما یک روش عمومی داریم که به توسعه دهندگان اجازه می دهد به جای پیاده سازی واقعی، mock را تزریق کنند و کلاس را آسان تر آزمایش کنند.

ملاحظات بیشتر

این @Autowired حاشیه نویسی را می توان از پایگاه های کد با استفاده از Spring Framework 4.3 یا بالاتر حذف کرد و Spring از سازنده پیش فرض استفاده می کند و زمانی که کلاس توسط Application Context مدیریت می شود، همه وابستگی های لازم را برای شما تزریق می کند. (https://docs.spring.io/spring -framework/docs/current/reference/html/core.html#beans-constructor-vs-setter-injection)

بنابراین، از آنجایی که تیم Spring تصمیم گرفت @Autowired باید برای سازنده پیش‌فرض اختیاری باشد. می‌توانیم نتیجه‌گیری کنیم زیرا به فریمورک Spring برای تصمیم‌گیری کمکی نمی‌کند، حضور آن فقط نویز است. از شرش خلاص شو!

نظر زیر از تیم توسعه Spring Framework دلیل دیگری برای انتخاب Constructor Injection به عنوان روش تزریق پیش‌فرض شما می‌دهد.

“علاوه بر این، اجزای تزریق شده توسط سازنده همیشه در یک حالت کاملاً مقداردهی اولیه به کد مشتری (در حال فراخوان) بازگردانده می شوند.” و “Setter injection باید در درجه اول فقط برای وابستگی های اختیاری استفاده شود که می توانند مقادیر پیش فرض معقولی را در کلاس اختصاص دهند.”

اساساً آنچه آنها می گویند این است که با استفاده از تزریق سازنده می توانید وضعیت آماده به استفاده کلاس را تضمین کنید. اگر از @Autowired یا حتی setter injection استفاده می‌کنید، نمونه کلاس شما می‌تواند توسط Spring در یک حالت بد نمونه‌سازی شود که می‌تواند باعث رفتار غیرمنتظره‌ای مانند NullPointerException شود.

چرا @Autowired حتی وجود دارد؟

اگر نیاز به تزریق وابستگی های اختیاری یا قابل تغییر دارید، ممکن است بخواهید از @Autowired برای علامت گذاری متد تنظیم کننده (یا یک سازنده غیر پیش فرض) به عنوان نقطه تزریق استفاده کنید. اما تنها زمانی منطقی است که وابستگی شما اختیاری یا قابل تغییر باشد و پیاده سازی شما بتواند عدم وجود آن وابستگی (nulls) را مدیریت کند.

اگر چند سازنده در دسترس هستند و سازنده اصلی/پیش‌فرض وجود ندارد، حداقل یکی از سازنده‌ها باید با @Autowired حاشیه‌نویسی شود تا به Spring دستور داده شود که از کدام یک استفاده کند. این سناریویی است که @Autowired را می توان در نظر گرفت.

نتیجه

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

چیزی که شما و تیمتان به صورت رایگان به دست خواهید آورد این است که بقیه پروژه به طور ضمنی منعکس کننده این تصمیم طراحی کد است. گاهی اوقات، توسعه‌دهندگان هرگز در مورد تزریق وابستگی مناسب، قراردادهای کلاس، یا حتی نحوه آزمایش مؤثر کد یاد نمی‌گیرند، زیرا از @Autowired استفاده می‌کنند و هرگز ضرورت و کاربرد واقعی آن را زیر سؤال نمی‌برند.

خواندن و مراجع اضافی

https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-constructor-vs-setter-injection
https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-autowired-annotation
https://betulsahinn.medium.com/why-is-autowired-annotation-not-recommended-4939c46da1f8
https://blog.marcnuri.com/field-injection-is-not-recommended
https://reflectoring.io/constructor-injection/
https://kinbiko.com/posts/2018-02-13-spring-dependency-injection-patterns/
https://blog.frankel.ch/my-case-against-autowiring/
https://www.linkedin.com/pulse/when-autowire-spring-boot-omar-ismail/?trk=articles_directory
https://eng.zemosolabs.com/when-not-to-autowire-in-spring-spring-boot-93e6a01cb793
https://stackoverflow.com/questions/41092751/spring-injects-dependencies-in-constructor-without-autowired-annotation

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا