تخطي إلى المحتوى الرئيسي

الخصائص للقراءة فقط (Readonly Properties) و Getters/Setters في TypeScript

الخصائص للقراءة فقط (Readonly Properties) و Getters/Setters في TypeScript

الدليل الشامل للخصائص للقراءة فقط (Readonly Properties) ودوال Getters و Setters في TypeScript

المقدمة والنظرة الشاملة

في برمجة TypeScript، تعتبر خصائص القراءة فقط (Readonly Properties) ومُحصّلات ومُعدّلات الوصول (Getters and Setters) من المفاهيم الهندسية المحورية التي تضمن استقرار البيانات وتنظيم تدفقها داخل الكائنات. تهدف هذه الأدوات إلى تطبيق ركيزة التغليف (Encapsulation) وحماية الحالة الداخلية للتطبيق من التعديلات العشوائية غير المصرح بها.

1. تعريف وخصائص القراءة فقط (Readonly Properties)

الخصائص للقراءة فقط هي خصائص ثابتة لا يمكن تعديل قيمتها بعد تهيئتها المبدئية. في TypeScript، تُستخدم الكلمة المفتاحية readonly لتحديد الخصائص داخل الفئات (Classes)، والواجهات (Interfaces)، والأنواع المعرفة (Type Aliases):

interface UserProfile {
    readonly id: string;
    name: string;
}

const user: UserProfile = {
    id: "USR-1092",
    name: "محمد"
};

user.name = "علي"; // ✅ مسموح: خاصية قابلة للتعديل
// user.id = "USR-9999"; // ❌ خطأ في وقت الترجمة: Cannot assign to 'id' because it is a read-only property.

2. فهم Getters و Setters

الـ Getters (المُحصّلات) والـ Setters (المُعدّلات) هما دوال ترتبط بخصائص الكائن، ولكنها تُستدعى كما لو كانت خصائص عادية دون أقواس (). تتيح لك هذه الميزة اعتراض قراءة القيمة لإجراء عمليات حسابية، أو اعتراض كتابتها لإجراء عمليات تحقق وفحص صارم قبل الحفظ في الذاكرة.

class UserBasic {
    private _name: string = "افتراضي";
    private _age: number = 20;

    // Getter للوصول إلى الاسم
    get name(): string {
        return this._name;
    }

    // Setter لتعديل الاسم مع التحقق
    set name(value: string) {
        if (value.trim().length === 0) {
            throw new Error("لا يمكن أن يكون الاسم فارغاً");
        }
        this._name = value;
    }

    get age(): number {
        return this._age;
    }

    set age(value: number) {
        if (value < 0) {
            throw new Error("لا يمكن أن يكون العمر سالباً");
        }
        this._age = value;
    }
}

3. الخصائص المحسوبة (Computed Properties) عبر Getters

واحدة من أعظم فوائد Getters هي اشتقاق قيم جديدة بناءً على الحقول الداخلية دون الحاجة لتخزين حقل إضافي يستهلك الذاكرة:

class FullUser {
    constructor(public firstName: string, public lastName: string, public birthYear: number) {}

    // خاصية محسوبة للقراءة فقط
    get fullName(): string {
        return `${this.firstName} ${this.lastName}`;
    }

    get calculatedAge(): number {
        const currentYear = new Date().getFullYear();
        return currentYear - this.birthYear;
    }
}

const person = new FullUser("محمد", "أحمد", 1995);
console.log(person.fullName);      // محمد أحمد
console.log(person.calculatedAge); // يحسب العمر ديناميكياً

المفاهيم الأساسية والأهمية

بعد أن تعرفنا في القسم السابق على لمحة عامة عن الخصائص للقراءة فقط (Readonly Properties) وGetters/Setters، حان الوقت للتعمق في المفاهيم الأساسية التي يقوم عليها كل منها، واستكشاف الأهمية الجوهرية التي تضفيها على تصميم تطبيقات TypeScript، وكيف تسهم في بناء برمجيات أكثر قوة، أمانًا، وسهولة في الصيانة.

الخصائص للقراءة فقط (Readonly Properties): ضمان الثبات وسلامة البيانات

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

  • سلامة البيانات (Data Integrity): حماية المعرفات الأساسية (IDs) ومفاتيح الربط والتواريخ التأسيسية من أي تعديل عرضي أثناء دورة حياة الكائن.
  • منع الآثار الجانبية (Predictable State): في الأنظمة الموزعة والتطبيقات التي تعتمد على إدارة الحالة، يضمن الثبات عدم حدوث تغييرات غير متوقعة تؤدي إلى صعوبة تعقب الأخطاء.
  • تقليل أخطاء وقت التشغيل: يلتقط مترجم TypeScript محاولة التعديل أثناء كتابة الكود مباشرة، مما يقلل احتمالية انهيار النظام في الإنتاج.

