الدليل الشامل لمعدّلات الوصول (Access Modifiers) في TypeScript: من الأساسيات إلى هندسة الكود الكائني
في عالم البرمجة الحديث، تتزايد تعقيدات الأنظمة والتطبيقات بشكل مطرد؛ حيث لم تعد البرمجيات مجرد نصوص متفرقة تؤدي وظائف بسيطة، بل أصبحت بنيات تحتية ضخمة يشترك في بنائها مئات المطورين. مع هذا التعقيد، تبرز الحاجة الماسة إلى تقنيات تضبط تدفق البيانات وتمنع التلاعب غير المصرح به بالحالات الداخلية للكائنات. هنا يأتي المفهوم الجوهري للبرمجة كائنية التوجه (OOP): التغليف (Encapsulation)، والذي يتحقق بصورة أساسية عبر ما يُعرف بـ معدّلات الوصول (Access Modifiers).
توفر لغة TypeScript، كامتداد فائق للغة JavaScript، آليات تحقق صارمة في وقت الترجمة (Compile Time) لتحديد واجهات التفاعل البرمجية ومنع الوصول العشوائي للخصائص والدوال الداخلية. سنتناول في هذا الدليل تشريحاً عملياً مفصلاً لمعدّلات الوصول ومقارناتها، وحل مشكلاتها البرمجية الأكثر شيوعاً، وبناء تطبيقات واقعية تحاكي الأنظمة المصرفية وإدارة المستخدمين.
المفاهيم الأساسية والأهمية
بعد أن ألقينا نظرة عامة على معدّلات الوصول ودورها المحوري في عالم البرمجة الكائنية، ننتقل الآن لنتعمق في فهم المفاهيم الأساسية التي تبنى عليها هذه المعدّلات. في لغة TypeScript، يُحدد كل معدّل وصول مساحة الرؤية (Visibility Scope) التي يمكن من خلالها استدعاء الحقل أو الطريقة البرمجية.
لماذا نحتاج إلى معدّلات الوصول؟
- حماية سلامة البيانات (Data Integrity): منع التعديل المباشر على المتغيرات الحساسة دون المرور عبر دوال التحقق والمنطق الرياضي المعين.
- تقليل الاعتمادية المتبادلة (Decoupling): عندما تخفي الفئة تفاصيلها الداخلية، يمكن تغيير خوارزميات العمل مستقبلاً دون كسر الشيفرات البرمجية التي تستخدم تلك الفئة.
- تحديد واجهات برمجة واضحة (Clear API Surface): يعلم المطور الذي يستدعي الفئة ما هي الطرق المتاحة للاستخدام الخارجي بدقة وما هي الأجزاء المخصصة للبناء الداخلي فقط.
- تعزيز صيانة النظم البرمجية (Maintainability): منع الأخطاء الناتجة عن تداخل الوظائف بين أعضاء الفريق الواحد في المشاريع المعقدة.
معدّلات الوصول في TypeScript: الشرح التفصيلي والخطوات العملية
في هذا القسم، سنغوص أعمق في مفهوم معدّلات الوصول في TypeScript ونتعرف على خصائصها وكيفية استخدامها في الشيفرة البرمجية الحية. تدعم اللغة ثلاثة معدّلات وصول أساسية مدعومة بمفهوم الفصل الداخلي للوحدات:
- العام (public): المعدّل الافتراضي؛ يمكن الوصول إلى العناصر من أي مكان داخل وخارج الفئة.
- الخاص (private): يقتصر الوصول إلى العنصر على حدود الفئة المعرفة له حصراً، ولا يمكن الوصول إليه حتى من الفئات المشتقة.
- المحمي (protected): يسمح بالوصول للعنصر داخل الفئة الحالية والفئات الفرعية (Subclasses) التي ترث منها، مع منعه تماماً من الوصول الخارجي.
- المستوى الداخلي للوحدات (Module Scoping): حصر الكائنات والدوال داخل نطاق الملف أو الحزمة عبر عدم تصديرها (Non-exported members).
الشفرة العملية لتطبيق معدّلات الوصول
يوضح المثال التالي السلوك الصارم لكل معدّل وصول وكيف يتصرف مفسر TypeScript عند محاولة الوصول غير المشروع:
// تعريف فئة أساسية توضح الحقول العامة والخاصة والمحمية
class BaseAccount {
// 1. حقل عام: متاح في كل مكان
public accountHolder: string;
// 2. حقل خاص: محصور داخل BaseAccount فقط
private internalSecretKey: string = "SECRET_KEY_12345";
// 3. حقل محمي: متاح للفئة الأساسية والفئات التي ترث منها
protected balance: number;
constructor(holder: string, initialBalance: number) {
this.accountHolder = holder;
this.balance = initialBalance;
}
// طريقة عامة: تمثل الواجهة الخارجية للفئة
public displayAccountInfo(): void {
console.log(`صاحب الحساب: ${this.accountHolder}`);
console.log(`الرصيد الداخلي: ${this.balance}`);
this.logInternalAccess();
}
// طريقة خاصة: وظيفة مساعدة داخلية لا تُستدعى من الخارج
private logInternalAccess(): void {
console.log(`تم التحقق من المفتاح الداخلي: ${this.internalSecretKey}`);
}
// طريقة محمية: وظيفة يمكن للفئات الفرعية الاستفادة منها وتعديلها
protected applyInternalFee(fee: number): void {
this.balance -= fee;
console.log(`تم خصم الرسوم بقيمة: ${fee}. الرصيد الجديد: ${this.balance}`);
}
}
// فئة فرعية ترث من الفئة الأساسية
class SavingsAccount extends BaseAccount {
private interestRate: number;
constructor(holder: string, initialBalance: number, interestRate: number) {
super(holder, initialBalance);
this.interestRate = interestRate;
}
public calculateAnnualProfit(): void {
// ✅ مسموح: الوصول إلى balance لأنه محمي (protected)
const profit = this.balance * (this.interestRate / 100);
console.log(`الأرباح السنوية المقدرة: ${profit}`);
// ✅ مسموح: استدعاء دالة محمية
this.applyInternalFee(10);
// ❌ خطأ ترجمة: الحقل internalSecretKey خاص بالفئة الأم فقط
// console.log(this.internalSecretKey); // Property 'internalSecretKey' is private
}
}
// إنشاء الكائنات وتجربة الوصول الخارجي
const account = new BaseAccount("محمد الشامي", 5000);
console.log(account.accountHolder); // ✅ مسموح: الحقل عام (public)
account.displayAccountInfo(); // ✅ مسموح: الطريقة عامة (public)
// ❌ أخطاء برمجية في وقت الترجمة:
// console.log(account.balance); // Property 'balance' is protected
// console.log(account.internalSecretKey); // Property 'internalSecretKey' is private
// account.logInternalAccess(); // Property 'logInternalAccess' is privateالمقارنات والتحليلات المتقدمة لمعدّلات الوصول
في عالم TypeScript، تُستخدم معدّلات الوصول لتنظيم مستوى أولوية الوصول للأجزاء المختلفة من الشيفرة البرمجية. يلخص الجدول التحليلي التالي الفروق الجوهرية بين هذه المعدّلات وبيئات تفاعلها:
| معدّل الوصول | داخل الفئة نفسها | داخل الفئات الفرعية الموروثة | من خارج الفئة (عبر الكائن) | التواجد بعد الترجمة إلى JavaScript |
|---|---|---|---|---|
| public | نعم (متاح) | نعم (متاح) | نعم (متاح) | متاح كحقل عادي |
| protected | نعم (متاح) | نعم (متاح) | لا (ممنوع في وقت الترجمة) | يتحول لحقل عادي في وقت التشغيل |
| private | نعم (متاح) | لا (ممنوع في وقت الترجمة) | لا (ممنوع في وقت الترجمة) | يتحول لحقل عادي في وقت التشغيل |
| # (Private Fields) | نعم (متاح) | لا (ممنوع) | لا (ممنوع في وقت التشغيل والترجمة) | محمي حقيقياً في الذاكرة (Hard Private) |
مقارنة تقنية: private في TypeScript مقابل الحقول الخاصة الحقيقية # في JavaScript
من أهم النقاط التي تخفى على الكثير من المطورين هي أن الكلمة المفتاحية private في TypeScript هي ميزة وقت الترجمة فقط (Soft Private). بمجرد ترجمة الكود إلى JavaScript قياسي، تتم إزالة الكلمة المفتاحية ويصبح الحقل متاحاً في وقت التشغيل (Runtime)، ويمكن الوصول إليه عبر فهارس النصوص مثل account["internalSecretKey"].
أما المعيار الحديث للغة JavaScript المدعوم في TypeScript بدءاً من إصدارات ES2020، فيوفر الحقول الخاصة الصارمة (Hard Private Fields) باستخدام علامة الهاش #. هذه الحقول محمية على مستوى محرك JavaScript في الذاكرة ولا يمكن الوصول إليها حتى عبر التلاعب بالـ Prototype:
class ModernSecureVault {
// حقل خاص حقيقي في الذاكرة مدعوم من محرك V8
#vaultCode: string = "9988-SECURE";
// حقل خاص بـ TypeScript فقط
private tsCode: string = "TS-1234";
public unlock(input: string): boolean {
return this.#vaultCode === input;
}
}
const vault = new ModernSecureVault();
// console.log((vault as any).tsCode); // يعمل ويطبع القيمة في وقت التشغيل
// console.log((vault as any)["#vaultCode"]); // SyntaxError: محمي حقيقياً على مستوى الذاكرةالتعريف المختصر في دوال البناء (Parameter Properties)
تقدم TypeScript ميزة قوية واختصاراً ذكياً يُعرف بـ Parameter Properties، حيث تتيح لك دمج تعريف الحقول وإسناد قيمها في سطر واحد مباشرة داخل دالة البناء (Constructor) بمجرد وضع معدّل الوصول أمام المعامل:
// الأسلوب التقليدي الطويل:
class TraditionalUser {
public id: string;
private token: string;
protected role: string;
constructor(id: string, token: string, role: string) {
this.id = id;
this.token = token;
this.role = role;
}
}
// الأسلوب الاحترافي المختصر في TypeScript:
class EfficientUser {
constructor(
public id: string,
private token: string,
protected role: string
) {
// يقوم TypeScript تلقائياً بإنشاء الحقول وإسناد قيمها هنا خلف الكواليس
}
}أفضل الممارسات والنصائح التطبيقية
بعد أن استكشفنا بعمق معدّلات الوصول في TypeScript وتفاصيل عمل كل منها، حان الوقت لنتعمق في كيفية تطبيق هذه المعرفة بفعالية في مشاريعك البرمجية لضمان كود نظيف، آمن، وسهل الصيانة:
- ابدأ دائماً بمبدأ الأقل صلاحية (Principle of Least Privilege): اجعل كل حقل وكل طريقة
privateافتراضياً ما لم يكن هناك سبب صريح وواضح لجعلهاprotectedأوpublic. - استخدم دوال الوصول (Getters and Setters) مع الحقول الخاصة: بدلاً من كشف المتغير العام، اجعل المتغير خاصاً واستخدم دوال
getوsetللتحقق من صحة المدخلات. - تجنب الحقول العامة القابلة للتعديل (Mutable Public Fields): إذا كان الحقل عاماً ولا يحتاج إلى تعديل، قم بدمجه مع الكلمة المفتاحية
readonlyلحمايته من التغيير بعد التهيئة. - قصر استخدام protected على البنى الهرمية المخططة: استخدم
protectedفقط عندما تكون الفئة مصممة لكي تكون فئة أساسية مجردة أو فئة قابلة للتوسعة مستقبلاً. - عدم الاعتماد على private لحماية الأسرار الحساسة على الواجهات الأمامية (Frontend): تذكر أن TypeScript يترجم إلى JavaScript، لذا فإن المفاتيح السرية يجب ألا تُخزن في كود الواجهة الأمامية معتمداً على
private.
حل المشكلات والأخطاء الشائعة واستكشاف الأعطال
عند التعامل مع معدّلات الوصول، يواجه المطورون تحديات متكررة تؤدي إلى ظهور أخطاء وقت الترجمة أو ثغرات تصميمية في بنية الكود. نستعرض فيما يلي أبرز تلك المشكلات وكيفية معالجتها برمجياً:
المشكلة الأولى: محاولة الوصول إلى أعضاء خاصة في فئات مشتقة
الخطأ: يحدث خطأ الترجمة Property 'x' is private and only accessible within class 'Parent' عندما تحاول الفئة الفرعية قراءة حقل تم تعريفه كـ private في الفئة الأم.
class Parent {
private secret = "بيانات داخلية";
}
class Child extends Parent {
showSecret() {
// ❌ خطأ ترجمة:
// return this.secret;
}
}
// ✅ الحل: تحويل الحقل إلى protected إذا كان مخصصاً للتوريث
class ProperParent {
protected secret = "بيانات داخلية موروثة";
}
class ProperChild extends ProperParent {
showSecret() {
return this.secret; // ✅ يعمل بنجاح
}
}المشكلة الثانية: كسر التغليف عبر تمرير مراجع الكائنات (Reference Leakage)
المشكلة: تعريف مصفوفة أو كائن كـ private، ثم إرجاع مرجعه المباشر عبر دالة public. في JavaScript، يؤدي هذا إلى إمكانية تعديل محتويات المصفوفة من خارج الفئة رغم كون الحقل خاصاً!
// ❌ أسلوب خاطئ يسبب تسريب المراجع:
class CartBad {
private items: string[] = ["كتاب", "قلم"];
public getItems(): string[] {
return this.items; // تسريب المرجع المباشر
}
}
const cartBad = new CartBad();
cartBad.getItems().push("عنصر دخيل"); // تم تعديل الحقل الخاص من الخارج!
// ✅ الأسلوب الصحيح: إرجاع نسخة جديدة أو مصفوفة للقراءة فقط
class CartGood {
private items: string[] = ["كتاب", "قلم"];
public getItems(): ReadonlyArray<string> {
return [...this.items]; // إرجاع نسخة مفصولة
}
}المشكلة الثالثة: التحايل على الحقول الخاصة عبر فهارس النصوص
في TypeScript، إذا قمت بكتابة instance["privateField"]، فقد يسمح المفسر بالقراءة إذا كانت إعدادات المشروع غير صارمة. يجب معالجة ذلك عبر استخدام علامة الهاش # الرسمية لمنع التلاعب البرمجي غير المقصود في بيئات الإنتاج الحساسة.
دراسات حالة وأمثلة واقعية متقدمة
لتطبيق هذه المفاهيم في مشاريع الإنتاج الفعلية، نستعرض دراستي حالة متكاملتين تغطيان التغليف في المعاملات المالية وهندسة قواعد البيانات:
دراسة الحالة 1: نظام المحفظة الرقمية المحمية (Digital Wallet)
يحتاج النظام المصرفي إلى ضمان عدم تعديل الرصيد إلا عبر عمليات إيداع وسحب دقيقة، مع تسجيل سجل العمليات (Audit Trail) بشكل محمي لا يمكن التلاعب به من الخارج:
interface Transaction {
id: string;
amount: number;
type: "DEPOSIT" | "WITHDRAW";
timestamp: Date;
}
class SecureWallet {
readonly walletId: string;
private _balance: number = 0;
private transactions: Transaction[] = [];
constructor(walletId: string, initialDeposit: number) {
this.walletId = walletId;
if (initialDeposit > 0) {
this.deposit(initialDeposit);
}
}
// دالة قراءة آمنة للرصيد
public get currentBalance(): number {
return this._balance;
}
// إيداع مالي مع فحص منطقي
public deposit(amount: number): boolean {
if (amount <= 0) {
console.error("قيمة الإيداع يجب أن تكون أكبر من الصفر");
return false;
}
this._balance += amount;
this.recordTransaction(amount, "DEPOSIT");
return true;
}
// سحب مالي مع التحقق من كفاية الرصيد
public withdraw(amount: number): boolean {
if (amount <= 0 || amount > this._balance) {
console.error("عملية السحب مرفوضة: رصيد غير كافٍ أو قيمة غير صالحة");
return false;
}
this._balance -= amount;
this.recordTransaction(amount, "WITHDRAW");
return true;
}
// وظيفة داخلية لتسجيل الحركات المالية
private recordTransaction(amount: number, type: "DEPOSIT" | "WITHDRAW"): void {
this.transactions.push({
id: `TX-${Date.now()}-${Math.floor(Math.random() * 1000)}`,
amount,
type,
timestamp: new Date()
});
}
// إرجاع سجل المعاملات للقراءة فقط
public getTransactionHistory(): ReadonlyArray<Transaction> {
return [...this.transactions];
}
}
// تجربة استخدام المحفظة الرقمية
const myWallet = new SecureWallet("W-8890", 1000);
myWallet.deposit(500);
myWallet.withdraw(200);
console.log(`الرصيد النهائي: ${myWallet.currentBalance}$`); // 1300$
console.log(`عدد الحركات المسجلة: ${myWallet.getTransactionHistory().length}`); // 3
// myWallet._balance = 50000; // ❌ خطأ: الحقل محمي تماماً من التلاعب الخارجيدراسة الحالة 2: مستودع البيانات التجريدي (Repository Pattern)
يوضح هذا المثال كيفية استخدام protected لبناء فئة مستودع بيانات أساسية توفر اتصال قاعدة البيانات للفئات المشتقة فقط مع إخفائه عن المستهلك النهائي:
abstract class BaseRepository<T> {
// اتصال قاعدة البيانات محمي: متاح فقط لفئات المستودعات المشتقة
protected dbConnection: string = "DATABASE_URI_ACTIVE";
abstract findById(id: string): T | null;
abstract save(entity: T): void;
// دالة مساعدة محمية لتسجيل الاستعلامات
protected executeQuery(query: string): void {
console.log(`[SQL EXEC] تنفيذ الاستعلام: ${query} عبر الاتصال ${this.dbConnection}`);
}
}
interface Product {
id: string;
name: string;
price: number;
}
class ProductRepository extends BaseRepository<Product> {
private storage = new Map<string, Product>();
public findById(id: string): Product | null {
// ✅ استخدام الدالة المحمية الموروثة
this.executeQuery(`SELECT * FROM products WHERE id = '${id}'`);
return this.storage.get(id) || null;
}
public save(product: Product): void {
this.executeQuery(`INSERT INTO products VALUES ('${product.id}', '${product.name}')`);
this.storage.set(product.id, product);
}
}
const prodRepo = new ProductRepository();
prodRepo.save({ id: "P-100", name: "لابتوب فائق الأداء", price: 1200 });
// prodRepo.dbConnection; // ❌ خطأ: الاتصال محمي ولا يمكن استدعاؤه مباشرة من الخارجمعدّلات الوصول في TypeScript: الأسئلة الشائعة وإجاباتها
معدّلات الوصول تعتبر جزءاً أساسياً من أساسيات البرمجة الكائنية التوجه (OOP). في هذا القسم، سنتناول أهم التساؤلات التقنية التي تدور في أذهان المطورين مع إجابات هندسية دقيقة:
1. ما الفرق الحقيقي بين private و protected؟
الفرق يكمن في سلوك التوريث (Inheritance). الحقل المعرف بـ private لا يمكن رؤيته أو استدعاؤه داخل الفئات الفرعية التي ترث الفئة، بينما الحقل المعرف بـ protected ينتقل عبر سلسلة التوريث ويكون متاحاً لجميع الفئات المشتقة مع بقائه مخفياً تماماً عن الاستدعاء الخارجي.
2. هل يؤدي استخدام private إلى تحسين أداء تشغيل التطبيق في المتصفح؟
لا، لأن private في TypeScript يُحذف بالكامل أثناء عملية الترجمة (Transpilation) إلى JavaScript ولا يترك أي بصمة في الذاكرة. الهدف منه هو أمان وقت التطوير (Developer Experience & Type Safety) ومنع كتابة أكواد غير مصرح بها أثناء البناء.
3. هل يمكن استخدام معدّلات الوصول مع الواجهات (Interfaces)؟
لا يسمح TypeScript بوضع معدّلات الوصول مثل private أو protected داخل تعريف الواجهة (Interface)، لأن الواجهات تمثل عقوداً مخصصة لتعريف الواجهة العامة الخارجية (Public API) للكائنات والأصناف. جميع أعضاء الواجهة تعتبر عامة (public) حكماً.
4. ما الفارق بين readonly ومعدّلات الوصول؟
معدّلات الوصول (public, private, protected) تتحكم في مكان رؤية واستدعاء العنصر، بينما readonly يتحكم في إمكانية تعديل القيمة بعد التهيئة الأولى. يمكنك الجمع بينهما بكل مرونة، مثل: private readonly id: string; لإنشاء حقل خاص بالقراءة فقط داخل الفئة.
خلاصة موضوع معدّلات الوصول والخطوات القادمة
في ختام هذا الدرس الشامل من سلسلة احتراف TypeScript، نلخص أهم المرتكزات الأساسية التي تعرفنا عليها:
- public: للواجهات العامة القابلة للاستدعاء بحرية عبر كامل أجزاء المشروع.
- protected: لمشاركة الوظائف والبيانات عبر الهياكل الهرمية الموروثة دون كشفها للخارج.
- private: لعزل المنطق الحساس والتفاصيل التنفيذية داخل الصنف الواحد ومنع الاعتماديات الخارجية.
- # Private Fields: للحصول على عزل حقيقي ومحكم على مستوى محرك JavaScript في وقت التشغيل.
- Parameter Properties: لاختصار كتابة الفئات وتقليل الشيفرة المتكررة داخل دوال البناء.
في الدروس القادمة، سنتوسع في المفاهيم المتقدمة للبرمجة الكائنية، ونستعرض بالتفصيل كيفية الدمج بين الفئات المجردة (Abstract Classes) والواجهات البرمجية (Interfaces) مع معدّلات الوصول لتطبيق مبادئ SOLID وتطوير منصات برمجية عملاقة بموثوقية فائقة.
المراجع والمصادر التعليمية المعتمدة:
- التوثيق الرسمي لـ TypeScript: وضوح الأعضاء ومعدلات الوصول في الفئات (Member Visibility)
- شبكة مطوري موزيلا (MDN): الخصائص الخاصة الصارمة في JavaScript (# Private Fields)
- دورة TypeScript من الصفر إلى الاحتراف – منصة كود التطور
اكتشاف المزيد من كود التطور
اشترك للحصول على أحدث التدوينات المرسلة إلى بريدك الإلكتروني.
