الغرباء المألوفين أو مرة أخرى حول استخدام أنماط التصميم

حول موضوع أنماط التصميم ، تمت كتابة الكثير من المقالات ونشر العديد من الكتب. ومع ذلك ، لا يتوقف هذا الموضوع عن كونه ذا صلة ، لأن الأنماط تسمح لنا باستخدام حلول جاهزة ومختبرة زمنياً ، مما يسمح لنا بتقليل وقت تطوير المشروع من خلال تحسين جودة الكود وتقليل الديون الفنية.


منذ ظهور أنماط التصميم ، هناك أمثلة جديدة من أي وقت مضى لاستخدامها الفعال. وهذا رائع. ومع ذلك ، كان هناك ذبابة في المرهم: كل لغة لها خصوصياتها. و golang - وحتى أكثر من ذلك (ليس لديها حتى نموذج OOP الكلاسيكي). لذلك ، هناك اختلافات في الأنماط ، فيما يتعلق بلغات البرمجة الفردية. في هذه المقالة ، أود أن أتطرق إلى موضوع أنماط التصميم فيما يتعلق golang.


مزخرف


يسمح لك قالب Decorator بتوصيل سلوك إضافي بالكائن (ثابتًا أو ديناميكيًا) دون التأثير على سلوك الكائنات الأخرى من نفس الفئة. غالبًا ما يتم استخدام القالب للالتزام بمبدأ المسئولية الفردية ، لأنه يسمح لك بتقسيم الوظيفة بين الفئات لحل مشاكل محددة.

يستخدم نمط الديكور المعروف على نطاق واسع في العديد من لغات البرمجة. لذلك ، في golang ، بنيت جميع الوسيطة على أساسها. على سبيل المثال ، قد تبدو ملفات تعريف الاستعلام كما يلي:


func ProfileMiddleware(next http.Handler) http.Handler { started := time.Now() next.ServeHTTP() elapsed := time.Now().Sub(started) fmt.Printf("HTTP: elapsed time %d", elapsed) } 

في هذه الحالة ، فإن واجهة الديكور هي الوظيفة الوحيدة. كقاعدة عامة ، يجب البحث عن هذا. ومع ذلك ، قد يكون الديكور ذو الواجهة الأوسع مفيدًا في بعض الأحيان. على سبيل المثال ، ضع في اعتبارك الوصول إلى قاعدة بيانات (حزمة قاعدة بيانات / sql). لنفترض أننا بحاجة إلى القيام بنفس ملف تعريف استعلامات قاعدة البيانات. في هذه الحالة ، نحتاج إلى:


  • بدلاً من التفاعل المباشر مع قاعدة البيانات من خلال مؤشر ، نحتاج إلى الانتقال إلى التفاعل من خلال الواجهة (لفصل السلوك عن التنفيذ).
  • قم بإنشاء مجمّع لكل طريقة تنفذ استعلام قاعدة بيانات SQL.

نتيجةً لذلك ، حصلنا على مصمم يسمح لك بتخصيص جميع الاستفسارات إلى قاعدة البيانات. مزايا هذا النهج لا يمكن إنكارها:


  • يحافظ على نظافة التعليمات البرمجية لمكون الوصول إلى قاعدة البيانات الأساسية.
  • ينفذ كل مصمم متطلب واحد. نتيجة لهذا ، يتم تحقيق سهولة التنفيذ.
  • نظرًا لتكوين الديكور ، فقد حصلنا على نموذج قابل للتمديد يتكيف بسهولة مع احتياجاتنا.
  • نحصل على أداء ضعيف في وضع الإنتاج بسبب الإغلاق البسيط للملف.

لذلك ، على سبيل المثال ، يمكنك تطبيق أنواع الديكور التالية:


  • ضربات القلب. تنفيذ الأمر ping على قاعدة بيانات للحفاظ على اتصال به.
  • التعريف. إخراج كل من نص الطلب ووقت تنفيذه.
  • الشم. مجموعة من مقاييس قاعدة البيانات.
  • استنساخ. استنساخ قاعدة البيانات الأصلية لأغراض التصحيح.

كقاعدة عامة ، عند تنفيذ أدوات الديكور الغنية ، فإن تنفيذ جميع الطرق غير مطلوب: يكفي تفويض الأساليب غير المنفذة إلى كائن داخلي.


افترض أننا نحتاج إلى تطبيق أداة تسجيل متقدمة لتتبع استعلامات DML لقاعدة بيانات (لتتبع استعلامات INSERT / UPDATE / DELETE). في هذه الحالة ، لسنا بحاجة إلى تطبيق واجهة قاعدة البيانات بالكامل - فقط تداخل فقط مع أسلوب Exec.


 type MyDatabase interface{ Query(...) (sql.Rows, error) QueryRow(...) error Exec(query string, args ...interface) error Ping() error } type MyExecutor struct { MyDatabase } func (e *MyExecutor) Exec(query string, args ...interface) error { ... } 

