Orang asing atau sekali lagi terbiasa menggunakan pola desain

Pada topik pola desain, banyak artikel telah ditulis dan banyak buku telah diterbitkan. Namun, topik ini tidak berhenti menjadi relevan, karena polanya memungkinkan kami untuk menggunakan solusi yang siap pakai dan teruji waktu, yang memungkinkan kami untuk mengurangi waktu pengembangan proyek dengan meningkatkan kualitas kode dan mengurangi hutang teknis.


Sejak munculnya pola desain, selalu ada contoh baru penggunaannya yang efektif. Dan ini luar biasa. Namun, ada lalat di salep: setiap bahasa memiliki spesifiknya sendiri. Dan golang - dan bahkan lebih (itu bahkan tidak memiliki model OOP klasik). Oleh karena itu, ada variasi pola, dalam kaitannya dengan bahasa pemrograman individual. Pada artikel ini, saya ingin menyentuh pada topik pola desain dalam kaitannya dengan golang.


Dekorator


Template Dekorator memungkinkan Anda untuk menghubungkan perilaku tambahan ke objek (secara statis atau dinamis) tanpa mempengaruhi perilaku objek lain dari kelas yang sama. Templat sering digunakan untuk mematuhi Prinsip Tanggung Jawab Tunggal, karena memungkinkan Anda untuk berbagi fungsionalitas antar kelas untuk memecahkan masalah tertentu.

Pola DECORATOR yang terkenal banyak digunakan dalam banyak bahasa pemrograman. Jadi, di golang, semua middleware dibangun atas dasar itu. Misalnya, profil permintaan mungkin terlihat seperti ini:


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

Dalam hal ini, antarmuka dekorator adalah satu-satunya fungsi. Sebagai aturan, ini harus dicari. Namun, dekorator dengan antarmuka yang lebih luas terkadang dapat bermanfaat. Sebagai contoh, pertimbangkan akses ke database (paket database / sql). Misalkan kita perlu melakukan profil yang sama dari permintaan basis data. Dalam hal ini, kita perlu:


  • Alih-alih berinteraksi secara langsung dengan database melalui pointer, kita perlu pergi ke interaksi melalui antarmuka (untuk memisahkan perilaku dari implementasi).
  • Buat pembungkus untuk setiap metode yang mengeksekusi query database SQL.

Sebagai hasilnya, kami mendapatkan dekorator yang memungkinkan Anda untuk membuat profil semua pertanyaan ke database. Keuntungan dari pendekatan ini tidak dapat dipungkiri:


  • Menjaga kebersihan kode komponen akses basis data inti.
  • Setiap dekorator menerapkan satu persyaratan. Karena itu, kemudahan implementasi tercapai.
  • Karena komposisi dekorator, kami mendapatkan model yang dapat diperluas yang dengan mudah menyesuaikan dengan kebutuhan kami.
  • Kami mendapatkan nol overhead kinerja dalam mode produksi karena shutdown sederhana dari profiler.

Jadi, misalnya, Anda dapat menerapkan jenis dekorator berikut:


  • Detak jantung Mem-ping sebuah basis data agar koneksi tetap hidup.
  • Profiler. Output dari tubuh permintaan dan waktu pelaksanaannya.
  • Sniffer. Kumpulan metrik basis data.
  • Klon Mengkloning basis data asli untuk tujuan debugging.

Sebagai aturan, ketika menerapkan dekorator kaya, penerapan semua metode tidak diperlukan: cukup untuk mendelegasikan metode yang tidak diimplementasikan ke objek internal.


Misalkan kita perlu mengimplementasikan logger tingkat lanjut untuk melacak kueri DML untuk basis data (untuk melacak kueri INSERT / UPDATE / DELETE). Dalam hal ini, kita tidak perlu mengimplementasikan seluruh antarmuka basis data - cukup tumpang tindih hanya dengan metode 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 { ... } 

Jadi, kita melihat bahwa membuat bahkan dekorator yang kaya dalam bahasa golang tidak terlalu sulit.


Metode templat


Metode Templat (Metode Templat Templat) - pola desain perilaku yang mendefinisikan dasar algoritma dan memungkinkan ahli waris untuk mendefinisikan kembali beberapa langkah algoritma tanpa mengubah strukturnya secara keseluruhan.

Bahasa golang mendukung paradigma OOP, sehingga templat ini tidak dapat diimplementasikan dalam bentuknya yang murni. Namun, tidak ada yang menghalangi kita untuk berimprovisasi dengan menggunakan fungsi yang sesuai.


