المفهوم الخاطئ للـ Encapsulation ؟!

السلام عليكم ورحمة الله وبركاته

وقت القراءة: ≈ 10 دقائق

المقدمة

في هذه المقالة من سلسلتنا الصغير عن الـ OOP أحببت أن أوضح مفهوم الـ Encapsulation لأنني لاحظت أن كثير ممن درس الـ Encapsulation لا يفهمون حقيقته أو لديهم نظرة سطحية أو ناقصة عنه

فعلا سبيل المثال عندما تدرس الـ OOP فيجب أن تمر على اول مبدأ من مبادئ الـ OOP وهو الـ Encapsulation

عندما تسمع للشرح الخاص به ستجد أنه يقول لك

هنا تكمن المشكلة لأن الشخص الذي يشرح يقول تلك هذا المعلومة فقط بأنها هي مفهوم الـ Encapsulation

حسنًا لنستمع لهذه المحادثة الصغيرة:

أن ظهر صوت صرصور الحقل فإذا هو فهم الـ Encapsulation بشكل خاطئ مثل غالبية الناس للآسف حتى أنا في بدايات تعلمي أخذت فكرة سطحية عنه، لكن هنا سأحاول شرح كل شيء يدور حول الـ Encapsulation وازالة سوء الفهم الدائر حوله

أساس المشكلة ولماذا نحتاج للـ Encapsulation

هنا الـ Encapsulation لديه بعض الخواص المهمة التي نستفيد منها، لكن قبل ان نتطرق لها يجب أن نشرح أصل المشكلة ولماذا نحتاج لتطبيق الـ Encapsulation

عندما يكون لديك بيانات للطلاب على سبيل المثال
كل طالب له اسم وعمر وقسم ودرجة و... و...

كيف ستخزنهم ؟ ستقوم بعمل متغيرات وتخزن القيم الخاصة بالطلاب

let name1 = 'Ahmed Mostafa';
let name2 = 'Kamal Mohamed';
let name3 = 'Mahmoud Ali';

let department1 = 'CS';
let department2 = 'IS';
let department3 = 'IT';

let age1 = 22;
let age2 = 21;
let age3 = 23;

ستجد أنك في كل مرة تنشئ كم هائل من البيانات لشخص واحد فقط
وكلها متغيرات متفرقة ليس هناك رابط برمجي يجمعها
بمعنى أن قلت لك age2 إلى من يعود ؟، أنت كإنسان أو كشخص كتب هذا الكود ستقول إلى name2
حسنًا إن سألنا نفس السؤال للبرنامج هل سيفهم أن الـ name2 مرتبطة بـ age2 مرتبطة بـ department2 ؟

مثال ابسط، أحضر لي أسماء جميع الطلاب الذين في قسم CS كيف ستفعلها برمجيًا ؟
لا يوجد رابط حقيقي بين المتغيرات بالنسبة للغة البرمجة إنها مجرد متغيرات مختلفة عشوائية لديها، ولا يوجد شيء حقيقي يربطهم
أنت كإنسان لديك الوعي لتفهم أن الـ age2 مرتبط بـ name2 بسبب أن إسم المتغير ينتهي برقم 2
هل سيفهم البرنامج او اللغة هذا الأمر ؟

المبدأ الأول: تجميع البيانات المرتبطة ببعضها تحت سقف واحد

هنا تظهر أول فائدة لمفهوم الـ Encapsulation
وهي أن تبدأ بتجميع البيانات التي لها علاقة ببعض تحت مسمى واحدة

وخير مثال عملي لهذا الأمر هو الـ Classes

بدلًا من أن تكون هذه البيانات منفصلة على حدا
تبدأ بتجميعها في كلاس واحد يضم تلك البيانات
وهذا هو التعريف الأساسي للـ Encapsulation
وهي الفكرة التي بنيت عليها الكلاسات

class Student {
  public name: string;
  public department: string;
  public age: number;

  constructor(name: string, department: string, age: number) {
    this.name = name;
    this.department = department;
    this.age = age;
  }
}

هنا طالما أنشأنا كلاس يضم البيانات فهذا هو تطبيق عملي لمفهوم الـ Encapsulation
بالتالي الـ object أصبح يضم كل البيانات في مكان واحد

let s1 = new Student('Ahmed Mostafa', 'CS', 22);