الشرح التفصيلي والخطوات العملية

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

قواعد تهيئة الخصائص ذات السمة readonly

في TypeScript، يمكن تهيئة الخاصية ذات السمة readonly في مكانين فقط لا ثالث لهما:

  1. عند الإعلان المباشر عن الخاصية في جسم الفئة.
  2. داخل دالة البناء (Constructor) الخاصة بالفئة.
class SystemConfig {
    readonly systemId: string = "SYS-DEFAULT"; // تهيئة مباشرة
    readonly initializedAt: Date;              // تهيئة لاحقة في دالة البناء

    constructor(customId?: string) {
        if (customId) {
            this.systemId = customId; // ✅ مسموح فقط داخل دالة البناء
        }
        this.initializedAt = new Date();
    }

    updateConfig() {
        // this.systemId = "NEW-ID"; // ❌ خطأ: ممنوع منعاً باتاً خارج Constructor
    }
}

التحكم الكامل عبر Getters و Setters المتقدمة

يوضح المثال التالي نمط التصميم الموصى به: جعل الحقل الفعلي خاصاً ومحمياً (private _propertyName) وكشفه للعالم الخارجي عبر getter و setter يحملان الاسم المطلوب:

class BankAccount {
    private _balance: number = 0;

    constructor(initialDeposit: number) {
        this.balance = initialDeposit; // تمرير القيمة عبر الـ Setter للتحقق
    }

    // الـ Getter
    get balance(): number {
        return this._balance;
    }

    // الـ Setter مع منطق عمل محكم
    set balance(amount: number) {
        if (amount < 0) {
            console.error("عملية مرفوضة: لا يمكن أن يكون الرصيد سالباً.");
            return;
        }
        this._balance = amount;
    }
}

const myAccount = new BankAccount(1000);
console.log(myAccount.balance); // استدعاء Getter كخاصية: 1000
myAccount.balance = 1500;       // استدعاء Setter كإسناد عادي: 1500
myAccount.balance = -50;        // يرفض العملية ويطبع رسالة التحذير

أفضل الممارسات والنصائح التطبيقية

للحفاظ على كود نظيف وقابل للتوسع عند استخدام الخصائص للقراءة فقط ومُعدّلات الوصول، يُفضل اتباع الإرشادات الهندسية التالية:

  • تسمية الحقول الداخلية بـ Underscore (_): استخدم بادئة _ للحقل الخاص (مثل _age) لتمييزه بوضوح عن دالة get age() و set age().
  • الحذر من ثبات الكائنات المتداخلة (Shallow Immutability): تذكّر أن readonly في TypeScript هي حماية سطحية؛ فإذا كانت الخاصية مصفوفة أو كائناً، يمنع readonly إعادة تعيين المرجع فقط، لكن محتويات الكائن الداخلية تظل قابلة للتعديل ما لم تستخدم ReadonlyArray<T> أو Readonly<T>.
  • تجنب العمليات الثقيلة داخل Getters: يجب أن تكون دوال الـ Getters سريعة وخفيفة جداً، لأن المطور الذي يستدعيها يعاملها كحقل عادي ولا يتوقع أن تُجري استعلامات قاعدة بيانات أو عمليات معالجة مكلفة قد تضر بالأداء.
  • إنشاء Getters بدون Setters لحقول القراءة فقط الديناميكية: إذا قمت بتعريف Getter لخاصية ما دون تعريف Setter لها، سيعتبرها TypeScript تلقائياً خاصية للقراءة فقط (Readonly).

مقارنات وتحليلات متقدمة

نستعرض في هذا التحليل الفروق الجوهرية بين الخصائص المعرفة بـ readonly، والخصائص المدارة عبر Getter Only، وثبات كائنات JavaScript في وقت التشغيل عبر Object.freeze():

