اذهب إلى المحتوى

الخاصيات والتوابع الخاصة والمحمية في جافاسكربت


صفا الفليج

في البرمجة كائنية التوجّه، يُعدّ أكثر المبادئ أهمية هو فصل الواجهة الداخلية عن الخارجية.

هذا المبدأ ”إلزامي“ متى ما طوّرنا تطبيقًا يفوق تطبيقات ”أهلًا يا عالم“ تعقيدًا.

سنضع البرمجة ”على جنب“ ونرى الواقع؛ لنفهم هذا المبدأ.

الأجهزة الّتي نستعملها يوميًا عادةً ما تكون معقّدة، إلّا أنّ فصل واجهتها الداخلية عن تلك الخارجية يُتيح لنا استعمالها دون مشاكل تُذكر.

مثال من الحياة العملية

لنقل مثلًا آلة صنع القهوة، من الخارج بسيطة: زرّ تشغيل وشاشة عرض وبعض الفتحات وطبعًا لا ننسى الناتج… القهوة المميّزة الطعم! :)

coffee.jpg

ولكن من الداخل نرى… (هذه الصورة من كتيّب التصليح)

coffee-inside.jpg

نرى تفاصيل وتفاصيل ولا شيء إلّا التفاصيل، ولكنّنا نستعملها دون أيّ فكرة عن تلك التفاصيل.

يمكننا أن نثق بجودة آلات صنع القهوة، ألا توافقني الرأي؟ فنحن نستعملها لسنوات وسنوات وإن ظهر أيّ عُطل، أخذناها للتصليح.

يكمن سرّ هذه الثقة وهذه البساطة لآلات صنع القهوة في أنّ التفاصيل كلّها مضبوطة كما يجب ومخفية داخلها.

إن أزلنا غطاء الحماية من الآلة وحاولنا استعمالها لكان الأمر أكثر تعقيدًا (أيّ زرّ نضغط؟) وخطرًا (الصدمات الكهربية).

كما سنرى أسفله، فالكائنات في البرمجة تشبه تمامًا آلات صنع القهوة.

ولكن لنُخفي التفاصيل الداخلية لن نستعمل غطاء حماية بل صياغة خاصّة في اللغة، كما وما هو متّفق عليه بين الناس.

الواجهتان الداخلية والخارجية

في البرمجة كائنية التوجّه، تنقسم الخاصيات والتوابِع إلى قسمين:

  • واجهة داخلية (Internal interface): فيها التوابِع والخاصيات الّتي يمكن أن تصل إليها توابِع هذا الصنف، ولا يمكن أن يصل إليها ما خارج هذا الصنف.
  • واجهة خارجية (External interface): فيها التوابِع والخاصيات الّتي يمكن أن يصل إليها ما خارج هذا الصنف أيضًا.

لو واصلنا الشرح على آلة القهوة تلك، فهذا بعض ممّا هو مخفي: أنبوب غلي، عنصر التسخين، وغيرهما من العناصر الداخلية التي تشكل الواجهة الداخلية الخلفية الخفية حيث نستعمل الواجهة الداخلية ليعمل هذا الشيء (أيًا كان)، إذ ترى تفاصيله هذه تلك. ولكن من الخارج نجد أن آلة القهوة لها غلاف محكم الإغلاق فلا يمكن لأحد أن يصل إلى تلك التفاصيل، فتكون مخفيّة ولا يمكن استعمال ميزات الآلة إلا عبر الواجهة الخارجية.

إذًا، ما نريده لنستعمل الكائنات هو معرفة واجهته الخارجية. أمّا معرفة طريقة عمله بالتفاصيل الدقيقة ليس أمرًا مهمًا أبدًا، وهذا في الواقع تسهيل عظيم.

شرحنا الآن الفكرة بشكلٍ عام.

أمّا في جافاسكربت فهناك نوعين من حقول الكائنات (الخاصيات والتوابِع):

  • عامّة: يمكن أن نصل إليها من أيّ مكان، وهي تصنع تلك الواجهة الخارجية. حتّى اللحظة في هذا الكتاب كنّا نستعمل الخاصيات والتوابِع العموميّة فقط.
  • خاصّة: يمكن أن نصل إليها من داخل الصنف فقط، وهي تصنع الواجهة الداخلية.

