کانتینرهای تزریق وابستگی در جاوا اسکریپت — از صفر تا صد
جاوا اسکریپت به جهت ماهیت انعطافپذیر خود امکان استفاده از تکنیکهای مختلفی را فراهم میسازد. در این مقاله به بررسی کانتینرهای تزریق وابستگی در جاوا اسکریپت خواهیم پرداخت. این الگو در عمل همان هدف تزریق وابستگی را اما به روشی انعطافپذیر و قدرتمند ارائه میکند و کانتینرها به عنوان میزبانهای وابستگیها تابع (یا کلاس) عمل میکنند که در موارد نیاز مانند مرحله مقداردهیشان به آنها دسترسی پیدا میکنیم.
تزریق وابستگی بدون کانتینر
در این بخش به مرور سریع ماهیت تزریق وابستگی، شیوه نمایش آن در کد، مشکلاتی که حل میکند و مشکلاتی که از آنها رنج میبرد خواهیم پرداخت. تزریق وابستگی الگویی است که به ما کمک میکند تا از هاردکد کردن وابستگیها در ماژولها جلوگیری کنیم و به فراخوانی کننده این امکان را بدهیم که آنها را تغییر داده و در صورت نیاز موارد مورد نظر خود را ارائه کند.
این وابستگیها میتوانند در مرحله سازنده (وهلهسازی) تزریق شوند و یا در ادامه آنها را با یک متد setter تزریق کنیم:
1class Frog {
2 constructor(name, gender) {
3 this.name = name
4 this.gender = gender
5 }
6
7 jump() {
8 console.log('jumped')
9 }
10}
11
12class Toad {
13 constructor(habitat, name, gender) {
14 this.habitat = habitat
15 this.frog = new Frog(name, gender)
16 }
17}
18
19const mikeTheToad = new Toad('land', 'mike', 'male')
این روش برخی مشکلات دارد که در ادامه آنها را توضیح میدهیم.
مشکل شماره 1
اگر لازم باشد شیوه ساخته شدن Toad را تغییر دهیم و از چیزی شکننده مانند موقعیتیابی آرگومانها یا ساختمان داده آنها استفاده کنیم، باید کد را به صورت دستی تغییر دهیم، زیرا در بلوک کد هاردکد شده است.
به عنوان مثال این سناریو در زمانی اتفاق میافتد که بخواهیم یک «تغییر ناسازگار» (breaking change) در کلاس ایجاد کنیم. اگر یک پارامتر سوم مانند weight به سازنده Frog اضافه کنیم، چنین اتفاقی رخ میدهد:
1class Toad {
2 constructor(habitat, name, gender, weight) {
3 this.habitat = habitat
4 this.frog = new Frog(name, gender, weight)
5 }
6}
در این صورت Toud باید بهروزرسانی شود، زیرا این وابستگی جدید در مرحله وهلهسازی Frog اضافه شده است:
1class Toad {
2 constructor(habitat, name, gender, weight) {
3 this.habitat = habitat
4 this.frog = new Frog(name, gender, weight)
5 }
6}
بنابراین اگر آن را به همین شیوه حفظ کنیم، به طور مکرر باید با هر تغییری که در پروژه رخ میدهد اقدام به بهروزرسانی کلاس Frog بکنیم که به طور بدیهی وضعیت نامطلوبی است.
مشکل شماره 2
باید هر بار بدانیم کدام وابستگی برای Toad استفاده شده است. مشکل دیگر این رویکرد آن است که باید بدانیم Toad هم اکنون به چهار آرگومان دقیقاً به همان ترتیب که در وهله Frog مقداردهی شده است نیاز دارد و حتی باید انواع داده آنها را نیز بدانیم، چون در غیر این صورت به آسانی باگ ایجاد میشود.
این وضعیت در صورتی که بدانیم Toad اساساً یک Frog است بغرنجتر میشود چون با دانستن این نکته ممکن است به صورت تصادفی تصور کنید که Toad باید Forg را بسط داده باشد.
از این رو درک میکنیم که به جای آن یک وهله از Frog درون Toad ایجاد شده است و اینک همه چیز با هم مخلوط میشود زیرا شما یک انسان هوشمند هستید و کدی که مقابل شما قرار دارد با دنیای واقعی همخوانی ندارد.
مشکل شماره 3
مشکل بعدی رویکرد فوق، گنجاندن کد غیر ضروری بیشتر است. و با استفاده از الگوی تزریق وابستگی، این مشکلها از طریق معکوس کردن کنترل روش وهلهسازی از وابستگیها حل میشود:
1class Frog {
2 constructor({ name, gender, weight }) {
3 this.name = name
4 this.gender = gender
5 this.weight = weight
6 }
7
8 jump() {
9 console.log('jumped')
10 }
11}
12
13class Toad {
14 constructor(habitat, frog) {
15 this.habitat = habitat
16 this.frog = frog
17 }
18}
این کد آسانی است. اکنون زمانی که تغییر ناسازگار دیگری در Frog رخ دهد، مثلاً آرگومانها درون شیء جاوا اسکریپت قرار گیرند، دیگر حتی لازم نیست به Toad مراجعه کنیم یا وقت خود با خواندن Toad و سپس Frpg و سپس بازگشتن به Toad و همین طور تا آخر به هدر بدهیم.
دلیل این امر آن است که اینک میتوانیم بخشی که یک وهله از Toad را ایجاد میکند تغییر دهیم. این وضعیت بسیار بهتر از این است که مجبور باشیم وارد قضایا شویم و کد مربوط به پیادهسازی Toad را تغییر دهیم که رویه بدی محسوب میشود.
دیگر لازم نیست در مورد شیوه ساخته شدن frog نگران باشد و باید تنها بداند که frog را به عنوان یک آرگومان میگیرد و آن را در مشخصه frog. برای استفادههای آتی ذخیره میکند. اکنون مسئولیت وابستگیهای آن را بر عهده میگیرید:
1const mikeTheToad = new Toad(
2 'land',
3 new Frog({
4 name: 'mike',
5 gender: 'male',
6 weight: 12.5,
7 }),
8)
بنابراین برخی رویههای کدنویسی تمیز را با جدا کردن جزییات پیادهسازی Frog از سازنده Toad تمرین کردهایم. این کار به این جهت مطلوب است که دیگر لازم نیست toad در مورد شیوه ساخته شدن Frog اطلاع داشته باشد و هر چه که باشد میتواند آن را بسط دهد.
الگوی کانتینر تزریق وابستگی (DIC)
اکنون که مفاهیم تزریق وابستگی را با هم مرور کردیم، نوبت آن رسیده که در مورد کانتینر تزریق وابستگی صحبت کنیم. سؤال این است که چرا به الگوی DIC نیاز داریم و چرا تزریق وابستگی بدون کانتینر در برخی سناریوهای پیچیده به تنهایی کافی نیست؟
مشکل اینجا است که این رویه مقیاسپذیر نیست. هر چه پروژه بزرگتر شود، اعتماد به نگهداری کد در بلندمدت کاهش مییابد، زیرا در طی زمان شلوغتر میشود. به علاوه، باید ترتیب تزریقهای وابستگی را نیز در ترتیب معینی حفظ کنید، به طوری که با مشکل undefined شدن یک بخش در زمان وهلهسازی بخش دیگر مواجه نشوید. بنابراین اساساً شش ماه بعد، کد ما به چیزی مانند زیر تبدیل میشود:
1class Frog {
2 constructor({ name, gender, weight }) {
3 this.name = name
4 this.gender = gender
5 this.weight = weight
6 }
7
8 jump() {
9 console.log('jumped')
10 }
11
12 setHabitat(habitat) {
13 this.habitat = habitat
14 }
15}
16
17class Toad extends Frog {
18 constructor(options) {
19 super(options)
20 }
21
22 leap() {
23 console.log('leaped')
24 }
25}
26
27class Person {
28 constructor() {
29 this.id = createId()
30 }
31
32 setName(name) {
33 this.name = name
34 return this
35 }
36
37 setGender(gender) {
38 this.gender = gender
39 return this
40 }
41
42 setAge(age) {
43 this.age = age
44 return this
45 }
46}
47
48function createId() {
49 var idStrLen = 32
50 var idStr = (Math.floor(Math.random() * 25) + 10).toString(36) + '_'
51 idStr += new Date().getTime().toString(36) + '_'
52 do {
53 idStr += Math.floor(Math.random() * 35).toString(36)
54 } while (idStr.length < idStrLen)
55 return idStr
56}
57
58class FrogAdoptionFacility {
59 constructor(name, description, location) {
60 this.name = name
61 this.description = description
62 this.location = location
63 this.contracts = {}
64 this.adoptions = {}
65 }
66
67 createContract(employee, client) {
68 const contractId = createId()
69 this.contracts[contractId] = {
70 id: contractId,
71 preparer: employee,
72 client,
73 signed: false,
74 }
75 return this.contracts[contractId]
76 }
77
78 signContract(id, signee) {
79 this.contracts[id].signed = true
80 }
81
82 setAdoption(frogOwner, frogOwnerLicense, frog, contract) {
83 const adoption = {
84 [frogOwner.id]: {
85 owner: {
86 firstName: frogOwner.owner.name.split(' ')[0],
87 lastName: frogOwner.owner.name.split(' ')[1],
88 id: frogOwner.id,
89 },
90 frog,
91 contract,
92 license: {
93 id: frogOwnerLicense.id,
94 },
95 },
96 }
97 this.adoptions[contract.id] = adoption
98 }
99
100 getAdoption(id) {
101 return this.adoptions[id]
102 }
103}
104
105class FrogParadiseLicense {
106 constructor(frogOwner, licensePreparer, frog, location) {
107 this.id = createId()
108 this.client = {
109 firstName: frogOwner.name.split(' ')[0],
110 lastName: frogOwner.name.split(' ')[1],
111 id: frogOwner.id,
112 }
113 this.preparer = {
114 firstName: licensePreparer.name.split(' ')[0],
115 lastName: licensePreparer.name.split(' ')[1],
116 id: licensePreparer.id,
117 }
118 this.frog = frog
119 this.location = `${location.street} ${location.city} ${location.state} ${location.zip}`
120 }
121}
122
123class FrogParadiseOwner {
124 constructor(frogOwner, frogOwnerLicense, frog) {
125 this.id = createId()
126 this.owner = {
127 id: frogOwner.id,
128 firstName: frogOwner.name.split(' ')[0],
129 lastName: frogOwner.name.split(' ')[1],
130 }
131 this.license = frogOwnerLicense
132 this.frog = frog
133 }
134
135 createDocument() {
136 return JSON.stringify(this, null, 2)
137 }
138}
کل فرایند adoption زمانی که setAdoption از FrogAdoptionFacility فراخوانی میشود، پایان مییابد. بیاید فرض کنیم که شروع به توسعه کد با استفاده از این کلاسها کردهایم و در نهایت به نسخهای مانند زیر میرسیم:
1const facilityTitle = 'Frog Paradise'
2
3const facilityDescription =
4 'Your new one-stop location for fresh frogs from the sea! ' +
5 'Our frogs are housed with great care from the best professionals all over the world. ' +
6 'Our frogs make great companionship from a wide variety of age groups, from toddlers to ' +
7 'senior adults! What are you waiting for? ' +
8 'Buy a frog today and begin an unforgettable adventure with a companion you dreamed for!'
9
10const facilityLocation = {
11 address: '1104 Bodger St',
12 suite: '#203',
13 state: 'NY',
14 country: 'USA',
15 zip: 92804,
16}
17
18const frogParadise = new FrogAdoptionFacility(
19 facilityTitle,
20 facilityDescription,
21 facilityLocation,
22)
23
24const mikeTheToad = new Toad({
25 name: 'mike',
26 gender: 'male',
27 weight: 12.5,
28})
29
30const sally = new Person()
31sally
32 .setName('sally tran')
33 .setGender('female')
34 .setAge(27)
35
36const richardTheEmployee = new Person()
37richardTheEmployee
38 .setName('richard rodriguez')
39 .setGender('male')
40 .setAge(77)
41
42const contract = frogParadise.createContract(richardTheEmployee, sally)
43frogParadise.signContract(contract.id, sally)
44
45const sallysLicense = new FrogParadiseLicense(
46 sally,
47 richardTheEmployee,
48 mikeTheToad,
49 facilityLocation,
50)
51
52const sallyAsPetOwner = new FrogParadiseOwner(sally, sallysLicense, mikeTheToad)
53frogParadise.setAdoption(sallyAsPetOwner, sallysLicense, mikeTheToad, contract)
54
55const adoption = frogParadise.getAdoption(contract.id)
56console.log(JSON.stringify(adoption, null, 2))
اگر این کد را اجرا کنیم، کار میکند و یک شیء adoption میسازد که به صورت زیر است:
1class FrogParadiseOwner {
2 constructor(frogOwner, frogOwnerLicense, frog) {
3 this.id = createId()
4 this.owner = frogOwner
5 this.license = frogOwnerLicense
6 this.frog = frog
7 }
8
9 createDocument() {
10 return JSON.stringify(this, null, 2)
11 }
12}
اینک یک اپلیکیشن کاملاً زیبا داریم که یک واگذاری frog را انجام میدهد و مشتریان میتوانند دران یک frog به دست آورند. اما فرایند adoption صرفاً یک تراکنش پولی دادن/گرفتن ساده نیست.
فرض میکنیم که قانونی وجود دارد که الزام میکند این فرایند برای هر adoption یک FROG به مالکان جدید باید اجرا شود. بنابراین لازم است که قراردادی ایجاد شده و امضای مشتری در آن وارد شود. در ادامه یک پروانه نیز ایجاد میشود که مشتریان باید برای حفاظت حقوقی خود داشته باشند. در نهایت adoption کامل میشود.
به کلاس FrogOwner زیر توجه کنید:
1class FrogParadiseOwner {
2 constructor(frogOwner, frogOwnerLicense, frog) {
3 this.id = createId()
4 this.owner = frogOwner
5 this.license = frogOwnerLicense
6 this.frog = frog
7 }
8
9 createDocument() {
10 return JSON.stringify(this, null, 2)
11 }
12}
این کلاس سه وابستگی به صورت frogOwner، frogOwnerLicense و frog دارد. فرض کنید یک بهروزرسانی در مورد frogOwner وجود دارد و به وهلهای از Client تبدیل میشود:
1class Client extends Person {
2
3 setName(name) {
4 this.name = name
5 }
6}
اکنون فراخوانیهای مقداردهی FrogParadiseOwner باید بهروزرسانی شوند. اما اگر FrogParadiseOwner را در سراسر کد در چندین جا مقداردهی کنیم چه اتفاقی میافتد؟ اگر کد طولانیتر شود و تعداد این وهلهها افزایش یابد، مشکل نگهداری نمود بیشتری مییابد.
این همان جایی است که کانتینر تزریق وابستگی میتواند موجب بهبود شود، زیرا در این حالت تنها باید کد در یک موقعیت تغییر یابد. بنابراین کانتینر تزریق وابستگی به صورت زیر خواهد بود:
1import parseFunction from 'parse-function'
2
3const app = parseFunction({
4 ecmaVersion: 2017,
5})
6
7class DIC {
8 constructor() {
9 this.dependencies = {}
10 this.factories = {}
11 }
12
13 register(name, dependency) {
14 this.dependencies[name] = dependency
15 }
16
17 factory(name, factory) {
18 this.factories[name] = factory
19 }
20
21 get(name) {
22 if (!this.dependencies[name]) {
23 const factory = this.factories[name]
24 if (factory) {
25 this.dependencies[name] = this.inject(factory)
26 } else {
27 throw new Error('No module found for: ' + name)
28 }
29 }
30 return this.dependencies[name]
31 }
32
33 inject(factory) {
34 const fnArgs = app.parse(factory).args.map((arg) => this.get(arg))
35 return new factory(...fnArgs)
36 }
37}
اینک به جای مقداردهی مستقیم آن مانند قبل و الزام به تغییر دادن همه وهلههای دیگر کد:
1class Client extends Person {
2 setName(name) {
3 this.name = name
4 }
5}
6
7const dic = new DIC()
8dic.register('frogOwner', Client)
9dic.register('frogOwnerLicense', sallysLicense)
10dic.register('frog', mikeTheToad)
11dic.factory('frog-owner', FrogParadiseOwner)
12
13const frogOwner = dic.get('frog-owner')
میتوانیم از DIC برای یکبار بهروزرسانی آن اقدام کنیم و دیگر نیازی به تغییر همه بخشهای کد نداریم، زیرا جهتگیری گردش برای آن کانتینر معکوس شده است:
1const frogOwner = new FrogParadiseOwner(Client, sallysLicense, mikeTheToad)
2// some other location
3const frogOwner2 = new FrogParadiseOwner(...)
4// some other location
5const frogOwner3 = new FrogParadiseOwner(...)
6// some other location
7const frogOwner4 = new FrogParadiseOwner(...)
8// some other location
9const frogOwner5 = new FrogParadiseOwner(...)
اینک کاری که DIC انجام میدهد را توضیح میدهیم. ما کلاسها یا تابعهایی را که میخواهیم از سوی DIC حل شوند را با ارسال به متد.factory() درج میکنیم که در نهایت در مشخصه.factory ذخیره میشوند:
در مورد هر کدام از این تابعها که به ()factory. ارسال میشوند باید آرگومانهایشان با استفاده از ()register ثبت شوند تا بتوان در زمان مقداردهی تابع درخواستی از سوی کانتینر مورد استفاده قرار گیرند. این آرگومانها از مشخصه dependencies. دریافت میشوند. با استفاده از متد ()dependencies. میتوانید چیزهایی به وابستگیها اضافه کنید:
زمانی که بخواهید چیزی را بازیابی کنید، میتوانید از get. به همراه نوعی key استفاده کنید. این متد از KEY برای گشتن به دنبال dependencies استفاده میکند و در صورتی که چیزی پیدا کند آن را بازگشت میدهد. در غیر این صورت به دنبال factories میگردد و در صورتی که چیزی پیدا کند با آن مانند یک تابع رفتار میکند که باید resolve شود.
سپس اجزا را به inject. میسپارد که نامهای وابستگیهای (آرگومانها را خوانده و آنها را از مشخصه dependencies. میگیرد و تابع را اجرا کرده و آرگومانها را تزریق میکند و نتیجه را بازگشت میدهد.
ما در مثال کدمان، از parse-function استفاده کردهایم تا به متد inject امکان بدهیم که نامهای آرگومان یک تابع را به دست آورد. برای این که این کار را بدون کتابخانه انجام دهیم، میتوانیم آرگومانهای دیگری به get. اضافه کنیم کرده و به صورت زیر به inject. آن ارسال کنیم:
1class DIC {
2 constructor() {
3 this.dependencies = {}
4 this.factories = {}
5 }
6
7 register(name, dependency) {
8 this.dependencies[name] = dependency
9 }
10
11 factory(name, factory) {
12 this.factories[name] = factory
13 }
14
15 get(name, args) {
16 if (!this.dependencies[name]) {
17 const factory = this.factories[name]
18 if (factory) {
19 this.dependencies[name] = this.inject(factory, args)
20 } else {
21 throw new Error('No module found for: ' + name)
22 }
23 }
24 return this.dependencies[name]
25 }
26
27 inject(factory, args = []) {
28 const fnArgs = args.map((arg) => this.get(arg))
29 return new factory(...fnArgs)
30 }
31}
1const dic = new DIC()
2dic.register('frogOwner', Client)
3dic.register('frogOwnerLicense', sallysLicense)
4dic.register('frog', mikeTheToad)
5dic.factory('frog-owner', FrogParadiseOwner)
6
7const frogOwner = dic.get('frog-owner', [
8 'frogOwner',
9 'frogOwnerLicense',
10 'frog',
11])
12
13console.log('frog-owner', JSON.stringify(frogOwner, null, 2))
در هر حال، نتیجه مشابهی به دست میآید:
1{
2 "id": "u_k8q16rjx_fgrw6b0yb528unp3trokb",
3 "license": {
4 "id": "m_k8q16rjk_jipoch164dsbpnwi23xin",
5 "client": {
6 "firstName": "sally",
7 "lastName": "tran",
8 "id": "b_k8q16rjk_0xfqodlst2wqh0pxcl91j"
9 },
10 "preparer": {
11 "firstName": "richard",
12 "lastName": "rodriguez",
13 "id": "g_k8q16rjk_f13fbvga6j2bjfmriir63"
14 },
15 "frog": {
16 "name": "mike",
17 "gender": "male",
18 "weight": 12.5
19 },
20 "location": "undefined undefined NY 92804"
21 },
22 "frog": {
23 "name": "mike",
24 "gender": "male",
25 "weight": 12.5
26 }
27}
به این ترتیب به پایان این مقاله میرسیم. امیدواریم از این راهنما بهره لازم را برده باشید.
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای برنامهنویسی
- آموزش JavaScript ES6 (جاوا اسکریپت)
- مجموعه آموزشهای JavaScript (جاوا اسکریپت)
- معرفی جاوا اسکریپت ناهمگام — به زبان ساده
- آموزش جاوا اسکریپت — مجموعه مقالات جامع وبلاگ فرادرس
==