Misalkan kita perlu mendefinisikan metode templat dengan tanda tangan berikut:


 func Method(s string) error 

Ketika mendeklarasikan itu cukup bagi kita untuk menggunakan bidang tipe fungsional. Untuk kenyamanan bekerja dengannya, kita dapat menggunakan fungsi wrapper untuk melengkapi panggilan dengan parameter yang hilang, dan untuk membuat contoh spesifik, fungsi konstruktor yang sesuai.


 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") ... } 

Seperti yang dapat Anda lihat dari contoh, semantik menggunakan pola hampir tidak berbeda dari OOP klasik.


Adaptor


Pola desain "Adaptor" memungkinkan Anda untuk menggunakan antarmuka kelas yang ada sebagai antarmuka lain. Templat ini sering digunakan untuk memastikan bahwa beberapa kelas bekerja dengan yang lain tanpa mengubah kode sumbernya.

Secara umum, sebagai adaptor dapat berfungsi, sebagai fungsi terpisah, dan seluruh antarmuka. Jika dengan antarmuka semuanya lebih atau kurang jelas dan dapat diprediksi, maka dari sudut pandang fungsi individu ada kehalusan.


Misalkan kita menulis beberapa layanan yang memiliki beberapa API internal:


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

Jika kita perlu menyediakan API publik dengan antarmuka yang berbeda (katakanlah untuk bekerja dengan gRPC), maka kita cukup menggunakan fungsi adaptor yang berhubungan dengan konversi antarmuka. Sangat nyaman untuk menggunakan penutupan untuk tujuan ini.


 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 } } 

Fungsi makeCreateEndpoint memiliki tiga langkah standar:


  • nilai decoding
  • memanggil metode dari API internal layanan yang sedang dilaksanakan
  • pengkodean nilai

Semua titik akhir dalam paket gokit dibangun berdasarkan prinsip ini.


Pengunjung


Template "Pengunjung" adalah cara untuk memisahkan algoritma dari struktur objek di mana ia beroperasi. Hasil pemisahan adalah kemampuan untuk menambahkan operasi baru ke struktur objek yang ada tanpa mengubahnya. Ini adalah salah satu cara untuk mematuhi prinsip terbuka / tertutup.

Perhatikan pola pengunjung yang terkenal pada contoh bentuk geometris.


 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) } 

Misalkan kita ingin menulis strategi untuk menghitung jarak dari titik tertentu ke bentuk tertentu.


 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)) } 

Demikian pula, kita dapat menerapkan strategi lain yang kita butuhkan:


  • Tingkat vertikal
  • Tingkat horizontal objek
  • Membangun square spanning minimum (MBR)
  • Primitif lain yang kita butuhkan.

Selain itu, angka yang didefinisikan sebelumnya (Point, Line, Circle ...) tidak tahu apa-apa tentang strategi ini. Pengetahuan mereka hanya terbatas pada antarmuka GeometryVisitor. Ini memungkinkan Anda mengisolasinya dalam paket terpisah.


Pada suatu waktu, saat mengerjakan proyek kartografi, saya mendapat tugas untuk menulis fungsi untuk menentukan jarak antara dua objek geografis yang arbitrer. Solusinya sangat berbeda, tetapi semuanya tidak cukup efisien dan elegan. Mengingat entah bagaimana pola Pengunjung, saya perhatikan bahwa itu berfungsi untuk memilih metode target dan agak menyerupai langkah rekursi yang terpisah, yang, seperti yang Anda tahu, menyederhanakan tugas. Ini mendorong saya untuk menggunakan Pengunjung Ganda. Bayangkan betapa terkejutnya saya ketika saya menemukan bahwa pendekatan semacam itu sama sekali tidak disebutkan di Internet.


 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}) } 

Dengan demikian, kami telah membangun mekanisme selektif dua tingkat, yang, sebagai hasil dari kerjanya, akan memanggil metode yang tepat untuk menghitung jarak antara dua primitif. Kami hanya dapat menulis metode ini dan tujuannya tercapai. Ini adalah bagaimana masalah elegan non-deterministik dapat direduksi menjadi sejumlah fungsi dasar.


Kesimpulan


Terlepas dari kenyataan bahwa tidak ada OOP klasik di golang, bahasa menghasilkan dialek pola sendiri yang bermain pada kekuatan bahasa. Pola-pola ini berubah dari penolakan ke penerimaan universal dan menjadi praktik terbaik dari waktu ke waktu.


Jika habrozhiteli yang dihormati memiliki pemikiran tentang pola, harap jangan malu dan ungkapkan pikiran Anda tentang hal ini.

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


All Articles