المفهوم الخاطئ للـ Encapsulation ؟!
السلام عليكم ورحمة الله وبركاته
الفهرس
المقدمة
في هذه المقالة من سلسلتنا الصغير عن الـ OOP
أحببت أن أوضح مفهوم الـ Encapsulation
لأنني لاحظت أن كثير ممن درس الـ Encapsulation
لا يفهمون حقيقته أو لديهم نظرة سطحية أو ناقصة عنه
فعلى سبيل المثال عندما تدرس الـ OOP
فيجب أن تمر على اول مبدأ من مبادئ الـ OOP
وهو الـ Encapsulation
عندما تسمع للشرح الخاص به ستجد أنه يقول لك
- الـ
Encapsulation
هو تحويل المتغيرات المهمة إلىprivate
ثم تقوم بعمل دوال وسيطة مثلsetter
وgetter
لكي تعدل عليها فقط! ثم ينتهي الشرح عند هذه النقطة!
هنا تكمن المشكلة لأن الشخص الذي يشرح يقول تلك هذا المعلومة فقط بأنها هي مفهوم الـ Encapsulation
حسنًا لنستمع لهذه المحادثة الصغيرة:
- الـ
Encapsulation
هي أن نخفي البيانات ونقوم بعمل دوالsetter
وgetter
لها لنعدل عليها ؟ - نعم!
- حسنًا، لما لا تقوم بعمل المتغيرات
public
فحسب ؟ - اااامممم ...
أن ظهر صوت صرصور الحقل فإذا هو فهم الـ 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
ولماذا نحتاج لها
لأننا نحتاج لربط هذه البيانات بطريقة ما
هنا أنا من وجه نظري الشخصية أرى أن الـ Encapsulation
نستطيع تقسيمها إلى 4
مبادئ أساسية
المبدأ الأول: تجميع البيانات المرتبطة ببعضها تحت سقف واحد
هنا تظهر أول فائدة لمفهوم الـ 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 ؟
غالبًا بيانات الطلاب ستكون مجمعة في أراي بهذا الشكل
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
- هل تسائلنا كيف تحضر تلك الدالة الـ
GPA
أو من أين ؟ لا، هل من المهم أن نعرف ؟ لا - هل عرفنا كيف حددت دالة
determineSuccessRate
الـGPA
الخاص بالطالب أو ما المعادلة التي بداخلها ؟ لا، هل من المهم أن نعرف ؟ لا - هل عرفنا ما الاجراءات التي فعلتها دالة
levelUp
أوmarkAsFailed
هل كلمتserver
أو خزنت شيء ما ؟ لا، هل من المهم أن نعرف ؟ لا
أن لا تعرف ومن الجيد أن لا تعرف، أن لا تتعب نفسك في التفكير في أمور ليست لها اهمية حقيقية لك
وكما قلت فنحن الآن نتعامل مع الـ 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
يعمل من الأساس ؟
هل البيانات الذي يريدها مازالت موجودة ؟, هل ... هل ... هل ...
فهناك الكثير من الاحتمالات فحتى احضار البيانات يحتاج لدالة وسيطة لتتحقق من بعض الامور مثل الصلاحيات كما ترى
أو حتى يمكنك ارجاع بيانات مختلفة بناءًا على البيانات التي تمتلكها
بمعنى أنك لديك تاريخ ميلاد الشخص لكنك تريد حساب عمره الحالي
وعمر الشخص لا يتم تخزينه في قاعدة البيانات
فيمكنك هنا إنشاء دالة جديدة تقوم بحساب هذه القيمة بناءًا على البيانات الموجودة
class Student {
private name: string = 'Unknown';
private birthday: string = 'Unknown';
public getAge(id: number) {
let student = getStudentById(id);
// Check and validate student's validation, permission ... etc.
if (!validate(student)) throw new Error('Something went wrong');
// if pass, calculate the age of the student
let today = new Date();
return today.getFullYear() - student.birthDate.getFullYear();
}
}
هنا قمنا بعمل دالة تحسب لنا عمر الشخص (بيانات لم نكن نمتلكها) عن طريق تاريخ ميلاد الشخص (بيانات نمتلكها)
لاحظ كيف فادنا الـ Encapsulation
في حساب والحصول على بيانات جديدة لم نكن نمتلكها
وأيضًا حمايتها من أي تلاعب أو عبث
المبدأ الرابع: لديك دوال وسيطة بينك وبين البيانات
الآن أنت منعت الشخص من الوصول لتلك البيانات لديك دوال هي المسؤولة عن المتغيرات والبيانات التي لديك
فأنت الآن تتحكم في كل شيء بشكل حرفي
الآن عن طريق تلك الدول
- تستطيع عمل
authentication
عن طريق تلك الدوال - تستطيع عمل
emit
لـevent
معين يفيد بأن القيمة أسندت بنجاح - تستطيع بعد التأكد من القيم حفظ البيانات الجديدة في الـ
Server
في قاعدة بيانات - أو تريد أخبار الشخص بأن يعيد إدخال البيانات بسبب أنها بيانات خاطئة
- تستطيع عمل
event
معين يقوم بعملbroadcast
لكل الأشخاص في الـServer
أن هناك شخص ما سجل دخوله للتو- لنفترض أنك لديك الأشخاص موجودين داخل مجموعة معينة وهناك شخص ما انضم لهم فأنت تريد تنبيه الآخرين
- يكون لديك دالة تنبه الجميع بهذا
- ولديك متغير يقوم بمعرفة الى مجموعة ينتمي هذا الشخص
- تستطيع جعل أشخاص معينة يتلقون إشعارات
notifications
من شيء ما هم يقومون بمتابعته- عن طريق إنشاء متغير معين يقوم بتحديد ماذا يتابع هذا الشخص على سبيل المثال
والكثير والكثير من الأمثلة العملية
ففكر في الأمر جيدًا ووسع نطاق تفكيرك فالأمر لا يختصر فقط على 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
أنه يقوم بضم البيانات المترابطة مع بعضها تحت سقف واحد- هذا أدى إلى تسهيل التعامل مع تلك بيانات
- بدل ما أن تكون البيانات متفرقة ولا يوجد رابط برمجي تستطيع من خلاله جمع تلك البيانات والقيام بعمليات عليها
- أصبحت البيانات سقف واحد يجمعها اسم واحد ويوحدها الـ
object
الذي من خلاله تستطيع عمل أي عملية على تلك البيانات لأكثر منobject
بشكل سلس
- تحقيق مفهوم الـ
Abstraction
بأن سمح لك بأن تتعامل مع الـobject
بشكل ذاتي مستقبل بغض النظر عن ما بداخله- لم تعد تهتم بالتفاصيل الداخلية للدوال أو للـ
object
بشكل عام - أصبحت تهتم بالوظيفة الأساسية التي تقوم بها
- التعامل مع الـ
object
ككيان مستقل يقوم بالوظيفة التي تريدها دون الاهتمام بكيف يقوم بها
- لم تعد تهتم بالتفاصيل الداخلية للدوال أو للـ
- أصبح بإمكانك اخفاء البيانات عن المستخدم وجعله يتعامل ما الدوال التي تريده أن يتعال معها
- وهذا جزء من الحماية والـ
abstraction
- وهذا جزء من الحماية والـ
- فكرة أن لديك دوال وسيطة بينك وبين البيانات
- يمكنك إخفاء البيانات وجعل الشخص يتعامل معها عن طريق الدوال
- أصبحت تستطيع عمل:
validation
authentication
security
emit some events & send broadcast to other users
get or send notifications
... ect
عليك أن تدرك أن مفهوم الـ Encapsulation
البسيط في ضم المتغيرات تحت سقف واحد أدى الى كل هذا وربما أكثر، في الحقيقة عالم الـ OOP
قائم على هذا المفهوم البسيط
يمكنك أن تدرك أن أمورًا مثل إخفاء البيانات وحمايتها وعمل دوال وسيطة لتضع فيها قواعد لتلك المتغيرات تمثل الـ Validation
او تحفظ تلك البيانات في الـ Server
أو ترسل emit
لـ event
بأن القيمة تم استنادها بنجاح أو ... أو .... أو...
هذه أمور استفادت من مفهوم الـ Encapsulation
وليست هي المفهوم بحد ذاته