الآن بالنسبة للغة البرامج أصبحت المتغيرات name, department وage مرتبطة مع بعض تحت سقف واحد يدعى Student

الأمر لا يختصر على مفهوم الكلاسات فقط، يمكنك أن ترى مفهوم الـ Encapsulation في الـ Dictionary او الـ Object في الجافاسكريبت، فعلى سبيل المثال عندما يسجل شخص ما دخوله للموقع او للتطبيق الخاص بك تقوم بتخزين بيانات هذا الشخص في مكان ما مثل الـ localStorage كـ object تحت مسمى user يضم كل بيانات هذا الشخص (تجمع بيانات متفرقة مرتبطة ببعضها تحت مسمى واحد)

user = {
  name: 'Ahmed',
  age: 22,
  address: 'real awesome address',
  email: '[email protected]',
  ...ect,
};

لنرجع ونسأل نفس السؤال السابق أحضر لي أسماء جميع الطلاب الذين في قسم CS ؟

غالبًا بيانات الطلاب ستكون مجمعة في array بهذا الشكل

let students: Student[] = [
  new Student('Ahmed Mostafa', 'CS', 22),
  new Student('Kamal Mohamed', 'IS', 21),
  new Student('Ali Hamada', 'CS', 23),
  new Student('Mahmoud Ali', 'IT', 23),
];

بما أن البيانات متجمعة في مكان واحد وليست متغيرات عشوائية غير مرتبطة ببعضها البعض كالسابق
فهنا سيكون البحث أسهل بكثير وبشكل برمجي وليس بشكل يدوي

for (let i = 0; i < students.length; i += 1)
  if (students[i].department === 'CS') console.log(students[i].name);

// OUTPUT:
// Ahmed Mostafa
// Ali Hamada

المبدأ الثاني: تطبيق مفهوم الـ Abstraction

الـ Abstraction هو مفهوم يركز على التعامل مع الأشياء دون الاهتمام بالتفاصيل التي تحيط بهذا الشيء
بمعنى أننا الآن لدينا object من الكلاس Student يدعى s1
هذا الـ s1 يمثل طالب بكل مواصفاته وبياناته ووظائف

فالـ Abstraction هو أن لا تعرف حقيقة ما في داخل هذا الـ object الذي يدعى s1 في الحقيقة قد يحتوي الـ s1 على متغيرات كثيرة أو دوال متعددة تقوم بوظائف كثيرة
لكن أنت الآن بسبب الـ Encapsulation أصبحنا نتعامل مع الـ s1 ككيان واحد بغض النظر عن ما يحتويه، نحن الآن نتعامل مع طالب

نتعامل معه على أنه طالب بغض النظر عن ما يحتويه من متغيرات أو دوال ولا نهتم بالتفاصيل التي تكونه أو التي تحيط به
فالـ Encapsulation جعلت s1 يكون Abstraction بالنسبة لنا نتعامل معه كما هو بغض النظر عن تفاصيله

لنأخذ مثال على هذا:

let GPA = s1.getGPA();
let isSuccess = Student.determineSuccessRate(GPA);

if (isSuccess) s1.levelUp();
else s1.markAsFailed();

في هذا المثال كل شيء داخل s1 هي بالنسبة لنا Abstraction كما قلنا
لاننا لا نهتم بكل تفاصيله نحن نهتم بكيانه هو كطالب، حتى الدوال التي تكونه نحن لا نهتم بتفاصيلها نحن نهتم بالوظيفة الاساسية التي تفعله

في الكود الذي بالأعلى لقد أحضرنا GPA الخاص بالطالب عن طريق دالة getGPA بداخل s1

أن لا تعرف ومن الجيد أن لا تعرف، أن لا تتعب نفسك في التفكير في أمور ليست لها اهمية حقيقية لك وكما قلت فنحن الآن نتعامل مع الـ s1 ككيان واحد كشيء واحد كطالب بغض النظر عن ما يحتويه

بما أن البيانات في مكان واحد بفضل الـ Encapsulation فيمكننا عمل أمور كثيرة سنراها في الفقرات التالية

ملحوظة: ما نقصده بإخفاء البيانات عن الأشخاص، هؤلاء الأشخاص ممكن يكونوا مبرمجين يريدون استخدام الكلاس الخاص بك فهم سيهتمون بالوظيفة الأساسية للكلاس ودواله، ولا يريدون أن يتعبه أنفهم في معرفة كيف صممت أنت هذا الكلاس، ما يهمهم أن الكلاس يقوم بوظيفته كما يجب بغض النظر عما يحتويه وكيف يقوم بها