كما هناك في اللغات الأخرى حقول ”محميّة“: أي نصل إليها من داخل الصنف ومن أيّ صنف آخر يوسّعه (تعمل مثل الخاصّة، علاوةً على الوصول إليها من الأصناف الموروثة). هذه الحقول مفيدة جدًا للواجهة الداخلية، وهي منطقيًا أكثر استعمالًا من الخاصّة إذ حين نرث صنفًا نريدُ أن نصل إلى تلك الحقول الّتي فيه عادةً.

ليس هناك تعريف لتنفيذ هذه الحقول في جافاسكربت على مستوى اللغة، ولكنّها عمليًا تُسهّل الأمور كثيرًا، بذلك نحاكي عملها.

الآن حان وقت صناعة آلة قهوة بجافاسكربت مستعملين تلك الأنواع من الخاصيات. طبعًا لآلة القهوة تفاصيل عديدة ولن نضعها كلها في صنفنا لتسهيل الأمور (ولكن ما من مانع لتفعل أنت ذلك).

حماية ”مقدار الماء“

لنصنع أولًا صنفًا بسيطًا لآلة قهوة:

class CoffeeMachine {
  waterAmount = 0; // مقدار الماء في الآلة

  constructor(power) {
    this.power = power;
    alert( `Created a coffee-machine, power: ${power}` ); // صنعنا آلة قهوة بقوّة كذا
  }

}

// نصنع آلة قهوة واحدة
let coffeeMachine = new CoffeeMachine(100);

// نُضيف إليها الماء
coffeeMachine.waterAmount = 200;

حاليًا فخاصيتي مقدار الماء waterAmount والقوّة power عامّتين، ويمكننا بسهولة جلبهما أو ضبطهما من خارج الصنف لأيّ قيمة نريد.

لنغيّر خاصية waterAmount فتصير محميّة، بذلك يكون في يدنا التحكّم. لنقل مثلًا بأنّه يُمنع ضبط القيمة إلى ما دون الصفر.

عادةً ما نضع شَرطة سفلية _ قبل أسماء الخاصيات المحميّة.

لا تفرض اللغة هذا الأمر ولكنّه اتّفاق بين معشر المطوّرين بأنّه ممنوع الوصول إلى هذه الخاصيات والتوابِع من خارج الأصناف.

إذًا، ستكون الخاصية باسم ‎_waterAmount:

class CoffeeMachine {
  _waterAmount = 0;

  set waterAmount(value) {
    if (value < 0) throw new Error("Negative water"); // قيمة الماء بالسالب
    this._waterAmount = value;
  }

  get waterAmount() {
    return this._waterAmount;
  }

  constructor(power) {
    this._power = power;
  }

}

// نصنع آلة القهوة المميّزة
let coffeeMachine = new CoffeeMachine(100);

// نُضيف الماء
coffeeMachine.waterAmount = -10; // خطأ: قيمة الماء بالسالب

الآن صرنا نتحكّم بالوصول إلى الخاصية، ومحاولة ضبط قيمة الماء إلى ما دون الصفر ستفشل.

”القوّة“ للقراءة فقط

أمّا عن خاصية القوّة power فسنجعلها للقراءة فقط. نواجه أحيانًا مواقف حيث تُضبط الخاصية أثناء إنشاء الصنف فقط، ويُمنع تعديلها قطعيًا بعد ذلك.

هذه هي حالة آلة القهوة هنا، إذ قوّة الآلة لا تتغيّر أبدًا.

لذلك سنصنع جالبًا فقط دون ضابِط:

class CoffeeMachine {
  // ...

  constructor(power) {
    this._power = power;
  }

  get power() {
    return this._power;
  }

}

// نصنع آلة القهوة
let coffeeMachine = new CoffeeMachine(100);

alert(`Power is: ${coffeeMachine.power}W`); // القوّة هي: 100 واط

coffeeMachine.power = 25; // خطأ (لا ضابِط‫)

ملاحظة:استخدامنا هنا صياغة الجوالِب/الضوابِط (Getter/setter) الإفتراضية ولكن دوالّ get.../set...‎ مفضلة في معظم الأحيان. هكذا:

class CoffeeMachine {
  _waterAmount = 0;

