التغليف (Encapsulation): حماية البيانات الحساسة داخل الفئات


ماذا سنتعلم اليوم؟ سنتعلم كيف نحمي البيانات الحساسة داخل الفئات (Classes) باستخدام مبدأ التغليف (Encapsulation) في البرمجة كائنية التوجه (OOP)، وكيف نتحكم في الوصول إليها بطريقة آمنة ومنظمة.

1. مفهوم التغليف وأهميته

التغليف هو أحد المبادئ الأساسية في البرمجة كائنية التوجه (OOP) الذي يهدف إلى تجميع البيانات (Attributes) والدوال (Methods) التي تعمل على هذه البيانات ضمن وحدة واحدة (الفئة). الأهم من ذلك، أنه يسمح بإخفاء التفاصيل الداخلية لتطبيق الفئة عن العالم الخارجي، ويتحكم في كيفية الوصول إلى بيانات الفئة وتعديلها. هذا يضمن سلامة البيانات ويقلل من الأخطاء المحتملة.

ملاحظة تقنية: في بايثون، لا يوجد مفهوم صارم للـ "خاص" (private) كما هو الحال في لغات أخرى مثل Java أو C++. بدلاً من ذلك، نستخدم اصطلاحات تسمية (مثل البادئة بشرطتين سفليتين __) للإشارة إلى أن الخاصية أو الدالة مخصصة للاستخدام الداخلي للفئة، ويتم التعامل معها من خلال "تغيير الاسم" (Name Mangling) لجعل الوصول المباشر إليها من الخارج أكثر صعوبة.

2. بناء الفئة الأساسية (بدون تغليف)

لنبدأ بفئة بسيطة تمثل حسابًا بنكيًا، حيث يمكن الوصول إلى رصيد الحساب وتعديله بشكل مباشر. هذا يوضح المشكلة التي يحلها التغليف.

class BankAccount:
    def __init__(self, account_number, initial_balance):
        # تهيئة رقم الحساب والرصيد
        self.account_number = account_number
        self.balance = initial_balance # يمكن الوصول والتعديل مباشرة
        print(f"تم إنشاء حساب رقم {self.account_number} برصيد {self.balance}")

# إنشاء كائن من الفئة
my_account = BankAccount("12345", 1000)

# الوصول وتعديل الرصيد مباشرة - هذه ممارسة سيئة!
print(f"الرصيد الحالي: {my_account.balance}")
my_account.balance = -200 # يمكن تعيين قيمة سالبة مباشرة، وهذا غير منطقي
print(f"الرصيد بعد التعديل الخاطئ: {my_account.balance}")

كما نرى، في هذا المثال، يمكننا تغيير قيمة balance مباشرة إلى قيمة غير صالحة (مثل -200)، مما قد يؤدي إلى مشاكل خطيرة في التطبيق المالي.

3. تطبيق التغليف باستخدام الخصائص الخاصة (Private Attributes)

لجعل الرصيد خاصًا (أو محميًا داخليًا)، سنستخدم البادئة __ (شرطتين سفليتين). هذا سيجعل الوصول المباشر إلى الخاصية من خارج الفئة أكثر صعوبة ويشير إلى أنها مخصصة للاستخدام الداخلي.

class SecureBankAccount:
    def __init__(self, account_number, initial_balance):
        self.account_number = account_number
        # استخدام __ للإشارة إلى أن الرصيد خاص
        self.__balance = initial_balance
        print(f"تم إنشاء حساب آمن رقم {self.account_number} برصيد {self.__balance}")

# إنشاء كائن من الفئة
my_secure_account = SecureBankAccount("67890", 1500)

# محاولة الوصول المباشر إلى الرصيد الخاص - ستفشل!
# print(f"الرصيد الحالي (محاولة وصول مباشر): {my_secure_account.__balance}") # سيؤدي إلى خطأ AttributeError

# الوصول عبر الاسم الملتوي (Name Mangling) - لا ينصح به!
print(f"الرصيد الحالي (وصول عبر الاسم الملتوي): {my_secure_account._SecureBankAccount__balance}")