وبالتالي ، فإننا نرى أن إنشاء ديكور غني بلغة golang ليس بالأمر الصعب.


طريقة القالب


طريقة القالب (طريقة قالب المهندس) - نمط تصميم سلوكي يحدد أساس الخوارزمية ويسمح للورثة بإعادة تعريف بعض خطوات الخوارزمية دون تغيير هيكلها ككل.

تدعم لغة golang نموذج OOP ، لذلك لا يمكن تطبيق هذا القالب بشكله النقي. ومع ذلك ، لا شيء يمنعنا من ارتجال المنشئات باستخدام وظائف مناسبة.


لنفترض أننا بحاجة إلى تحديد طريقة قالب بالتوقيع التالي:


 func Method(s string) error 

عند التصريح ، يكفي أن نستخدم حقلًا من النوع الوظيفي. لتوفير الراحة للعمل معها ، يمكننا استخدام وظيفة المجمع لتكملة المكالمة بالمعلمة المفقودة ، ولإنشاء مثيل مُحدّد ، وظيفة المُنشئ المناظرة.


 type MyStruct struct { MethodImpl func (me *MyStruct, s string) error } // Wrapper for template method func (ms *MyStruct) Method(s string) error { return ms.MethodImpl(ms, s) } // First constructor func NewStruct1() *MyStruct { return &MyStruct{ MethodImpl: func(me *MyStruct, s string) error { // Implementation 1 ... }, } } // Second constructor func NewStruct2() *MyStruct { return &MyStruct{ MethodImpl: func(me *MyStruct, s string) error { // Implementation 2 ... }, } } func main() { // Create object instance o := NewStruct2() // Call the template method err := o.Method("hello") ... } 

كما ترون من المثال ، فإن دلالات استخدام النمط لا تختلف تقريبًا عن OOP الكلاسيكية.


محول


يتيح لك نمط تصميم "المحول" استخدام واجهة فئة موجودة كواجهة أخرى. غالبًا ما يستخدم هذا القالب لضمان عمل بعض الفئات مع الآخرين دون تغيير التعليمات البرمجية المصدر الخاصة بهم.

بشكل عام ، حيث يمكن أن تعمل المحولات ، كوظائف منفصلة ، وواجهة كاملة. إذا كان كل شيء مع الواجهات أكثر وضوحًا ويمكن التنبؤ به ، فحينئذٍ ، من وجهة نظر الوظائف الفردية ، هناك تفاصيل دقيقة.


لنفترض أننا نكتب بعض الخدمات التي تحتوي على واجهة برمجة تطبيقات داخلية:


 type MyService interface { Create(ctx context.Context, order int) (id int, err error) } 