  setWaterAmount(value) {
    if (value < 0) throw new Error("Negative water");
    this._waterAmount = value;
  }

 getWaterAmount() {
    return this._waterAmount;
  }
}

new CoffeeMachine().setWaterAmount(100);

تبدو هذه الصياغة أطول قليلًا ولكن الدوالّ هنا أكثر مرونة. إذ يمكننا تمرير عدة وسطاء لهم (حتى ولو لم نحتاج لهذا الأمر في هذا المثال).

من ناحية أخرى إن صياغة الإفتراضية أقصر لذا لا توجد قاعدة صارمة للأمر، ولك مطلق الحرية في الاختيار.

ملاحظة: الخاصيات المحميّة موروثة. إذا ورثنا class MegaMachine extends CoffeeMachine، فلا شيء سيمنعنا من الوصول إلى this._waterAmount أو this._power من توابع الصنف الجديد.

لذا فالوضع الطبيعي للخاصيات المحمية أن تكون قابلة للتوريث. على عكس الخاصيات الخاصة والّتي سنراها أدناه.

”#حدّ الماء“ خاصّة

انتبه: إضافة حديثة
انتبه رجاءً إلى أن هذه الميزة أضيفت حديثًا إلى اللغة، لذا هي ليست مدعومة على محركات جافاسكربت بعد ولا حتى جزئيًا وتحتاج إلى ترقيع يسد نقص الدعم هذا (polyfilling).

هناك مُقترح للغة جافاسكربت (كاد أن يصل إلى المعيار) يُقدّم لنا دعمًا على مستوى اللغة للخاصيات والتوابِع الخاصة.

تبدأ الخاصيات والتوابِع بعلامة #، ولا يمكن الوصول إليها إلّا من داخل الصنف.

فمثلًا إليك خاصية ‎#waterLimit الخاصّة وتابِع فحص مستوى الماء ‎#checkWater الخاصّ:

class CoffeeMachine {
  #waterLimit = 200; // هنا

  // هذا
  #checkWater(value) {
    if (value < 0) throw new Error("Negative water"); // قيمة الماء بالسالب
    if (value > this.#waterLimit) throw new Error("Too much water"); // قيمة الماء فوق اللزوم
  }

}

let coffeeMachine = new CoffeeMachine();

// محال الوصول إلى الخاصيات والتوابِع الخاصّة من خارج الصنف
coffeeMachine.#checkWater(); // Error
coffeeMachine.#waterLimit = 1000; // Error

علامة # على مستوى اللغة هي علامة خاصّة تقول بأنّ هذا الحقل حقلٌ خاصّ، وتمنع أيّ شيء من الوصول إليه من الخارج أو من الأصناف الموروثة.

كما وأنّ الحقول الخاصّة لا تتضارب مع تلك العامّة: يمكن أن نضع حقلين خاصًا ‎#waterAmount وعامًا waterAmount في آنٍ واحد.

فمثلًا ليكن waterAmount خاصية وصول للخاصية ‎#waterAmount:

class CoffeeMachine {

  #waterAmount = 0;

  get waterAmount() {
    return this.#waterAmount;
  }

  set waterAmount(value) {
    if (value < 0) throw new Error("Negative water"); // قيمة الماء بالسالب
    this.#waterAmount = value;
  }
}

let machine = new CoffeeMachine();

