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 بتحويل إدارة الموارد اليدوية المعرضة للخطأ إلى عملية تنظيف تلقائية وآمنة.
التحدي
سهللنقم ببناء مدير مجمع اتصالات (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 على المجمع.- أخذ مرجع (reference) لـ
main.cpp: توضيح التنظيف التلقائي لنمط RAII من خلال النطاقات (scopes).اقرأ مدخلين:
- سعة المجمع (عدد صحيح)
- عدد الاتصالات المطلوبة (عدد صحيح)
قم بإنشاء
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;
}
يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.
جميع دروس Object Oriented Programming
1أساسيات الـ OOP
الملفات الخارجيةبناء وتجميع (Compilation) لغة C++ملفات الـ Header وملفات الـ Sourceالـ Namespaces والـ Scopeمقدمة في الـ OOP في C++الـ Classes مقابل الـ Objectsالمؤشر 'this'الـ Methods (الدوال الأعضاء)الـ Attributes (بيانات الأعضاء)أساسيات الـ Ctors والـ Dtorsمراجعة - آلة حاسبة بسيطة4خصائص الـ Class
أعضاء الـ Instance مقابل الـ Staticدوال الـ Getters والـ Settersدوال الأعضاء الثابتة (Const)الكلمة المفتاحية Mutableالدوال والمتغيرات الساكنة (Static)الدوال والفئات الصديقة (Friend)مراجعة - مدير الحساب البنكي7الوراثة
الوراثة الأساسيةمستويات الوصول في الوراثةترتيب استدعاء الـ Ctor والـ Dtorإعادة تعريف الدوال (Method Overriding)الدوال الافتراضية والـ VTableالوراثة المتعددةالوراثة الافتراضيةمراجعة - هيكلية الموظفين10نظرة عامة على STL
نظرة عامة وفلسفة STLحاويات STLالـ Iteratorsخوارزميات STLالـ Functors وتعبيرات Lambdaمراجعة - تكرار الكلمات13أنماط التصميم - الجزء الأول
مقدمة في أنماط التصميمنمط Singletonنمط Factory و Abstract Factoryنمط Builderنمط Observerنمط Strategy2إدارة الذاكرة
ذاكرة Stack مقابل Heapالمؤشرات والمراجعالذاكرة الديناميكية (new/delete)المؤشرات الذكية في C++RAII في C++مراجعة - مدير المصفوفات الديناميكية5التغليف (Encapsulation)
محددات الوصول في C++محددات الوصول بشكل متعمقإخفاء المعلوماتStruct مقابل Classالأصناف المتداخلة والداخليةمراجعة - نظام سجلات الطلاب8تعدد الأشكال (Polymorphism)
تعدد الأشكال: وقت التجميع مقابل وقت التشغيلالتحميل الزائد للدوال (Function Overloading)مراجعة الدوال الافتراضية (Virtual Functions)الدوال الافتراضية البحتة (Pure Virtual Functions)الأصناف المجردة (Abstract Classes)تصميم الواجهات (Interface) في C++التحويل الديناميكي (Dynamic Casting) و RTTIملخص - حاسبة الأشكال11مفاهيم OOP المتقدمة
التركيب مقابل الوراثةالـ Mixins عبر CRTPنمط Pimplمحو النوع (Type Erasure)أصناف Enum والأنواع القويةمعالجة الاستثناءات في OOPتسلسلات الاستثناءات المخصصة14أنماط التصميم - الجزء الثاني
نمط الأمرنمط المحولنمط المزيننمط قالب الطريقةنمط الحالةنمط التركيبRAII كنمط3المنشئات والموادم
المنشئ الافتراضيالمنشئ ذو المعاملاتمنشئ النسخمنشئ النقلقوائم تهيئة المنشئالمنشئات المفوضةتعمق في الموادمقاعدة الثلاثة / الخمسة / الصفرمراجعة - فئة String6تحميل العوامل (Operator Overloading)
مقدمة في تحميل العواملتحميل العوامل الحسابيةتحميل عوامل المقارنةعوامل Streamتحميل عامل التعيينتحميل العوامل [] و ()عوامل تحويل النوعمراجعة - Matrix Class9القوالب
قوالب الدوالقوالب الأصنافتخصيص القوالبالقوالب المتغيرةأساسيات SFINAE و Type Traitsمراجعة - الحاويات العامة