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

نمط Functional Options

جزء من قسم Object Oriented Programming في رحلة GO على Coddy — الدرس 79 من 107.

عند إنشاء هياكل (structs) تحتوي على العديد من حقول التكوين الاختيارية، يمكن أن تصبح دوال المنشئ (constructor functions) غير عملية. يحل نمط الخيارات الوظيفية (Functional Options Pattern) هذه المشكلة بأناقة من خلال استخدام الدوال لتكوين الكائنات.

ضع في اعتبارك بنية خادم (server struct) مع إعدادات اختيارية متعددة. بدلاً من استخدام منشئ (constructor) بمعلمات عديدة، نقوم بتعريف نوع خيار (option type) ودوال تُرجع خيارات:

type Server struct {
    host    string
    port    int
    timeout int
}

type Option func(*Server)

func WithPort(port int) Option {
    return func(s *Server) {
        s.port = port
    }
}

func WithTimeout(timeout int) Option {
    return func(s *Server) {
        s.timeout = timeout
    }
}

يقبل المنشئ قائمة متغيرة من الخيارات ويقوم بتطبيق كل منها:

func NewServer(host string, opts ...Option) *Server {
    s := &Server{
        host:    host,
        port:    8080,    // افتراضي
        timeout: 30,      // افتراضي
    }
    for _, opt := range opts {
        opt(s)
    }
    return s
}

الآن يصبح إنشاء الخوادم سهلاً ومرناً:

// استخدام القيم الافتراضية
s1 := NewServer("localhost")

// تخصيص خيارات محددة
s2 := NewServer("localhost", WithPort(3000), WithTimeout(60))

يتألق هذا النمط لأن المستدعين يحددون فقط ما يحتاجون إليه، والقيم الافتراضية واضحة في مكان واحد، وإضافة خيارات جديدة لا تؤدي إلى كسر الكود الحالي. يُستخدم هذا النمط على نطاق واسع في مكتبات Go مثل grpc و zap.

challenge icon

التحدي

سهل

لنقم ببناء اتصال قاعدة بيانات قابل للتكوين باستخدام نمط الخيارات الوظيفية (Functional Options Pattern)! يتألق هذا النمط عندما يكون لديك العديد من الإعدادات الاختيارية مع قيم افتراضية منطقية—وهو بالضبط ما يحتاجه اتصال قاعدة البيانات.

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

  • database.go: حدد نوع اتصال قاعدة البيانات ودوال الخيارات.

    قم بإنشاء هيكل DBConnection مع هذه الحقول غير المصدرة (unexported):

    • host (string)
    • port (int)
    • username (string)
    • password (string)
    • maxConnections (int)
    • timeout (int) - بالثواني

    حدد نوع Option كدالة تقوم بتعديل *DBConnection.

    قم بإنشاء دوال الخيارات هذه التي تعيد Option:

    • WithPort(port int)
    • WithCredentials(username, password string)
    • WithMaxConnections(max int)
    • WithTimeout(seconds int)

    قم بإنشاء منشئ NewDBConnection(host string, opts ...Option) يعيد *DBConnection. قم بتعيين هذه القيم الافتراضية: المنفذ 5432، واسم المستخدم "admin"، وكلمة المرور "secret"، و maxConnections بقيمة 10، و timeout بقيمة 30. قم بتطبيق جميع الخيارات المقدمة بعد تعيين القيم الافتراضية.

    أضف تابع ConnectionString() يعيد معلومات الاتصال بهذا التنسيق:

    [username]:[password]@[host]:[port] (max:[maxConnections], timeout:[timeout]s)
  • main.go: قم ببناء اتصالات قاعدة البيانات بتكوينات متنوعة.

    اقرأ المضيف (host)، ثم اقرأ عدد الخيارات المراد تطبيقها. لكل خيار، اقرأ نوع الخيار وقيمته (أو قيمه):

    • port متبوعاً برقم المنفذ
    • credentials متبوعاً باسم المستخدم وكلمة المرور (في سطرين)
    • maxconn متبوعاً بعدد أقصى للاتصالات
    • timeout متبوعاً بالمهلة الزمنية بالثواني

    قم بإنشاء DBConnection بالخيارات المحددة واطبع سلسلة الاتصال الخاصة به.

سيتم توفير المدخلات التالية:

  • السطر 1: اسم المضيف (Host name)
  • السطر 2: عدد الخيارات
  • الأسطر التالية: نوع الخيار والقيمة (أو القيم)

على سبيل المثال، إذا كان المدخل:

localhost
0

يجب أن يكون المخرج:

admin:secret@localhost:5432 (max:10, timeout:30s)

وإذا كان المدخل:

