この記事では、依存性逆転の原則について説明します。簡単に言うと、依存性逆転の原則とは何かを説明し、簡単な Go アプリケーションを例にしてこの原則を検証します。
依存性逆転の原則 (DIP) は、オブジェクト指向プログラミング (OOP) の 5 つの SOLID 原則の 1 つであり、Robert C. Martin によって初めて導入されました。この原則では、次のように述べられています。
高レベルモジュールは低レベルモジュールから何もインポートしないでください。両方とも抽象化 (インターフェースなど) に依存する必要があります。
抽象化は詳細に依存すべきではありません。詳細 (具体的な実装) は抽象化に依存すべきです。
これは OOP 設計の世界では非常によく知られた原則ですが、これまで一度も遭遇したことがない場合は、一見して不明瞭に思えるかもしれません。そこで、具体的な例を使用してこの原則を分解してみましょう。
DI 原則を Go で実装するとどうなるか考えてみましょう。まず、ID に基づいて書籍に関する情報を返す、エンドポイント/書籍が 1 つだけの HTTP アプリケーションの簡単な例から始めます。書籍に関する情報を取得するために、アプリケーションは外部の HTTP サービスと対話します。
cmd - Go コマンドのフォルダー。メイン関数はここに配置されます。
internal - 内部アプリケーション コードを含むフォルダー。すべてのコードはここに保存されます。
main.go は単に 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)) }
HTTP エンドポイントを処理するコードは次のとおりです。
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 } }
ご覧のとおり、現在、すべてのコードはハンドラー内に直接配置されています (Book モデルを除く)。ハンドラーでは、HTTP クライアントを作成し、外部サービスにリクエストを送信します。次に、本に価格を割り当てます。ここで、これは最適な設計ではないことはどの開発者にとっても明らかであり、外部サービスを呼び出すためのコードはハンドラーから抽出する必要があると思います。それを実行してみましょう。
最初のステップとして、このコードを別の場所に移動しましょう。これを行うには、書籍を取得して処理するためのロジックを格納するinternal/pkg/getbook/usecase.goというファイルと、必要な getbook タイプを格納するinternal/pkg/getbook/types.goというファイルを作成します。
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 }
types.go コード
package getbook type Book struct { ID string `json:"id"` Title string `json:"title"` Author string `json:"author"` Price float64 `json:"price"` }
新しいハンドラー コード:
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 } }
ご覧のとおり、ハンドラーのコードははるかにきれいになりましたが、今度はgetbook/usecase.goを見てみるのの方がずっと興味深いです。
type UseCase struct { bookServiceClient *http.Client }
UseCase には *http.Client という形式の依存関係がありますが、これは現在まったく初期化されていません。*http.Client を NewUseCase() コンストラクターに渡すか、コンストラクター内で直接 *http.Client を作成することができます。ただし、DI 原則が何を教えてくれるのかをもう一度思い出してみましょう。
高レベルモジュールは低レベルモジュールから何もインポートすべきではありません。両方とも抽象化(例:インターフェース)に依存する必要があります。
ただし、このアプローチでは、まったく逆のことを行っています。高レベル モジュールの getbook は、低レベル モジュールの HTTP をインポートします。
これを修正する方法を考えてみましょう。まず、 internal/pkg/bookserviceclient/client.goというファイルを作成しましょう。このファイルには、外部サービスへの HTTP リクエストの実装と対応するインターフェースが含まれます。
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 }
次に、bookserviceclient パッケージのインターフェースを使い始めるように、UseCase を更新する必要があります。
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 }
状況は大幅に改善されたようで、低レベル モジュールの useсase の依存関係の問題に対処しました。ただし、まだ十分ではありません。さらに一歩進めてみましょう。現在、依存関係を宣言するために、 useсase は低レベル モジュールのインターフェイスを使用しています。これを改善できますか? 必要なインターフェイスをpkg/getbook/types.goで宣言したらどうなるでしょうか?
この方法では、低レベル モジュールへの明示的な依存関係を削除します。つまり、高レベル モジュールは、その動作に必要なすべてのインターフェイスを宣言し、低レベル モジュールへのすべての依存関係を削除します。アプリケーションの最上位レベル ( main.go ) で、ユースケースが機能するために必要なすべてのインターフェイスを実装します。
また、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 }
タイプ.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"` }
クライアント
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 }
メイン.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)) }
この記事では、Go で依存性逆転の原則を実装する方法について説明しました。この原則を実装すると、コードがスパゲッティになるのを防ぎ、保守と読み取りが容易になります。クラスの依存関係を理解し、それを正しく宣言する方法を知っておくと、アプリケーションをさらにサポートするときに作業が大幅に簡素化されます。