Menu
Coddy logo textTech

דפוס האפשרויות הפונקציונליות

חלק מהיחידה תכנות מונחה עצמים במסלול ה-GO של Coddy. שיעור 79 מתוך 107.

כשיוצרים מבנים עם שדות תצורה אופציונליים רבים, פונקציות בנייה עלולות להפוך למסורבלות. תבנית האפשרויות הפונקציונליות פותרת זאת באלגנטיות באמצעות פונקציות להגדרת אובייקטים.

נבחן struct של שרת עם מספר הגדרות אופציונליות. במקום להשתמש בבנאי עם פרמטרים רבים, נגדיר טיפוס אפשרות ופונקציות שמחזירות אפשרויות:

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

אתגר

קל

בואו נבנה חיבור למסד נתונים שניתן להגדיר באמצעות תבנית האפשרויות הפונקציונליות! התבנית הזאת מצטיינת כשיש הגדרות אופציונליות רבות עם ערכי ברירת מחדל הגיוניים — בדיוק מה שנדרש לחיבור למסד נתונים.

תארגנו את הקוד בשני קבצים:

  • database.go: הגדירו את טיפוס חיבור מסד הנתונים ואת פונקציות האפשרויות שלכם.

    צרו מבנה DBConnection עם השדות הלא־מיוצאים הבאים:

    • host (מחרוזת)
    • port (מספר שלם)
    • username (מחרוזת)
    • password (מחרוזת)
    • maxConnections (מספר שלם)
    • timeout (מספר שלם) - בשניות

    הגדירו טיפוס Option כפונקציה שמשנה *DBConnection.

    צרו את פונקציות האפשרויות הבאות, שמחזירות Option:

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

    צרו בנאי NewDBConnection(host string, opts ...Option) שמחזיר *DBConnection. הגדירו את ברירות המחדל הבאות: port 5432, username "admin", password "secret", maxConnections 10, timeout 30. החילו את כל האפשרויות שסופקו לאחר הגדרת ברירות המחדל.

    הוסיפו מתודה ConnectionString() שמחזירה את פרטי החיבור בפורמט הזה:

    [username]:[password]@[host]:[port] (max:[maxConnections], timeout:[timeout]s)
  • main.go: בנו חיבורי מסד נתונים עם הגדרות שונות.

    קראו את שם המארח, ואז קראו את מספר האפשרויות להחלה. עבור כל אפשרות, קראו את סוג האפשרות ואת הערך או הערכים שלה:

    • port ואחריו מספר הפורט
    • credentials ואחריו שם המשתמש והסיסמה (שתי שורות)
    • maxconn ואחריו מספר החיבורים המרבי
    • timeout ואחריו הזמן הקצוב בשניות

    צרו DBConnection עם האפשרויות שצוינו והדפיסו את מחרוזת החיבור שלו.

הקלטים הבאים יסופקו:

  • שורה 1: שם המארח
  • שורה 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)

שימו לב שהקוראים מציינים רק את האפשרויות שנחוצות להם: הגדרות שלא צוינו משתמשות בערכי ברירת מחדל הגיוניים. זהו היופי שבתבנית האפשרויות הפונקציונליות!

נסו בעצמכם

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בחנו את עצמכם

השיעור הזה כולל חידון קצר. התחילו את השיעור כדי לענות עליו ולעקוב אחרי ההתקדמות.

כל השיעורים ביחידה תכנות מונחה עצמים

תרגלו בעצמכם: קומפיילר Go אונליין