Middleware كنمط مزين
جزء من قسم Object Oriented Programming في رحلة GO على Coddy — الدرس 97 من 107.
تُعد Middleware واحدة من أكثر التطبيقات العملية لنمط Decorator في لغة Go. في تطوير الويب، تقوم وظائف middleware بتغليف معالجات HTTP لإضافة اهتمامات شاملة (cross-cutting concerns) مثل تسجيل البيانات (logging)، أو المصادقة، أو التوقيت دون تعديل المعالج الأصلي.
الفكرة الجوهرية هي أن البرمجيات الوسيطة (middleware) تأخذ معالجاً (handler) وتعيد معالجاً جديداً بسلوك مضاف. وهذا يتبع نمط المزيّن (Decorator pattern) تماماً—نفس الواجهة في الدخول والخروج، مع وظائف محسنة:
type Handler func(request string) string
func LoggingMiddleware(next Handler) Handler {
return func(request string) string {
result := next(request)
return "[LOG] " + result
}
}
func AuthMiddleware(next Handler) Handler {
return func(request string) string {
return "[AUTH] " + next(request)
}
}تقوم كل برمجية وسيطة (middleware) بتغليف المعالج التالي في السلسلة، مع إضافة سلوكها قبل أو بعد استدعاء المعالج المغلف. يمكنك تكديس عدة برمجيات وسيطة من خلال تركيبها:
func MainHandler(request string) string {
return "Response to: " + request
}
handler := LoggingMiddleware(AuthMiddleware(MainHandler))
fmt.Println(handler("GET /users"))
// [LOG] [AUTH] Response to: GET /usersيحدد ترتيب التغليف ترتيب التنفيذ - حيث يتم تشغيل البرمجية الوسيطة (middleware) الخارجية أولاً. يُستخدم هذا النمط على نطاق واسع في أطر عمل الويب بلغة Go لسلاسل معالجة الطلبات، مما يسهل إضافة أو إزالة الوظائف دون المساس بمنطق المعالج الأساسي.
التحدي
سهللنقم ببناء خط معالجة طلبات (request processing pipeline) باستخدام نمط Middleware as Decorator! ستقوم بإنشاء نظام حيث تقوم دوال الـ middleware بتغليف المعالجات (handlers) لإضافة وظائف مثل التوقيت، والتحقق من الصحة، والتنسيق — كل ذلك دون تعديل منطق المعالج الأساسي.
ستقوم بتنظيم الكود الخاص بك عبر ثلاثة ملفات:
handler.go: حدد نوع المعالج (handler type) والمعالج الأساسي الذي يعالج الطلبات.قم بإنشاء نوع
Handlerكدالة تأخذ سلسلة نصية للطلب (request string) وتعيد سلسلة نصية للاستجابة (response string):type Handler func(request string) stringقم بتنفيذ دالة
BaseHandlerالتي تعيدProcessed: [request]لأي طلب تستقبله.middleware.go: قم بإنشاء دوال الـ middleware التي تغلف المعالجات بسلوك إضافي.قم ببناء ثلاث دوال middleware، كل منها تأخذ
HandlerوتعيدHandlerجديداً:TimingMiddleware— يغلف الاستجابة بمعلومات التوقيت، ويعيد[TIMING] [response]ValidationMiddleware— يضيف بادئة التحقق، ويعيد[VALID] [response]UppercaseMiddleware— يحول الاستجابة بالكامل إلى أحرف كبيرة (uppercase)
يجب على كل middleware استدعاء المعالج المغلف وتحسين نتيجته.
main.go: قم ببناء خط معالجة عن طريق تركيب الـ middleware.اقرأ عدد طبقات الـ middleware المراد تطبيقها. ثم لكل طبقة، اقرأ نوع الـ middleware (إما
timing، أوvalidation، أوuppercase) وقم بتغليف المعالج الحالي بهذا الـ middleware. قم بتطبيقها بالترتيب الذي قُرئت به — بحيث يصبح أول middleware تمت قراءته هو الغلاف الخارجي (outermost wrapper).بعد بناء خط المعالجة، اقرأ سلسلة الطلب النصية، وقم بمعالجتها من خلال المعالج المركب، واطبع النتيجة.
سيتم توفير المدخلات التالية:
- السطر 1: عدد طبقات الـ middleware
- الأسطر التالية: نوع middleware واحد في كل سطر (
timing، أوvalidation، أوuppercase) - السطر الأخير: الطلب المراد معالجته
على سبيل المثال، إذا كان المدخل:
2
timing
validation
GET /usersيجب أن يكون مخرجك:
[TIMING] [VALID] Processed: GET /usersوإذا كان المدخل:
3
uppercase
timing
validation
POST /dataيجب أن يكون مخرجك:
[TIMING] [VALID] PROCESSED: POST /DATAوإذا كان المدخل:
1
uppercase
hello worldيجب أن يكون مخرجك:
PROCESSED: HELLO WORLDوإذا كان المدخل:
0
simple requestيجب أن يكون مخرجك:
Processed: simple requestلاحظ كيف يؤثر ترتيب تغليف الـ middleware على المخرج — أول middleware يتم تطبيقه (الأبعد خارجياً) يعمل أولاً ويرى النتيجة النهائية من جميع الـ middleware الداخلية. الـ middleware الخاص بـ uppercase في المثال الثاني يعمل آخراً (الأكثر داخلية)، لذا فهو يحول الاستجابة الأساسية إلى أحرف كبيرة قبل أن يضيف التوقيت والتحقق بادئاتهما!
ورقة مرجعية
تتبع البرمجيات الوسيطة (Middleware) في لغة Go نمط المزيّن (Decorator pattern) من خلال تغليف المعالجات (handlers) لإضافة وظائف دون تعديل المعالج الأصلي.
عرّف نوع المعالج كدالة:
type Handler func(request string) stringتأخذ دوال البرمجيات الوسيطة معالجاً وتُرجع معالجاً جديداً بسلوك محسّن:
func LoggingMiddleware(next Handler) Handler {
return func(request string) string {
result := next(request)
return "[LOG] " + result
}
}
func AuthMiddleware(next Handler) Handler {
return func(request string) string {
return "[AUTH] " + next(request)
}
}قم بتكديس برمجيات وسيطة متعددة من خلال دمجها:
func MainHandler(request string) string {
return "Response to: " + request
}
handler := LoggingMiddleware(AuthMiddleware(MainHandler))
fmt.Println(handler("GET /users"))
// المخرجات: [LOG] [AUTH] Response to: GET /usersيتم تشغيل البرمجية الوسيطة الخارجية أولاً، مما يحدد ترتيب التنفيذ. يمكن لكل برمجية وسيطة إضافة سلوك قبل أو بعد استدعاء المعالج المغلّف.
جرّب بنفسك
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// قراءة عدد طبقات middleware
nLine, _ := reader.ReadString('\n')
n, _ := strconv.Atoi(strings.TrimSpace(nLine))
// قراءة أنواع middleware
middlewareTypes := make([]string, n)
for i := 0; i < n; i++ {
line, _ := reader.ReadString('\n')
middlewareTypes[i] = strings.TrimSpace(line)
}
// قراءة request
request, _ := reader.ReadString('\n')
request = strings.TrimSpace(request)
// TODO: ابدأ بـ BaseHandler
// TODO: قم ببناء pipeline عن طريق تغليف handler بـ middleware
// تطبيق middleware بالترتيب - أول middleware يتم قراءته يصبح الغلاف الخارجي
// تلميح: قم بالمرور عبر middlewareTypes بترتيب عكسي (REVERSE) لتحقيق ذلك
// آخر middleware يتم تطبيقه سيكون هو الخارجي (يعمل أولاً)
// TODO: قم بمعالجة request من خلال handler المركب واطبع النتيجة
fmt.Println("TODO: implement pipeline")
}
يتضمن هذا الدرس اختبارًا قصيرًا. ابدأ الدرس للإجابة عليه وتتبّع تقدمك.
جميع دروس 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