المبدأ الثالث: حماية البيانات من أي تلاعب

حسنًا ما أعنيه بالحماية هو التأكد من صحة البيانات المعطاه لنا
وأمنع أي تلاعب أو عبث في البيانات الخاصة بنا، والتأكد أن البيانات التي تأتي لنا بيانات صحيحة ومتوافقة معنا

فنحتاج لطريقة لوضع بعض القواعد للبيانات الخاصة بنا فعلى سبيل الإسم
فنحتاج أن نقول

الآن في الكود الحالي لا يوجد لدينا طريقة لفحص القيمة قبل إسنادها
فيمكن لأي الشخص القيام بتغير الإسم كهذا ببساطة

s.name = '234#3';

يمكن لأي أحد أن يعطي إسم عشوائي حتى لو أرقام ولن يعترض لأن متغير الـ name هو public بالتالي فيمكننا أن نعدل عليه بشكل مباشر دون قيود

حتى مع الـ constructor لا يوجد أي شيء يفحص ويتأكد من صحة البيانات


// Bad Input
let name: '234#3';
let department: 012015125125812.24521;
let age: 'hello world';

let s1 = new Student(name, department, age)

لكن سنتجاهل الـ constructor في الوقت الحالي ثم نعود اليه لاحقًا

نعود لموضوعنا، الـ name حاليًا public بالتالي أي شخص يمكنه التعديل عليه بشكل مباشر دون قيود لذا نحتاج لوسيط ما بين الإسم وبين المتغيرات لنضع تلك القواعد

قد تفكر الآن بجعل المتغيرات تكون private ثم تنشيء دوال تقوم بدور الوسيط

class Student {
  private name: string;

  public setName(name: string) {
    // Set some validation to name
    // if the name pass our roles
    // then set the name to the variable
    // else throw Exception
  }
}

في حالة جعل المتغير private وعمل دالة setName أصبحت القيمة تمر عبر وسيط الآن قبل أن تُسند
لذا فيمكننا عمل بعض الـ Validation في الدالة تمثل القواعد والأمور المسموح بها وغير المسموح بها
ثم يمكننا حفظ البيانات الجديدة في قاعدة البيانات الخاصة بنا في الـ Server على سبيل المثال

لنحاكي الأمر بشيء من التفصيل لتكون الصورة أوضح

class Student {
  private name: string = 'Unknown';

  public setName(name: string) {
    // Set some validation to name

    // check if the name is not between 3 and 20 characters
    if (name.length < 3 || name.length > 20)
      throw new Error(
        'Name must be minimum of 3 characters and maximum of 20'
      );
    // check if it start with a number
    if (name.match(/^[0-9]/))
      throw new Error("Name can't start with a number");

    // if the name pass our roles
    // then set the name to the variable
    this.name = name;
  }
}

let s1 = new Student();
s1.setName('Ahmed'); // valid

s1.setName('234#3'); // throw exception
s1.setName('ab'); // throw exception

كما ترى قمنا بعمل validation للإسم ثم بعد ما تخطى كل القواعد التي وضعناها أسندنا القيمة للمتغير الذي نريده

بالطبع يمكنك عمل try-catch لتتفقد أي خطأ قد حدث

try {
  let s1 = new Student();
  s1.setName('234#3'); // throw exception
} catch (err) {
  console.log(err.message); // OUTPUT: Name can't start with a number
}

ماذا عن الـ constructor

كيف سنضع تلك القواعد في الـ constructor ؟ قد تقول لي "فقط ننسخ الكود من دالة setName داخل الـ constructor"
هنا تكرار نقل الكود ليس حلًا وتكرار الكود هو اسوء شيء قد تفعله
وخصوصًا لو كان لدينا setName, setAge, setAddress ....
وكل واحدة لديها أسطر كثيرة من القواعد هل ننقل كل هذه الأسطر داخل الـ constructor ؟ بالطبع لا

الحل بسيط وهناك ألف حل لكن سأعطيك حل بسيط من أجل أن تصل المعلومة لا أكثر

constructor(name: string, department: string, age: number){
  try{
    this.name = setName(name)
    this.department = setDepartment(department)
    this.age = setAge(age)
  }
  catch(err){
    throw err;
  }
}

