تزریق وابستگی (DI) الگویی است که در آن کامپوننت های لازم برای اجرای کد شما قابل تعویض هستند. این بدان معناست که وابستگیهای شما در پیادهسازی شما کدگذاری سختی ندارند و میتوانند با تغییر محیط شما تغییر کنند.با وراثت فعال شده است، DI یک الگوی به خوبی استفاده شده در برنامه نویسی شی گرا (OOP) است که برای استفاده مجدد از کد در اشیاء و کلاس های مختلف طراحی شده است. با این حال، دلیل اصلی استفاده از تزریق وابستگی در React، ماک و آزمایش آسان کامپوننت های React است. برخلاف Angular، DI در حین کار با React یک الزام نیست، بلکه ابزاری مفید برای استفاده در هنگام تمیز کردن چیزها است.
اگرچه مفاهیم وارونگی کنترل، تزریق وابستگی و وارونگی وابستگی دارای نقاط مشترکی هستند، تمایز بین آنها می تواند چالش برانگیز باشد. در این مقاله به بررسی هر یک از این مفاهیم می پردازیم و نمونه های عملی از نحوه استفاده از آنها در برنامه های React را بررسی می کنیم. برای آشنایی با تفاوت های بین تزریق وابستگی، وارونگی کنترل و وارونگی وابستگی می توانید این مقاله را بررسی کنید.
به عنوان توسعه دهندگان، ما با انواع بهترین شیوه ها و دیزاین پترن ها در کارهای روزمره خود کار می کنیم. چه از آنها آگاه باشیم و چه به سادگی از کدهایی که به ما تحویل داده شده پیروی می کنیم، دانستن این شیوه ها می تواند درک بهتری از اینکه چرا از روش های تکراری خاصی پیروی می کنیم و چگونه می توانیم پروژه های معماری بهتری را در دراز مدت ایجاد کنیم، به ما ارائه دهد. برای آشنایی بیشتر با مفهموم تزریق وابستگی این مقاله را بررسی کنید.
وارونگی کنترل
وارونگی کنترل (IoC) یک الگوی طراحی است که در آن جریان کنترل یک برنامه کامپیوتری توسط بخشی از برنامه غیر از برنامه اصلی مدیریت می شود.
یکی از پیادهسازیهای IoC، الگوی طراحی تزریق وابستگی است که زمانی استفاده میشود که یک تابع نیاز به استفاده از یک سرویس داشته باشد. با تزریق وابستگی، تابع نیازی به دانستن جزئیات نحوه عملکرد سرویس ندارد. در عوض، کد خارجی سرویس باید به عنوان یک وابستگی ارائه شود (تزریق شود).
ما میتوانیم رویکرد مشابهی را با callback ها در جاوا اسکریپت ببینیم. هنگامی که یک کنترلر تماس را به یک تابع ارسال می کنیم، مسئولیت فراخوانی کنترلر را در زمان مناسب و با مقادیر مناسب به آن تابع واگذار می کنیم. این الگوی طراحی، ماژولار بودن را افزایش میدهد و در نتیجه سیستمهای جفتشدهتری ایجاد میکند که جداسازی نگرانیها و سادگی را ترویج میکنند.
با این حال، هیچ خوبی بدون هزینه به دست نمی آید. از آنجایی که ما کنترل برنامه را به عملکرد دیگری می دهیم، باید قبل از اعتماد به کتابخانه ها یا خدمات شخص ثالث دو بار فکر کنیم.
خب، قبل از آشنایی با تزریق وابستگی در ری اکت، ابتدا با تزریق وابستگی در جاوا اسکریپت را آشنا شویم.
تزریق وابستگی در جاوا اسکریپت
برای نشان دادن اصول DI، یک ماژول npm
را تصور کنید که تابع ping
زیر را نشان می دهد:
export const ping = (url) => {
return new Promise((res) => {
fetch(url)
.then(() => res(true))
.catch(() => res(false))
})
}
استفاده از تابع ping
در یک مرورگر مدرن به خوبی کار می کند.
import { ping } from "./ping"
ping("https://logrocket.com").then((status) => {
console.log(status ? "site is up" : "site is down")
})
اما اجرای این کد در Node.js باعث خطا می شود زیرا fetch در Node.js پیاده سازی نشده است. با این حال، پیادهسازیهای fetch و polyfillهای زیادی برای Node.js وجود دارد که میتوانیم از آنها استفاده کنیم. برای آشنایی با تزریق وابستگی در Node.js می توانید این مقاله را بررسی کنید.
DI به ما امکان می دهد واکشی را به یک وابستگی تزریقی پینگ تبدیل کنیم، مانند موارد زیر:
export const ping = (url, fetch = window.fetch) => {
return new Promise((res) => {
fetch(url)
.then(() => res(true))
.catch(() => res(false))
})
}
ما نیازی به دادن مقدار پیشفرض window.fetch
به fetch
نداریم، اما مجبور نیستیم هر بار که از پینگ استفاده میکنیم آن را اضافه کنیم، تجربه توسعه بهتری را تجربه میکنیم.
اکنون، در یک محیط Node، میتوانیم از node-fetch
در ارتباط با تابع پینگ خود استفاده کنیم، مانند:
import fetch from "node-fetch"
import { ping } from "./ping"
ping("https://logrocket.com", fetch).then((status) => {
console.log(status ? "site is up" : "site is down")
})
کار با وابستگی های متعدد
اگر چندین وابستگی داشته باشیم، ادامه افزودن آنها به عنوان پارامتر امکان پذیر نخواهد بود: func (param، dep1، dep2، dep3،…)
. در عوض، یک گزینه بهتر این است که یک شی برای وابستگی ها داشته باشید:
const ping = (url, deps) => {
const { fetch, log } = { fetch: window.fetch, log: console.log, ...deps }
log("ping")
return new Promise((res) => {
fetch(url)
.then(() => res(true))
.catch(() => res(false))
})
}
ping("https://logrocket.com", {
log(str) {
console.log("logging: " + str)
}
})
عمق پارامتر ما در یک شیء پیاده سازی پخش می شود و توابعی را که ارائه می دهد override می کند. با تخریب ساختار از این شیء اصلاح شده، از ویژگی های باقی مانده به عنوان وابستگی استفاده می شود.
با استفاده از این الگو، میتوانیم یکی از وابستگیها را نادیده بگیریم اما بقیه را نه.
تزریق وابستگی در React
در حین کار با React، ما از هوک های سفارشی برای واکشی داده ها، ردیابی رفتار کاربر و انجام محاسبات پیچیده استفاده زیادی می کنیم. نیازی به گفتن نیست که ما نمی خواهیم (و نه می توانیم) این هوک ها را در همه محیط ها اجرا کنیم. برای آشنایی با ویژگی های ری اکت 19 می توانید این مقاله را بررسی کنید.
ردیابی بازدید از صفحه در طول آزمایش، دادههای تحلیلی ما را خراب میکند، و واکشی دادهها از یک بک اند واقعی به آزمایشهای آهسته تبدیل میشود.
تست تنها چنین محیطی نیست. پلتفرمهایی مانند Storybook اسناد را ساده میکنند و میتوانند بدون استفاده از بسیاری از هوکها و منطق تجاری ما کار کنند.
در React، بلوک های سازنده ما کامپوننت هستند. اگر مفهوم تزریق وابستگی را به React اعمال کنیم، به این معنی است که وقتی یک کامپوننت نیاز به استفاده از یک تابع یا داده (مانند یک سرویس) دارد، آن سرویس به یک مجری نیاز دارد که وقتی کامپوننت آن را فراخوانی می کند، کنترل را به دست بگیرد. ما می توانیم این معماری را در مکان های مختلف برنامه های خود ببینیم.
یک مثال ساده میتواند زمانی باشد که دادهها یا تابعی را از یک کامپوننت والد به کامپوننت فرزند ارسال میکنیم، یا زمانی که از هوکهای سفارشی برای استفاده مجدد از برخی تغییرات در دادهها استفاده میکنیم که در چندین کامپوننت استفاده میشوند.
اما بیایید در سطح سیستم به آن نگاه کنیم، زمانی که به اطلاعات در یک کامپوننت سطح بالا نیاز داریم در حالی که برخی از کامپوننت های سطح پایینی داریم که به داده ها نیاز دارند و ممکن است نیاز به تغییر آن داشته باشیم. این ایده خوبی نیست که داده های زبان برنامه یا اطلاعات موضوعی را به صورت دستی از طریق هر عنصر در صفحه منتقل کنید.
میتوانیم از Context API برای به اشتراک گذاشتن این نوع دادهها بین کامپوننتها استفاده کنیم، بدون اینکه نیازی به ارسال آن از طریق props باشد. با انجام این کار، می توانیم از props drilling جلوگیری کنیم و اطمینان حاصل کنیم که داده ها برای هر عنصری که به آن نیاز دارد در دسترس است. با به حداقل رساندن فرکانس ارسال داده ها از طریق چندین کامپوننت، می توانیم یک پایگاه کد سازمان یافته تر و کارآمدتر ایجاد کنیم. این کار نگهداری و به روز رسانی برنامه ما را در طول زمان آسان تر می کند.
همانطور که در مثال زیر می بینید، ما Context و ارائه دهنده را ایجاد می کنیم. می توان آن را با قرار دادن روی یک کامپوننت سطح بالا و مصرف زبان برنامه از طریق هوک useContext
در هر فرزند آن استفاده کرد.
تزریق وابستگی از طریق props
برای مثال کامپوننت زیر را در نظر بگیرید:
import { useTrack } from '~/hooks'
function Save() {
const { track } = useTrack()
const handleClick = () => {
console.log("saving...")
track("saved")
}
return <button onClick={handleClick}>Save</button>
}
همانطور که قبلا ذکر شد، اجرای useTrack
(و با فرمت، track) چیزی است که باید از آن اجتناب کرد. بنابراین، ما useTrack
را به وابستگی مولفه Save از طریق props تبدیل می کنیم:
import { useTracker as _useTrack } from '~/hooks'
function Save({ useTrack = _useTrack }) {
const { track } = useTrack()
/* ... */
}
با نام مستعار useTracker برای جلوگیری از برخورد نام و استفاده از آن به عنوان مقدار پیشفرض یک prop، ما هوک را در برنامه خود حفظ میکنیم و میتوانیم هر زمان که نیاز باشد آن را لغو کنیم.
نام useTracker_
یکی از قراردادهای نامگذاری است: useTrackImpl
، useTrackImplementation
، و useTrackDI
همگی قراردادهایی هستند که به طور گسترده برای جلوگیری از برخورد استفاده می شوند.
در داخل Storybook، میتوانیم با استفاده از یک پیادهسازی ماک، هوک را override کنیم.
import Save from "./Save"
export default {
component: Save,
title: "Save"
}
const Template = (args) => <Save {...args} />
export const Default = Template.bind({})
Default.args = {
useTrack() {
return { track() {} }
}
}
با استفاده از TypeScript
هنگام کار با TypeScript، مفید است که به توسعه دهندگان دیگر اطلاع دهید که پایه تزریق وابستگی دقیقاً همین است و از نوع دقیق پیاده سازی برای حفظ ایمنی نوع استفاده کنید:
function App({ useTrack = _useTrack }: Props) {
/* ... */
}
interface Props {
/**
* For testing and storybook only.
*/
useTrack?: typeof _useTrack
}
تزریق وابستگی از طریق Context API
کار با Context API باعث می شود که تزریق وابستگی شبیه یک شهروند درجه یک React باشد. داشتن قابلیت تعریف مجدد زمینه ای که در آن هوک های ما در هر سطحی از کامپوننت اجرا می شوند، هنگام تعویض محیط مفید است.
بسیاری از کتابخانه های معروف، پیاده سازی های ماک ارائه دهندگان خود را برای اهداف آزمایشی ارائه می کنند. React Router v5 دارای MemoryRouter
است، در حالی که Apollo Client یک MockedProvider
ارائه می دهد. اما، اگر ما از یک رویکرد مبتنی بر DI استفاده کنیم، چنین ارائه دهندگان ماک شده ای ضروری نیستند.
React Query نمونه بارز این موضوع است. ما میتوانیم از یک ارائهدهنده هم در توسعه و هم در آزمایش استفاده کنیم و آن را به کلاینت های مختلف در هر محیط ارائه دهیم.
در توسعه، ما میتوانیم از یک queryClient
با تمام گزینههای پیشفرض دست نخورده استفاده کنیم.
import { QueryClient, QueryClientProvider } from "react-query"
import { useUserQuery } from "~/api"
const queryClient = new QueryClient()
function App() {
return (
<QueryClientProvider client={queryClient}>
<User />
</QueryClientProvider>
)
}
function User() {
const { data } = useUserQuery()
return <p>{JSON.stringify(data)}</p>
}
اما هنگام آزمایش کد ما، ویژگیهایی مانند تلاش مجدد، واکشی مجدد در فوکوس پنجره، و زمان حافظه پنهان، همگی میتوانند بر این اساس تنظیم شوند.
// storybook/preview.js
import { QueryClient, QueryClientProvider } from "react-query"
const queryClient = new QueryClient({
queries: {
retry: false,
cacheTime: Number.POSITIVE_INFINITY
}
})
/** @type import('@storybook/addons').DecoratorFunction[] */
export const decorators = [
(Story) => {
return (
<QueryClientProvider client={queryClient}>
<Story />
</QueryClientProvider>
)
},
]
تزریق وابستگی در React منحصر به هوک ها نیست، بلکه JSX، JSON و هر چیزی که بخواهیم تحت شرایط مختلف انتزاع کنیم یا تغییر دهیم نیز می باشد.
جایگزین های تزریق وابستگی
بسته به زمینه، تزریق وابستگی ممکن است ابزار مناسبی برای کار نباشد. برای مثال، هوکهای واکشی دادهها بهتر است با استفاده از یک رهگیر (مانند MSW) به جای تزریق هوکها در سراسر کد آزمایشی مورد ماک قرار گیرند، و توابع ماک آشکار همچنان یک ابزار پیشرفته و دست و پا گیر برای مشکلات بزرگتر هستند.
اصل وارونگی وابستگی
"D" در اصول طراحی SOLID بیانگر اصل وارونگی وابستگی است. این اصل بر روابط وابستگی سنتی بین ماژولهای سطح بالا و سطح پایین تأکید میکند، که هر دو به یک انتزاع بستگی دارند که به عنوان پل ارتباطی بین آنها عمل میکند.
این اصل به کاهش وابستگی متقابل بین ماژولها کمک میکند و تغییر، جایگزینی یا آزمایش کامپوننت های جداگانه را بدون تأثیر بر کل سیستم آسانتر میکند.
در نتیجه، اصل وارونگی وابستگی همان مقادیر تزریق وابستگی را در تصویر بزرگ ترویج می کند، اما آنها را به روشی متفاوت به دست می آورد.
وارونگی وابستگی در React
هنگام ساخت یک برنامه React، ما اغلب از کامپوننت های سطح پایین مانند دکمه ها استفاده می کنیم. با این حال، دکمهها میتوانند انواع، سبکها، حالتها و انواع مختلفی داشته باشند که آنها را پیچیدهتر از آنچه در ابتدا به نظر میرسد میسازد.
برای استفاده از یک دکمه در یک کامپوننت سطح بالا، مانند یک صفحه یا فرم اصلی، باید یک انتزاع از کامپوننت دکمه را وارد کنیم که تمام پیچیدگی های درون شاخه فرعی آن را در بر دارد و فقط یک API نهایی را در انتزاع نمایش می دهد.
این رویکرد به ما امکان میدهد تا زمانی که API در معرض نمایش یکسان باقی میماند، سبکها، حالتها و رفتار دکمهها را بدون تغییر چیزی در کامپوننتهای سطح بالا و بدون تأثیرگذاری بر عملکرد کلی برنامه تغییر دهیم.
این به ویژه هنگام استفاده از سیستم های طراحی شخص ثالث مفید است، زیرا ما می توانیم به روز رسانی ها و تغییرات جدید آن سیستم طراحی را تنظیم کرده و به آن واکنش نشان دهیم یا حتی کل سیستم طراحی را با سیستم دیگری تغییر دهیم، تنها با اعمال تغییرات در انتزاع دکمه خود (کم- کامپوننت های سطح) در حالی که کامپوننت های سطح بالا دست نخورده باقی می مانند.
مثال UML نشان میدهد که تا زمانی که کامپوننت های اصلی و پیشفرض «MyProjectButton» تغییر نکنند، کامپوننتهای سطح بالا نیازی به دانستن نحوه ایجاد، سبکدهی و نگهداری دکمه ندارند.
هنگامی که اصل وارونگی وابستگی را پیاده سازی می کنیم، می توانیم استایل نوع اولیه را در کامپوننت دکمه خود فقط در «MyProjectButton» تغییر دهیم و بر این اساس همه موارد استفاده در عناصر سطح بالا تحت تأثیر قرار خواهند گرفت. بدون وارونگی وابستگی، ما باید تغییرات را در دو مکان (در حال حاضر) پیادهسازی کنیم و تکرار کد حفظ ثبات را با رشد پروژه و گذر زمان دشوار میکند.
چرا باید از تزریق وابستگی استفاده کرد؟
دلایل استفاده از DI:
بدون سربار در توسعه، آزمایش یا تولید
اجرای بسیار آسان
نیازی به کتابخانه ماک ندارد زیرا بومی جاوا اسکریپت است
برای تمام نیازهای شما مانند کامپوننت ها، کلاس ها و عملکردهای معمولی کار می کند
دلایل عدم استفاده از DI:
ورودی ها و کامپوننت های سازنده شما / API را به هم می زند
ممکن است برای توسعه دهندگان دیگر گیج کننده باشد
نتیجه
در این مقاله، نگاهی به راهنمای بدون کتابخانه برای تزریق وابستگی در جاوا اسکریپت انداختیم و استفاده از آن را در React برای آزمایش و مستندسازی مورد استفاده قرار دادیم. ما از Storybook برای نشان دادن استفاده خود از DI استفاده کردیم، و در نهایت، دلایلی را که چرا باید و نباید از DI در کد خود استفاده کنید، منعکس کردیم.