はじめに
こんにちは、H×Hのセンリツ大好きエンジニアです。
オニオンアーキテクチャをGoで実装してみたので、ご紹介します。
涙なしでは語れない深い理解()を目指しますが、その涙は感動か、それともオニオン(玉ねぎ)のせいかは、読み終わる頃にはわかるはずです。
オニオンアーキテクチャとは?
ソフトウェアアプリケーションの設計パターンの一つで、アプリケーションを複数の層に分けることによって、依存関係を管理しやすくする設計方法です!
メリット
- 強固な依存性の管理
- 高いテスト容易性
- 柔軟なフレームワークとの結合
- ドメイン中心の設計
- 分離された懸念事項
オニオンアーキテクチャの構造
- ドメインモデル層(Entity):ビジネスロジックやビジネスルールを表す
- アプリケーション層(Usecase):アプリケーションのユースケースやビジネスケースを実装する
- ドメインサービス層:複数のドメインモデル間で行われる操作を実装する
- インフラストラクチャ層(Infrastructure):データベースや外部APIとの通信を担う
- インターフェイス層(Interface):ユーザーインターフェイスやAPIエンドポイントを提供する
- DI層(Dependency Injection):依存性の注入を行い、各層間の結合を緩める
実装してみる
記事をDBから取得するAPIをオニオンアーキテクチャで実装してみます。
ディレクトリ構成は以下のようになってます。
.
├── di
│ └── article.go
├── database
│ └── database.go
├── domain
│ ├── model
│ │ └── article.go
│ └── repository
│ └── article.go
├── infrastrcuture
│ └── persistence
│ └── article.go
├── interface
│ └── handler
│ └── article.go
├── main.go
├── router
│ └── router.go
└── usecase
└── article.go
。。。長くて見るのめんどくさいなんて言わないでください(涙)
正直、ここまで分ける必要もないです。(diやrouter)
Model
ここでは、ビジネスエンティティを定義します。
package model
import "time"
type Article struct {
ID string `json:"id" gorm:"primaryKey"`
Title string `json:"title"`
Url string `json:"url"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
PublisherId string `json:"publisher_id"`
PublisherName string `json:"publisher_name"`
PublisherImageURL string `json:"publisher_image_url"`
LikesCount int `json:"likes_count"`
QuoteSource string `json:"quote_source"`
}
Repository
この層では、infrastrucure層とusecase層に依存が発生しないように中間に入ります。
(就活でよく聞く円滑剤です。)
package repository
import "domain/model"
type ArticleRepository interface {
AllArticles() ([]model.Article, error)
}
記事を取得するので、返り値にはArticleのスライスとエラーを設定します。
Infrastructure
DBやAPIの操作を行う層です。今回の例で言うと「記事をDBから一括で取得してくる」です。
DBの操作はGoのORMであるGormを使ってます。
ここではRepository層に依存させて、Repository層で設定した記事取得関数の中身を書いてます。
package persistence
import (
"domain/model"
"domain/repository"
"gorm.io/gorm"
)
type articlePersistence struct {
db *gorm.DB
}
func NewArticlePersistence(db *gorm.DB) repository.ArticleRepository {
return &articlePersistence{db}
}
func (ap *articlePersistence) AllArticles() ([]model.Article, error) {
articles := []model.Article{}
res := ap.db.Find(&articles)
if res.Error != nil {
return []model.Article{}, res.Error
}
return articles, nil
}
Usecase
振る舞いのみを記述する層です。今回の例で言うと「記事を取得する」です。
(どこから取得するかはinfrastructure層で記述してます)
ここでも、Repository層に依存させることで実際の取得方法を記述する前に振る舞いのみ記述することができます。(日本語ムズカシイ。。。)
package usecase
import (
"domain/model"
"domain/repository"
)
type ArticleUsecase interface {
AllArticles() ([]model.Article, error)
}
type articleUsecase struct {
ar repository.ArticleRepository
}
func NewArticleUsecase(ar repository.ArticleRepository) ArticleUsecase {
return &articleUsecase{ar}
}
func (au *articleUsecase) AllArticles() ([]model.Article, error) {
articles, err := au.ar.AllArticles()
if err != nil {
return []model.Article{}, err
}
return articles, nil
}
Interface
APIエンドポイントを提供する層です。ここの内容がAPIのレスポンスになります。
今回はEchoを使用しています。
package handler
import (
"usecase"
"net/http"
"github.com/labstack/echo/v4"
)
type ArticleHandler interface {
AllArticles(ctx echo.Context) error
}
type articleHandler struct {
au usecase.ArticleUsecase
}
func NewArticleHandler(au usecase.ArticleUsecase) ArticleHandler {
return &articleHandler{au}
}
func (ah *articleHandler) AllArticles(ctx echo.Context) error {
resp, err := ah.au.AllArticles()
if err != nil {
return ctx.JSON(http.StatusInternalServerError, err.Error())
}
return ctx.JSON(http.StatusOK, resp)
}
Database
ここは、ただDBに接続するだけなので説明ははしょります!
package database
import (
"fmt"
"log"
"os"
"time"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
var db *gorm.DB
// dbコンテナが立ち上がるまで接続を行う
func RetryConnectDB(dialector gorm.Dialector, opt gorm.Option, count uint) error {
var err error
for count > 1 {
if db, err = gorm.Open(dialector, opt); err != nil {
time.Sleep(time.Second * 2)
count--
fmt.Printf("retry... coutn:%v\n", count)
continue
}
break
}
return err
}
func NewDB() *gorm.DB {
// MySQLに接続
dsn := fmt.Sprintf(`%s:%s@tcp(db:3306)/%s?charset=utf8mb4&parseTime=True&loc=Local`,
os.Getenv("MYSQL_USER"), os.Getenv("MYSQL_PASSWORD"), os.Getenv("MYSQL_DATABASE"))
if err := RetryConnectDB(mysql.Open(dsn), &gorm.Config{}, 100); err != nil {
log.Fatalln(err)
}
fmt.Println("Connected")
return db
}
func CloseDB(db *gorm.DB) {
sqlDB, _ := db.DB()
if err := sqlDB.Close(); err != nil {
log.Fatalln(err)
}
}
DI
依存性の注入を行っていきます!!!!
infrastructure層とusecase層では、Repositoryに依存させているのですが、DI層で実装した中身を渡していきます。
何を言ってるんだこいつは。。。って思ってるんでしょう。
自分も何言ってるんだ俺は。。。状態です。
package di
import (
"infrastrcuture/persistence"
"interface/handler"
"usecase"
"gorm.io/gorm"
)
func Article(db *gorm.DB) handler.ArticleHandler {
ap := persistence.NewArticlePersistence(db)
au := usecase.NewArticleUsecase(ap)
ah := handler.NewArticleHandler(au)
return ah
}
Router
皆さんご存知ルーティングですね!(説明放棄)
今回はEchoを使用しています。
package router
import (
"interface/handler"
"github.com/labstack/echo/v4"
)
func NewRouter(ah handler.ArticleHandler) *echo.Echo {
e := echo.New()
e.GET("/articles", ah.GetAllArticles)
return e
}
Main
これでラストです!
routerとdiを組み合わせることで、実際に動き出します!
(この組み合わせて動く感じ。。。バックエンドって気持ちいいですね!)
package main
import (
"batch"
"database"
"di"
"router"
"fmt"
"time"
)
func main() {
db := database.NewDB()
defer database.CloseDB(db)
e := router.NewRouter(di.Article(db))
e.Logger.Fatal(e.Start(":8080"))
}
実際のレスポンスを見てみましょう!
うおおおおおおおお!
漢泣きしてしまうくらい成功しました。
フロントエンドでこのエンドポイントを叩くことで、記事の一覧を取得できます。
おわりに
オニオンアーキテクチャは、アプリケーションの設計を整理し、メンテナンス性やテスト性を向上させる有効な手法です。
Go言語での実装を通じて、その構造と実装方法について理解を深めることができました(感動)。
最後まで読んでくださりありがとうございました!
実装内容でアドバイス等ありましたら、書いていただけると幸いです。
また次の記事でお会いしましょう!
。。。あれ?完全理解って。。。なんだ。。。?