Menu
Coddy logo textTech
flag Ar iconالعربيةdown icon

RAII كنمط

جزء من قسم Object Oriented Programming في رحلة C++ على Coddy — الدرس 102 من 104.

RAII (الحصول على الموارد هو تهيئة - Resource Acquisition Is Initialization) هي أكثر من مجرد اصطلاح في لغة C++ - إنها نمط تصميم قوي يربط إدارة الموارد بدورة حياة الكائن. لقد رأيت بالفعل RAII مع المؤشرات الذكية (smart pointers)، ولكن هذا النمط ينطبق على أي مورد: مقابض الملفات (file handles)، أو اتصالات الشبكة، أو الـ mutexes، أو معاملات قاعدة البيانات.

الفكرة الأساسية بسيطة: الحصول على الموارد في المُنشئ (constructor)، وتحريرها في المُهدم (destructor). وبما أن لغة C++ تضمن تشغيل المُهدمات عندما تخرج الكائنات عن النطاق (scope)، فإن عملية التنظيف تحدث تلقائياً - حتى عند حدوث استثناءات (exceptions):

#include <iostream>
#include <fstream>

class FileGuard {
    std::ofstream file;
public:
    FileGuard(const std::string& filename) : file(filename) {
        if (!file.is_open()) {
            std::cout << "Failed to open file\n";
        }
    }
    
    void write(const std::string& text) {
        if (file.is_open()) file << text;
    }
    
    ~FileGuard() {
        if (file.is_open()) {
            file.close();
            std::cout << "File closed automatically\n";
        }
    }
};

int main() {
    {
        FileGuard guard("output.txt");
        guard.write("Hello RAII");
    }  // يتم استدعاء المدمر هنا - تم إغلاق الملف
    
    std::cout << "After scope\n";
}

يبرز نمط RAII بوضوح عند إدارة الأقفال (locks) في الأكواد متعددة الخيوط (multithreaded code). تتبع std::lock_guard الموجودة في المكتبة القياسية هذا النمط - حيث تقوم بالاستحواذ على الـ mutex عند الإنشاء وتحريره عند التدمير، مما يمنع حدوث حالات الجمود (deadlocks) الناتجة عن نسيان فك الأقفال.

عند تنفيذ فئات RAII، تذكر إما حذف أو تنفيذ عمليات النسخ/النقل (Rule of Five) بشكل صحيح لمنع تكرار الموارد أو مشكلات التحرير المزدوج. يقوم RAII بتحويل إدارة الموارد اليدوية المعرضة للخطأ إلى عملية تنظيف تلقائية وآمنة.

challenge icon

التحدي

سهل

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

ستقوم بتنظيم الكود الخاص بك عبر ثلاثة ملفات:

  • ConnectionPool.h: قم بإنشاء مجمع اتصالات بسيط يدير عددًا محدودًا من الاتصالات.

    يجب أن يتتبع كلاس ConnectionPool الخاص بك عدد الاتصالات المتاحة (ابدأ بسعة يتم تمريرها إلى المنشئ). قم بتنفيذ ما يلي:

    • acquire() — إذا كان الاتصال متاحًا، فقم بتقليل العدد وطباعة Connection acquired (X available) حيث X هو العدد المتبقي؛ وأرجع true إذا نجحت العملية، و false إذا لم تكن هناك اتصالات متاحة.
    • release() — قم بزيادة العدد المتاح وطباعة Connection released (X available).
    • available() — يرجع العدد الحالي للاتصالات المتاحة.
  • ConnectionGuard.h: قم ببناء غلاف RAII الذي يدير اتصالاً واحدًا بأمان.

    يجسد كلاس ConnectionGuard نمط RAII. يجب عليه:

    • أخذ مرجع (reference) لـ ConnectionPool في المنشئ الخاص به ومحاولة الحصول على اتصال.
    • تخزين ما إذا كانت عملية الحصول على الاتصال ناجحة أم لا.
    • توفير ميثود isConnected() للتحقق مما إذا كان الحارس (guard) يحمل اتصالاً صالحًا.
    • تحرير الاتصال تلقائيًا وإعادته إلى المجمع في المدمر (destructor) (فقط إذا تم الحصول على اتصال).
    • حذف منشئ النسخ (copy constructor) ومعامل إسناد النسخ (copy assignment) لمنع تكرار الموارد (مراعاة لقاعدة الخمسة Rule of Five).

    عند تشغيل المدمر، إذا كان هناك اتصال محتجز، قم بطباعة Guard releasing connection قبل استدعاء release على المجمع.

  • main.cpp: توضيح التنظيف التلقائي لنمط RAII من خلال النطاقات (scopes).

    اقرأ مدخلين:

    1. سعة المجمع (عدد صحيح)
    2. عدد الاتصالات المطلوبة (عدد صحيح)

    قم بإنشاء ConnectionPool بالسعة المعطاة. ثم، داخل نطاق متداخل (باستخدام الأقواس المتعرجة)، قم بإنشاء العدد المطلوب من كائنات ConnectionGuard المخزنة في vector. لكل حارس، اطبع ما إذا كان قد اتصل بنجاح:

    • إذا اتصل: Guard N: Connected
    • إذا لم يتصل: Guard N: Failed to connect

    (حيث يبدأ N من 1)

    بعد انتهاء النطاق (تدمير الحراس)، اطبع After scope: X connections available لتوضيح الحالة النهائية للمجمع.

