نمط تجميع الكائنات

نمط مجمع الكائنات هو نمط تصميم برمجي يستخدم مجموعة من الكائنات المُهيأة والمُجهزة للاستخدام - " مجمع " - بدلاً من تخصيصها وتدميرها عند الطلب. يطلب عميل المجمع كائنًا منه ويُجري عمليات عليه. عند انتهاء العميل من العملية، يُعيد الكائن إلى المجمع بدلاً من تدميره ؛ ويمكن القيام بذلك يدويًا أو تلقائيًا.

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

وصف

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

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

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

يُستخدم نمط تصميم مجمع الكائنات في عدة مواضع ضمن الفئات القياسية لإطار عمل .NET. ومن الأمثلة على ذلك موفر بيانات .NET لخادم SQL. ولأن إنشاء اتصالات قاعدة بيانات SQL Server قد يستغرق وقتًا طويلاً، يتم الاحتفاظ بمجمع من الاتصالات. ولا يؤدي إغلاق الاتصال فعليًا إلى قطع الاتصال بخادم SQL، بل يُحفظ الاتصال في مجمع، ويمكن استرجاعه منه عند طلب اتصال جديد. وهذا يُحسّن بشكل كبير من سرعة إنشاء الاتصالات.

فوائد

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

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

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

تطبيق

يمكن تنفيذ مجموعات الكائنات تلقائيًا في لغات مثل C++ باستخدام المؤشرات الذكية . في مُنشئ المؤشر الذكي، يُمكن طلب كائن من المجموعة، وفي مُدمّره، يُمكن تحرير الكائن وإعادته إلى المجموعة. في اللغات التي تعتمد على جمع البيانات المهملة، حيث لا توجد مُدمّرات (يُضمن استدعاؤها كجزء من عملية فكّ المكدس)، يجب تنفيذ مجموعات الكائنات يدويًا، وذلك بطلب كائن صراحةً من المصنع وإعادته باستدعاء دالة التخلص (كما في نمط التخلص ). لا يُنصح باستخدام دالة الإنهاء ( finalizer ) لهذا الغرض، إذ لا توجد عادةً ضمانات بشأن وقت (أو حتى ما إذا) سيتم تشغيلها. بدلًا من ذلك، يُفضّل استخدام "try ... finally" لضمان أن يكون الحصول على الكائن وتحريره خاليًا من الاستثناءات.

تعتبر مجموعات الكائنات اليدوية سهلة التنفيذ، ولكنها أصعب في الاستخدام، لأنها تتطلب إدارة يدوية للذاكرة لكائنات المجموعة.

التعامل مع المسابح الفارغة

تستخدم مجموعات الكائنات إحدى ثلاث استراتيجيات للتعامل مع الطلب عندما لا توجد كائنات احتياطية في المجموعة.

  1. فشل في توفير كائن (وإرجاع خطأ إلى العميل).
  2. قم بتخصيص عنصر جديد، مما يزيد من حجم المجموعة. عادةً ما تسمح لك المجموعات التي تقوم بذلك بتحديد الحد الأقصى لعدد العناصر المستخدمة على الإطلاق.
  3. في بيئة متعددة الخيوط ، قد يقوم مجمع الخيوط بحظر العميل حتى يقوم خيط آخر بإرجاع كائن إلى المجمع.

المخاطر

يجب توخي الحذر لضمان إعادة ضبط حالة الكائنات المُعادة إلى المجموعة إلى حالة مناسبة لاستخدامها التالي، وإلا فقد تكون في حالة غير متوقعة من قِبل العميل، مما قد يؤدي إلى فشله. المجموعة هي المسؤولة عن إعادة ضبط الكائنات، وليس العملاء. تُسمى مجموعات الكائنات المليئة بكائنات ذات حالة قديمة وخطيرة أحيانًا بـ"مستنقعات الكائنات"، وتُعتبر نمطًا سيئًا .

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

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

إذا تم استخدام مجموعة الموارد بواسطة عدة خيوط، فقد تحتاج إلى آلية لمنع الخيوط المتوازية من محاولة إعادة استخدام نفس الكائن بالتوازي. هذا غير ضروري إذا كانت الكائنات المجمعة غير قابلة للتغيير أو آمنة للاستخدام في بيئة متعددة الخيوط.

نقد

