見出し画像

ヘキサゴナルアーキテクチャを組もうとしたけど一周回った話

前回の記事ではヘキサゴナルアーキテクチャについて見ていきました。元々自分が組んでいたクリーンアーキテクチャのディレクトリ構成から変えようと思い自分なりに組んでいたのですが組んでいく内に元々のアーキテクチャを改良するだけで十分ことが済むと思ったという話です。

前回の記事を受けてのディレクトリ構成

まず前回の記事を見てきてコードを書きつつディレクトリ構成を考えていました。ディレクトリだけですが以下のような感じにしました。

.
├── adapters
│   ├── primary
│   │   └── grpc
│   └── secondary
│       ├── logger
│       └── mysql
├── core
│   ├── model
│   ├── ports
│   └── service
└── proto
    └── article
        └── v1

全体としてはcoreそしてadaptersに分けました。コアに関してはモデルのみを定義したmodel、データベースやロガーの種類にかかわらない共通処理のみを書き出したサービス、受け入れ・提供のポートを示したportsで構成しました。
アダブターに関してはアプリケーションを動かすprimaryとアプリケーションによって動かされるsecondaryに明示的に分類しました。
gRPCのprotoファイルに関してはprotoディレクトリ内で管理してコード生成も同じディレクトリ内で行いました。

これだけではイメージをつかむことができないと思うので具体的なコードを見ていきます。ただ全てを実装したわけではないのでファイルのないディレクトリも存在します。また実装してあるところも重複している箇所は省略します。

Proto

まずProtoを紹介してどのような物を作ろうとしていたのか軽くイメージをもってもらいます。

syntax = "proto3";

package article.v1;

import "google/protobuf/empty.proto";

service ArticleService {
  rpc DeleteArticle(DeleteArticleRequest) returns (google.protobuf.Empty);
  rpc GetArticle(GetArticleRequest) returns (GetArticleResponse);
  rpc GetArticles(google.protobuf.Empty) returns (GetArticlesResponse);
  rpc CreateArticle(CreateArticleRequest) returns (CreateArticleResponse);
  rpc UpdateArticle(UpdateArticleRequest) returns (google.protobuf.Empty);
}

message Article {
  string id = 1;
  string title = 2;
  string price = 3;
}

このような感じで記事サービスを想定してCRUDを実装しようとしていました。各リクエストのメッセージ形式に関しては省略しています。(priceは数値だろというツッコミはいらないです)

コア

モデル

// core/model/article.go

type Article struct {
	ID    string
	Title string
	Price string
}

特に解説するところはないです。

ポート

各ポート関数ひとつのみに絞っています。

// core/ports/primary.go

type PrimaryPorts interface {
	Create(model.Article) string
}


// core/ports/secondary.go

type SecondaryPorts interface {
	Create(model.Article) string
}

マイクロサービスを前提として作っておりモデルは一つになるためCreateArticleといった感じで「Article」という文字は入れませんでした。このときPrimaryはgRPCが処理内で使う関数、SecondaryはMySQLなどのデーターベースが実装すべき関数となっています。

サービス

// core/service/service.go

type ArticleService struct {
	repo ports.SecondaryPorts
}

func NewArticleService() *ArticleService {
	return &ArticleService{}
}

func (a *ArticleService) Create(article model.Article) string {
	return a.repo.Create(article)
}

共通処理を切り出しているだけですが現状リポジトリを呼んでいるだけになっています。実装が進んでいくともうすこし書くことが出てくると思います。ここではSecondaryPortsが実装されたものが入ってくることを前提としていて入ってきた物をrepoと名づけています。実装時にはNewArticleServiceの引数としてその構造体を受け付ける必要があります。

アダブター

Primary

// adapters/primary/grpc/handler.go

type GrpcAdapter struct {
	articlev1.UnimplementedArticleServiceServer
	service ports.PrimaryPorts
}

func NewGrpcAdapter(service service.ArticleService) GrpcAdapter {
	return GrpcAdapter{
		service: &service,
	}
}

func (g *GrpcAdapter) CreateArticle(_ context.Context, req *articlev1.CreateArticleRequest) (*articlev1.CreateArticleResponse, error) {
	articleID := g.service.Create(model.Article{
		Title: req.GetTitle(),
		Price: req.GetPrice(),
	})
	return &articlev1.CreateArticleResponse{
		Id: articleID,
	}, nil
}

簡単に言えばgRPCのハンドラー部分を実装しています。最終的にはPrimaryPortsを呼び出して処理を行っています。実装はしていませんがここでバリデーションなども行う必要があります。

Secondary

ここは実装を行っていないです。ただ例として adapters/secondary/mysql に関してはcore/ports/secondary.goのインターフェースの実装を行う形になるはずです。

ここまで実装してみて

コアを実装している時は結構綺麗にポートを書くことができていると感じていたのです結構致命的な点に気づきました。

あまりにgrpcフォルダ内が大きくなり関係ないコードも入ってしまう

ということ。理想としてはgRPCフォルダ内にサーバーの設定からミドルウェア、関数の登録までを行って例えばmain.goとかでStart関数を呼び出したらサーバーが立ち上がるとかだと思います。ただそれにはさまざまな処理が必要です。

以下のリポジトリが自分が目指していた理想に近いです。

ただある程度ミドルウェアを入れたりしようとするとここまで綺麗に収めることは難しいです。ミドルウェアも認証からバリデーション、ロギングと複数必要になってそれもgrpcフォルダ内に設定やロジック記述コミコミで入れる必要があります。

そう考えるとシンプルな構成では難しいです。

gRPCの複雑な部分を切り出したらいいじゃん

先ほどまでgrpcフォルダが大きくなってしまうという課題について書きました。そこで実装している中で複雑になるのであれば切り出せばいいじゃんと思いadapters内に入れるのはsecondaryアダブターのみにしてgRPC関係は外に出すつまりhandler・ミドルウェア・サーバー関係はadaptersでもcoreでもない別のディレクトリに切り出すといったことを思いつきました。

イメージとしては以下のような形です。

.
├── adapters \\ もしかしたら secondaryとかに変えた方が良いかも?
│   ├── logger
│   └── mysql
├── core
│   ├── model
│   ├── ports
│   └── service
├── handler \\ ハンドラー周り
├── infrastructure \\ サーバーやミドルウェアなど
└── proto
    └── article
        └── v1

ただここまできたら自分が元々作っていたクリーンアーキテクチャのディレクトリ構造と似てきてきます。ここまでハンドラーやサーバーなどを切り出した場合アダブターを切り替えやすくなるヘキサゴナルアーキテクチャを使っていくメリットが薄い気もしてきました。

簡単にまとめ

これまでの経緯を簡単にまとめると

  1. ヘキサゴナルアーキテクチャを自分なりに組んでみた

  2. アダブター部分が複雑になってしまい、アダブターと関係ないコードも増えてしまう

  3. 複雑な箇所はアダブターの外に切り出し

  4. クリーンアーキテクチャと変わらないディレクトリ構造となる → 最初からクリーンアーキテクチャで良いのでは?

  5. (現在)今のクリーンアーキテクチャのディレクトリを少し改良しよう

といった感じです。
自分が組んでみた箇所は使わないことになりましたが一方で組んでいる中で学んだこともあるので今のクリーンのディレクトリ構造に活かしていこうと思っています!


この記事が気に入ったらサポートをしてみませんか?