حفاظت از اشیا با تایپ اسکریپت – راهنمای کاربردی


یکی از قابلیتهای کلیدی که جاوا اسکریپت فاقد آن است، توانایی حفاظت آسان از دادههای مربوط به یک شیء است. به این منظور چند تکنیک در طول عمر این زبان برنامهنویسی ظهور یافتهاند، اما یا عملاً حفاظتی از دادههای شیء انجام ندادهاند و یا نیازمند دانش پیشرفتهای مانند دانستن طرز کار کلوژرها (+) بودهاند. در این مطلب و در ادامه قصد داریم به همین موضوع بپردازیم و نحوه حفاظت از اشیا با تایپ اسکریپت را مورد بررسی قرار دهیم.
راهاندازی
در این بخش میبینیم که حفاظت از دادههای اشیا تا چه حد میتواند آسان باشد. ابتدا یک پروژه npm کوچک به نام ts-protected راهاندازی میکنیم.
به این منظور یک پنجره ترمینال جدید باز کنید تا دستورهای زیر را اجرا نمایید:
mkdir ts-protected cd ts-protected npm init -y npm i -D typescript ts-node-dev touch protected.ts
این دستورها یک دایرکتوری جدید ایجاد میکنند، یک پروژه npm درون آن راهاندازی میشود و چند پکیج که مورد نیاز خواهند بود نیز نصب میشوند. در ادامه فایلی ایجاد میشود که کد خود را درون آن خواهیم نوشت. پروژه را در ویژوال استودیو کد باز کنید:
code .
فایل package.json را باز کرده و بخش اسکریپتها را ویرایش کنید:
1{
2 "name": "ts-protected",
3 "version": "1.0.0",
4 "description": "",
5 "main": "index.js",
6 "scripts": {
7 "start": "npx ts-node-dev --respawn protected.ts"
8 },
9 "keywords": [],
10 "author": "",
11 "license": "ISC",
12 "devDependencies": {
13 "ts-node-dev": "^1.0.0-pre.44",
14 "typescript": "^3.7.2"
15 }
16}
ما همه کدها را در فایل protected.ts قرار میدهیم که لحظاتی پیشساخته شده است. در ویژوال استودیو کد به نوار منو رفته و در منوی Terminal گزینه New Terminal را انتخاب کنید. کد را با دستور زیر اجرا کنید:
npm start
کلاس BankAccount
یک کلاس جدید به نام BankAccount با دو مشخصه داده به نامهای checking و savings تعریف میکنیم. همچنین یک متد سازنده برای مقداردهی این مشخصهها اضافه میکنیم.
به این منظور فایل protected.ts را باز کرده و کد زیر را در آن قرار دهید:
1class BankAccount {
2 checking;
3 savings;
4
5 constructor(checking, savings) {
6 this.checking = checking;
7 this.savings = savings;
8 }
9}
یک متد برای پرینت کردن جزییات حساب در ترمینال اضافه میکنیم و سپس یک وهله از یک BankAccount میسازیم:
1class BankAccount {
2 checking;
3 savings;
4
5 constructor(checking, savings) {
6 this.checking = checking;
7 this.savings = savings;
8 }
9
10 accountDetails() {
11 console.log(`Checking: ${this.checking}, Savings: ${this.savings}`);
12 }
13}
14
15const myAccount = new BankAccount(500, 10000);
16myAccount.accountDetails();
17
18myAccount.checking = 0;
19myAccount.savings = "hahahaha";
20myAccount.accountDetails();
در ادامه اتفاقاتی که رخ میدهد را بررسی میکنیم.
این آن نوع از رفتار است که دوست نداریم رخ بدهد. در یک اپلیکیشن بزرگ که از کتابخانههای شخص ثالث زیادی استفاده میکند، قرار دادن یک شیء در حالت غیرمجاز به سهولت رخ میدهد و این مسئله به طور بالقوه موجب بروز اشکالی در دادهها یا از کار افتادن اپلیکیشن میشود.
دو مشکل عمده در اینجا وجود دارند:
- انواع دادهها برای checking و savings را میتوان به هر چیزی عوض کرد. در این کد ما نوع savings را از عدد به رشته تغییر دادهایم.
- ما به دادههای خود اجازه میدهیم که به روشی کنترل نشده تغییر یابند.
تایپ اسکریپت میتواند هر دو این مشکلات را به سهولت حل کند. در بخش بعدی طرز حل این مشکلات را میبینیم.
محدودسازی نوع دادههای یک مشخصه
تنها کاری که باید انجام دهیم این است که انواع مجاز برای مشخصهها را به تایپ اسکریپت اعلام کنیم. هر دو مشخصه ما تنها باید به صورت عددی باشند. در ادامه با افزودن نوع هر مشخصه این موضوع را به تایپ اسکریپت اعلام میکنیم. این کار را برای آرگومانهای سازنده نیز انجام میدهیم:
1class BankAccount {
2 checking: number;
3 savings: number;
4
5 constructor(checking: number, savings: number) {
6 this.checking = checking;
7 this.savings = savings;
8 }
9
10 accountDetails() {
11 console.log(`Checking: ${this.checking}, Savings: ${this.savings}`);
12 }
13}
14
15const myAccount = new BankAccount(500, 10000);
16myAccount.accountDetails();
17
18myAccount.checking = 0;
19myAccount.savings = "hahahaha";
20myAccount.accountDetails();
در تصویر زیر به بروز خطا توجه کنید. به این ترتیب کد در ترمینال از کار میافتد:
محدودسازی دسترسی خارجی به یک مشخصه
میتوان از دسترسی خارجی به یک مشخصه شیء و تغییر نادرست آن جلوگیری کرد. ابتدا باید مشخصههای خود را به صورت private اعلان کنیم، یعنی دیگر قابل دسترسی از خارج از کلاس نخواهند بود:
1class BankAccount {
2 private checking: number;
3 private savings: number;
4
5 constructor(checking: number, savings: number) {
6 this.checking = checking;
7 this.savings = savings;
8 }
9
10 accountDetails() {
11 console.log(`Checking: ${this.checking}, Savings: ${this.savings}`);
12 }
13}
14
15const myAccount = new BankAccount(500, 10000);
16myAccount.accountDetails();
17
18myAccount.checking = 0;
19myAccount.savings = "hahahaha";
20myAccount.accountDetails();
بدین ترتیب دو خط کد زیر دیگر کار نمیکنند. آنها را به همراه گزاره ()myAccount.accountDetails که پس از آنها آمده است، حذف کنید:
1myAccount.checking = 0;
2myAccount.savings = "hahahaha";
اینک سؤال این است که چگونه میتوانیم دادههای شیء را تغییر دهیم؟ این کار از طریق یک Setter میسر میشود. Setter-ها متدهای خاصی هستند که درون یک کلاس تعریف نمیکنیم و میتوانند مشخصههای شیء را به روشی کنترلشده تغییر دهند.
محدودیتها
در این بخش محدودیتهایی برای دادههای شیء به صورت زیر تعریف میکنیم:
- Checking میتواند در بازهای بین 10000- تا 10000 قرار گیرد.
- Savings میتواند در بازهای بین 0 تا 1000000 قرار گیرد.
اکنون متدهای setter خود را نوشته و برخی تستها را اجرا میکنیم:
1class BankAccount {
2 private checking: number;
3 private savings: number;
4
5 constructor(checking: number, savings: number) {
6 this.checking = checking;
7 this.savings = savings;
8 }
9
10 accountDetails() {
11 console.log(`Checking: ${this.checking}, Savings: ${this.savings}`);
12 }
13
14 setChecking(newAmount: number) {
15 if (newAmount < -10000 || newAmount > 10000) {
16 throw new Error('Checking value is out of range');
17 }
18 this.checking = newAmount;
19 }
20
21 setSavings(newAmount: number) {
22 if (newAmount < 0 || newAmount > 1000000) {
23 throw new Error('Savings value is out of range');
24 }
25 this.savings = newAmount;
26 }
27
28}
29
30const myAccount = new BankAccount(500, 10000);
31myAccount.accountDetails();
32
33// Change checking amount to valid value
34console.log('Changing checking amount to new valid amount');
35myAccount.setChecking(5000);
36myAccount.accountDetails();
37
38try {
39 // Change checking amount to invalid value
40 console.log('Changing checking amount to invalid amount');
41 myAccount.setChecking(-20000);
42 myAccount.accountDetails();
43} catch (error) {
44 console.error(error);
45}
46
47// Change savings amount to valid value
48console.log('Changing savings amount to new valid amount');
49myAccount.setSavings(42000);
50myAccount.accountDetails();
51
52try {
53 // Change savings amount to invalid value
54 console.log('Changing savings amount to invalid amount');
55 myAccount.setSavings(-1);
56 myAccount.accountDetails();
57} catch (error) {
58 console.error(error);
59}
توجه کنید که اگر checking یا savings را روی مقدار نامجازی تنظیم کنیم، کد یک استثنا تولید میکند که در صورتی که آن را catch نکنیم، منجر به از کار افتادن برنامه خواهد شد. به همین دلیل است که دو تست درون یک بلوک try-catch قرار گرفتهاند. خروجی کدهای فوق در ترمینال به صورت زیر است:
این دقیقاً همان چیزی است که ما میخواهیم. نه تنها دیگر نمیتوانیم انواع دادههای خود را به چیز غیرمجازی تغییر دهیم، بلکه کد یک استثنا صادر میکند که میتوان آن را به سهولت ردگیری کرد تا به منبع خطا پی برد و مقادیر غیرمجاز را شناسایی کرد.
متدهای Getter و منافذ فرار
پیش از آن که این راهنما را جمعبندی کنیم، بهتر است کمی در مورد یک دام که ممکن است به آن بیافتید صحبت کنیم تا هنگام کار با دادههای private به خاطر داشته باشید. ابتدا یک متد getter برای savings اضافه میکنیم. سپس یک ارجاع به آن خارج از کلاس به دست میآوریم و تلاش میکنیم آن را تغییر دهیم:
1class BankAccount {
2 private checking: number;
3 private savings: number;
4
5 constructor(checking: number, savings: number) {
6 this.checking = checking;
7 this.savings = savings;
8 }
9
10 accountDetails() {
11 console.log(`Checking: ${this.checking}, Savings: ${this.savings}`);
12 }
13
14 setChecking(newAmount: number) {
15 if (newAmount < -10000 || newAmount > 10000) {
16 throw new Error('Checking value is out of range');
17 }
18 this.checking = newAmount;
19 }
20
21 setSavings(newAmount: number) {
22 if (newAmount < 0 || newAmount > 1000000) {
23 throw new Error('Savings value is out of range');
24 }
25 this.savings = newAmount;
26 }
27
28 getSavings(): number {
29 return this.savings;
30 }
31
32}
33
34const myAccount = new BankAccount(500, 10000);
35myAccount.accountDetails();
36
37// Change checking amount to valid value
38console.log('Changing checking amount to new valid amount');
39myAccount.setChecking(5000);
40myAccount.accountDetails();
41
42try {
43 // Change checking amount to invalid value
44 console.log('Changing checking amount to invalid amount');
45 myAccount.setChecking(-20000);
46 myAccount.accountDetails();
47} catch (error) {
48 console.error(error);
49}
50
51// Change savings amount to valid value
52console.log('Changing savings amount to new valid amount');
53myAccount.setSavings(42000);
54myAccount.accountDetails();
55
56try {
57 // Change savings amount to invalid value
58 console.log('Changing savings amount to invalid amount');
59 myAccount.setSavings(-1);
60 myAccount.accountDetails();
61} catch (error) {
62 console.error(error);
63}
64
65let savingsRef = myAccount.getSavings();
66savingsRef = -100;
67myAccount.accountDetails();
چنان که میبینید هیچ چیزی تغییر نیافته است و این دقیقاً همان چیزی که ما میخواهیم. دلیل آن این است که savings مانند checking یک نوع «داده مقدماتی» (Primitive Type) است. انواع داده مقدماتی، زمانی که به صورت آرگومان ارسال میشوند در واقع درون تابعها کپی میشوند و زمانی که از سوی تابع بازگشت مییابند، کپیهایی از مقادیر آنها گرفته میشود. با این حال انواع غیر مقدماتی مانند شیءها طرز کار متفاوتی دارند. استفاده از یک متد gettre برای بازیابی یک مشخصه که یک شیء است موجب بازگشت یک ارجاع به آن شیء و نه یک کپی از آن میشود.
این بدان معنی است که اگر مراقب نباشیم و یک ارجاع به یک شیء بازگشت دهیم، امکان تغییر دادن شیء در خارج از کد نیز پدید میآید. یک روش برای حل این مشکل این است که یک کپی از شیء تهیه کرده و ارجاع به آن کپی را به جای شیء اصلی بازگشت دهیم. بدین ترتیب به پایان این راهنما میرسیم.
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای JavaScript (جاوا اسکریپت)
- مجموعه آموزشهای برنامهنویسی
- آموزش جاوا اسکریپت (JavaScript)
- راهنمای جامع تایپ اسکریپت (Typescript) — از صفر تا صد
- درک انواع در تایپ اسکریپت — به زبان ساده
==