نمط Singleton
جزء من قسم البرمجة كائنية التوجه في رحلة GO على Coddy. الدرس 86 من 107.
يضمن نمط Singleton أن يكون للنوع مثيل واحد فقط طوال برنامجك، ويوفّر نقطة وصول عامة إليه. وهذا مفيد للموارد المشتركة مثل مديري الإعدادات، واتصالات قواعد البيانات، أو المسجّلات.
في Go، نطبّق Singleton باستخدام متغيّر على مستوى package مع sync.Once لضمان تهيئة آمنة للخيوط:
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 أن تعمل وظيفة التهيئة once بالضبط، حتى عندما تستدعي عدة goroutines الدالة GetInstance() في الوقت نفسه. يعيد كل استدعاء لاحق نفس instance دون إعادة تنفيذ كود التهيئة.
cfg1 := config.GetInstance()
cfg2 := config.GetInstance()
// cfg1 و cfg2 يشيران إلى نفس المثيلاستخدم Singleton باعتدال. ورغم أنه مريح، فإنه يُدخل حالة عامة قد تجعل الاختبار أكثر صعوبة وتخفي التبعيات. فكّر فيما إذا كان حقن التبعيات قد يكون بديلاً أفضل لحالة الاستخدام الخاصة بك.
التحدي
سهللنبنِ Logger singleton يضمن وجود مثيل Logger واحد فقط طوال تطبيقك! هذه حالة استخدام كلاسيكية لنمط Singleton. تريد أن تشارك جميع أجزاء برنامجك إعدادات Logger وحالته نفسهما.
ستنظّم شيفرتك عبر ملفين:
logger.go: Implement Logger Singleton آمنًا للخيوط.أنشئ بنية
Loggerبحقلين:Prefix(من النوع string) لبادئات رسائل السجل، وMessageCount(من النوع int) لتتبّع عدد الرسائل التي تم تسجيلها.استخدم متغيرات على مستوى الحزمة مع
sync.Onceلضمان إنشاء مثيل واحد فقط على الإطلاق. Implement دالةGetLogger()التي تهيّئ Logger ببادئة افتراضية هي"[LOG]"وتعيد المثيل الوحيد.أضف هذه الأساليب إلى Logger:
SetPrefix(prefix string)- يغيّر بادئة LoggerLog(message string) string- يزيد عدد الرسائل ويعيد سلسلة منسّقة:[prefix] #[count]: [message]GetCount() int- يعيد إجمالي عدد الرسائل التي تم تسجيلها
main.go: Demonstrate أن الاستدعاءات المتعددة لـGetLogger()تعيد المثيل نفسه.اقرأ قيمة بادئة جديدة، ثم اقرأ عدد الرسائل متبوعًا بكل رسالة لتسجيلها.
احصل على مثيل Logger، واضبط البادئة المخصّصة، ثم سجّل كل رسالة واطبع النتيجة. بعد تسجيل جميع الرسائل، احصل على مثيل Logger مرة أخرى (لإثبات أنه المثيل نفسه) واطبع إجمالي عدد الرسائل.
ستُ 제공 المدخلات التالية:
- السطر 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 واحدًا فقط موجودًا!
جرّب بنفسك
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: احصل على مثيل المُسجِّل مرة أخرى (لإثبات أنه نفس المثيل)
// and print the total message count in format: "Total messages logged: X"
}
يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.
جميع دروس البرمجة كائنية التوجه
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تدرّب بنفسك: مترجم Go عبر الإنترنت