وجه المقارنةreadonly في TypeScriptGetter بدون SetterObject.freeze() في JavaScript
وقت فرض القيودوقت الترجمة فقط (Compile-time)وقت التشغيل والترجمةوقت التشغيل (Runtime)
القدرة على الحساب الديناميكيلا (قيمة مخزنة في حقل)نعم (تُحسب في كل عملية قراءة)لا (تجميد قيم ثابتة)
الأداء وسرعة الوصولفائقة السرعة (وصول مباشر للذاكرة)سريعة (تنفيذ دالة عند كل قراءة)سريعة
التأثير بعد الترجمة لـ JSيُحذف تماماً ويصبح حقلاً عادياًيتحول إلى Object.definePropertyيمنع تعديل خصائص الكائن فعلياً

تحذير هندسي: معضلة الثبات السطحي (Shallow vs Deep Immutability)

class Team {
    // مصفوفة محمية كمرجع فقط
    readonly members: string[] = ["محمد", "سارة"];
}

const team = new Team();
// team.members = ["أحمد"]; // ❌ خطأ ترجمة: لا يمكن استبدال المصفوفة

// ⚠️ خطر: TypeScript يسمح بهذا التعديل الداخلي!
team.members.push("خالد"); 
console.log(team.members); // ["محمد", "سارة", "خالد"]

// ✅ الحل الهندسي الصارم:
class SecureTeam {
    readonly members: ReadonlyArray<string> = Object.freeze(["محمد", "سارة"]);
}
const secure = new SecureTeam();
// secure.members.push("خالد"); // ❌ خطأ: الخاصية push غير موجودة على ReadonlyArray

حل المشكلات والأخطاء الشائعة واستكشاف الأعطال

عند التعامل مع الخصائص للقراءة فقط و Getters/Setters، يقع العديد من المطورين في أخطاء تؤدي إلى انهيار التطبيق أو ظهور سلوكيات غريبة. إليك أبرزها مع الحلول العملية:

1. حلقة التكرار اللانهائي في دوال الوصول (Infinite Recursion Call Stack)

الخطأ القاتل: استخدام نفس اسم الخاصية داخل الـ Getter أو الـ Setter:

// ❌ خطأ كارثي: يسبب RangeError: Maximum call stack size exceeded
class BuggyClass {
    get name(): string {
        return this.name; // استدعاء الـ Getter لنفسها بشكل لانهائي!
    }
    set name(val: string) {
        this.name = val;  // استدعاء الـ Setter لنفسها بشكل لانهائي!
    }
}

// ✅ الحل الصحيح: استخدام متغير داخلي مستقل بحرف سفلي
class FixedClass {
    private _name: string = "";
    get name(): string {
        return this._name;
    }
    set name(val: string) {
        this._name = val;
    }
}

2. استدعاء Getters و Setters كدوال عادية بالأقواس

الـ Getter يُستدعى كخاصية (user.name) وليس كدالة (user.name()). محاولة وضع أقواس ستؤدي إلى خطأ: This expression is not callable.

3. عدم تطابق أنواع البيانات بين Getter و Setter

في TypeScript، يجب أن يكون نوع القيمة العائدة من Getter متوافقاً تماماً مع نوع المعامل المستقبل في Setter. عدم التوافق يسبب خطأ ترقيع الأنواع في المشاريع المتقدمة.

دراسات حالة وأمثلة واقعية متقدمة

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

دراسة الحالة 1: نموذج تسجيل محمي ضد التعديل بعد الإرسال (Locked Registration Form)

نموذج يسمح بتعديل بيانات المستخدم في مرحلة المسودة (Draft)، ولكن بمجرد قفل النموذج وإرساله، يمنع الـ Setter أي تعديل إضافي:

class UserFormModel {
    readonly formId: string;
    private _username: string;
    private _email: string;
    private _isLocked: boolean = false;

    constructor(id: string, initialUsername: string, initialEmail: string) {
        this.formId = id;
        this._username = initialUsername;
        this._email = initialEmail;
    }

    get username(): string {
        return this._username;
    }

    set username(value: string) {
        if (this._isLocked) {
            throw new Error("عملية مرفوضة: تم إرسال النموذج مسبقاً وتجميده لمنع التعديل.");
        }
        if (value.trim().length < 3) {
            throw new Error("اسم المستخدم يجب ألا يقل عن 3 أحرف.");
        }
        this._username = value;
    }

    get isLocked(): boolean {
        return this._isLocked;
    }

    // قفل النموذج نهائياً
    public submitAndLockForm(): void {
        this._isLocked = true;
        console.log(`تم قفل النموذج رقم: ${this.formId} بنجاح.`);
    }
}

const form = new UserFormModel("FORM-101", "محمد", "dev@test.com");
form.username = "محمد_الشامي"; // ✅ مسموح: ما زال النموذج مفتوحاً
form.submitAndLockForm();