في هذا الجزء، عندما نحاول الوصول إلى __balance مباشرة، ستحصل على خطأ AttributeError. بايثون تقوم بتغيير اسم الخاصية إلى _SecureBankAccount__balance، مما يجعل الوصول المباشر أقل وضوحًا ويشجع على استخدام طرق الوصول المحددة. ومع ذلك، الوصول عبر الاسم الملتوي لا يزال ممكنًا، وهذا يوضح أن التغليف في بايثون هو "اتفاق" أكثر منه "إجبار" صارم.

4. الوصول الآمن للبيانات عبر الدوال (Getters and Setters)

للسماح بالوصول الآمن وتعديل الرصيد، سنضيف دوال خاصة (تسمى "Getters" و "Setters"). هذه الدوال توفر واجهة تحكم للتعامل مع الخاصية الخاصة، مما يسمح لنا بإضافة منطق تحقق (Validation Logic).

class FullyEncapsulatedAccount:
    def __init__(self, account_number, initial_balance):
        self.account_number = account_number
        self.__balance = 0 # نبدأ بصفر ثم نستخدم الـ setter للتحقق
        self.set_balance(initial_balance) # استخدام الـ setter لتعيين الرصيد الأولي

        print(f"تم إنشاء حساب مغلف بالكامل رقم {self.account_number} برصيد {self.get_balance()}")

    # Getter: دالة للحصول على قيمة الرصيد
    def get_balance(self):
        return self.__balance

    # Setter: دالة لتعيين قيمة الرصيد مع التحقق
    def set_balance(self, new_balance):
        if new_balance >= 0:
            self.__balance = new_balance
        else:
            print("خطأ: الرصيد لا يمكن أن يكون سالباً!")

    # دالة للإيداع
    def deposit(self, amount):
        if amount > 0:
            self.__balance += amount
            print(f"تم إيداع {amount}. الرصيد الجديد: {self.get_balance()}")
        else:
            print("خطأ: مبلغ الإيداع يجب أن يكون موجباً.")

    # دالة للسحب
    def withdraw(self, amount):
        if 0 < amount <= self.__balance:
            self.__balance -= amount
            print(f"تم سحب {amount}. الرصيد الجديد: {self.get_balance()}")
        else:
            print("خطأ: مبلغ السحب غير صالح أو لا يوجد رصيد كافٍ.")

# إنشاء كائن من الفئة المغلفة
my_encapsulated_account = FullyEncapsulatedAccount("11223", 2000)

# محاولة تعيين رصيد سالب مباشرة عبر الـ setter
my_encapsulated_account.set_balance(-500)

# سحب مبلغ
my_encapsulated_account.withdraw(300)

# إيداع مبلغ
my_encapsulated_account.deposit(100)

# محاولة سحب مبلغ أكبر من الرصيد
my_encapsulated_account.withdraw(2500)

# الحصول على الرصيد الحالي
print(f"الرصيد النهائي للحساب المغلف: {my_encapsulated_account.get_balance()}")

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

الكود النهائي الكامل

إليك الكود كاملاً الذي يوضح الفئات الثلاثة ومقارنة السلوك:

# الفئة بدون تغليف
class BankAccount:
    def __init__(self, account_number, initial_balance):
        self.account_number = account_number
        self.balance = initial_balance
        print(f"تم إنشاء حساب رقم {self.account_number} برصيد {self.balance}")

# الفئة باستخدام الخصائص الخاصة (Private Attributes)
class SecureBankAccount:
    def __init__(self, account_number, initial_balance):
        self.account_number = account_number
        self.__balance = initial_balance
        print(f"تم إنشاء حساب آمن رقم {self.account_number} برصيد {self.__balance}")