أنظر كم أن الأمر بسيط وجميل، بالطبع هذا الكود مجرد توضيح لأغراض الشرح لا أكثر قد تجد في المشاريع الكبيرة شيء مشابه أو مختلف، لكن أظن الفكرة قد وصلت

بما أننا نستطيع الآن التحقق من البيانات عن طريق الـ constructor فيمكننا جعل setName, setDepartment, setAge جعلها private ونكتفي بالتحقق من البيانات مرة واحدة فقط أثناء إنشاء الـ object من خلال الـ constructor, وإن أردنا تعديلها نقوم بعمل دوال اخرى للـ update ونضع قواعد وشروط مختلفة كما نشاء

كيف يكون الأمر بالنسبة لدالة الـ getter

حسنًا قد يأتي شخص ويقول أننا لا نقوم بعمل validation عند إحضار البيانات لذا لا يوجد له فائدة ؟

بالطبع يوجد عليك فقط توسيع تفكيرك ونظرتك للأمر وأن ترى المشاريع الكبيرة ماذا تحتاج
الشخص الذي يريد الحصول على البيانات هل له الصلاحيات للوصول لتلك البيانات ؟, هل الـ Server يعمل من الأساس ؟
هل البيانات الذي يريدها مازالت موجودة ؟, هل ... هل ... هل ...

فهناك الكثير من الاحتمالات فحتى احضار البيانات يحتاج لدالة وسيطة لتتحقق من بعض الامور مثل الصلاحيات كما ترى

المبدأ الرابع: لديك دوال وسيطة بينك وبين البيانات

الآن أنت منعت الشخص من الوصول لتلك البيانات لديك دوال هي المسؤولة عن المتغيرات والبيانات التي لديك
فأنت الآن تتحكم في كل شيء بشكل حرفي

الآن عن طريق تلك الدول

والكثير والكثير من الأمثلة العملية
ففكر في الأمر جيدًا ووسع نطاق تفكيرك فالأمر لا يختصر فقط على setter وgetter فقط كما ترى ولا تختصر على الـ validation للبيانات

كل هذه الأمور لم تكن لتتواجد لولا أننا جمعناهم في مكان واحد وحققنا مفهوم الـ Encapsulation

لنحاكي مجددًا شيء ما بشيء من التفصيل لتكون الصورة أوضح

import { EventEmitter } from 'events';
const databaseEmitter = new EventEmitter();

databaseEmitter.on('save-it', (data) => {
  console.log(`Saving to database... ${data}`);
});

class Student {
  private name: string = 'Unknown';

  public setName(name: string) {
    if (name.length < 3 || name.length > 20)
      throw new Error(
        'Name must be minimum of 3 characters and maximum of 20'
      );
    if (name.match(/^[0-9]/))
      throw new Error("Name can't start with a number");

    this.name = name;

    // emit event to save it to database
    databaseEmitter.emit('save-it', { name });
  }
}

let s1 = new Student();
s1.setName('Ahmed'); // valid

كما ترى قمنا بعمل validation للإسم ثم بعد ما تخطى كل القواعد التي وضعناها عملنا emit لـ event معين
الـ event تستقبل الإسم تقوم بتخزينها في الـ Server، أو يمكنك فعل ما تريد هنا الأمر عائد لك

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

خلاصة الـ Encapsulation وأهم النقاط

حسنًا خذ نفسًا كر بكل ما قلناه فوق
هل كنا نستطيع عمل كل تلك الأمور بدون تطبيق مفهوم الـ Encapsulation ؟ فقط فكر وستفهم كل شيء

أهم النقاط هنا كانت:

عليك أن تدرك أن مفهوم الـ Encapsulation البسيط في ضم المتغيرات تحت سقف واحد أدى الى كل هذا وربما أكثر، في الحقيقة عالم الـ OOP قائم على هذا المفهوم البسيط

يمكنك أن تدرك أن أمورًا مثل إخفاء البيانات وحمايتها وعمل دوال وسيطة لتضع فيها قواعد لتلك المتغيرات تمثل الـ Validation
او تحفظ تلك البيانات في الـ Server أو ترسل emit لـ event بأن القيمة تم استنادها بنجاح أو ... أو .... أو... هذه أمور استفادت من مفهوم الـ Encapsulation وليست هي المفهوم بحد ذاته