try {
    form.username = "علي"; // ❌ سيرمي استثناء القفل فوراً
} catch (error: any) {
    console.error(`تم التقاط الخطأ: ${error.message}`);
}

دراسة الحالة 2: محول درجات الحرارة والقياسات المتزامنة (Dynamic Unit Converter)

استخدام Getters و Setters لتنفيذ مزامنة آنية للقيم؛ حيث يُخزن الكائن درجة الحرارة بنظام سيلزيوس داخلياً، ويسمح للمستخدم بقراءتها وتعديلها بنظام فهرنهايت مع التحويل التلقائي:

class Temperature {
    private _celsius: number = 0;

    constructor(celsius: number) {
        this._celsius = celsius;
    }

    // القراءة والكتابة بالنظام المئوي
    get celsius(): number {
        return this._celsius;
    }

    set celsius(val: number) {
        this._celsius = val;
    }

    // القراءة والكتابة بنظام فهرنهايت المحسوب
    get fahrenheit(): number {
        return (this._celsius * 9) / 5 + 32;
    }

    set fahrenheit(val: number) {
        this._celsius = ((val - 32) * 5) / 9;
    }
}

const temp = new Temperature(25);
console.log(`بالسيلزيوس: ${temp.celsius}°C`);       // 25°C
console.log(`بالفهرنهايت: ${temp.fahrenheit}°F`);   // 77°F

// تعديل القيمة بنظام فهرنهايت ينعكس تلقائياً على السيلزيوس
temp.fahrenheit = 212;
console.log(`السيلزيوس بعد التحديث: ${temp.celsius}°C`); // 100°C (درجة الغليان)

الأسئلة الشائعة وإجاباتها

1. ما الفارق بين استخدام دالة get/set واستخدام دوال عادية مثل getProperty() / setProperty()؟

الفارق الأساسي هو تجربة الاستخدام وتناسق الـ API. دوال get/set توفر صياغة نظيفة ومريحة تسمح بقراءة وتعديل الخاصية كمتغير عادي دون إجبار المطور على كتابة أقواس الاستدعاء في كل مرة، مع الحفاظ على القدرة الكاملة لإجراء فحوصات منطقية خلف الكواليس.

2. هل الكلمة المفتاحية readonly تحمي البيانات في متصفحات المستخدمين وقت التشغيل؟

لا، الكلمة readonly هي ميزة فحص نوعي وقت الترجمة (Compile-time) في TypeScript فقط، ويتم حذفها بالكامل عند تحويل الكود إلى JavaScript. إذا كنت تريد حماية البيانات فعلياً ضد التعديل في بيئة التشغيل، يجب استخدام Object.freeze() أو حصر التعديل داخل Setter يرمي خطأ صريحاً عند محاولة التغيير.

3. هل يمكن دمج الكلمة المفتاحية readonly مع دالة Getter؟

لا يمكنك كتابة readonly get property() لأن دالة الـ Getter التي لا تمتلك دالة Setter مقابلة تُعتبر بطبيعتها للقراءة فقط حكماً وفق قواعد لغة TypeScript، ولا حاجة لإضافة الكلمة المفتاحية أمامها.

الخلاصة والخطوات القادمة

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

الخطوة القادمة في مسيرتك البرمجية هي دمج هذه المفاهيم مع معدّلات الوصول (public, private, protected) والفئات المجردة والواجهات لتصميم هياكل معمارية متينة، خالية من الآثار الجانبية، وقادرة على إدارة البيانات المعقدة في بيئات الإنتاج الكبرى بأعلى كفاءة وموثوقية.

المراجع والمصادر التعليمية المعتمدة:


اكتشاف المزيد من كود التطور

اشترك للحصول على أحدث التدوينات المرسلة إلى بريدك الإلكتروني.

🎓 التقدم التعليمي في هذا الدرس
هل أتممت فهم وتطبيق هذا الدرس؟ اضغط على الزر لتسجيل إنجازه وكسب 10 نقاط!
MOHAMMAD ABU KASHREEF

MOHAMMAD ABU KASHREEF

محمد، مهندس برمجيات وصانع محتوى تقني، مؤسس كود التطور. أشارك خبرتي في البرمجة والتقنية بأسلوب عملي وتجارب واقعية، بهدف تبسيط المعرفة وبناء محتوى عربي موثوق وقابل للتطبيق.

اترك رد