در اصل شاید سرفصل بحث ما رو بشه گفت "وابستگی وارون | Dependency Inversion" که این مبحث یکی از موارد مهم در بحث مهندسی نرم افزار هستش که همون D توی SOLID هستش.
همین اول بحث بگم که این موضوع یکی از مفاهیمی هست توی مهندسی نرم افزار که ماشالله تعداد کلماتی کلیدی ای که توش مطرح می شه، که برای هر کدوم از این کلمات کلیدی می شه صد ها مقاله نوشت، خیلی زیاده! من تلاش کردم هر کدومشون رو خیلی کوتاه توضیح بدم، ولی طبیعتا نمی شد به صورت مفصل بحث کرد راجع بهشون.
یکی از چالش های مهم در توسعه نرم افزار های شرکتی بحث پیچیدگی نرم افزار و همین طور سادگی توسعه اون هستش یعنی ما دوست داریم نرم افزار های خیلی پیچیده رو به آسونی بتونیم توسعه بدیم و علاوه بر این قابل گسترش هم باشن.
یک نکته مهمی که این جا باید متذکر بشم این هستش که بحث پیچیدگی نرم افزار با لود بالای سیستم خیلی فرق داره. اگه بخوایم یک اشاره کوتاه بهش بکنیم، مثلا توسعه نرم افزار مدیریت ورود و خروج کارمندان یک شرکت تقریبا لود پایینی داره، یعنی در هر لحظه تعداد درخواست هایی که به سمت سرور یا سرورها ارسال می شه به نسبت پایین هستش. اما شاید پیچیدگی زیادی داشته باشه، مثلا باید این نرم افزار به چندین نرم افزار دیگه متصل باشه که حقوق کارمندان رو بر اساس زمان ورود و خروجشون محاسبه بکنه، نمودار های مختلف بکشه و تحلیل های متفاوتی بر اساس اون ها بکنه که این ها و موارد متعدد دیگر باعث پیچیده شدن سیستم می شه ولی لود اون سیستم رو لزوما بالا نمی بره.
قابلیت گسترش نرم افزار به پارامتر های متفاوتی وابسته است که در این مقاله می خوایم راجع به وابستگی های برنامه صحبت بکنیم.
سوال: به نظر شما تفاوت عمده بین فریم وورک (Framework) و کتابخانه (Library) در چیست؟
جواب: حتما شما به تفاوت های متعددی اشاره کردین و شاید مهم ترین چیزی که به ذهنتون رسیده باشه این باشه که فریم وورک ها مجموعه ای از کتابخانه ها هستند. قطعا همین طور هستش و همه مواردی که بهش فکر کردین جزو تفاوت ها و ویژگی های هر کدوم از ایناست. اما یک تفاوت مهم که این جا می خوایم بهش اشاره بکنیم که شاید پایه ای ترین تفاوت این دو مورد باشه، بحث نوع کنترل هستش. به صورت عادی ماها توی برنامه نویسی وقتی از کتابخونه ها یا هر تابعی استفاده می کنیم، ما اون رو صدا می زنیم و ورودی هامون رو بهش می دیم و اونم خروجی ها رو بهمون می ده. این روش متداول ترین روش کنترل است، که کنترل مستقیم شاید بشه اسمش رو گذاشت. اما نکته ای که هست اینه که وقتی ما از یک فریم وورک استفاده می کنیم، این جا اون فریم وورک هست که کد های ما رو تحت کنترل خودش داره؛ یعنی چی؟ همون جوری که توی شکل 2 می بینید، به زبان ساده بخوایم صحبت کنیم فریم وورک ها یک سری موارد رو تعیین می کنند که ما باید با کد خودمون اون جا ها رو پر کنیم تا کارایی مورد نیاز خودمون رو از فریم وورک بگیریم، اگه بخوایم یکم تخصصیش کنیم یعنی فریم وورک ها به صورت ابسترکت (Abstract) مواردی که بهشون نیاز دارند رو تعیین می کنند و ما اون ها رو مطابق با میل خودمون و کارایی که می خوایم فریم وورک برای ما داشته باشه تعیین می کنیم. به این حالت که فریم وورک داره کد ما رو کنترل می کنه، کنترل معکوس (Inversion Of Control) می گیم.
از یک زاویه دیگه اگه بهش نگاه بکنیم، یک فریم وورک به صورت کلی یک کارایی ثابت داره، مثلا فریم وورک های بک اند، کارشون این هست که یک درخواستی رو از کلاینت بگیرند و یک سری وظایف رو انجام بدن و بعدش یک جوابی رو به کلاینت بفرستن. اما کتابخانه ها ممکنه هرجایی کاربرد داشته باشند، مثلا یک کتابخونه ای که با الگوریتم های مختلف سورت انجام می ده، ما می تونیم هرجایی برای هر کاری استفاده بکنیم ازش و یک کاربری خاص منظوره دیگه نداره.
امیدوارم تفاوت عمده این دو مورد رو متوجه شده باشین ?. اگه بازم جاییش ابهام داشت خوشحال می شم توی کامنت ها با من در میون بگذارید.
وابستگی وارون، می شه همون بحثی که توی فریم وورک کردیم با هم دیگه. اما یک تعریف خوب بخوایم ازش بکنیم یعنی ماژول های نرم افزاری یک سیستم نرم افزاری را به گونه ای از هم جداسازی کنیم و ارتباطاتشون رو شکل بدیم که وابستگی اونا به هم دیگه به شکلی باشه که اجزای بزرگ وابسته به نحوه پیاده سازی اجزای کوچک تر نباشند.
البته این تعریف شاید یک مقداری گنگ باشه، جزء بزرگ و کوچک یک چیز کاملا نسبی هستش، شاید یک جزء از نظر شما کوچک باشه ولی از نظر طراح سیستم بزرگ باشه. مهم اینه که سیستمی که طراحی می شه کم ترین وابستگی رو به نحوه پیاده سازی ماژول های خودش داشته باشه. یعنی اگه یک ماژول رو برداشتیم و یک ماژول دیگه جایگزین اون کردیم که پیاده سازیش کلا با قبلی فرق داشت، سیستم بدون هیچ اشکالی همون عملکرد قبلی رو داشته باشه.
برای ملموس تر شدن موضوع یک مثال می زنیم: فرض کنید می خوایم توی یک سیستم به دیتابیس متصل بشیم، خب برای این که کدمون از قواعد مهندسی نرم افزار تبعیت کنه، یک کلاس مجزا برای ارتباط با دیتابیس طراحی می کنیم که رابط ما با دیتابیس MSSQL باشه. حالا اگر در جایی مجبور بشیم به جای این دیتابیس از دیتابیس مثلا MySQL استفاده بکنیم، دچار مشکل می شیم چرا که این کلاس خاص منظوره برای دیتابیس MSSQL طراحی شده و لازمه تغییرات اساسی ای توی کلاس ایجاد کنیم. چالشی که باهاش رو به رو می شیم این هست که لازمه یک کلاس دیگه ایجاد کنیم برای این کار و در نتیجه لازمه یک بار دیگه کل سیستم کامپایل بشه، که جفت این موارد بسیار زمان بر و هزینه بر خواهد بود.
برای حل این مشکل همانگونه که در شکل 3 مشاهده می کنید، یک اینترفیس به صورت ابسترکت موارد مورد نیاز را پیاده سازی می کنیم و سپس هر یک از کلاس های مورد نیاز با پیاده سازی این اینترفیس قابلیت اتصال به برنامه را پیدا می کنند، که نحوه اتصال به شکل تزریق وابستگی می باشد که در ادامه توضیح می دهیم.
همانگونه که در بحث وابستگی وارون مطرح کردیم، یک اینترفیس به صورت ابسترکت به پیاده سازی میپردازیم که هماهنگونه که در شکل 4 مشاهده می کنید با نام سرویس مشخص شده است که در صورتی که مطابق با قواعد آن پیاده سازی صورت گیرد قابل تزریق است.
درخواست یک سرویس از سمت کلاینت اعلام می شود و مطابق با تنظیمات صورت گرفته در سیستم نرمافزاری تزریق کننده جواب این درخواست را به کلاینت ارسال می کند. یعنی به نوعی سرویس تزریق شده از آن به بعد به عنوان یک شیء برای کلاینت شناخته می شود و عملیات های لازم صورت می گیرد.
برای ساده سازی مطلب یکی از ساده ترین روش های این کار، ایجاد یک دیکشنری به صورت کلید-مقداری که زمانی که درخواست یک سرویس ارسال شد، در این آرایه جست و جو انجام شود و سرویس متصل یافت شود و به کلاینت ارسال شود.
البته که این بحث به شکل های مختلفی امکان پذیر است، که شاید از جمله معرف ترین روش های این کار عبارت است از:
+ به ازای درخواست: یعنی به ازای هر درخواست ارسال شده یک ترد (thread) مجزا ایجاد شود و از سرویس شیء مجزایی ساخته شود.
+ بر اساس کاربر: یعنی به ازای هر کاربر براساس شناسایی کاربران، یک ترد (thread) مجزا ایجاد شود و از سرویس شیء مجزایی ساخته شود.
+ تنها یک ترد: یعنی تنها یک ترد و یک شیء از سرویس ایجاد شود و به نوعی به صورت استاتیک از این کلاس استفاده شود.
البته که روش های دیگری نیز می تواند در نظر گرفت برای تزریق وابستگی.
نکته مهمی که قبل از این هم اشاره کردیم، بحث زمان بر بودن کامپایل دوباره (Re-Compiling) یکی از مهم ترین بحث هایی است که در کتاب های توسعه نرم افزار های شرکتی نیز به آن اشاره شده است و حتی برای آن ها پیشنهاداتی داده شده است که توسعه دهنده ها می بایست در این زمان چه کار هایی انجام دهند، چرا که در سیستم های بزرگ کامپایل دوباره بسیار زمان بر خواهد بود و تا جای ممکن لازم است از کامپایل دوباره سیستم دوری کرد.
همانگونه که قبل از این اشاره شد تغییر در کد ها و اضافه کردن کلاس های پیاده سازی اینترفیس ها طبیعتا لازم است تا کامپایل دوباره صورت گیرد. برای جلوگیری از این کار لازم است تا فایلی را برای این کار اختصاص داد که ارتباط میان کلاس ها و اینترفیس ها را ایجاد کند. این فایل به نوعی همان تنظیمات تزریق های وابستگی هستند. که اصطلاحا به آن IOC Container که مخفف Inversion Of Control Container می باشد، می گویند. به عنوان مثال در فریم وورک های همچون Spring و ASP.net این فایل تحت عنوان json فایل در کنار برنامه قرار دارد مطابق با شماتیک شکل 5 ارتباطات این ماژول ها و موارد دیگر را تعین می کند.
این فایل این امکان را فراهم می کند که بعد از کامپایل شدن سیستم بتوان اتصالات ماژول ها به یک دیگر را تغییر داد و دیگر نیازی به کامپایل مجدد نباشد.
امیدوارم مفید بوده باشه براتون ?
حتما نظراتتون رو با من در میون بزارید
• Dr. R.Khosravi. “Internet Engineering Course – Tehran University” 2018 - https://maktabkhooneh.org/course/%D9%85%D9%87%D9%86%D8%AF%D8%B3%DB%8C-%D8%A7%DB%8C%D9%86%D8%AA%D8%B1%D9%86%D8%AA-mk208
• Kirk Larkin, Steve Smith, Scott Addie, Brandon Dahler. “Dependency injection in ASP.NET Core – Microsoft Documentation” 2020 - https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-3.1