はじめに
初投稿です。
勉強がてらGoでヘキサゴナルアーキテクチャの考え方を採用したコードを書いてみました。
ヘキサゴナルアーキテクチャの翻訳サイトをはじめ、多くのサイトを参照しましたが、まだ理解が浅く記載内容に間違いがあるかもしれません。もし間違いに気付いたらご指摘いただければ助かります。
ヘキサゴナルアーキテクチャとは
ヘキサゴナルアーキテクチャについての解説はこのサイトに記載されてありますが、
少しわかりづらいかと思うので、以下に重要だと思った特徴を記載します。
- ヘキサゴナルアーキテクチャは内側(アダプターレイヤ)と外側(アプリケーションレイヤ)という2つのレイヤに分離するアーキテクチャ
- 外側のアダプターレイヤにはHTTPやEmail、RDBといった外部のエージェントとのやりとりを実装することで、アプリーケーション(内側)と外部エージェントを分離する。
- アダプターレイヤはアプリケーションレイヤに依存するが、アプリケーションレイヤはアダプタに依存してはいけない
イメージとしては下記の図のような感じになります。
(ヘキサゴナルアーキテクチャーは下記の図のように六角形の図で表現されることが多いです。)
レイヤの責務
前述したとおりヘキサゴナルアーキテクチャは、アダプターレイヤとアプリケーションレイヤの2つのレイヤから成り立っています。
それぞれのレイヤは以下の責務を持ちます。
アダプターレイヤ
- Email、RDBなどの外部エージェントとのやりとりを実装する
- アダプタレイヤはアプリケーションレイヤに依存する
アプリケーションレイヤ
- アプリケーションレイヤはビジネスロジックを実装する
- ただしヘキサゴナルアーキテクチャではビジネスロジックについては定義していないので、アプリケーションレイヤにどのように実装するかは、実装者次第となる
- アプリケーションレイヤはアダプタレイヤに依存してはいけない
実装例
これまでレイヤの役割について解説してきましが、実際にコードを見た方がイメージしやすいかと思いますので、実装例をみていきましょう。
最終的な構成はこんな感じです。
それでは各レイヤごとの実装例を解説していきます。
アダプターレイヤの実装
アダプターレイヤはRDBなどの外部エージェントとのやりとりを実装します。そのため、DB情報を取得・作成するように実装を持ちます。
具体的な実装は以下のような感じです。
後述しますが、アプリケーションレイヤのインターフェイスに注入するための構造体を定義して、その構造体にDB操作をするメソッドを追加します。
package dao
import (
"database/sql"
"hexagonal-architecture-sample/server/application/model"
"log"
_ "github.com/go-sql-driver/mysql"
)
type User struct {
db *sql.DB
}
func ProveideUser(db *sql.DB) *User {
return &User{db: db}
}
//アダプターレイヤはアプリケーションレイヤに依存するため、アプリケーションレイヤのModelを利用しています。
func (u *User) GetByID(id string) (*model.User, error) {
var user model.User
result := u.db.QueryRow("select * from users where id=?", id)
err := result.Scan(&user.ID, &user.FirstName, &user.LastName, &user.Email)
if err != nil {
return nil, err
}
return &user, nil
}
アプリケーションレイヤの実装
アプリケーションレイヤではビジネスロジックを実装する責務を持ちます。
ただしヘキサゴナルアーキテクチャではビジネスロジックについての定義がされていないため、何を実装するかは実装者次第となります。
またヘキサゴナルアーキテクチャではアプリケーションレイヤはアダプターレイヤに依存してはいけません。
そのためアプリケーションレイヤではアダプターレイヤに対応するインターフェイスを定義して、そのインターフェイスにアダプターレイヤの実装を注入することですることで、アダプターレイヤへの依存避けることができます。
具体的な実装は以下のような感じです。
package application
import (
"hexagonal-architecture-sample/server/application/model"
)
//アプリケーションレイヤでは、アダプターに対応するインターフェイスを定義します。
//このインターフェイスにアダプターの実装を注入することで、アダプターレイヤへの依存を避けることができます
type User struct {
Interface repository.User
}
func (u *User) Create(user model.User) error {
return u.Interface.Create(user)
}
func (u *User) GetAll() ([]model.User, error) {
return u.Interface.GetAll()
}
func (u *User) Update(user model.User) error {
return u.Interface.Update(user)
}
func (u *User) GetByID(id string) (*model.User, error) {
return u.Interface.GetByID(id)
}
アプリケーションレイヤのインターフェイスの定義
package repository
import "hexagonal-architecture-sample/server/application/model"
type User interface {
Create(user model.User) error
GetAll() ([]model.User, error)
Update(user model.User) error
GetByID(id string) (*model.User, error)
}
main関数で、アプリケーションレイヤで定義しているインターフェイスにアダプターレイヤの実装を注入させることにより、アプリケーションレイヤがアダプタに依存しないようにします。
package main
import (
"hexagonal-architecture-sample/server/adapter/mysql"
"hexagonal-architecture-sample/server/adapter/mysql/dao"
"hexagonal-architecture-sample/server/adapter/router"
"hexagonal-architecture-sample/server/application"
"log"
"net/http"
)
func main() {
resources := mysql.NewResource()
resources.Initialize()
defer resources.Finalize()
p := &router.Provide{
User: router.User{
//アプリケーションレイヤのインターフェイスに、アダプターレイヤのUser構造体を注入しています。
User: application.User{
Interface: dao.ProveideUser(resources.DB),
},
},
}
err := http.ListenAndServe(":8080", router.NewRouter(resources, *p))
if err != nil {
log.Fatal("ListenAndServe", err)
}
}
最後に
Go言語にはフルスタックのフレームワークが存在しないため、ディレクトリー構成をどうすれば良いかわからなかったので、今回ヘキサゴナルアーキテクチャについて学習してみました。
ヘキサゴナルアーキテクチャは、レイヤ構成をそのままディレクトリーに落とし込むことで、それなりにまとまりのあるディレクトリー構成になるかと思います。
Go言語でディレクトリー構成のベストプラクティスがわからない場合は、アーキテクチャについて学習することが効果的だと感じました。
記事内容としては以上となります。
参考文献
ヘキサゴナルアーキテクチャ
Goでヘキサゴナルアーキテクチャ
pospomeのサーバサイドアーキテクチャ
ドメイン駆動設計で実装を始めるのに一番とっつきやすいアーキテクチャは何か[DDD]