İyi yapılandırılmış Mantık: Bir Golang OOP Eğitimi

Yayınlanan: 2022-03-11

Go nesne yönelimli mi? Olabilir mi? Go (veya “Golang”) yapısını (paketleri, türleri, işlevleri) Algol/Pascal/Modula dil ailesinden ödünç alan bir OOP sonrası programlama dilidir. Bununla birlikte, Go'da nesne yönelimli modeller, bir programı açık ve anlaşılır bir şekilde yapılandırmak için hala yararlıdır. Bu Golang öğreticisi basit bir örnek alacak ve bağlama işlevleri kavramlarının türlere (diğer adıyla sınıflara), oluşturuculara, alt tiplemeye, polimorfizm, bağımlılık enjeksiyonu ve alaylarla test etmeye nasıl uygulanacağını gösterecektir.

Golang OOP'de Örnek Olay İncelemesi: Bir Araç Kimlik Numarasından (VIN) Üretici Kodunu Okumak

Her arabanın benzersiz araç tanımlama numarası, bir "çalışıyor" (yani seri) numarasının yanı sıra, üretici, üretim fabrikası, araba modeli ve soldan sürülüp sürülmediği gibi araçla ilgili bilgileri içerir. sağ taraf.

Üretici kodunu belirleme işlevi şöyle görünebilir:

 package vin func Manufacturer(vin string) string { manufacturer := vin[: 3] // if the last digit of the manufacturer ID is a 9 // the digits 12 to 14 are the second part of the ID if manufacturer[2] == '9' { manufacturer += vin[11: 14] } return manufacturer }

Ve işte örnek bir VIN'in çalıştığını kanıtlayan bir test:

 package vin_test import ( "vin-stages/1" "testing" ) const testVIN = "W09000051T2123456" func TestVIN_Manufacturer(t *testing.T) { manufacturer := vin.Manufacturer(testVIN) if manufacturer != "W09123" { t.Errorf("unexpected manufacturer %s for VIN %s", manufacturer, testVIN) } }

Bu nedenle, doğru girdi verildiğinde bu işlev düzgün çalışır, ancak bazı sorunları vardır:

  • Giriş dizesinin bir VIN olduğunun garantisi yoktur.
  • Üç karakterden kısa dizeler için işlev bir panic neden olur.
  • Kimliğin isteğe bağlı ikinci kısmı, yalnızca Avrupa VIN'lerinin bir özelliğidir. İşlev, üretici kodunun üçüncü basamağı olarak 9 olan ABD arabaları için yanlış kimlikler döndürür.

Bu sorunları çözmek için nesne yönelimli kalıpları kullanarak onu yeniden düzenleyeceğiz.

Git OOP: İşlevleri Bir Türe Bağlama

İlk yeniden düzenleme, VIN'leri kendi türleri haline getirmek ve Manufacturer() işlevini buna bağlamaktır. Bu, işlevin amacını daha net hale getirir ve düşüncesiz kullanımı önler.

 package vin type VIN string func (v VIN) Manufacturer() string { manufacturer := v[: 3] if manufacturer[2] == '9' { manufacturer += v[11: 14] } return string(manufacturer) }

Ardından testi uyarlarız ve geçersiz VIN sorununu ortaya koyarız:

 package vin_test import( "vin-stages/2" "testing" ) const ( validVIN = vin.VIN("W0L000051T2123456") invalidVIN = vin.VIN("W0") ) func TestVIN_Manufacturer(t * testing.T) { manufacturer := validVIN.Manufacturer() if manufacturer != "W0L" { t.Errorf("unexpected manufacturer %s for VIN %s", manufacturer, validVIN) } invalidVIN.Manufacturer() // panic! }

Son satır, Manufacturer() işlevini kullanırken bir panic nasıl tetikleneceğini göstermek için eklenmiştir. Bir testin dışında, bu, çalışan programı çökertir.

Golang'da OOP: Yapıcıları Kullanma

Geçersiz bir VIN'i işlerken panic önlemek için Manufacturer() işlevinin kendisine geçerlilik kontrolleri eklemek mümkündür. Dezavantajları ise, Manufacturer() işlevine yapılan her çağrıda kontrollerin yapılması ve bir hata dönüş değerinin eklenmesi gerekmesidir, bu da dönüş değerini bir ara değişken olmadan doğrudan kullanmayı imkansız hale getirir (örn. bir harita anahtarı).

Daha zarif bir yol, geçerlilik kontrollerini VIN türü için bir kurucuya koymaktır, böylece Manufacturer() işlevi yalnızca geçerli VIN'ler için çağrılır ve kontrollere ve hata işlemeye ihtiyaç duymaz:

 package vin import "fmt" type VIN string // it is debatable if this func should be named New or NewVIN // but NewVIN is better for greping and leaves room for other // NewXY funcs in the same package func NewVIN(code string)(VIN, error) { if len(code) != 17 { return "", fmt.Errorf("invalid VIN %s: more or less than 17 characters", code) } // ... check for disallowed characters ... return VIN(code), nil } func (v VIN) Manufacturer() string { manufacturer := v[: 3] if manufacturer[2] == '9' { manufacturer += v[11: 14] } return string(manufacturer) }

Tabii ki, NewVIN işlevi için bir test ekliyoruz. Geçersiz VIN'ler artık yapıcı tarafından reddediliyor:

 package vin_test import ( "vin-stages/3" "testing" ) const ( validVIN = "W0L000051T2123456" invalidVIN = "W0" ) func TestVIN_New(t *testing.T) { _, err := vin.NewVIN(validVIN) if err != nil { t.Errorf("creating valid VIN returned an error: %s", err.Error()) } _, err = vin.NewVIN(invalidVIN) if err == nil { t.Error("creating invalid VIN did not return an error") } } func TestVIN_Manufacturer(t *testing.T) { testVIN, _ := vin.NewVIN(validVIN) manufacturer := testVIN.Manufacturer() if manufacturer != "W0L" { t.Errorf("unexpected manufacturer %s for VIN %s", manufacturer, testVIN) } }

Manufacturer() işlevi testi, NewVIN yapıcısı tarafından zaten reddedilmiş olacağından, geçersiz bir VIN'in test edilmesini artık atlayabilir.

OOP Tuzağına Git: Polimorfizm Yanlış Yol

Ardından, Avrupa ve Avrupa dışı VIN'ler arasında ayrım yapmak istiyoruz. Bir yaklaşım, VIN type bir yapıya genişletmek ve struct Avrupa olup olmadığını depolamak ve yapıcıyı buna göre geliştirmek olacaktır:

 type VIN struct { code string european bool } func NewVIN(code string, european bool)(*VIN, error) { // ... checks ... return &VIN { code, european }, nil }

Daha zarif çözüm, Avrupa VIN'leri için bir VIN alt türü oluşturmaktır. Burada, bayrak örtük olarak tür bilgisinde saklanır ve Avrupa dışı VIN'ler için Manufacturer() işlevi güzel ve özlü hale gelir:

 package vin import "fmt" type VIN string func NewVIN(code string)(VIN, error) { if len(code) != 17 { return "", fmt.Errorf("invalid VIN %s: more or less than 17 characters", code) } // ... check for disallowed characters ... return VIN(code), nil } func (v VIN) Manufacturer() string { return string(v[: 3]) } type EUVIN VIN func NewEUVIN(code string)(EUVIN, error) { // call super constructor v, err := NewVIN(code) // and cast to subtype return EUVIN(v), err } func (v EUVIN) Manufacturer() string { // call manufacturer on supertype manufacturer := VIN(v).Manufacturer() // add EU specific postfix if appropriate if manufacturer[2] == '9' { manufacturer += string(v[11: 14]) } return manufacturer }

Java gibi OOP dillerinde, VIN tipinin belirtildiği her yerde EUVIN alt tipinin kullanılabilir olmasını bekleriz. Ne yazık ki, bu Golang OOP'de çalışmıyor.

 package vin_test import ( "vin-stages/4" "testing" ) const euSmallVIN = "W09000051T2123456" // this works! func TestVIN_EU_SmallManufacturer(t *testing.T) { testVIN, _ := vin.NewEUVIN(euSmallVIN) manufacturer := testVIN.Manufacturer() if manufacturer != "W09123" { t.Errorf("unexpected manufacturer %s for VIN %s", manufacturer, testVIN) } } // this fails with an error func TestVIN_EU_SmallManufacturer_Polymorphism(t *testing.T) { var testVINs[] vin.VIN testVIN, _ := vin.NewEUVIN(euSmallVIN) // having to cast testVIN already hints something is odd testVINs = append(testVINs, vin.VIN(testVIN)) for _, vin := range testVINs { manufacturer := vin.Manufacturer() if manufacturer != "W09123" { t.Errorf("unexpected manufacturer %s for VIN %s", manufacturer, testVIN) } } }

Bu davranış, Go geliştirme ekibinin arabirim olmayan türler için dinamik bağlamayı desteklememesi için kasıtlı seçimiyle açıklanabilir. Derleyicinin derleme zamanında hangi işlevin çağrılacağını bilmesini sağlar ve dinamik yöntem gönderiminin ek yükünü ortadan kaldırır. Bu seçim aynı zamanda kalıtımın genel bir kompozisyon modeli olarak kullanılmasını da engeller. Bunun yerine, arayüzler gitmenin yoludur (punto için kusura bakmayın).

Golang OOP Başarısı: Polimorfizm Doğru Yol

Go derleyicisi, bildirilen işlevleri (ördek yazma) uyguladığında, bir türü bir arabirimin uygulaması olarak ele alır. Bu nedenle, polimorfizmden yararlanmak için VIN tipi, bir genel ve bir Avrupa VIN tipi tarafından uygulanan bir arayüze dönüştürülür. Avrupa VIN tipinin genel olanın bir alt tipi olmasının gerekli olmadığını unutmayın.

 package vin import "fmt" type VIN interface { Manufacturer() string } type vin string func NewVIN(code string)(vin, error) { if len(code) != 17 { return "", fmt.Errorf("invalid VIN %s: more or less than 17 characters", code) } // ... check for disallowed characters ... return vin(code), nil } func (v vin) Manufacturer() string { return string(v[: 3]) } type vinEU vin func NewEUVIN(code string)(vinEU, error) { // call super constructor v, err := NewVIN(code) // and cast to own type return vinEU(v), err } func (v vinEU) Manufacturer() string { // call manufacturer on supertype manufacturer := vin(v).Manufacturer() // add EU specific postfix if appropriate if manufacturer[2] == '9' { manufacturer += string(v[11: 14]) } return manufacturer }

Polimorfizm testi şimdi küçük bir değişiklikle geçiyor:

 // this works! func TestVIN_EU_SmallManufacturer_Polymorphism(t *testing.T) { var testVINs[] vin.VIN testVIN, _ := vin.NewEUVIN(euSmallVIN) // now there is no need to cast! testVINs = append(testVINs, testVIN) for _, vin := range testVINs { manufacturer := vin.Manufacturer() if manufacturer != "W09123" { t.Errorf("unexpected manufacturer %s for VIN %s", manufacturer, testVIN) } } }

Aslında her iki tip de VIN arayüz tanımına uyduğu için artık her iki VIN tipi de VIN arayüzünü belirten her yerde kullanılabilir.

Nesneye Yönelik Golang: Bağımlılık Enjeksiyonu Nasıl Kullanılır?

Son olarak, bir VIN'in Avrupalı ​​olup olmadığına karar vermemiz gerekiyor. Bize bu bilgiyi veren harici bir API bulduğumuzu ve bunun için bir istemci oluşturduğumuzu varsayalım:

 package vin type VINAPIClient struct { apiURL string apiKey string // ... internals go here ... } func NewVINAPIClient(apiURL, apiKey string) *VINAPIClient { return &VINAPIClient {apiURL, apiKey} } func (client *VINAPIClient) IsEuropean(code string) bool { // calls external API and returns correct value return true }

Ayrıca, VIN'leri işleyen ve özellikle bunları oluşturabilen bir hizmet oluşturduk:

 package vin type VINService struct { client *VINAPIClient } type VINServiceConfig struct { APIURL string APIKey string // more configuration values } func NewVINService(config *VINServiceConfig) *VINService { // use config to create the API client apiClient := NewVINAPIClient(config.APIURL, config.APIKey) return &VINService {apiClient} } func (s *VINService) CreateFromCode(code string)(VIN, error) { if s.client.IsEuropean(code) { return NewEUVIN(code) } return NewVIN(code) }

Bu, değiştirilmiş testin gösterdiği gibi iyi çalışıyor:

 func TestVIN_EU_SmallManufacturer(t *testing.T) { service := vin.NewVINService( & vin.VINServiceConfig {}) testVIN, _ := service.CreateFromCode(euSmallVIN) manufacturer := testVIN.Manufacturer() if manufacturer != "W09123" { t.Errorf("unexpected manufacturer %s for VIN %s", manufacturer, testVIN) } }

Buradaki tek sorun, testin harici API'ye canlı bir bağlantı gerektirmesidir. API çevrimdışı olabileceğinden veya erişilemeyeceğinden bu talihsiz bir durumdur. Ayrıca, harici bir API'yi çağırmak zaman alır ve paraya mal olabilir.

API çağrısının sonucu bilindiği için, onu bir sahte ile değiştirmek mümkün olmalıdır. Ne yazık ki, yukarıdaki kodda VINService , API istemcisini kendisi oluşturur, dolayısıyla onu değiştirmenin kolay bir yolu yoktur. Bunu mümkün kılmak için, API istemci bağımlılığı VINService enjekte edilmelidir. Yani, VINService yapıcısını çağırmadan önce oluşturulmalıdır.

Buradaki Golang OOP yönergesi, hiçbir kurucunun başka bir kurucu çağırmaması gerektiğidir . Bu tam olarak uygulanırsa, bir uygulamada kullanılan her tekil en üst düzeyde oluşturulacaktır. Tipik olarak, bu, yapıcılarını uygun sırayla çağırarak ve programın amaçlanan işlevselliği için uygun bir uygulama seçerek gerekli tüm nesneleri yaratan bir önyükleme işlevi olacaktır.

İlk adım, VINAPIClient bir arayüz yapmaktır:

 package vin type VINAPIClient interface { IsEuropean(code string) bool } type vinAPIClient struct { apiURL string apiKey string // .. internals go here ... } func NewVINAPIClient(apiURL, apiKey string) *VINAPIClient { return &vinAPIClient {apiURL, apiKey} } func (client *VINAPIClient) IsEuropean(code string) bool { // calls external API and returns something more useful return true }

Ardından, yeni istemci VINService enjekte edilebilir:

 package vin type VINService struct { client VINAPIClient } type VINServiceConfig struct { // more configuration values } func NewVINService(config *VINServiceConfig, apiClient VINAPIClient) *VINService { // apiClient is created elsewhere and injected here return &VINService {apiClient} } func (s *VINService) CreateFromCode(code string)(VIN, error) { if s.client.IsEuropean(code) { return NewEUVIN(code) } return NewVIN(code) }

Bununla, artık test için bir API istemci alayı kullanmak mümkün. Testler sırasında harici bir API'ye yapılan çağrılardan kaçınmanın yanı sıra, sahte, API kullanımı hakkında veri toplamak için bir araştırma görevi görebilir. Aşağıdaki örnekte, sadece IsEuropean fonksiyonunun gerçekten çağrıldığını kontrol ediyoruz.

 package vin_test import ( "vin-stages/5" "testing" ) const euSmallVIN = "W09000051T2123456" type mockAPIClient struct { apiCalls int } func NewMockAPIClient() *mockAPIClient { return &mockAPIClient {} } func (client *mockAPIClient) IsEuropean(code string) bool { client.apiCalls++ return true } func TestVIN_EU_SmallManufacturer(t *testing.T) { apiClient := NewMockAPIClient() service := vin.NewVINService( & vin.VINServiceConfig {}, apiClient) testVIN, _ := service.CreateFromCode(euSmallVIN) manufacturer := testVIN.Manufacturer() if manufacturer != "W09123" { t.Errorf("unexpected manufacturer %s for VIN %s", manufacturer, testVIN) } if apiClient.apiCalls != 1 { t.Errorf("unexpected number of API calls: %d", apiClient.apiCalls) } }

IsEuropean araştırmamız CreateFromCode çağrısı sırasında bir kez çalıştığından bu test başarılı olur.

Go'da Nesne Yönelimli Programlama: Kazanan Bir Kombinasyon (Doğru Yapıldığında)

Eleştirmenler, "Yine de OOP yapıyorsanız neden Java kullanmıyorsunuz?" diyebilir. Eh, çünkü Go'nun diğer tüm şık avantajlarını, kaynağa aç bir VM/JIT'den, açıklama voodoo'lu karanlık çerçevelerden, istisna işlemeden ve testleri çalıştırırken kahve molalarından kaçınırken elde edersiniz (ikincisi bazıları için bir sorun olabilir).

Yukarıdaki örnekte, Go'da nesne yönelimli programlama yapmanın, basit, zorunlu bir uygulamaya kıyasla nasıl daha iyi anlaşılır ve daha hızlı çalışan kod üretebileceği açıktır. Go bir OOP dili olarak tasarlanmasa da, bir uygulamayı nesne yönelimli bir şekilde yapılandırmak için gerekli araçları sağlar. Paketlerdeki gruplama işlevleriyle birlikte, Golang'daki OOP, büyük uygulamalar için yapı taşları olarak yeniden kullanılabilir modüller sağlamak üzere kullanılabilir.


Google Cloud İş Ortağı rozeti.

Bir Google Cloud İş Ortağı olarak, Toptal'ın Google sertifikalı uzmanları, en önemli projeleri için şirketlere talep üzerine hizmet vermektedir.