LoginSignup
0
1

Goを用いてオニオンアーキテクチャを完全理解()したい。。。

Posted at

はじめに

こんにちは、H×Hのセンリツ大好きエンジニアです。
オニオンアーキテクチャをGoで実装してみたので、ご紹介します。
涙なしでは語れない深い理解()を目指しますが、その涙は感動か、それともオニオン(玉ねぎ)のせいかは、読み終わる頃にはわかるはずです。

オニオンアーキテクチャとは?

ソフトウェアアプリケーションの設計パターンの一つで、アプリケーションを複数の層に分けることによって、依存関係を管理しやすくする設計方法です!

メリット

  1. 強固な依存性の管理
  2. 高いテスト容易性
  3. 柔軟なフレームワークとの結合
  4. ドメイン中心の設計
  5. 分離された懸念事項

オニオンアーキテクチャの構造

  1. ドメインモデル層(Entity):ビジネスロジックやビジネスルールを表す
  2. アプリケーション層(Usecase):アプリケーションのユースケースやビジネスケースを実装する
  3. ドメインサービス層:複数のドメインモデル間で行われる操作を実装する
  4. インフラストラクチャ層(Infrastructure):データベースや外部APIとの通信を担う
  5. インターフェイス層(Interface):ユーザーインターフェイスやAPIエンドポイントを提供する
  6. 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

ここでは、ビジネスエンティティを定義します。

domain/model/article.go
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層に依存が発生しないように中間に入ります。
(就活でよく聞く円滑剤です。)

domain/repository/article.go
package repository

import "domain/model"

type ArticleRepository interface {
	AllArticles() ([]model.Article, error)
}

記事を取得するので、返り値にはArticleのスライスとエラーを設定します。

Infrastructure

DBやAPIの操作を行う層です。今回の例で言うと「記事をDBから一括で取得してくる」です。

DBの操作はGoのORMであるGormを使ってます。
ここではRepository層に依存させて、Repository層で設定した記事取得関数の中身を書いてます。

infrastructure/persistence/article.go
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層に依存させることで実際の取得方法を記述する前に振る舞いのみ記述することができます。(日本語ムズカシイ。。。)

usecase/article.go
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を使用しています。

interface/handler/article.go
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に接続するだけなので説明ははしょります!

database/database.go
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層で実装した中身を渡していきます。

何を言ってるんだこいつは。。。って思ってるんでしょう。
自分も何言ってるんだ俺は。。。状態です。

di/article.go
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を使用しています。

router/article.go
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を組み合わせることで、実際に動き出します!
(この組み合わせて動く感じ。。。バックエンドって気持ちいいですね!)

main.go
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"))
}

実際のレスポンスを見てみましょう!

スクリーンショット 2024-02-17 18.45.06.png

うおおおおおおおお!
漢泣きしてしまうくらい成功しました。
フロントエンドでこのエンドポイントを叩くことで、記事の一覧を取得できます。

おわりに

オニオンアーキテクチャは、アプリケーションの設計を整理し、メンテナンス性やテスト性を向上させる有効な手法です。
Go言語での実装を通じて、その構造と実装方法について理解を深めることができました(感動)。

最後まで読んでくださりありがとうございました!
実装内容でアドバイス等ありましたら、書いていただけると幸いです。

また次の記事でお会いしましょう!

。。。あれ?完全理解って。。。なんだ。。。?

0
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
0
1