machine.waterAmount = 100;
alert(machine.#waterAmount); // خطأ

وعلى عكس الحقول المحميّة، فتلك الخاصّة تفرضها اللغة نفسها، وهذا أمر طيّب.

ولكن لو ورثنا شيئًا من CoffeeMachine فلن يمكننا الوصول إلى ‎#waterAmount مباشرةً، بل الاعتماد على جالِب وضابِط waterAmount:

class MegaCoffeeMachine extends CoffeeMachine {
  method() {
    alert( this.#waterAmount ); // ‫خطأ: يمكنك أن تصل إليه من داخل CoffeeMachine فقط
  }
}

هناك حالات عديدة يكون فيها هذا القيد صارم كثيرًا. حين نُوسّع CoffeeMachine يكون لدينا أسباب منطقية لنصل إلى خوارزمياته الداخلية. لهذا السبب فالحقول المحميّة أكثر فائدة بكثير حتّى لو لم تدعمها صياغة اللغة.

تحذير: لا يمكنك الوصول إلى الحقول الخاصّة هكذا: this[name]‎ إذ عادةً ما يمكننا الوصول إلى الحقول العادية هكذا : this[name]‎

class User {
  ...
  sayHi() {
    let fieldName = "name";
    alert(`Hello, ${this[fieldName]}`);
  }
}

ولكن مع الحقول الخاصة من المحال الوصول إليها this['#name']‎ فإنها لن تعمل. وذلك بسبب القيد الموجود على صياغة استدعاء الحقول الخاصة لضمان خصوصية الحقل.

خلاصة

لو استعرنا تسميات البرمجة كائنية التوجّه، فعملية فصل الواجهة الداخلية عن الخارجية تُسمّى تغليف (Encapsulation)، وهي تقدّم لنا فوائد منها:

  • حماية المستخدمين كي لا يضروا أنفسهم: تخيّل فريق مطوّرين يستعمل آلةً للقهوة صنعتها شركة ”Best CoffeeMachine“، وهي تعمل كما يجب لكنّها دون غطاء حماية، أيّ أنّ الواجهة الداخلية مكشوفة للعيان.

    المطوّرون متحضّرون فيستعملون الآلة كما يفترض استعمالها، ولكنّ أحدهم (واسمه John) قال بأنّه الأذكى بين الجميع وعدّل على ميكانيكا الآلة الداخلية قليلًا. بعد يومين، لم تعد تعمل.

    هذه ليست غلطة John طبعًا، بل الشخص الّذي أزال غطاء الحماية وسمح لأشخاص (مثل John) أن يقوموا بهذه التعديلات.

    الأمر ذاته في البرمجة. فلو غيّر أحد مستخدمي الصنف أشياء لا يُفترض تغييرها من خارج الكائن، يكون التكهّن بعواقب ذلك مستحيلًا.

  • تحظى بالدعم: تختلف البرمجة عن آلة القهوة الحقيقية إذ الأولى معقّدة أكثر لأنّنا لا نشتريها مرّة واحدة فقط، بل تمرّ الشيفرة في مراحل تطوير وتحسين عديدة لا تنتهي أحيانًا.

    إن أجبرنا فصل الواجهة الداخلية عن الخارجية، فيمكن للمطوّر تغيير خاصيات الأصناف وتوابِعها الداخلية كما يشاء دون أن يُبلغ مستخدميها حتّى. لو كنت تطوّر صنفًا مثل هذا، ففكرة أنّ تغيير اسم التوابِع الخاصة أو تغيير مُعاملتها أو حتّى إزالتها - فكرة رائعة إذ ليست هناك أيّ شيفرة خارجية تعتمد عليها. أمّا للمستخدمين، فلو صدرت نسخة جديدة (وقد تكون كتابة كاملةً داخليًا) سيكون سهلًا جدًا الترقية إليها لو بقيت الواجهة الخارجية كما هي.

  • إخفاء التعقيد: يعشق الناس استعمال ما هو بسيط، على الأقلّ من الخارج. الداخل لا يهمّ.

    وليس المبرمج استثناءً

    دومًا ما يكون أفضل لو كانت تفاصيل التنفيذ مخفية، وكانت هناك واجهة خارجية بسيطة موثّقة كما يجب. لنُخفي الواجهات الداخلية، نستعمل إمّا الخاصيات المحميّة أو الخاصّة:

  • تبدأ الحقول المحميّة بعلامة الشرطة السفلية _. هذا اتّفاق بين معشر المبرمجين وليس فرضًا من اللغة نفسها. على المبرمجين الوصول إلى الحقول الّتي تبدأ بهذه العلامة داخلَ الصنف والأصناف الموروثة فقط لا غير.
  • تبدأ الحقول الخاصّة بعلامة #. تضمن لنا لغة جافاسكربت بأن لا يقدر أحد على الوصول إليها إلّا من داخل الصنف.

حاليًا فالحقول الخاصّة ليست مدعومة تمامًا في المتصفّحات، ولكن يمكننا تعويض نقص الدعم هذا.

ترجمة -وبتصرف- للفصل Private and protected properties and methods من كتاب The JavaScript language


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...