LoginSignup
1
1

More than 1 year has passed since last update.

【Go】Hexagonal Architectureに関するまとめと実装例

Last updated at Posted at 2022-03-21

はじめに

Goの勉強も兼ねて、Hexagonal Architectureについてまとめてみました。
他の方がすでにとても詳しい記事を書かれていたのですが、理解のために自分なりのアウトプットをしてみました。

Hexagonal Architecture

Hexagonal Architectureは、ドメインロジックを中心に置き、ポート(Ports)経由で外界(DB、外部API、メッセージングサービス、コマンドラインインターフェースなど)とつながるようなアーキテクチャのことをいいます。

ポートはPrimary portsとSecondary portsの2種類に区別されます。

  • Primary ports:システムを駆動する(例:API、コマンドラインのようなインターフェース)
  • Secondary ports:システムによって駆動する(例:DB、メッセージングサービス)

ドメインロジックと外界をつなぐためには、ポートだけではなくアダプタ(Adapters)というものも必要となります。
Hexagonal Architectureでは「外界→ドメイン」のように外側から内側への依存関係にするのが定石なのですが、Secondary ports側をそのまま実装しようとすると、「ドメイン→外界」という内側から外側への依存関係になってしまいます。
このような関係を回避するため、ドメインと外界の間にアダプタを置くことで、依存関係を正しい方向に逆転させてあげます(Dependency inversion)。
アダプタを置くことで、ドメイン側はデータソースがDBかスタブかどうかなどを意識することなく、データ操作を行うことができます(Repository pattern)。

また、フレームワークやライブラリについては、ドメインに依存しないようにアダプタ内で独立させるようにします。

image.png
HEXAGONAL ARCHITECTURE

実装

Hexagonal Architectureでbanking-practiceというプロジェクトをつくります。
DBとスタブを用意して、データソースを簡単に切り替えられることを確認します。

登場人物を並べると以下のようになります。

アダプタ

  • REST Handlers(ユーザ側)
  • Mock Adapter(サーバ側)
  • Database Adapter(サーバ側)

ポート

  • Service(Primary ports)
  • Repository(Secondary ports)

また、プロジェクトのディレクトリ構成は以下のようになります。

Repositoryの実装

まずはdomain/customer.goに取得するデータであるCustomerの構造体(Structs)とCustomerのInterfaceを定義します。
今回、スタブとDBからデータを取得するため、Repositoryは2つ必要です。

domain/customer.go
package domain

type Customer struct {
	Id string
	Name string
	City string
	Zipcode string
	DateofBirth string
	Status string
}

type CustomerRepository interface {
	FindAll() ([]Customer, error)
}

スタブ側のRepositoryは以下のようになります。
NewCustomerRepositoryStub()は自分で用意したcustomersを返します。
また、FindAll()メソッドもレシーバのcustomersをそのまま返します。

domain/customerRepositoryStub.go
package domain

type CustomerRepositoryStub struct {
	customers []Customer
}

func (s CustomerRepositoryStub) FindAll() ([]Customer, error) {
	return s.customers, nil
}

func NewCustomerRepositoryStub() CustomerRepositoryStub {
	customers := []Customer {
		{"1001", "Yamada", "Kanagawa", "1110000", "1970-01-01", "1"},
		{"1002", "Akiyama", "Akita", "2220000", "1960-01-01", "2"},
	}
	return CustomerRepositoryStub{customers: customers}
}

DB側のRepositoryは以下のようになります。
NewCustomerRepositoryDb()でDB(今回はMySQL)から取得したデータを返します。
FindAll()メソッドでは、DBから取得したデータからcustomersを返すような実装を行っています。

d.client.Query(findAllSql)でクエリを実行してcustomersテーブルのデータを取得した後に、forループでcustomersにデータを挿入していきます。

domain/customerRepositoryDb.go
package domain

import (
	"database/sql"
	"log"
	"time"

	_ "github.com/go-sql-driver/mysql"
)

type CustomerRepositoryDb struct {
	client *sql.DB
}

