واجهات مثل أنواع البيانات المجردة في الذهاب

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

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

أيضًا ، عند استخدام الواجهات في Go ، غالبًا ما يكون هناك نقاش حول الهندسة الزائدة. ويحدث أيضًا أنه بعد قراءة هذا النوع من التوصيات ، لا يتوقف الأشخاص عن إساءة استخدام الواجهات فحسب ، بل يحاولون التخلي عنها تمامًا تقريبًا ، مما يحرم أنفسهم من استخدام أحد أقوى مفاهيم البرمجة من حيث المبدأ (وواحدة من نقاط القوة في Go in على وجه الخصوص). حول موضوع الأخطاء النموذجية في Go ، بالمناسبة ، هناك تقرير جيد من Stive Francia من Docker. هناك ، على وجه الخصوص ، يتم ذكر واجهات عدة مرات.

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

الرجوع إلى الأصل


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

package animal type Animal interface { Speaks() string } // implementation of Animal type Dog struct{} func (a Dog) Speaks() string { return "woof" } 

 package circus import "animal" func Perform(a animal.Animal) string { return a.Speaks() } 

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

 package animal // implementation of Animal type Dog struct{} func (a Dog) Speaks() string { return "woof" } 

 package circus type Speaker interface { Speaks() string } func Perform(a Speaker) string { return a.Speaks() } 

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

دعنا نلعب مع التجريدات


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

 package circus type Tamer struct{} func (t *Tamer) Speaks() string { return "WAT?" } 

جيد جدا نحن نذهب أبعد من ذلك. دعونا نعلم تامر لدينا لإعطاء أوامر للحيوانات الأليفة؟ حتى الآن ، سيكون لدينا أمر صوت واحد. :)

 package circus const ( ActVoice = iota ) func (t *Tamer) Command(action int, a Speaker) string { switch action { case ActVoice: return a.Speaks() } return "" } 

 package main import ( "animal" "circus" ) func main() { d := &animal.Dog{} t := &circus.Tamer{} t2 := &circus.Tamer{} t.Command(circus.ActVoice, d) // woof t.Command(circus.ActVoice, t2) // WAT? } 

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

 package circus type Animal interface { Speaker } func (t *Tamer) Command(action int, a Animal) string { /* ... */ } 

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

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

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

 package circus type Animal interface { Speaker IsTrained() bool } 

الآن لا يمكن انزلاق تامر بدلا من حيوان أليف.

توسيع السلوك


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

 package animal type Dog struct{} func (d Dog) IsTrained() bool { return true } func (d Dog) Speaks() string { return "woof" } func (d Dog) Jump() string { return "jumps" } func (d Dog) Sit() string { return "sit" } type Cat struct{} func (c Cat) IsTrained() bool { return false } func (c Cat) Speaks() string { return "meow!" } func (c Cat) Jump() string { return "meow!!" } func (c Cat) Sit() string { return "meow!!!" } 

 package circus const ( ActVoice = iota ActSit ActJump ) type Animal interface { Speaker IsTrained() bool Jump() string Sit() string } func (t *Tamer) Command(action int, a Animal) string { switch action { case ActVoice: return a.Speaks() case ActSit: return a.Sit() case ActJump: return a.Jump() } return "" } 

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

 package main import ( "animal" "circus" ) func main() { t := &circus.Tamer{} d := &animal.Dog{} t.Command(circus.ActVoice, d) // "woof" t.Command(circus.ActJump, d) // "jumps" t.Command(circus.ActSit, d) // "sit" t2 := &circus.Tamer{} c := &animal.Cat{} t2.Command(circus.ActVoice, c) // "meow" t2.Command(circus.ActJump, c) // "meow!!" t2.Command(circus.ActSit, c) // "meow!!!" } 

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

 package circus func (t *Tamer) Command(action int, a Animal) string { if !a.IsTrained() { panic("Sorry but this animal doesn't understand your commands") } // ... } 

هذا أفضل. على عكس واجهة Animal الأولية ، التي تكرّر مكبر الصوت ، لدينا الآن واجهة "Animal" (والتي تعد في الأساس نوع بيانات مجردة) تنفذ سلوكًا ذا مغزى.

دعونا مناقشة أحجام الواجهة


الآن دعونا نفكر في مشكلة مثل استخدام واجهات واسعة.

هذا هو الموقف الذي نستخدم فيه واجهات مع عدد كبير من الأساليب. في هذه الحالة ، التوصية هي مثل هذا: "يجب أن تقبل الوظائف الواجهات التي تحتوي على الطرق التي تحتاجها . "

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

ردا على الثناء ، سوف يلقي الحيوانات الأليفة صوتا.

 package circus func (t *Tamer) Praise(a Speaker) string { return a.Speaks() } 

يبدو أن كل شيء على ما يرام ، ونحن نستخدم الحد الأدنى من الواجهة الضرورية. لا يوجد شيء لا لزوم له. ولكن هنا مرة أخرى المشكلة. اللعنة ، والآن يمكننا "الثناء" على المدرب الآخر وسوف "يعطي صوت" . : D الاستيلاء عليها؟ .. السياق يهم دائما.

 package main import ( "animal" "circus" ) func main() { t := &circus.Tamer{} t2 := &circus.Tamer{} d := &animal.Dog{} c := &animal.Cat{} t.Praise(d) // woof t.Praise(c) // meow! t.Praise(t2) // WAT? } 

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

 package circus // Now we are using Animal interface here. func (t *Tamer) Praise(a Animal) string { return a.Speaks() } 

أفضل بكثير. يمكننا أن نمدح الحيوانات الأليفة ، لكن لا يمكننا مدح التامر. أصبح الرمز مرة أخرى أبسط وأكثر وضوحًا.

الآن قليلا عن قانون السرير


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

يستشهد المؤلف بالقانون نفسه:.
"كن متحفظًا فيما تفعله ، وكن ليبراليًا معك"

ويفسرها فيما يتعلق بلغة الذهاب
"اذهب": "اقبل الواجهات ، واعاد الهياكل"
func funcName(a INTERFACETYPE) CONCRETETYPE

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

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

 package african import "circus" type Elephant struct{} func (e Elephant) Speaks() string { return "pawoo!" } func (e Elephant) Jump() string { return "o_O" } func (e Elephant) Sit() string { return "sit" } func (e Elephant) IsTrained() bool { return true } func GetElephant() circus.Animal { return &Elephant{} } 

 package main import ( "african" "circus" ) func main() { t := &circus.Tamer{} e := african.GetElephant() t.Command(circus.ActVoice, e) // "pawoo!" t.Command(circus.ActJump, e) // "o_O" t.Command(circus.ActSit, e) // "sit" } 

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

لتلخيص


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

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

الشيء الرئيسي ، أيها الأصدقاء ، هو أنك تتطور باستمرار وتتلقى متعة حقيقية من العمل. جيد للجميع!

PS. رمز عينة والنسخة النهائية يمكن العثور عليها على جيثب .

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


All Articles