😝

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