نحن نتعامل مع واجهات في الذهاب


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

  1. لشرح في لغة الإنسان ما هي واجهات.
  2. اشرح كيف تكون مفيدة وكيف يمكنك استخدامها في التعليمات البرمجية الخاصة بك.
  3. تحدث عن interface{} (واجهة فارغة).
  4. وتصفح بعض أنواع الواجهات المفيدة التي يمكنك العثور عليها في المكتبة القياسية.

إذن ما هي الواجهة؟


نوع الواجهة في Go هو نوع من التعريف . إنه يعرّف ويصف الطرق المحددة التي يجب أن يكون لدى نوع آخر .

أحد أنواع الواجهات من المكتبة القياسية هي واجهة fmt.Stringer :

 type Stringer interface { String() string } 

نقول أن هناك شيئ ما يرضي هذه الواجهة (أو ينفذ هذه الواجهة ) إذا كان هذا "الشيء" يحتوي على طريقة ذات قيمة سلسلة توقيع محددة String() .

على سبيل المثال ، يفي نوع Book بالواجهة لأنه يحتوي على طريقة السلسلة String() :

 type Book struct { Title string Author string } func (b Book) String() string { return fmt.Sprintf("Book: %s - %s", b.Title, b.Author) } 

لا يهم ما هو نوع Book أو ما يفعله. كل ما يهم هو أنه يحتوي على طريقة تسمى String() تقوم بإرجاع قيمة سلسلة.

هنا مثال آخر. يفي نوع Count أيضًا بواجهة fmt.Stringer لأنه يحتوي على طريقة بنفس قيمة سلسلة التوقيع String() .

 type Count int func (c Count) String() string { return strconv.Itoa(int(c)) } 

من المهم أن نفهم هنا أن لدينا نوعين مختلفين من Book Count ، والتي تعمل بشكل مختلف. لكنهم متحدون من حقيقة أن كلاهما fmt.Stringer واجهة fmt.Stringer .

يمكنك إلقاء نظرة عليه من الجانب الآخر. إذا كنت تعرف أن الكائن يرضي واجهة fmt.Stringer ، فيمكنك افتراض أن لديه طريقة مع سلسلة سلسلة القيمة String() التي يمكنك الاتصال بها.

والآن الشيء الأكثر أهمية.

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

دعنا نقول لدينا وظيفة:

 func WriteLog(s fmt.Stringer) { log.Println(s.String()) } 

بما أن WriteLog() يستخدم نوع الواجهة fmt.Stringer في تعريف المعلمة ، يمكننا تمرير أي كائن يرضي واجهة fmt.Stringer . على سبيل المثال ، يمكننا تمرير أنواع Book Count التي أنشأناها مسبقًا في WriteLog() ، وستعمل التعليمة البرمجية بشكل جيد.

بالإضافة إلى ذلك ، نظرًا لأن الكائن الذي تم تمريره يفي بواجهة fmt.Stringer ، فإننا نعرف أن لديه أسلوب String() ، والذي يمكن استدعاءه بأمان بواسطة وظيفة WriteLog() .

فلنضعها جميعًا في مثال واحد ، نوضح قوة الواجهات.

 package main import ( "fmt" "strconv" "log" ) //   Book,    fmt.Stringer. type Book struct { Title string Author string } func (b Book) String() string { return fmt.Sprintf("Book: %s - %s", b.Title, b.Author) } //   Count,    fmt.Stringer. type Count int func (c Count) String() string { return strconv.Itoa(int(c)) } //   WriteLog(),    , //   fmt.Stringer   . func WriteLog(s fmt.Stringer) { log.Println(s.String()) } func main() { //   Book    WriteLog(). book := Book{"Alice in Wonderland", "Lewis Carrol"} WriteLog(book) //   Count    WriteLog(). count := Count(3) WriteLog(count) } 

هذا رائع. في الوظيفة الرئيسية ، أنشأنا أنواعًا مختلفة من Book and Count ، لكننا WriteLog() وظيفة WriteLog() نفسها . ودعت وظائف String() المناسبة وكتبت النتائج في السجل.