إذا كنا بحاجة إلى توفير واجهة برمجة تطبيقات عامة بواجهة مختلفة (قل للعمل مع gRPC) ، فيمكننا ببساطة استخدام وظائف المحول التي تتعامل مع تحويل الواجهة. أنها مريحة للغاية لاستخدام الإغلاق لهذا الغرض.


 type Endpoint func(ctx context.Context, request interface{}) (interface{}, error) type CreateRequest struct { Order int } type CreateResponse struct { ID int, Err error } func makeCreateEndpoint(s MyService) Endpoint { return func(ctx context.Context, request interface{}) (interface{}, error) { // Decode request req := request.(CreateRequest) // Call service method id, err := s.Create(ctx, req.Order) // Encode response return CreateResponse{ID: id, Err: err}, nil } } 

تحتوي الدالة makeCreateEndpoint على ثلاث خطوات قياسية:


  • القيم فك التشفير
  • استدعاء طريقة من API الداخلي للخدمة قيد التنفيذ
  • ترميز القيمة

جميع نقاط النهاية في حزمة gokit مبنية على هذا المبدأ.


زائر


قالب "الزائر" هو وسيلة لفصل الخوارزمية عن بنية الكائن الذي تعمل فيه. نتيجة الفصل هي القدرة على إضافة عمليات جديدة إلى بنيات الكائنات الموجودة دون تعديلها. هذه طريقة واحدة للامتثال لمبدأ الفتح / الإغلاق.

النظر في نمط زائر المعروفة على سبيل المثال من الأشكال الهندسية.


 type Geometry interface { Visit(GeometryVisitor) (interface{}, error) } type GeometryVisitor interface { VisitPoint(p *Point) (interface{}, error) VisitLine(l *Line) (interface{}, error) VisitCircle(c *Circle) (interface{}, error) } type Point struct{ X, Y float32 } func (point *Point) Visit(v GeometryVisitor) (interface{}, error) { return v.VisitPoint(point) } type Line struct{ X1, Y1 float32 X2, Y2 float32 } func (line *Line) Visit(v GeometryVisitor) (interface{}, error) { return v.VisitLine(line) } type Circle struct{ X, Y, R float32 } func (circle *Circle) Visit(v GeometryVisitor) (interface{}, error) { return v.VisitCircle(circle) } 

لنفترض أننا نريد أن نكتب استراتيجية لحساب المسافة من نقطة معينة إلى شكل محدد.


 type DistanceStrategy struct { X, Y float32 } func (s *DistanceStrategy) VisitPoint(p *Point) (interface{}, error) { // Evaluate distance from point(X, Y) to point p } func (s *DistanceStrategy) VisitLine(l *Line) (interface{}, error) { // Evaluate distance from point(X, Y) to line l } func (s *DistanceStrategy) VisitCircle(c *Circle) (interface{}, error) { // Evaluate distance from point(X, Y) to circle c } func main() { s := &DistanceStrategy{X: 1, Y: 2} p := &Point{X: 3, Y: 4} res, err := p.Visit(s) if err != nil { panic(err) } fmt.Printf("Distance is %g", res.(float32)) } 

وبالمثل ، يمكننا تنفيذ الاستراتيجيات الأخرى التي نحتاجها:


  • المدى الرأسي
  • المدى الأفقي للكائن
  • بناء الحد الأدنى مربع تمتد (MBR)
  • بدائية أخرى نحتاجها.

علاوة على ذلك ، لا تعرف الأشكال المحددة مسبقًا (النقطة ، الخط ، الدائرة ...) أي شيء عن هذه الاستراتيجيات. معرفتهم الوحيدة مقصورة على واجهة GeometryVisitor. هذا يتيح لك عزلهم في حزمة منفصلة.


في وقت واحد ، أثناء العمل في مشروع رسم الخرائط ، كان لدي مهمة كتابة وظيفة لتحديد المسافة بين كائنين جغرافيين تعسفيين. كانت الحلول مختلفة تمامًا ، ولكن لم تكن جميعها فعالة بما يكفي وأنيقة. بالنظر إلى نمط الزائر بطريقة أو بأخرى ، لاحظت أنه يعمل على تحديد الطريقة المستهدفة ويشبه إلى حد ما خطوة تكرار منفصلة ، والتي ، كما تعلم ، تبسط المهمة. هذا دفعني إلى استخدام الزائر المزدوج. تخيل دهشتي عندما اكتشفت أن مثل هذا النهج غير مذكور على الإطلاق على الإنترنت.


 type geometryStrategy struct{ G Geometry } func (s *geometryStrategy) VisitPoint(p *Point) (interface{}, error) { return sGVisit(&pointStrategy{Point: p}) } func (d *geometryStrategy) VisitLine(l *Line) (interface{}, error) { return sGVisit(&lineStrategy{Line: l}) } func (d *geometryStrategy) VisitCircle(c *Circle) (interface{}, error) { return sGVisit(&circleStrategy{Circle: c}) } type pointStrategy struct{ *Point } func (point *pointStrategy) Visit(p *Point) (interface{}, error) { // Evaluate distance between point and p } func (point *pointStrategy) Visit(l *Line) (interface{}, error) { // Evaluate distance between point and l } func (point *pointStrategy) Visit(c *Circle) (interface{}, error) { // Evaluate distance between point and c } type lineStrategy struct { *Line } func (line *lineStrategy) Visit(p *Point) (interface{}, error) { // Evaluate distance between line and p } func (line *lineStrategy) Visit(l *Line) (interface{}, error) { // Evaluate distance between line and l } func (line *lineStrategy) Visit(c *Circle) (interface{}, error) { // Evaluate distance between line and c } type circleStrategy struct { *Circle } func (circle *circleStrategy) Visit(p *Point) (interface{}, error) { // Evaluate distance between circle and p } func (circle *circleStrategy) Visit(l *Line) (interface{}, error) { // Evaluate distance between circle and l } func (circle *circleStrategy) Visit(c *Circle) (interface{}, error) { // Evaluate distance between circle and c } func Distance(a, b Geometry) (float32, error) { return a.Visit(&geometryStrategy{G: b}) } 

وبالتالي ، قمنا ببناء آلية انتقائية على مستويين ، والتي ، نتيجة لعملها ، سوف تستدعي الطريقة المناسبة لحساب المسافة بين البدائيين. يمكننا فقط كتابة هذه الطرق وتحقيق الهدف. هذه هي الطريقة التي يمكن بها تقليل مشكلة غير حتمية إلى عدد من الوظائف الأولية.


استنتاج


على الرغم من حقيقة أنه لا يوجد OOP الكلاسيكية في golang ، فإن اللغة تنتج لهجتها الخاصة من الأنماط التي تلعب على نقاط القوة في اللغة. تذهب هذه الأنماط بالطريقة القياسية من الرفض إلى القبول العالمي وتصبح أفضل الممارسات مع مرور الوقت.


إذا كان لدى habrozhiteli المحترم أي أفكار حول الأنماط ، فالرجاء عدم الخجل والتعبير عن أفكارك حول هذا الموضوع.

Source: https://habr.com/ru/post/ar448590/


All Articles