paint-brush
Go'da Bağımlılığı Tersine Çevirme Prensibi: Nedir ve Nasıl Kullanılır?ile@kirooha
27,452 okumalar
27,452 okumalar

Go'da Bağımlılığı Tersine Çevirme Prensibi: Nedir ve Nasıl Kullanılır?

ile Kirill Parasotchenko10m2024/05/12
Read on Terminal Reader

Çok uzun; Okumak

Bu yazımızda Bağımlılığı Tersine Çevirme ilkesini tartışacağız. Kısaca ne olduğundan bahsedeceğiz ve basit bir Go uygulamasını örnek alarak bu prensibi inceleyeceğiz.
featured image - Go'da Bağımlılığı Tersine Çevirme Prensibi: Nedir ve Nasıl Kullanılır?
Kirill Parasotchenko HackerNoon profile picture

Giriş

Bu yazımızda Bağımlılığı Tersine Çevirme ilkesini tartışacağız. Kısaca ne olduğundan bahsedeceğiz ve basit bir Go uygulamasını örnek alarak bu prensibi inceleyeceğiz.

Bağımlılığı Ters Çevirme İlkesi Nedir?

Bağımlılığı Ters Çevirme İlkesi (DIP), ilk olarak Robert C. Martin tarafından tanıtılan, nesne yönelimli programlamanın (OOP) beş SOLID ilkesinden biridir. Belirtir:


  1. Yüksek seviyeli modüller, düşük seviyeli modüllerden hiçbir şeyi içe aktarmamalıdır. Her ikisi de soyutlamalara (örneğin arayüzlere) bağlı olmalıdır.


  2. Soyutlamalar ayrıntılara bağlı olmamalıdır. Ayrıntılar (somut uygulamalar) soyutlamalara bağlı olmalıdır.


Bu, OOP tasarımı dünyasında çok iyi bilinen bir prensiptir, ancak daha önce hiç karşılaşmadıysanız, ilk bakışta belirsiz görünebilir; bu nedenle, belirli bir örnek kullanarak bu prensibi parçalara ayıralım.

Örnek

DI ilkesinin uygulamasının Go'da nasıl görünebileceğini düşünelim. Bir kitabın kimliğine göre bilgi döndüren, tek bir uç noktaya/kitabına sahip basit bir HTTP uygulaması örneğiyle başlayacağız. Kitap hakkında bilgi almak için uygulama harici bir HTTP hizmetiyle etkileşime girecektir.

Proje Yapısı

cmd - Git komutlarını içeren klasör. Ana işlev burada yer alacaktır.


dahili - dahili uygulama kodunu içeren klasör. Tüm kodumuz burada yer alacak.

DI Olmayan Spagetti Kodu Örneği

main.go basitçe HTTP sunucusunu başlatır.

 package main import ( "log" "net/http" "example.com/books/internal/app/httpbookapi" ) func main() { http.Handle("/book", &httpbookapi.Handler{}) log.Print("server listening at 9090") log.Fatal(http.ListenAndServe(":9090", nil)) }


İşte HTTP uç noktamızı işlemeye yönelik kod:

 package httpbookapi import ( "encoding/json" "fmt" "net/http" "example.com/books/internal/model" ) type Handler struct { } func (h *Handler) ServeHTTP(writer http.ResponseWriter, request *http.Request) { var ( ctx = request.Context() id = request.URL.Query().Get("id") book model.Book ) url := fmt.Sprintf("http://localhost:8080/book?id=%s", id) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { http.Error(writer, err.Error(), http.StatusInternalServerError) return } resp, err := http.DefaultClient.Do(req) if err != nil { http.Error(writer, err.Error(), http.StatusInternalServerError) return } defer resp.Body.Close() if err := json.NewDecoder(resp.Body).Decode(&book); err != nil { http.Error(writer, err.Error(), http.StatusInternalServerError) return } book.Price = 10.12 if book.Title == "Pride and Prejudice" { book.Price += 2 } writer.Header().Add("Content-Type", "application/json") if err := json.NewEncoder(writer).Encode(book); err != nil { http.Error(writer, err.Error(), http.StatusInternalServerError) return } }