لا تنصح بعض المنشورات باستخدام تجميع الكائنات مع لغات برمجة معينة، مثل جافا ، خاصةً للكائنات التي تستخدم الذاكرة فقط ولا تحتفظ بموارد خارجية (مثل الاتصالات بقواعد البيانات). عادةً ما يقول المعارضون إن تخصيص الكائنات سريع نسبيًا في اللغات الحديثة المزودة بجامعات البيانات المهملة ؛ فبينما يحتاج عامل التجميع newإلى عشر تعليمات فقط، يتطلب الزوج الكلاسيكي newالموجود deleteفي تصميمات التجميع مئات التعليمات لأنه يقوم بعمل أكثر تعقيدًا. أيضًا، تفحص معظم جامعات البيانات المهملة مراجع الكائنات "النشطة"، وليس الذاكرة التي تستخدمها هذه الكائنات لمحتواها. هذا يعني أنه يمكن التخلص من أي عدد من الكائنات "الميتة" التي ليس لها مراجع بتكلفة قليلة. في المقابل، يؤدي الاحتفاظ بعدد كبير من الكائنات "النشطة" ولكن غير المستخدمة إلى زيادة مدة جمع البيانات المهملة. [ 1 ]

أمثلة

لغة سي++

في C++26 ، تُقدّم مكتبة C++ القياسية ملف رأس جديدًا <hive>يحتوي على بنية البيانات std::hive، والتي تُنفّذ أساسًا مجمعًا للكائنات. وهو عبارة عن مجموعة تُعيد استخدام ذاكرة العناصر المحذوفة. ويُرفق به فئة std::hive_limitsلمعلومات التخطيط المتعلقة بحدود سعة الكتلة. [ 2 ]std::hive مُستند إلى الفئة plf::colonyالموجودة في plfالمكتبة. [ 3 ]

