😝
Clean Architecture:Controller・Presenter・AdapterとPortで組む拡張性アーキテクチャ
はじめに
Clean Architecture ではレイヤを明確に区切り、ドメインロジックを外部依存から切り離すことを重視します。
この記事では、ドメインより外側の要素を Go 実装例と共に解説します:
- Controller / Presenter(Interface Adapter 層)
- Repository 実装や Web フレームワークなどの Infrastructure 層
- Port(Input / Output)
アーキテクチャ全体構成
レイヤ構造図
┌────────────────────────────┐
│ infra/http/echo/router │ ← Web フレームワーク
│ infra/db/mysql │ ← DB 実装 (Repository)
└────────────────────────────┘
↑
┌────────────────────────────┐
│ adapter/controller │ ← Controller
│ adapter/presenter │ ← Presenter
└────────────────────────────┘
↑
┌────────────────────────────┐
│ usecase/inputport │ ← Input Port
│ usecase/interactor │ ← ユースケース実装
│ usecase/outputport │ ← Output Port
└────────────────────────────┘
↑
┌────────────────────────────┐
│ domain/entity │
│ domain/repository │ ← Repository Interface
└────────────────────────────┘
Port設計:ユースケースとの境界定義
InputPort
Controller からユースケースを呼び出すためのインターフェースです。
// usecase/inputport/user_input_port.go
package inputport
// 入力値専用 DTO — ドメイン(entity) には依存しない
type GetUserInput struct {
ID int
}
type UserInputPort interface {
GetUser(in GetUserInput) (*entity.User, error)
}
OutputPort
Presenter が実装し、ユースケースの出力を外部形式へ変換します。
// usecase/outputport/user_output_port.go
package outputport
import "clean/domain/entity"
type UserOutputPort interface {
Output(user *entity.User) error
}
※Presenter は OutputPort の具象として Adapter 層に実装されます。
ユースケース層の実装(Interactor)
ユースケース本体は、ドメインロジックの呼び出しに集中します。
// usecase/interactor/user_interactor.go
package interactor
import (
"clean/domain/entity"
"clean/domain/repository"
"clean/usecase/inputport"
)
type UserInteractor struct {
Repo repository.UserRepository
}
func NewUserInteractor(r repository.UserRepository) *UserInteractor {
return &UserInteractor{Repo: r}
}
func (uc *UserInteractor) GetUser(in inputport.GetUserInput) (*entity.User, error) {
return uc.Repo.FindByID(in.ID)
}
Adapter層の実装
Controller
Controller は Presenter を注入し、InputPort を介してユースケースを呼び出します。
// adapter/controller/user_controller.go
package controller
import (
"net/http"
"strconv"
"clean/usecase/inputport"
"clean/usecase/outputport"
)
type UserController struct {
Input inputport.UserInputPort
Presenter outputport.UserOutputPort
}
func NewUserController(in inputport.UserInputPort, out outputport.UserOutputPort) *UserController {
return &UserController{Input: in, Presenter: out}
}
func (uc *UserController) GetUser(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(r.URL.Query().Get("id"))
if err != nil {
http.Error(w, "invalid id", http.StatusBadRequest)
return
}
user, err := uc.Input.GetUser(inputport.GetUserInput{ID: id})
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
_ = uc.Presenter.Output(user)
}
Presenter
// adapter/presenter/json_presenter.go
package presenter
import (
"encoding/json"
"net/http"
"clean/domain/entity"
)
type JSONPresenter struct{ w http.ResponseWriter }
func NewJSONPresenter(w http.ResponseWriter) *JSONPresenter {
return &JSONPresenter{w: w}
}
func (p *JSONPresenter) Output(user *entity.User) error {
p.w.Header().Set("Content-Type", "application/json")
return json.NewEncoder(p.w).Encode(user)
}
ドメイン層の設計
Entity
// domain/entity/user.go
package entity
type User struct {
ID int
Name string
}
Repository インターフェース
// domain/repository/user_repository.go
package repository
import "clean/domain/entity"
type UserRepository interface {
FindByID(id int) (*entity.User, error)
}
Infrastructure層の実装
Repository 実装
// infra/db/mysql/user_repository.go
package mysql
import "clean/domain/entity"
type UserRepositoryImpl struct{}
func (r *UserRepositoryImpl) FindByID(id int) (*entity.User, error) {
return &entity.User{ID: id, Name: "Haruto"}, nil
}
Router 実装
// infra/http/echo/router.go
package router
import (
"net/http"
"clean/adapter/controller"
"clean/adapter/presenter"
"clean/infra/db/mysql"
"clean/usecase/interactor"
)
func NewHandler() http.Handler {
repo := &mysql.UserRepositoryImpl{}
uc := interactor.NewUserInteractor(repo)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/user" && r.Method == http.MethodGet {
pres := presenter.NewJSONPresenter(w)
ctrl := controller.NewUserController(uc, pres)
ctrl.GetUser(w, r)
return
}
http.NotFound(w, r)
})
}
アプリケーション起動
// cmd/server/main.go
package main
import (
"log"
"net/http"
"clean/infra/http/echo/router"
)
func main() {
h := router.NewHandler()
log.Fatal(http.ListenAndServe(":8080", h))
}
ディレクトリ構成
clean-arch-sample/
├── cmd/
│ └── server/main.go
├── domain/
│ ├── entity/
│ │ └── user.go
│ └── repository/
│ └── user_repository.go
├── usecase/
│ ├── inputport/
│ │ └── user_input_port.go
│ ├── outputport/
│ │ └── user_output_port.go
│ └── interactor/
│ └── user_interactor.go
├── adapter/
│ ├── controller/
│ │ └── user_controller.go
│ └── presenter/
│ └── json_presenter.go
├── infra/
│ ├── db/
│ │ └── mysql/
│ │ └── user_repository.go
│ └── http/
│ └── echo/
│ └── router.go
└── go.mod
この構成が有効なケース
- ドメインロジックが将来複雑化する見込みがある
- Web 以外の UI(CLI, gRPC, バッチ処理など)も視野に入れている
- 技術刷新(DB 変更、HTTP → gRPC など)に柔軟に対応したい
Portを使う設計のメリット
項目 | 説明 |
---|---|
疎結合 | UseCase は Controller や Presenter に依存しない |
フレームワーク非依存 | 技術依存は Infrastructure 層に閉じ込められる |
テスト容易 | 各層がインターフェースで接続されており Mock 可能 |
拡張性 | CLI や gRPC への切替も Router / Controller / Presenter を差替えるだけ |
まとめ
Port を境界としてユースケースを純粋に保ちつつ、Adapter で入力・出力を整形し、技術依存実装は Infrastructure 層に閉じ込める構成は、拡張性と保守性に優れます。
複数 UI や将来の技術変更を視野に入れたプロジェクトほど、このアプローチの恩恵を大きく受けられます。
Discussion