Gördüğünüz gibi, şu anda kodun tamamı doğrudan işleyicinin içindedir (Kitap modeli hariç). İşleyicide bir HTTP istemcisi oluşturup harici bir hizmete istekte bulunuyoruz. Daha sonra kitaba bir miktar fiyat belirliyoruz. Burada, herhangi bir geliştiricinin bunun en iyi tasarım olmadığını ve harici hizmeti çağırma kodunun işleyiciden çıkarılması gerektiğinin açık olduğuna inanıyorum. Hadi bunu yapalım.

İyileştirmenin İlk Adımı

İlk adım olarak bu kodu ayrı bir yere taşıyalım. Bunu yapmak için, kitabımızı alma ve işleme mantığının bulunacağı internal/pkg/getbook/usecase.go ve kitabı saklayacağımız internal/pkg/getbook/types.go adında bir dosya oluşturacağız. gerekli getbook türleri.


usecase.go kodu

 package getbook import ( "context" "encoding/json" "fmt" "net/http" ) type UseCase struct { bookServiceClient *http.Client } func NewUseCase() *UseCase { return &UseCase{} } func (u *UseCase) GetBook(ctx context.Context, id string) (*Book, error) { var ( book Book url = fmt.Sprintf("http://localhost:8080/book?id=%s", id) ) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return nil, err } resp, err := u.bookServiceClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if err := json.NewDecoder(resp.Body).Decode(&book); err != nil { return nil, err } book.Price = 10.12 if book.Title == "Pride and Prejudice" { book.Price += 2 } return &book, nil }


type.go kodu

 package getbook type Book struct { ID string `json:"id"` Title string `json:"title"` Author string `json:"author"` Price float64 `json:"price"` }


Yeni işleyici kodu:

 package httpbookapi import ( "encoding/json" "net/http" "example.com/books/internal/pkg/getbook" ) type Handler struct { getBookUseCase *getbook.UseCase } func NewHandler(getBookUseCase *getbook.UseCase) *Handler { return &Handler{ getBookUseCase: getBookUseCase, } } func (h *Handler) ServeHTTP(writer http.ResponseWriter, request *http.Request) { var ( ctx = request.Context() id = request.URL.Query().Get("id") ) book, err := h.getBookUseCase.GetBook(ctx, id) if err != nil { http.Error(writer, err.Error(), http.StatusInternalServerError) return } writer.Header().Add("Content-Type", "application/json") if err := json.NewEncoder(writer).Encode(book); err != nil { http.Error(writer, err.Error(), http.StatusInternalServerError) return } }


Gördüğünüz gibi işleyici kodu çok daha temiz hale geldi, ancak artık getbook/usecase.go'ya göz atmak bizim için çok daha ilginç.

 type UseCase struct { bookServiceClient *http.Client }


UseCase'in *http.Client biçiminde bir bağımlılığı var ve şu anda bunu hiçbir şekilde başlatmıyoruz. *http.Client'i NewUseCase() yapıcısına aktarabilir veya doğrudan yapıcının içinde *http.Client oluşturabiliriz. Ancak DI ilkesinin bize söylediklerini bir kez daha hatırlayalım.


Yüksek seviyeli modüller, düşük seviyeli modüllerden hiçbir şeyi içe aktarmamalıdır. Her ikisi de soyutlamalara bağlı olmalıdır (örneğin, arayüzler)


Ancak biz bu yaklaşımla tam tersini yaptık. Üst düzey modülümüz getbook, düşük düzeyli modül olan HTTP'yi içe aktarır.

Bağımlılığı Tersine Çevirmeye Giriş

Bunu nasıl düzeltebileceğimizi düşünelim. Başlamak için internal/pkg/bookserviceclient/client.go adında bir dosya oluşturalım. Bu dosya, harici hizmete ve ilgili arayüze HTTP isteklerinin uygulanmasını içerecektir.

 package bookserviceclient import ( "context" "fmt" "io" "net/http" ) type Client interface { GetBook(ctx context.Context, id string) ([]byte, error) } type client struct { httpClient *http.Client } func NewClient() Client { return &client{ httpClient: http.DefaultClient, } } func (c *client) GetBook(ctx context.Context, id string) ([]byte, error) { var ( url = fmt.Sprintf("http://localhost:8080/book?id=%s", id) ) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return nil, err } resp, err := c.httpClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() b, err := io.ReadAll(resp.Body) if err != nil { return nil, err } return b, nil }