إذا قمت بتنفيذ الرمز ، فستحصل على نتيجة مماثلة:

 2009/11/10 23:00:00 Book: Alice in Wonderland - Lewis Carrol 2009/11/10 23:00:00 3 

لن نتناول هذا بالتفصيل. الشيء الرئيسي الذي يجب تذكره: باستخدام نوع الواجهة في إعلان الدالة WriteLog() ، جعلنا الوظيفة غير مبالية (أو مرنة) بنوع الكائن المستلم. ما يهم هو ما الأساليب التي لديه .

ما هي واجهات مفيدة؟


هناك عدد من الأسباب التي تجعلك تبدأ في استخدام واجهات في Go. وفي تجربتي ، أهمها:

  1. تساعد الواجهات على تقليل الازدواجية ، أي مقدار رمز النمطي.
  2. أنها تجعل من الأسهل استخدام كعب الروتين في اختبارات الوحدة بدلاً من الكائنات الحقيقية.
  3. كونه أداة معمارية ، تساعد الواجهات على فك أجزاء من قاعدة الكود.

دعونا نلقي نظرة فاحصة على هذه الطرق لاستخدام واجهات.

قلل من مقدار كود الغليان


افترض أن لدينا بنية Customer تحتوي على نوع من بيانات العميل. في جزء واحد من الكود ، نريد أن نكتب هذه المعلومات على بايت . وفي الجزء الآخر نريد أن نكتب بيانات العميل إلى os.File على القرص. ولكن ، في كلتا الحالتين ، نريد أولاً إجراء تسلسل بنية ustomer إلى JSON.

في هذا السيناريو ، يمكننا تقليل مقدار كود boilerplate باستخدام واجهات Go.

Go لديه نوع واجهة io.Writer :

 type Writer interface { Write(p []byte) (n int, err error) } 

ويمكننا الاستفادة من حقيقة أن bytes.Buffer ونوع os.File يرضيان هذه الواجهة ، لأنهما يمتلكان طريقتي bytes.Buffer.Write () و os.File.Write () ، على التوالي.

تنفيذ بسيط:

 package main import ( "encoding/json" "io" "log" "os" ) //   Customer. type Customer struct { Name string Age int } //   WriteJSON,   io.Writer   . //    ustomer  JSON,     // ,     Write()  io.Writer. func (c *Customer) WriteJSON(w io.Writer) error { js, err := json.Marshal(c) if err != nil { return err } _, err = w.Write(js) return err } func main() { //   Customer. c := &Customer{Name: "Alice", Age: 21} //    Buffer    WriteJSON var buf bytes.Buffer err := c.WriteJSON(buf) if err != nil { log.Fatal(err) } //   . f, err := os.Create("/tmp/customer") if err != nil { log.Fatal(err) } defer f.Close() err = c.WriteJSON(f) if err != nil { log.Fatal(err) } } 

بالطبع ، هذا مجرد مثال وهمية (يمكننا هيكلة الكود بطريقة مختلفة لتحقيق نفس النتيجة). ولكنه يوضح جيدًا مزايا استخدام الواجهات: يمكننا إنشاء طريقة Customer.WriteJSON() مرة واحدة io.Writer في كل مرة نحتاج فيها إلى الكتابة إلى شيء يرضي واجهة io.Writer .

ولكن إذا كنت جديدًا في تطبيق Go ، فسوف يكون لديك سؤالان: " كيف أعرف ما إذا كانت واجهة io.Writer موجودة على الإطلاق؟ وكيف تعرف مقدمًا أنه راضٍ عن bytes.Buffer و os.File ؟ "

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

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

اختبار وحدة و بذرة


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

افترض أن لديك متجرًا وتخزين معلومات حول المبيعات وعدد العملاء في PostgreSQL. تريد كتابة رمز يحسب حصة المبيعات (عدد محدد من المبيعات لكل عميل) لليوم الأخير ، تقريبًا إلى منزلتين عشريتين.

