نمط Singleton
جزء من قسم Object Oriented Programming في رحلة GO على Coddy — الدرس 86 من 107.
يضمن نمط Singleton أن النوع يحتوي على مثيل واحد فقط طوال برنامجك ويوفر نقطة وصول عالمية إليه. هذا مفيد للموارد المشتركة مثل مديري التكوين، أو اتصالات قاعدة البيانات، أو المسجلات.
في لغة Go، نقوم بتنفيذ نمط Singleton باستخدام متغير على مستوى الحزمة مدمجاً مع sync.Once لضمان تهيئة آمنة برمجياً (thread-safe):
package config
import "sync"
type Config struct {
DatabaseURL string
MaxRetries int
}
var (
instance *Config
once sync.Once
)
func GetInstance() *Config {
once.Do(func() {
instance = &Config{
DatabaseURL: "localhost:5432",
MaxRetries: 3,
}
})
return instance
}تضمن sync.Once تشغيل دالة التهيئة مرة واحدة فقط، حتى عندما تستدعي عدة goroutines دالة GetInstance() في وقت واحد. يعيد كل استدعاء لاحق نفس المثيل دون إعادة تنفيذ كود التهيئة.
cfg1 := config.GetInstance()
cfg2 := config.GetInstance()
// cfg1 و cfg2 يشيران إلى نفس المثيلاستخدم نمط Singleton باعتدال. فعلى الرغم من كونه ملائماً، إلا أنه يقدم حالة عامة (global state) يمكن أن تجعل الاختبار أكثر صعوبة وتخفي التبعيات. فكر فيما إذا كان حقن التبعية (dependency injection) قد يكون بديلاً أفضل لحالة الاستخدام الخاصة بك.
التحدي
سهللنقم ببناء مسجل (Logger) بنمط Singleton يضمن وجود مثيل واحد فقط للمسجل في جميع أنحاء تطبيقك! هذه حالة استخدام كلاسيكية لنمط Singleton — حيث تريد أن تشترك جميع أجزاء برنامجك في نفس إعدادات وحالة المسجل.
ستقوم بتنظيم الكود الخاص بك عبر ملفين:
logger.go: قم بتنفيذ مسجل Singleton آمن من حيث الخيوط (thread-safe).أنشئ هيكل
Loggerيحتوي على حقلين:Prefix(string) لبادئات رسائل السجل، وMessageCount(int) لتتبع عدد الرسائل التي تم تسجيلها.استخدم متغيرات على مستوى الحزمة (package-level) مع
sync.Onceلضمان إنشاء مثيل واحد فقط على الإطلاق. قم بتنفيذ دالةGetLogger()التي تقوم بتهيئة المسجل ببادئة افتراضية هي"[LOG]"وتعيد مثيل Singleton.أضف هذه الأساليب (methods) إلى Logger الخاص بك:
SetPrefix(prefix string)- يغير بادئة المسجلLog(message string) string- يزيد عداد الرسائل ويعيد سلسلة نصية منسقة:[prefix] #[count]: [message]GetCount() int- يعيد إجمالي عدد الرسائل المسجلة
main.go: أثبت أن الاستدعاءات المتعددة لـGetLogger()تعيد نفس المثيل.اقرأ قيمة بادئة جديدة، ثم اقرأ عدد الرسائل متبوعاً بكل رسالة لتسجيلها.
احصل على مثيل المسجل، واضبط البادئة المخصصة، ثم سجل كل رسالة واطبع النتيجة. بعد تسجيل جميع الرسائل، احصل على مثيل المسجل مرة أخرى (لإثبات أنه نفس المثيل) واطبع إجمالي عدد الرسائل.
سيتم توفير المدخلات التالية:
- السطر 1: البادئة المخصصة لضبطها
- السطر 2: عدد الرسائل
- الأسطر التالية: كل رسالة لتسجيلها
على سبيل المثال، إذا كان المعطى:
[APP]
3
Server started
User connected
Request processedيجب أن يكون مخرجك:
[APP] #1: Server started
[APP] #2: User connected
[APP] #3: Request processed
Total messages logged: 3وإذا كان المعطى:
[DEBUG]
2
Initializing cache
Cache readyيجب أن يكون مخرجك:
[DEBUG] #1: Initializing cache
[DEBUG] #2: Cache ready
Total messages logged: 2وإذا كان المعطى:
[ERROR]
1
Connection failedيجب أن يكون مخرجك:
[ERROR] #1: Connection failed
Total messages logged: 1الفكرة الأساسية هي أنه بغض النظر عن عدد المرات التي تستدعي فيها GetLogger()، فإنك تحصل دائماً على نفس المثيل بحالته المشتركة. يستمر عداد الرسائل لأن هناك Logger واحداً فقط موجوداً!
ورقة مرجعية
يضمن نمط Singleton أن النوع له مثيل واحد فقط (instance) طوال برنامجك ويوفر نقطة وصول عامة إليه. هذا مفيد للموارد المشتركة مثل مديري الإعدادات (configuration managers)، أو اتصالات قاعدة البيانات، أو المسجلات (loggers).
قم بتنفيذ Singleton باستخدام متغير على مستوى الحزمة (package-level variable) مدمج مع sync.Once لضمان تهيئة آمنة برمجياً (thread-safe):
package config
import "sync"
type Config struct {
DatabaseURL string
MaxRetries int
}
var (
instance *Config
once sync.Once
)
func GetInstance() *Config {
once.Do(func() {
instance = &Config{
DatabaseURL: "localhost:5432",
MaxRetries: 3,
}
})
return instance
}تضمن sync.Once تشغيل دالة التهيئة مرة واحدة فقط، حتى عندما تستدعي عدة goroutines دالة GetInstance() في وقت واحد. كل استدعاء لاحق يعيد نفس المثيل:
cfg1 := config.GetInstance()
cfg2 := config.GetInstance()
// cfg1 and cfg2 point to the same instanceاستخدم Singleton بحذر لأنه يقدم حالة عامة (global state) يمكن أن تجعل الاختبار أكثر صعوبة وتخفي التبعيات (dependencies). فكر في استخدام حقن التبعية (dependency injection) كبديل.
جرّب بنفسك
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// قراءة البادئة المخصصة
prefix, _ := reader.ReadString('\n')
prefix = strings.TrimSpace(prefix)
// قراءة عدد الرسائل
countStr, _ := reader.ReadString('\n')
countStr = strings.TrimSpace(countStr)
count, _ := strconv.Atoi(countStr)
// قراءة كل رسالة
messages := make([]string, count)
for i := 0; i < count; i++ {
msg, _ := reader.ReadString('\n')
messages[i] = strings.TrimSpace(msg)
}
// TODO: GetLogger() الحصول على مثيل المسجل باستخدام
// TODO: SetPrefix() تعيين البادئة المخصصة باستخدام
// TODO: تسجيل كل رسالة وطباعة النتيجة
// TODO: الحصول على مثيل المسجل مرة أخرى (لإثبات أنه نفس المثيل)
// "Total messages logged: X" وطباعة إجمالي عدد الرسائل بالتنسيق التالي:
}
يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.
جميع دروس Object Oriented Programming
1أساسيات الـ OOP في Go
الملفات الخارجيةمساحة العمل والـ Modules في Goالـ Packages والـ Importsالأسماء المصدرة مقابل غير المصدرةمقدمة إلى الـ OOP في Goالـ Structs كـ Classesتعريف الـ Methods في الـ Structsالـ Pointer Receivers مقابل الـ Value Receiversتهيئة الـ Structدوال الـ Constructorمراجعة - آلة حاسبة بسيطة4الواجهات
مقدمة في الواجهاتالتنفيذ الضمنيالواجهة كعقدالواجهة الفارغة (any)تأكيد النوع (Type Assertion)تبديل النوع (Type Switch)تركيب الواجهاتواجهات Stringer و Errorمراجعة - حاسبة الأشكال7التغليف
الحقول المصدرة وغير المصدرةالتغليف على مستوى الحزمةدوال الـ Getter والـ Setterإخفاء المعلومات في Goملخص - سجلات الطلاب10الأنواع العامة (Go 1.18+)
مقدمة في الأنواع العامةمعاملات الأنواعقيود الأنواعالـ Structs العامةحل بديل للـ Methods العامةملخص - المجموعات العامة13أنماط التصميم - الجزء الأول
مقدمة في أنماط التصميمنمط Singletonنمط Factoryنمط Abstract Factoryنمط Observerنمط Strategy2تعمق في الأنواع و Structs
الأنواع الأساسية والمركبةتعريفات الأنواع المخصصةStruct TagsStructs مجهولةStructs متداخلةالقيم الصفرية والافتراضيةمراجعة - دفتر العناوين5التركيب بدلاً من الوراثة
لماذا لا تدعم Go الوراثةأساسيات تضمين الـ Structترقية الـ Methodتضمين عدة Structsالتضمين مقابل التجميعحجب الـ Methods المضمنةملخص - الهيكل الهرمي للموظفين8معالجة الأخطاء و OOP
واجهة errorأنواع الأخطاء المخصصةتغليف الأخطاء (fmt.Errorf)أخطاء Sentinelerrors.Is() و errors.As()Panic و Defer و Recoverملخص - File Parser11المكتبة القياسية والبرمجة كائنية التوجه (OOP)
io.Reader و io.Writerواجهة sort.Interfaceواجهة fmt.Stringerencoding/json مع Structsواجهة http.Handlerمراجعة - نماذج REST API14أنماط التصميم - الجزء الثاني
نمط الأمرنمط المحولنمط المزيننمط طريقة القالبنمط الحالةنمط التركيبMiddleware كنمط مزين3المؤشرات والذاكرة
أساسيات المؤشرات في Goالمؤشرات إلى الـ Structsالتمرير بالقيمة مقابل التمرير بالمرجعدالة ()newGarbage Collection في Goملخص - بناء Linked List6تعدد الأشكال في Go
تعدد الأشكال عبر InterfacesDuck Typing في Goقواعد استيفاء الـ Interfaceمجموعات متعددة الأشكالحقن التبعيةمراجعة - معالج الدفع9التزامن والبرمجة كائنية التوجه (OOP)
أساسيات الـ Goroutinesالقنوات (Channels) والاتصالالقنوات المخزنة (Buffered) مقابل غير المخزنةجملة Selectsync.Mutex و sync.RWMutexsync.WaitGroupتصميم الـ Structs الآمنة للخيوط (Thread-Safe)مراجعة - Worker Pool