یکی از مهمترین قواعد برنامهنویسی شیءگرا – محدود کردن رابط داخلی از رابط بیرونی است.
یعنی یک کارِ «بایدی» در توسعه هر چیزی پیچیدهتر از یک برنامهی «hello world».
برای فهمیدن این موضوع، بیایید از توسعه دور شویم و به دنیای واقعی نگاه کنیم.
معمولا، دستگاههایی که ما استفاده میکنیم بسیار پیچیده هستند. اما محدود کردن رابط داخلی از رابط بیرونی به ما این امکان را میدهد که بدون مشکل از آنها استفاده کنیم.
یک مثال در زندگی واقعی
برای مثال، یک قهوهساز. از بیرون ساده است: یک دکمه، یک نمایشگر، چند سوراخ…و قطعا، نتیجه – یک قهوه عالی :)
اما از درون… (تصویری از دفترچه راهنمای تعمیرات)
مقدار زیادی جزئیات. اما میتوانیم بدون دانستن چیزی از آن استفاده کنیم.
قهوهسازها بسیار قابل اطمینان هستند نه؟ میتوانیم برای سالها از آنها استفاده کنیم و اگر چیزی درست نبود – آن را به تعمیراتی ببرید.
راز قابل اطمینان و ساده بودن یک قهوهساز – تمام جزئیات به خوبی تنظیم شده و درون آن پنهان است.
اگر ما پوشش حفاظتی را از قهوهساز برداریم، سپس استفاده از آن پیچیدهتر (کجا را فشار دهیم؟) و خطرناکتر (میتواند باعث برق گرفتگی شود) خواهد بود.
همانطور که خواهیم دید، در برنامهنویسی شیءها مانند قهوهسازها هستند.
اما برای مخفیسازی جزئیات درونی،ما از پوشش حفاظتی استفاده نمیکنیم، بلکه از سینتکس خاص زبان و قراردادها استفاده میکنیم.
رابط درونی و بیرونی
در برنامهنویسی شیءگرا، ویژگیها و متدها به دو گروه تقسیم میشوند:
- رابط درونی – متدها و ویژگیها، قابل دسترس از متدهای دیگر کلاس، اما نه از بیرون.
- رابط بیرونی – متدها و ویژگیها، قابل دسترس از بیرون از کلاس.
اگر ما مقایسه را با قهوهساز ادامه دهیم – چیزی که درون آن است: یک مجرای بخار، المنت حرارت و غیره – رابط درونی است.
یک رابط درونی برای اینکه شیء کار کند استفاده میشود، جزئیات آن از یکدیگر استفاده میکنند. برای مثال، یک مجرای بخار به المنت حرارت متصل شده است.
اما از بیرون یک قهوهساز توسط پوشش محافظ بسته شده است پس کسی نمیتواند به آنها دسترسی داشته باشد. جزئیات پنهان و غیر قابل دسترس شدهاند. ما میتوانیم از طریق رابط بیرونی از خصوصیات آن استفاده کنیم.
پس تمام چیزی که برای استفاده از یک شیء نیاز داریم این است که رابط بیرونی آن را بشناسیم. شاید کاملا از اینکه چگونه کار میکند و این عالی است.
این یک معرفی کلی بود.
در جاوااسکریپت، دو نوع فیلد شیء داریم (ویژگیها و متدها):
- عمومی (public): قابل دسترس از هر جا. آنها شامل رابط بیرونی میشوند. تا اینجا ما فقط از ویژگیها و متدهای عمومی استفاده میکردیم.
- خصوصی (private): فقط درون کلاس قابل دسترس است. اینها برای رابط درونی هستند.
در بسیاری از زبانهای دیگر فیلدهای «محافظتشده» (protected) هم وجود دارد: فقط از درون کلاس و کلاسهایی که آن را تعمیم میدهند قابل دسترس است (مانند نوع خصوصی اما قابل دسترس از کلاسهای ارثبر). آنها هم برای رابط درونی مفید هستند. آنها در کل نسبت به نوع خصوصی بیشتر رایج هستند چون ما معمولا میخواهیم کلاسهای ارثبر به آنها دسترسی داشته باشند.
فیلدهای محافظتشده در جاوااسکریپت در سطح زبان پیادهسازی نشدهاند اما در عمل بسیار مناسب هستند پس تقلید شدهاند.
حالا در جاوااسکریپت یک قهوهساز همراه با انواع ویژگی خواهیم ساخت. یک قهوهساز جزئیات زیادی دارد، ما برای ساده بودن آنها را مدلسازی نمیکنیم (اگرچه میتوانستیم).
فیلد “waterAmount” محافظتشده
بیایید یک کلاس ساده قهوهساز ایجاد کنیم:
class CoffeeMachine {
waterAmount = 0; // مقدار آب درون
constructor(power) {
this.power = power;
alert( `یک قهوهساز ایجاد کردیم، توان: ${power}` );
}
}
// ایجاد قهوهساز
let coffeeMachine = new CoffeeMachine(100);
// اضافه کردن آب
coffeeMachine.waterAmount = 200;
حالا ویژگیهای waterAmount
و power
عمومی هستند. میتوانیم به راحتی از بیرون آنها را دریافت کنیم یا مقداردهی کنیم.
بیایید برای داشتن کنترل بیشتر ویژگی waterAmount
را به محافظتشده تغییر دهیم. برای مثال، ما نمیخواهیم کسی آن را کمتر از صفر تنظیم کند.
قبل از ویژگیهای محافظتشده معمولا یک زیرخط (underscore) _
میآید.
این نوع در سطح زبان اجرایی نشده اما یک قرارداد شناختهشده بین برنامهنویسان وجود دارد که نباید از بیرون به چنین ویژگیها و متدهایی دسترسی پیدا کرد.
پس ویژگی ما _waterAmount
خواهد بود:
class CoffeeMachine {
_waterAmount = 0;
set waterAmount(value) {
if (value < 0) {
value = 0;
}
this._waterAmount = value;
}
get waterAmount() {
return this._waterAmount;
}
constructor(power) {
this._power = power;
}
}
// ایجاد قهوهساز
let coffeeMachine = new CoffeeMachine(100);
// اضافه کردن آب
coffeeMachine.waterAmount = -10; // -برابر با 0 خواهد بود نه 10 _waterAmount
حالا دسترسی تحت کنترل است پس تنظیم مقدار آب کمتر از صفر ممکن نیست.
ویژگی “power” فقطخواندنی
بیایید ویژگی power
را فقطخواندنی کنیم. گاهی اوقات یک ویژگی باید فقط زمان ایجاد کردن مقداردهی شود و دیگر هیچوقت تغییر نکند.
این دقیقا برای قهوهساز هم صدق میکند: توان (power) هیچوقت تغییر نمیکند.
برای انجام این کار، ما فقط نیاز داریم که یک getter ایجاد کنیم اما setter را نه:
class CoffeeMachine {
// ...
constructor(power) {
this._power = power;
}
get power() {
return this._power;
}
}
// ایجاد قهوهساز
let coffeeMachine = new CoffeeMachine(100);
alert(`توان: ${coffeeMachine.power} وات`); // توان: 100 وات
coffeeMachine.power = 25; // (نداریم setter) ارور
اینجا ما از سینتکس getter/setter استفاده کردیم.
اما اکثر اوقات تابعهای get.../set...
ترجیح داده میشوند، مثلا اینگونه:
class CoffeeMachine {
_waterAmount = 0;
setWaterAmount(value) {
if (value < 0) value = 0;
this._waterAmount = value;
}
getWaterAmount() {
return this._waterAmount;
}
}
new CoffeeMachine().setWaterAmount(100);
این کمی طولانیتر بنظر میرسد اما تابعها بیشتر منعطف هستند. آنها میتوانند چند آرگومان دریافت کنند (حتی اگر ما همین الان به آنها نیاز نداشته باشیم).
از سویی دیگر، سینتکس get/set` کوتاهتر است پس در نهایت هیچ قانونی وجود ندارد، تصمیم با شماست.
اگر ما class MegaMachine extends CoffeeMachine
را ارثبری کنیم، سپس چیزی جلوی ما را برای دسترسی به this._waterAmount
یا this._power
از متدهای کلاس جدید نمیگیرد.
پس فیلدهای محافظتشده به طور طبیعی قابل ارثبری هستند. برخلاف فیلدهای خصوصی که پایین خواهیم دید.
فیلد “#waterLimit” خصوصی
یک طرح پیشنهادی تمام شده جاوااسکریپت وجود دارد، تقریبا درون استاندارد وارد شده، که پشتیبانی برای ویژگیها و متدهای خصوصی (private) را در سطح زبان فراهم میکند.
ویژگیها خصوصی با #
شروع میشوند. آنها فقط از درون کلاس قابل دسترس هستند.
برای مثال، اینجا ویژگی خصوصی #waterLimit
و متد خصوصی #fixWaterAmount
را داریم:
class CoffeeMachine {
#waterLimit = 200;
#fixWaterAmount(value) {
if (value < 0) return 0;
if (value > this.#waterLimit) return this.#waterLimit;
}
setWaterAmount(value) {
this.#waterLimit = this.#fixWaterAmount(value);
}
}
let coffeeMachine = new CoffeeMachine();
// نمیتوان بیرون از کلاس به خصوصیها دسترسی پیدا کرد
coffeeMachine.#fixWaterAmount(123); // ارور
coffeeMachine.#waterLimit = 1000; // ارور
در سطح زبان، #
نمادی خاص است که یعنی فیلد خصوصی است. ما نمیتوانیم از بیرون یا از کلاسهای ارثبر به آن دسترسی داشته باشیم.
فیلدهای خصوصی با فیلدهای عمومی ناسازگار نیستند. میتوانیم در یک زمان هم #waterAmount
خصوصی داشته باشیم و هم waterAmount
عمومی.
برای مثال، بیایید waterAmount
را به عنوان اکسسر برای #waterAmount
ایجاد کنیم:
class CoffeeMachine {
#waterAmount = 0;
get waterAmount() {
return this.#waterAmount;
}
set waterAmount(value) {
if (value < 0) value = 0;
this.#waterAmount = value;
}
}
let machine = new CoffeeMachine();
machine.waterAmount = 100;
alert(machine.#waterAmount); // ارور
برخلاف محافظتشدهها، فیلدهای خصوصی در سطح خود زبان اجرایی شدهاند. این چیز خوبی است.
اما اگر ما از CoffeMachine
ارثبری کنیم، سپس دسترسی مستقیم به #waterAmount
نداریم. باید به سراغ getter/setter برای waterAmount
برویم:
class MegaCoffeeMachine extends CoffeeMachine {
method() {
alert( this.#waterAmount ); // ممکن است CoffeMachine ارور: دسترسی فقط از
}
}
در بسیاری از سناریوها چنین محدودیتی خیلی سختگیرانه است. اگر ما CoffeMachine
را تعمیم دهیم، شاید دلایل قابل قبولی برای دسترسی به درون آن داشته باشیم. به همین دلیل فیلدهای محافظتشده اغلب اوقات استفاده میشوند حتی اگر آنها توسط سینتکس زبان پشتیبانی نمیشوند.
فیلدهای خصوصی خاص هستند.
همانطور که میدانیم، معمولا با استفاده از this[name]
به فیلدها دسترسی پیدا میکنیم:
class User {
...
sayHi() {
let fieldName = "name";
alert(`Hello, ${this[fieldName]}`);
}
}
این برای فیلدهای خصوصی غیر ممکن است: this['#name']
کار نمیکند. این یک محدودیت سینتکسی است تا حریم اطمینان حاصل کند.
خلاصه
از نظر برنامهنویسی شیءگرا، جدا کردن رابط درونی از بیرونی را کپسولهسازی میگویند.
این مزایا را به ما میدهد:
- حفاظت از کاربران، تا آنها خودشان را در مخمصه نیاندازند
-
تصور کنید، تیمی از توسعهدهندگان در حال استفاده از قهوهساز هستند. این دستگاه توسط شرکت «بهترین قهوهساز» ساخته شده و به خوبی کار میکند، اما یک پوشش محافظ برداشته شد. پس رابط درونی افشاء شده است.
تمام توسعهدهندگان متمدن هستند – آنها از قهوهساز همانطور که توقع میرود استفاده میکنند. اما یکی از آنها، John، تصمیم گرفته است که باهوشترین است و درون قهوهساز تغییراتی ایجاد کرد. قهوهساز دو روز بعد از کار میافتد.
قطعا این تقصیر John نیست بلکه تقصیر کسی است که پوشش محافظ را برداشت و اجازه داد که John قهوهساز را دستکاری کند.
همچین چیزی در برنامهنویسی هم وجود دارد. اگر یک کاربرِ کلاس چیزهایی را که قرار نیست تغییر کنند را از بیرون تغییر دهد – عواقب آن غیر قابل پیشبینی هستند.
- قابل پشتیبانی
-
این وضعیت در برنامهنویسی پیچیدهتر از یک قهوهساز در زندگی واقعی است چون ما فقط آن را یک بار نمیخریم. کد دائما در توسعه و پیشرفت است.
اگر ما به صورت سختگیرانه رابط درونی را جداسازی کنیم، سپس توسعهدهندهی کلاس آزادانه میتواند ویژگیها و متدهای درون آن را تغییر دهد، حتی بدون اینکه کاربران را خبردار کند.
اگر شما توسعهدهنده چنین کلاسی باشید، این خوب است که بتوانید متدهای خصوصی را با خیال راحت تغییر نام دهید، پارامترهای آنها را تغییر دهید و حتی حذف کنید، چون هیچ کد بیرونی به آنها وابسته نیست.
برای کاربران، زمانی که نسخهای جدید منتشر میشود، ممکن است از درون تعمیرات اساسی نیاز داشته باشد اما اگر رابط بیرونی یکسان باشد هنوز برای ارتقا دادن ساده است.
- پنهان کردن پیچیدگی
-
مردم عاشق استفاده از چیزهای ساده هستند. حداقل از بیرون. چیزی که درون وجود دارد موضوع متفاوتی است.
برنامهنویسان استثنا نیستند.
همیشه زمانی که جزئیات پیادهسازی پنهان هستند و یک رابط بیرونی ساده و به خوبی مستند شده وجود دارد کار راحت است.
برای پنهانسازی یک رابط درونی میتوانیم یا از ویژگیهای محافظتشده استفاده کنیم یا ویژگیهای خصوصی:
- فیلدهای محافظتشده با
_
شروع میشوند. این یک قرارداد شناختهشده است و در سطح زبان اجرایی نشدهاند. برنامهنویسان فقط باید از درون کلاس و کلاسهایی که از آن ارث میبرند، به فیلدی که با_
شروع میشود دسترسی پیدا کنند. - فیلدهای خصوصی با
#
شروع میشوند. جاوااسکریپت مطمئن میشود که ما فقط از درون کلاس بتوانیم به آنها دسترسی پیدا کنیم.
در حال حاضر، فیلدهای خصوصی به خوبی در میان مرورگرها پشتیبانی نمیشوند اما میتوان برای آنها از پلیفیلها استفاده کرد.