func (d CustomerRepositoryDb) FindAll() ([]Customer, error) {
	findAllSql := "select customer_id, name, city, zipcode, date_of_birth, status from customers"

	rows, err := d.client.Query(findAllSql)
	if err != nil {
		log.Println("Error while querying customer table " + err.Error())
		return nil, err
	}

	customers := make([]Customer, 0)
	for rows.Next() {
		var c Customer
		err := rows.Scan(&c.Id, &c.Name, &c.City, &c.Zipcode, &c.DateofBirth, &c.Status)
		if err != nil {
			log.Println("Error while scanning customers " + err.Error())
			return nil, err
		}
		customers = append(customers, c)
	}
	return customers, nil
}

func NewCustomerRepositoryDb() CustomerRepositoryDb {
	client, err := sql.Open("mysql", "root:password@tcp(localhost:3306)/banking_practice")
	if err != nil {
		panic(err)
	}
	client.SetConnMaxLifetime(time.Minute * 3)
	client.SetMaxOpenConns(10)
	client.SetMaxIdleConns(10)
	return CustomerRepositoryDb{client}
}

Serviceの実装

CustomerRepositoryを型にもつフィールドrepoDefaultCustomerServiceの構造体で定義します。
DefaultCustomerServiceを定義することで、Repositoryの実装に関わらず、GetAllCustomerNewCustomerServiceを利用することができます(CustomerRepositoryStubCustomerRepositoryDbのどちらも引数にとることができます)。

service/customerService.go
package service

import (
	"github.com/suzuki0430/banking-practice/domain"
)

type CustomerService interface {
	GetAllCustomer() ([]domain.Customer, error)
}

type DefaultCustomerService struct {
	repo domain.CustomerRepository
}

func (s DefaultCustomerService) GetAllCustomer() ([]domain.Customer, error) {
	return s.repo.FindAll()
}

func NewCustomerService(repository domain.CustomerRepository) DefaultCustomerService {
	return DefaultCustomerService{repo: repository}
}

Handlerの実装

HTTPリクエストに応じた処理(Handler)として、getAllCustomersを実装します。
リクエストヘッダーのContent-Typeによって、出力形式もエンコードされるようにします。

customerHandler.go
package app

import (
	"encoding/json"
	"encoding/xml"
	"net/http"

	"github.com/gorilla/mux"
	"github.com/suzuki0430/banking-practice/service"
)

type CustomerHandlers struct {
	service service.CustomerService
}

func (ch *CustomerHandlers) getAllCustomers(w http.ResponseWriter, r *http.Request) {
	customers, _ := ch.service.GetAllCustomer()

	if r.Header.Get("Content-Type") == "application/xml" {
		w.Header().Add("Content-Type", "application/xml")
		xml.NewEncoder(w).Encode(customers)
	} else {
		w.Header().Add("Content-Type", "application/json")
		json.NewEncoder(w).Encode(customers)
	}	
}

マルチプレクサ(どのURLにどんな処理をさせるかを設定できるもの)の実装にはgorilla/muxを利用し、ルートをrouter.HandleFunc("/customers", ch.getAllCustomers).Methods(http.MethodGet)のように定義しています。
CustomerHandlerの中のRepositoryを切り替えることで、接続先をスタブかDBに設定することができます。

app.go
package app

import (
	"log"
	"net/http"

	"github.com/gorilla/mux"
	"github.com/suzuki0430/banking-practice/domain"
	"github.com/suzuki0430/banking-practice/service"
)

func Start() {
	
	router := mux.NewRouter()

	// wiring Stub
	// ch := CustomerHandlers{service: service.NewCustomerService(domain.NewCustomerRepositoryStub())}
	// wiring Db
	ch := CustomerHandlers{service: service.NewCustomerService(domain.NewCustomerRepositoryDb())}

	// define routes
	router.HandleFunc("/customers", ch.getAllCustomers).Methods(http.MethodGet)

	// starting server
	log.Fatal(http.ListenAndServe("localhost:8000", router))
}

go run main.goでローカルサーバをたてて、localhost:8000/customersにアクセスすることでデータを取得することができます。

参考資料

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1