على سبيل المثال، مع المدخلات 2 و 3:

Connection acquired (1 available)
Guard 1: Connected
Connection acquired (0 available)
Guard 2: Connected
Guard 3: Failed to connect
Guard releasing connection
Connection released (1 available)
Guard releasing connection
Connection released (2 available)
After scope: 2 connections available

مع المدخلات 3 و 2:

Connection acquired (2 available)
Guard 1: Connected
Connection acquired (1 available)
Guard 2: Connected
Guard releasing connection
Connection released (2 available)
Guard releasing connection
Connection released (3 available)
After scope: 3 connections available

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

ورقة مرجعية

RAII (Resource Acquisition Is Initialization) هو نمط تصميم يربط إدارة الموارد بدورة حياة الكائن. يتم الحصول على الموارد في المُنشئ (constructor) وتحريرها في المُهدم (destructor).

بما أن لغة C++ تضمن تشغيل المُهدمات عندما تخرج الكائنات عن النطاق (scope)، فإن عملية التنظيف تحدث تلقائيًا - حتى عند حدوث استثناءات:

class FileGuard {
    std::ofstream file;
public:
    FileGuard(const std::string& filename) : file(filename) {
        if (!file.is_open()) {
            std::cout << "Failed to open file\n";
        }
    }
    
    void write(const std::string& text) {
        if (file.is_open()) file << text;
    }
    
    ~FileGuard() {
        if (file.is_open()) {
            file.close();
            std::cout << "File closed automatically\n";
        }
    }
};

int main() {
    {
        FileGuard guard("output.txt");
        guard.write("Hello RAII");
    }  // يتم استدعاء المُهدم هنا - يتم إغلاق الملف تلقائيًا
    
    std::cout << "After scope\n";
}

يُعد RAII مفيدًا بشكل خاص لإدارة الأقفال (locks) في الأكواد متعددة الخيوط (multithreaded). تقوم std::lock_guard في المكتبة القياسية بالحصول على الـ mutex عند الإنشاء وتحريره عند الهدم، مما يمنع حدوث حالات الجمود (deadlocks) الناتجة عن نسيان فك الأقفال.

عند تنفيذ فئات (classes) RAII، قم بحذف أو تنفيذ عمليات النسخ/النقل (Rule of Five) بشكل صحيح لمنع تكرار الموارد أو مشاكل التحرير المزدوج.

جرّب بنفسك

#include <iostream>
#include <vector>
#include "ConnectionPool.h"
#include "ConnectionGuard.h"

using namespace std;

int main() {
    // قراءة المدخلات
    int capacity;
    int numConnections;
    cin >> capacity;
    cin >> numConnections;
    
    // TODO: إنشاء ConnectionPool بالسعة المحددة
    
    // TODO: إنشاء نطاق متداخل (nested scope) باستخدام الأقواس المتعرجة
    {
        // TODO: إنشاء vector لتخزين كائنات ConnectionGuard
        // تلميح: ستحتاج إلى استخدام المؤشرات (pointers) أو المؤشرات الذكية (smart pointers) لأن ConnectionGuard
        // يحتوي على copy constructor محذوف
        
        // TODO: حلقة تكرار لإنشاء numConnections من الـ guards
        // لكل guard، اطبع إما:
        // "Guard N: Connected" أو "Guard N: Failed to connect"
        // حيث تبدأ N من 1
        
    }
    // يتم تدمير الـ Guards هنا عند انتهاء النطاق (scope)
    
    // TODO: اطبع "After scope: X connections available"
    
    return 0;
}
quiz iconاختبر نفسك

يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.

جميع دروس Object Oriented Programming