سيبدو التنفيذ الأدنى كالتالي:

 // : main.go package main import ( "fmt" "log" "time" "database/sql" _ "github.com/lib/pq" ) type ShopDB struct { *sql.DB } func (sdb *ShopDB) CountCustomers(since time.Time) (int, error) { var count int err := sdb.QueryRow("SELECT count(*) FROM customers WHERE timestamp > $1", since).Scan(&count) return count, err } func (sdb *ShopDB) CountSales(since time.Time) (int, error) { var count int err := sdb.QueryRow("SELECT count(*) FROM sales WHERE timestamp > $1", since).Scan(&count) return count, err } func main() { db, err := sql.Open("postgres", "postgres://user:pass@localhost/db") if err != nil { log.Fatal(err) } defer db.Close() shopDB := &ShopDB{db} sr, err := calculateSalesRate(shopDB) if err != nil { log.Fatal(err) } fmt.Printf(sr) } func calculateSalesRate(sdb *ShopDB) (string, error) { since := time.Now().Sub(24 * time.Hour) sales, err := sdb.CountSales(since) if err != nil { return "", err } customers, err := sdb.CountCustomers(since) if err != nil { return "", err } rate := float64(sales) / float64(customers) return fmt.Sprintf("%.2f", rate), nil } 

الآن نريد إنشاء اختبار وحدة لوظيفة calculateSalesRate() للتحقق من صحة العمليات الحسابية.

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

واجهات تأتي لإنقاذ!

CountSales() نوع الواجهة الخاص بنا الذي يصف CountSales() و CountCustomers() ، والتي تعتمد عليها وظيفة calculateSalesRate() . ثم قم بتحديث التوقيع calculateSalesRate() لاستخدام نوع الواجهة هذا كمعلمة بدلاً من نوع *ShopDB المحدد.

مثل هذا:

 // : main.go package main import ( "fmt" "log" "time" "database/sql" _ "github.com/lib/pq" ) //    ShopModel.     //     ,     //  -,     . type ShopModel interface { CountCustomers(time.Time) (int, error) CountSales(time.Time) (int, error) } //  ShopDB    ShopModel,   //       -- CountCustomers()  CountSales(). type ShopDB struct { *sql.DB } func (sdb *ShopDB) CountCustomers(since time.Time) (int, error) { var count int err := sdb.QueryRow("SELECT count(*) FROM customers WHERE timestamp > $1", since).Scan(&count) return count, err } func (sdb *ShopDB) CountSales(since time.Time) (int, error) { var count int err := sdb.QueryRow("SELECT count(*) FROM sales WHERE timestamp > $1", since).Scan(&count) return count, err } func main() { db, err := sql.Open("postgres", "postgres://user:pass@localhost/db") if err != nil { log.Fatal(err) } defer db.Close() shopDB := &ShopDB{db} sr := calculateSalesRate(shopDB) fmt.Printf(sr) } //       ShopModel    //    *ShopDB. func calculateSalesRate(sm ShopModel) string { since := time.Now().Sub(24 * time.Hour) sales, err := sm.CountSales(since) if err != nil { return "", err } customers, err := sm.CountCustomers(since) if err != nil { return "", err } rate := float64(sales) / float64(customers) return fmt.Sprintf("%.2f", rate), nil } 

بعد قيامنا بذلك ، سنقوم فقط بإنشاء روتين يرضي واجهة ShopModel . ثم يمكنك استخدامه أثناء اختبار الوحدة للعملية الصحيحة للمنطق الرياضي في الدالة calculateSalesRate() . مثل هذا:

 // : main_test.go package main import ( "testing" ) type MockShopDB struct{} func (m *MockShopDB) CountCustomers() (int, error) { return 1000, nil } func (m *MockShopDB) CountSales() (int, error) { return 333, nil } func TestCalculateSalesRate(t *testing.T) { //  . m := &MockShopDB{} //     calculateSalesRate(). sr := calculateSalesRate(m) // ,        //   . exp := "0.33" if sr != exp { t.Fatalf("got %v; expected %v", sr, exp) } } 

الآن قم بإجراء الاختبار وكل شيء يعمل بشكل جيد.

بنية التطبيق