Daha sonra, Bookserviceclient paketindeki arayüzü kullanmaya başlaması için UseCase'imizi güncellememiz gerekiyor.

 package getbook import ( "context" "encoding/json" "example.com/books/internal/pkg/bookserviceclient" ) type UseCase struct { bookClient bookserviceclient.Client } func NewUseCase(bookClient bookserviceclient.Client) *Usecase { return &UseCase{ bookClient: bookClient, } } func (u *UseCase) GetBook(ctx context.Context, id string) (*Book, error) { var ( book Book ) b, err := u.bookClient.GetBook(ctx, id) if err != nil { return nil, err } if err := json.Unmarshal(b, &book); err != nil { return nil, err } book.Price = 10.12 if book.Title == "Pride and Prejudice" { book.Price += 2 } return &book, nil }


Görünüşe göre işler önemli ölçüde iyileşti ve kullanım alanının düşük seviyeli modüle bağımlılık sorununu çözdük. Ancak henüz tam olarak orada değil. Bir adım daha ileri gidelim. Şu anda, bağımlılıkları bildirmek için useсase düşük seviyeli modülden bir arayüz kullanıyor. Bunu geliştirebilir miyiz? İhtiyacımız olan arayüzleri pkg/getbook/types.go dosyasında bildirirsek ne olur?


Bu şekilde, düşük seviyeli modüllere olan açık bağımlılıkları ortadan kaldıracağız. Yani, yüksek seviyeli modülümüz, çalışması için gerekli tüm arayüzleri bildirecek ve böylece düşük seviyeli modüllere olan tüm bağımlılıkları ortadan kaldıracaktır. Uygulamanın en üst seviyesinde ( main.go ), usease'in çalışması için gereken tüm arayüzleri uygulayacağız.


Ayrıca Go'da dışa aktarılan ve aktarılmayan türleri de hatırlayalım. Use-ase arayüzlerini dışa aktarmamız gerekiyor mu? Bu arayüzlere yalnızca bu paketin çalışması için gereken bağımlılıkları belirtmek için ihtiyaç duyulur, bu nedenle bunları dışa aktarmamak daha iyidir.

Son Kod

usecase.go

 package getbook import ( "context" "encoding/json" ) type UseCase struct { bookClient bookClient } func NewUseCase(bookClient bookClient) *UseCase { return &UseCase{ bookClient: bookClient, } } func (u *UseCase) GetBook(ctx context.Context, id string) (*Book, error) { var ( book Book ) b, err := u.bookClient.GetBook(ctx, id) if err != nil { return nil, err } if err := json.Unmarshal(b, &book); err != nil { return nil, err } book.Price = 10.12 if book.Title == "Pride and Prejudice" { book.Price += 2 } return &book, nil }


type.go

 package getbook import "context" type bookClient interface { GetBook(ctx context.Context, id string) ([]byte, error) } type Book struct { ID string `json:"id"` Title string `json:"title"` Author string `json:"author"` Price float64 `json:"price"` }


client.go

 package bookserviceclient import ( "context" "fmt" "io" "net/http" ) type Client struct { httpClient *http.Client } func NewClient() *Client { return &Client{ httpClient: http.DefaultClient, } } func (c *Client) GetBook(ctx context.Context, id string) ([]byte, error) { var ( url = fmt.Sprintf("http://localhost:8080/book?id=%s", id) ) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return nil, err } resp, err := c.httpClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() b, err := io.ReadAll(resp.Body) if err != nil { return nil, err } return b, nil }


ana.go

 package main import ( "log" "net/http" "example.com/books/internal/app/httpbookapi" "example.com/books/internal/pkg/bookserviceclient" "example.com/books/internal/pkg/getbook" ) func main() { bookServiceClient := bookserviceclient.NewClient() useCase := getbook.NewUsecase(bookServiceClient) handler := httpbookapi.NewHandler(useCase) http.Handle("/book", handler) log.Print("server listening at 9090") log.Fatal(http.ListenAndServe(":9090", nil)) }

Özet

Bu makalede, Bağımlılığı tersine çevirme ilkesinin Go'da nasıl uygulanacağını araştırdık. Bu prensibi uygulamak, kodunuzun spagetti haline gelmesini önlemeye yardımcı olabilir ve bakımını ve okunmasını kolaylaştırabilir. Sınıflarınızın bağımlılıklarını ve bunların nasıl doğru şekilde bildirileceğini anlamak, başvurunuzu daha da desteklerken hayatınızı büyük ölçüde kolaylaştırabilir.