Trong bài viết này, chúng ta sẽ thảo luận về nguyên tắc Đảo ngược phụ thuộc. Tóm lại, chúng ta sẽ nói về nó là gì và xem xét nguyên tắc này bằng cách sử dụng ứng dụng Go đơn giản làm ví dụ.
Nguyên tắc đảo ngược phụ thuộc (DIP) là một trong năm nguyên tắc RẮN của lập trình hướng đối tượng (OOP), được giới thiệu lần đầu tiên bởi Robert C. Martin. Nó nói:
Các mô-đun cấp cao không được nhập bất cứ thứ gì từ các mô-đun cấp thấp. Cả hai nên phụ thuộc vào sự trừu tượng (ví dụ: giao diện).
Sự trừu tượng không nên phụ thuộc vào chi tiết. Chi tiết (triển khai cụ thể) sẽ phụ thuộc vào sự trừu tượng.
Đó là một nguyên tắc rất nổi tiếng trong thế giới thiết kế OOP, nhưng nếu bạn chưa từng gặp nó trước đây thì thoạt nhìn nó có vẻ không rõ ràng, vì vậy hãy chia nhỏ nguyên tắc này bằng một ví dụ cụ thể.
Hãy xem xét cách triển khai nguyên tắc DI trong Go. Chúng ta sẽ bắt đầu với một ví dụ đơn giản về ứng dụng HTTP có một điểm cuối/cuốn sách duy nhất, trả về thông tin về một cuốn sách dựa trên ID của nó. Để lấy thông tin về sách, ứng dụng sẽ tương tác với dịch vụ HTTP bên ngoài.
cmd - thư mục chứa lệnh Go. Chức năng chính sẽ nằm ở đây.
nội bộ - thư mục có mã ứng dụng nội bộ. Tất cả mã của chúng tôi sẽ nằm ở đây.
main.go chỉ cần khởi động máy chủ HTTP.
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)) }
Đây là mã để xử lý điểm cuối HTTP của chúng tôi:
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 } }
Như bạn có thể thấy, hiện tại tất cả mã đều nằm ngay bên trong trình xử lý (không bao gồm mô hình Sách). Trong trình xử lý, chúng tôi tạo một ứng dụng khách HTTP và đưa ra yêu cầu tới một dịch vụ bên ngoài. Sau đó chúng tôi ấn định một số giá cho cuốn sách. Ở đây, tôi tin rằng bất kỳ nhà phát triển nào cũng thấy rõ rằng đây không phải là thiết kế tốt nhất và mã để gọi dịch vụ bên ngoài cần được trích xuất từ trình xử lý. Hãy làm điều đó.
Bước đầu tiên, hãy di chuyển mã này đến một nơi riêng biệt. Để thực hiện việc này, chúng tôi sẽ tạo một tệp có tên là Internal/pkg/getbook/usecase.go , trong đó logic để truy xuất và xử lý sách của chúng tôi sẽ nằm ở đó và Internal/pkg/getbook/types.go , nơi chúng tôi sẽ lưu trữ các loại getbook cần thiết.
mã usecase.go
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 }
mã loại.go
package getbook type Book struct { ID string `json:"id"` Title string `json:"title"` Author string `json:"author"` Price float64 `json:"price"` }
Mã xử lý mới:
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 } }
Như bạn có thể thấy, mã xử lý đã trở nên rõ ràng hơn nhiều, nhưng bây giờ, sẽ thú vị hơn nhiều khi chúng ta xem xét getbook/usecase.go
type UseCase struct { bookServiceClient *http.Client }
UseCase có phần phụ thuộc ở dạng *http.Client mà chúng tôi hiện không khởi tạo theo bất kỳ cách nào. Chúng ta có thể chuyển *http.Client vào hàm tạo NewUseCase() hoặc tạo *http.Client trực tiếp trong hàm tạo. Tuy nhiên, chúng ta hãy nhớ lại một lần nữa nguyên tắc DI cho chúng ta biết điều gì.
Các mô-đun cấp cao không được nhập bất cứ thứ gì từ các mô-đun cấp thấp. Cả hai nên phụ thuộc vào sự trừu tượng (ví dụ: giao diện)
Tuy nhiên, với cách tiếp cận này, chúng tôi đã làm điều ngược lại. Mô-đun cấp cao của chúng tôi, getbook, nhập mô-đun cấp thấp, HTTP.
Hãy suy nghĩ về cách chúng ta có thể khắc phục điều này. Để bắt đầu, hãy tạo một tệp có tên Internal/pkg/bookserviceclient/client.go . Tệp này sẽ chứa việc triển khai các yêu cầu HTTP tới dịch vụ bên ngoài và giao diện tương ứng.
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 }
Tiếp theo, chúng ta cần cập nhật UseCase để nó bắt đầu sử dụng giao diện từ gói bookserviceclient.
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 }
Có vẻ như mọi thứ đã được cải thiện đáng kể và chúng tôi đã giải quyết vấn đề phụ thuộc của useсase trên mô-đun cấp thấp. Tuy nhiên, nó vẫn chưa hoàn toàn ở đó. Hãy tiến thêm một bước nữa. Hiện tại, để khai báo các phần phụ thuộc, useсase đang sử dụng giao diện từ mô-đun cấp thấp. Chúng ta có thể cải thiện điều này không? Điều gì sẽ xảy ra nếu chúng ta khai báo các giao diện mà chúng ta cần trong pkg/getbook/types.go ?
Bằng cách này, chúng tôi sẽ loại bỏ sự phụ thuộc rõ ràng vào các mô-đun cấp thấp. Nghĩa là, mô-đun cấp cao của chúng tôi sẽ khai báo tất cả các giao diện cần thiết cho hoạt động của nó, do đó, loại bỏ tất cả các phụ thuộc vào các mô-đun cấp thấp. Ở cấp cao nhất của ứng dụng ( main.go ), sau đó chúng tôi sẽ triển khai tất cả các giao diện cần thiết để useсase hoạt động.
Ngoài ra, hãy nhớ lại các loại đã xuất và chưa xuất trong Go. Chúng ta có cần xuất giao diện useсase không? Các giao diện này chỉ cần thiết để chỉ định các phần phụ thuộc mà gói này yêu cầu cho hoạt động của nó, vì vậy tốt hơn hết là bạn không nên xuất chúng.
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 }
các loại.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 }
main.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)) }
Trong bài viết này, chúng ta đã khám phá cách triển khai nguyên tắc đảo ngược Phụ thuộc trong Go. Việc thực hiện nguyên tắc này có thể giúp ngăn mã của bạn trở thành spaghetti và giúp duy trì và đọc dễ dàng hơn. Hiểu được sự phụ thuộc của các lớp và cách khai báo chúng một cách chính xác có thể đơn giản hóa đáng kể cuộc sống của bạn khi hỗ trợ thêm cho ứng dụng của bạn.