# الفئة المغلفة بالكامل باستخدام Getters و Setters
class FullyEncapsulatedAccount:
    def __init__(self, account_number, initial_balance):
        self.account_number = account_number
        self.__balance = 0
        self.set_balance(initial_balance)
        print(f"تم إنشاء حساب مغلف بالكامل رقم {self.account_number} برصيد {self.get_balance()}")

    def get_balance(self):
        return self.__balance

    def set_balance(self, new_balance):
        if new_balance >= 0:
            self.__balance = new_balance
        else:
            print("خطأ: الرصيد لا يمكن أن يكون سالباً!")

    def deposit(self, amount):
        if amount > 0:
            self.__balance += amount
            print(f"تم إيداع {amount}. الرصيد الجديد: {self.get_balance()}")
        else:
            print("خطأ: مبلغ الإيداع يجب أن يكون موجباً.")

    def withdraw(self, amount):
        if 0 < amount <= self.__balance:
            self.__balance -= amount
            print(f"تم سحب {amount}. الرصيد الجديد: {self.get_balance()}")
        else:
            print("خطأ: مبلغ السحب غير صالح أو لا يوجد رصيد كافٍ.")

print("--- مثال الفئة بدون تغليف ---")
my_account = BankAccount("12345", 1000)
print(f"الرصيد الحالي: {my_account.balance}")
my_account.balance = -200
print(f"الرصيد بعد التعديل الخاطئ: {my_account.balance}")

print("\n--- مثال الفئة مع خصائص خاصة ---")
my_secure_account = SecureBankAccount("67890", 1500)
# print(f"الرصيد الحالي (محاولة وصول مباشر): {my_secure_account.__balance}") # سيؤدي إلى خطأ AttributeError
print(f"الرصيد الحالي (وصول عبر الاسم الملتوي): {my_secure_account._SecureBankAccount__balance}")

print("\n--- مثال الفئة المغلفة بالكامل ---")
my_encapsulated_account = FullyEncapsulatedAccount("11223", 2000)
my_encapsulated_account.set_balance(-500)
my_encapsulated_account.withdraw(300)
my_encapsulated_account.deposit(100)
my_encapsulated_account.withdraw(2500)
print(f"الرصيد النهائي للحساب المغلف: {my_encapsulated_account.get_balance()}")

النتيجة المتوقعة

عند تشغيل السكربت، ستلاحظ النتائج التالية على الشاشة:

  • سيتم إنشاء الحسابات وطباعة رصيدها الأولي.
  • في مثال الفئة بدون تغليف، ستتمكن من تعديل الرصيد مباشرة إلى قيمة سالبة دون أي تحذير.
  • في مثال الفئة مع خصائص خاصة، ستظهر رسالة خطأ AttributeError إذا حاولت الوصول إلى __balance مباشرة (السطر معلق في الكود)، ولكن يمكن الوصول إليه عبر الاسم الملتوي.
  • في مثال الفئة المغلفة بالكامل، سيتم رفض محاولة تعيين رصيد سالب بواسطة دالة set_balance، وستتم معالجة عمليات الإيداع والسحب مع التحقق من الصحة، مما يضمن أن الرصيد لا يصبح سالباً أبدًا وأن العمليات تتم بشكل صحيح.

وإليك مثال على جزء من الإخراج المتوقع:

-- مثال الفئة بدون تغليف ---
تم إنشاء حساب رقم 12345 برصيد 1000
الرصيد الحالي: 1000
الرصيد بعد التعديل الخاطئ: -200

--- مثال الفئة مع خصائص خاصة ---
تم إنشاء حساب آمن رقم 67890 برصيد 1500
الرصيد الحالي (وصول عبر الاسم الملتوي): 1500

--- مثال الفئة المغلفة بالكامل ---
تم إنشاء حساب مغلف بالكامل رقم 11223 برصيد 2000
خطأ: الرصيد لا يمكن أن يكون سالباً!
تم سحب 300. الرصيد الجديد: 1700
تم إيداع 100. الرصيد الجديد: 1800
خطأ: مبلغ السحب غير صالح أو لا يوجد رصيد كافٍ.
الرصيد النهائي للحساب المغلف: 1800