في المثال السابق ، رأينا كيف يمكنك استخدام واجهات لفصل أجزاء معينة من التعليمات البرمجية من استخدام أنواع محددة. على سبيل المثال ، لا يهم الدالة calculateSalesRate() ما تقوم ShopModel إليها ، طالما أنها ترضي واجهة ShopModel .

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

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

ما هي واجهة فارغة؟


إذا كنت تقوم بالبرمجة على Go لبعض الوقت ، فمن المحتمل أنك واجهت واجهة نوع واجهة فارغة interface{} . سأحاول شرح ما هو عليه. في بداية هذا المقال ، كتبت:

نوع الواجهة في Go هو نوع من التعريف . إنه يعرّف ويصف الطرق المحددة التي يجب أن يكون لدى نوع آخر .

لا يصف نوع واجهة فارغة الطرق . ليس لديه قواعد. وهكذا يرضي أي كائن واجهة فارغة.

في الجوهر ، واجهة نوع الواجهة الفارغة interface{} هي نوع من الجوكر. إذا قابلتها في إعلان (متغير أو معلمة دالة أو حقل بنية) ، فيمكنك استخدام كائن من أي نوع .

النظر في الكود:

 package main import "fmt" func main() { person := make(map[string]interface{}, 0) person["name"] = "Alice" person["age"] = 21 person["height"] = 167.64 fmt.Printf("%+v", person) } 

نحن هنا نهيئ الخريطة person ، والذي يستخدم نوع السلسلة للمفاتيح وواجهة نوع واجهة فارغة interface{} للقيم. قمنا بتعيين ثلاثة أنواع مختلفة كقيم خريطة (سلسلة ، عدد صحيح و float32) ، ولا مشكلة. نظرًا لأن الكائنات من أي نوع تفي بالواجهة الفارغة ، تعمل الشفرة بشكل رائع.

يمكنك تشغيل هذا الرمز هنا ، سترى نتيجة مماثلة:

 map[age:21 height:167.64 name:Alice] 

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

 package main import "log" func main() { person := make(map[string]interface{}, 0) person["name"] = "Alice" person["age"] = 21 person["height"] = 167.64 person["age"] = person["age"] + 1 fmt.Printf("%+v", person) } 

سوف تتلقى رسالة خطأ:

 invalid operation: person["age"] + 1 (mismatched types interface {} and int) 

والسبب هو أن القيمة المخزنة في الخريطة تأخذ نوع interface{} وتفقد نوع int الأساسي الأصلي. وبما أن القيمة لم تعد عددًا صحيحًا ، فلا يمكننا إضافة 1 إليها.

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

 package main import "log" func main() { person := make(map[string]interface{}, 0) person["name"] = "Alice" person["age"] = 21 person["height"] = 167.64 age, ok := person["age"].(int) if !ok { log.Fatal("could not assert value to int") return } person["age"] = age + 1 log.Printf("%+v", person) } 

إذا قمت بتشغيل هذا ، فسيعمل كل شيء كما هو متوقع:

 2009/11/10 23:00:00 map[age:22 height:167.64 name:Alice] 

إذن متى يجب عليك استخدام نوع واجهة فارغ؟

ربما ليس في كثير من الأحيان . إذا توصلت إلى هذا ، فتوقف ثم فكر فيما إذا كان من المناسب استخدام interface{} . كنصيحة عامة ، يمكنني القول أنه سيكون أكثر وضوحًا وأمانًا وإنتاجية لاستخدام أنواع معينة ، أي أنواع الواجهات غير الفارغة. في المثال أعلاه ، كان من الأفضل تحديد بنية Person مع الحقول المكتوبة بشكل مناسب:

 type Person struct { Name string Age int Height float32 } 

من ناحية أخرى ، تكون الواجهة الفارغة مفيدة عندما تحتاج إلى الوصول إلى أنواع غير معروفة أو معرفة من قبل المستخدم والعمل بها. لسبب ما ، يتم استخدام هذه الواجهات في أماكن مختلفة في المكتبة القياسية ، على سبيل المثال ، في وظائف gob.Encode و fmt.Print و template.Execute .

أنواع واجهة مفيدة


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


قائمة أطول من المكتبات القياسية متاحة هنا أيضا.

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


All Articles