db.example.com
2
port
3306
credentials
root
mypassword

يجب أن يكون المخرج:

root:mypassword@db.example.com:3306 (max:10, timeout:30s)

وإذا كان المدخل:

production.server
4
port
5433
credentials
dbuser
securepass123
maxconn
50
timeout
60

يجب أن يكون المخرج:

dbuser:securepass123@production.server:5433 (max:50, timeout:60s)

لاحظ كيف يحدد المستدعون فقط الخيارات التي يحتاجونها—الإعدادات غير المحددة تستخدم قيمًا افتراضية منطقية. هذه هي أناقة نمط الخيارات الوظيفية!

ورقة مرجعية

يوفر نمط الخيارات الوظيفية (Functional Options Pattern) طريقة أنيقة لتهيئة الهياكل (structs) التي تحتوي على العديد من الحقول الاختيارية باستخدام الدوال بدلاً من معاملات المنشئ (constructor parameters) الطويلة.

قم بتعريف نوع Option كدالة تقوم بتعديل مؤشر (pointer) إلى الهيكل الخاص بك:

type Server struct {
    host    string
    port    int
    timeout int
}

type Option func(*Server)

قم بإنشاء دوال خيارات تعيد إغلاقات (closures) من نوع Option:

func WithPort(port int) Option {
    return func(s *Server) {
        s.port = port
    }
}

func WithTimeout(timeout int) Option {
    return func(s *Server) {
        s.timeout = timeout
    }
}

يقبل المنشئ (constructor) قائمة متغيرة (variadic list) من الخيارات، ويقوم بتعيين القيم الافتراضية، ثم يطبق كل خيار:

func NewServer(host string, opts ...Option) *Server {
    s := &Server{
        host:    host,
        port:    8080,    // افتراضي
        timeout: 30,      // افتراضي
    }
    for _, opt := range opts {
        opt(s)
    }
    return s
}

الاستخدام نظيف ومرن — يحدد المستدعون فقط ما يحتاجون إليه:

// استخدام القيم الافتراضية
s1 := NewServer("localhost")

// تخصيص خيارات محددة
s2 := NewServer("localhost", WithPort(3000), WithTimeout(60))

الفوائد: القيم الافتراضية مركزية، ويحدد المستدعون فقط ما يحتاجون إليه، وإضافة خيارات جديدة لا تؤدي إلى كسر الكود الحالي.

جرّب بنفسك

package main

import (
	"bufio"
	"fmt"
	"os"
	"strconv"
	"strings"
)

func main() {
	reader := bufio.NewReader(os.Stdin)

	// قراءة host
	host, _ := reader.ReadString('\n')
	host = strings.TrimSpace(host)

	// قراءة عدد الخيارات
	numOptionsStr, _ := reader.ReadString('\n')
	numOptions, _ := strconv.Atoi(strings.TrimSpace(numOptionsStr))

	// جمع الخيارات
	var opts []Option

	for i := 0; i < numOptions; i++ {
		optionType, _ := reader.ReadString('\n')
		optionType = strings.TrimSpace(optionType)

		switch optionType {
		case "port":
			portStr, _ := reader.ReadString('\n')
			port, _ := strconv.Atoi(strings.TrimSpace(portStr))
			// TODO: أضف خيار WithPort إلى opts
			_ = port // قم بإزالة هذا السطر عند التنفيذ

		case "credentials":
			username, _ := reader.ReadString('\n')
			username = strings.TrimSpace(username)
			password, _ := reader.ReadString('\n')
			password = strings.TrimSpace(password)
			// TODO: أضف خيار WithCredentials إلى opts
			_, _ = username, password // قم بإزالة هذا السطر عند التنفيذ

		case "maxconn":
			maxStr, _ := reader.ReadString('\n')
			maxConn, _ := strconv.Atoi(strings.TrimSpace(maxStr))
			// TODO: أضف خيار WithMaxConnections إلى opts
			_ = maxConn // قم بإزالة هذا السطر عند التنفيذ

		case "timeout":
			timeoutStr, _ := reader.ReadString('\n')
			timeout, _ := strconv.Atoi(strings.TrimSpace(timeoutStr))
			// TODO: أضف خيار WithTimeout إلى opts
			_ = timeout // قم بإزالة هذا السطر عند التنفيذ
		}
	}

	// TODO: قم بإنشاء DBConnection باستخدام NewDBConnection مع host و opts
	// TODO: اطبع سلسلة الاتصال باستخدام طريقة ConnectionString()

	_ = opts // قم بإزالة هذا السطر عند التنفيذ
	fmt.Println("TODO: Print connection string here")
}
quiz iconاختبر نفسك

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

جميع دروس Object Oriented Programming