استيراد std ؛باستخدام std :: hive ؛ باستخدام std :: string ؛int main () { hive < string > stringHive ;// إدراج سلسلة نصية . إدراج ( "foo" ); سلسلة نصية . إدراج ( "bar" );// إزالة std :: erase ( stringHive , string ( "foo" ));// تكرار for ( const auto & value : stringHive ) { std :: println ( "{}" , value ); }return 0 ; }

سي شارب

تحتوي مكتبة الفئات الأساسية لـ .NET على عدد قليل من الكائنات التي تُطبّق هذا النمط. System.Threading.ThreadPoolيتم تكوينها بحيث يكون لديها عدد مُحدد مُسبقًا من الخيوط المُخصصة. عند إرجاع الخيوط، تكون مُتاحة لإجراء عملية حسابية أخرى. وبالتالي، يُمكن استخدام الخيوط دون تكبّد تكلفة إنشائها والتخلص منها.

يوضح ما يلي الكود الأساسي لنمط تصميم مجمع الكائنات المُنفذ باستخدام لغة C#. يظهر مجمع الكائنات كفئة ثابتة، نظرًا لقلة الحاجة إلى مجمعات متعددة. مع ذلك، من المقبول أيضًا استخدام فئات مثيل لمجمعات الكائنات.

namespace Wikipedia.Examples ;باستخدام System ؛ باستخدام System.Collections.Generic ؛// فئة PooledObject هي النوع الذي يكون مكلفًا أو بطيئًا في الإنشاء، // أو الذي يكون توفره محدودًا، لذلك يجب الاحتفاظ به في مجمع الكائنات. public class PooledObject { private DateTime _createdAt = DateTime . Now ;public DateTime CreatedAt => _createdAt ;public string TempData { get ; set ; } }// تتحكم فئة Pool في الوصول إلى الكائنات المجمعة. وهي تحتفظ بقائمة بالكائنات المتاحة ومجموعة من الكائنات التي تم الحصول عليها من المجمع وهي قيد الاستخدام. يضمن المجمع إعادة الكائنات المُحررة إلى حالة مناسبة، جاهزة لإعادة الاستخدام. public static class Pool { private static List <PooledObject> _available = new ( ) ; private static List <PooledObject> _inUse = new ( );public static PooledObject GetObject ( ) { lock ( _available ) { if ( _available.Count ! = 0 ) { PooledObject po = _available [ 0 ] ; _inUse.Add ( po ) ; _available.RemoveAt ( 0 ) ; return po ; } else { PooledObject po = new ( ) ; _inUse.Add ( po ) ; return po ; } } }public static void ReleaseObject ( PooledObject po ) { CleanUp ( po );lock ( _available ) { _available . Add ( po ); _inUse . Remove ( po ); } }private static void CleanUp ( PooledObject po ) { po . TempData = null ; } }

في الكود أعلاه، يحتوي الكائن المُجمّع (PooledObject) على خصائص خاصة بوقت إنشائه، وخاصية أخرى قابلة للتعديل من قِبل العميل، تُعاد ضبطها عند إعادة الكائن إلى المجمع. يوضح الشكل عملية التنظيف عند إعادة الكائن، لضمان صلاحيته قبل طلبه من المجمع مرة أخرى.

يذهب

يقوم كود Go التالي بتهيئة مجموعة موارد بحجم محدد (تهيئة متزامنة) لتجنب مشاكل تضارب الموارد من خلال القنوات، وفي حالة المجموعة الفارغة، يقوم بتعيين معالجة المهلة لمنع العملاء من الانتظار لفترة طويلة جدًا.

// حزمة تجمع تجمعاستيراد ( "errors" "log" "math/rand" "sync" "time" )const getResMaxTime = 3 * time.Secondvar ( ErrPoolNotExist = errors . New ( "المجمع غير موجود" ) ErrGetResTimeout = errors . New ( "انتهت مهلة الحصول على المورد" ) )// نوع المورد Resource struct { resId int }// محاكاة إنشاء مورد جديد (NewResource) - محاكاة عملية إنشاء موارد بطيئة // (على سبيل المثال، اتصال TCP، والحصول على مفتاح SSL متماثل، ومصادقة auth، كلها عمليات تستغرق وقتًا طويلاً) func NewResource ( id int ) * Resource { time . Sleep ( 500 * time . Millisecond ) return & Resource { resId : id } }// تستغرق موارد المحاكاة وقتًا طويلاً، ويتراوح الاستهلاك العشوائي بين 0 و400 مللي ثانية . func ( r * Resource ) Do ( workId int ) { time . Sleep ( time . Duration ( rand . Intn ( 5 )) * 100 * time . Millisecond ) log . Printf ( "using resource #%d finished work %d finish\n" , r . resId , workId ) }// مجمع موارد مبني على تطبيق قناة Go، لتجنب مشكلة حالة تضارب الموارد من النوع Pool chan * Resource// إنشاء مجموعة موارد جديدة بالحجم المحدد // يتم إنشاء الموارد بشكل متزامن لتوفير وقت تهيئة الموارد func New ( size int ) Pool { p : = make ( Pool , size ) wg : = new ( sync.WaitGroup ) wg.Add ( size ) for i : = 0 ; i < size ; i ++ { go func ( resId int ) { p < - NewResource ( resId ) wg.Done ( ) } ( i ) } wg.Wait ( ) return p }// يتم الحصول على المورد بناءً على القناة، ويتم تجنب حالة تضارب الموارد، ويتم تعيين مهلة الحصول على المورد للمجمع الفارغ. func ( p Pool ) GetResource () ( r * Resource , err error ) { select { case r := <- p : return r , nil case <- time . After ( getResMaxTime ): return nil , ErrGetResTimeout } }// تُعيد الدالة GiveBackResource الموارد إلى مجمع الموارد func ( p Pool ) GiveBackResource ( r * Resource ) error { if p == nil { return ErrPoolNotExist } p <- r return nil }// الحزمة الرئيسية package mainاستيراد ( "github.com/tkstorm/go-design/creational/object-pool/pool" "log" "sync" )func main () { // تهيئة مجموعة من خمسة موارد، // والتي يمكن تعديلها إلى 1 أو 10 لرؤية الفرق size := 5 p : = pool.New ( size )// يستدعي موردًا لتنفيذ المهمة ذات المعرف doWork := func ( workId int , wg * sync . WaitGroup ) { defer wg . Done () // الحصول على المورد من مجمع الموارد res , err := p . GetResource () if err != nil { log . Println ( err ) return } // الموارد المراد إرجاعها defer p . GiveBackResource ( res ) // استخدام الموارد لمعالجة العمل res . Do ( workId ) }// محاكاة 100 عملية متزامنة للحصول على موارد من مجمع الأصول num := 100 wg := new ( sync . WaitGroup ) wg . Add ( num ) for i := 0 ; i < num ; i ++ { go doWork ( i , wg ) } wg . Wait () }

جافا

تدعم لغة جافا تجميع الخيوط عبر java.util.concurrent.ExecutorServiceفئات أخرى ذات صلة. تحتوي خدمة التنفيذ على عدد محدد من الخيوط "الأساسية" التي لا يتم التخلص منها أبدًا. إذا كانت جميع الخيوط مشغولة، تخصص الخدمة عددًا إضافيًا مسموحًا به من الخيوط، والتي يتم التخلص منها لاحقًا إذا لم تُستخدم خلال فترة زمنية محددة. في حال عدم السماح بمزيد من الخيوط، يمكن وضع المهام في قائمة الانتظار. أخيرًا، إذا طالت قائمة الانتظار، يمكن ضبطها لتعليق الخيط الطالب.

في ملف PooledObject.java :

package org.wikipedia.examples ;public class PooledObject { private String temp1 ; private String temp2 ; private String temp3 ; public String getTemp1 () { return temp1 ; }public void setTemp1 ( String temp1 ) { this . temp1 = temp1 ; }public String getTemp2 () { return temp2 ; }public void setTemp2 ( String temp2 ) { this . temp2 = temp2 ; }public String getTemp3 () { return temp3 ; }public void setTemp3 ( String temp3 ) { this . temp3 = temp3 ; } }

في ملف PooledObjectPool.java :

package org.wikipedia.examples ;استيراد java.util.HashMap ؛ استيراد java.util.Map ؛public class PooledObjectPool { public static final long EXPIRY_TIME = 6000 ; // 6 ثوانٍprivate static Map < PooledObject , Long > available = new HashMap <> ( ); private static Map < PooledObject , Long > inUse = new HashMap <> (); public synchronized static PooledObject getObject ( ) { long now = System.currentTimeMillis ( ); if ( ! available.isEmpty ( )) { for ( Map.Entry < PooledObject , Long > entry : available.entrySet ()) { if ( now - entry.getValue ( ) > EXPIRY_TIME ) { // انتهت صلاحية الكائن popElement ( available ) ; } else { PooledObject po = popElement ( available , entry.getKey ( ) ) ; push ( inUse , po , now ) ; return po ; } } }// إما أن الكائن المجمع غير متوفر أو أن كل كائن قد انتهت صلاحيته، لذا يتم إرجاع كائن جديد return createPooledObject ( now ); } private synchronized static PooledObject createPooledObject ( long now ) { PooledObject po = new PooledObject (); push ( inUse , po , now ); return po ; }private synchronized static void push ( HashMap < PooledObject , Long > map , PooledObject po , long now ) { map . put ( po , now ); }public static void releaseObject ( PooledObject po ) { cleanUp ( po ) ; available.put ( po , System.currentTimeMillis ( )); inUse.remove ( po ) ; } private static PooledObject popElement ( HashMap < PooledObject , Long > map ) { Map.Entry < PooledObject , Long > entry = map.entrySet ( ) . iterator ( ) . next ( ) ; PooledObject key = entry.getKey ( ) ; // Long value = entry.getValue( ) ; map.remove ( entry.getKey ( ) ) ; return key ; } private static PooledObject popElement ( HashMap < PooledObject , Long > map , PooledObject key ) { map.remove ( key ) ; return key ; } public static void cleanUp ( PooledObject po ) { po.setTemp1 ( null ) ; po.setTemp2 ( null ) ; po.setTemp3 ( null ) ; } }

الصدأ

توجد مكتبة Rust تسمى Colony مبنية على std::hiveلغة plf::colonyC++. [ 4 ] لم تعد تُحدَّث منذ يناير 2024.

استخدم المستعمرة :: المستعمرة ؛fn main () { let mut colony : Colony = Colony :: new ();// إدراج let foo_handle = colony.insert ( " foo" ); let bar_handle = colony.insert ( " bar" ) ;// إزالة assert_eq! ( colony . remove ( foo_handle ), Some ( "foo" ));// التحقق من أن ( colony.get ( foo_handle ), None ); التحقق من أن ( colony.get ( bar_handle ) , Some ( & " bar" ) ) ;// تكرار for ( key , & value ) in colony . iter () { assert_eq! (( key , value ), ( bar_handle , "bar" )); } }

انظر أيضاً

ملحوظات

  1. 1 2 غوتز، برايان (27-09-2005). "نظرية جافا وتطبيقها: أساطير الأداء الحضري، مُعاد النظر فيها" . آي بي إم . آي بي إم ديفيلوبر وركس. مؤرشف من الأصل في 14-02-2012 . تم الاسترجاع في 15-03-2021 .
  2. "ملف رأس المكتبة القياسية <hive> (C++26) - cppreference.com" . cppreference.com . تم الاطلاع عليه في 1 أكتوبر 2025 .
  3. ماثيو بنتلي (1 ديسمبر 2025). "مكتبة PLF - المستعمرة" . plflib.org . plflib.
  4. LlewVallis (4 أغسطس 2023). "مستعمرة الصناديق" . docs.rs. docs.rs.

مراجع