構成要件と名前空間
想定する構成要件と対応付ける名前空間を次のように定めた。
- リクエストをドメインサービスに引き渡すクラスがある -> controllers
- 外側からドメインサービスへの境界はインターフェースを用いる -> inputs
- ドメインサービスは外側に対して入力を定義する ->
- ドメインサービスは永続データのリポジトリを利用できる -> repositories
- 出力層はドメインサービスの外側でデータを受け取ってレスポンス出力する -> presenters
- ドメインサービスから外側への境界はインターフェースを用いる -> outputs
このうち、inputsとoutputsはinterfaceのみの定義となる。またrepositoriesはこれもinterfaceのみの定義であり、実体は対応するインフラ層とのアダプタになる。今回の場合はデータベースでありredisを利用しているため、redis_repositories名前空間に実装した。
命名規則
golangのパッケージ(package)はここでの「名前空間」と対応させている。複数形で表記。
クラス(または、インターフェースまたは構造体)としては単数形のキャメルケースで表記する。例えばcontrollersに属するクラスはControllerとする。
golangのnet/httpの仕様と設計
クリーンアーキテクチャの実装においてgolangが有利と感じる点は、一方通行の処理が書きやすい点にある。golangのnet/httpはRubyのRackサーバやSymphonyなどと異なり、アプリケーション層の戻り値からではなくhttp.ResponseWriterへの書き込みからレスポンスを返却する。従って出力層はhttp.ResponseWriterを注入されていれば一方通行で出力まで完遂できる。
(ただ、エラー時の出力に関しては出力層到達前にエラーになるケースを考えると、戻して処理させる方が楽そうである。)
(参考)golangの命名規則について
言語仕様としてアッパーケースとローワ―ケースでは読み取られ方が異なるため、この使い分けは宣言する内容よりも仕様に従う。
Names are as important in Go as in any other language. They even have semantic effect: the visibility of a name outside a package is determined by whether its first character is upper case. It's therefore worth spending a little time talking about naming conventions in Go programs.
アッパーケースならファイル外で参照可能、ローワ―ケースなら参照不能となる。従って、逆に参照可能にすべきものはアッパーケース、ファイル内にとどめたいものはローワ―ケース、という判断になる。
golangにおける命名規則のガイドラインは Effective Goにある。ただし、ここに表記されている内容以上に慣例としてよしとされているルールもあり、逆にEffective Goのルールに逆らうコードも一般的に存在しているようだ。
例えば名前空間はEffective Goによれば「1単語にすべき」とあるが、実際にはアンダーラインで区切ることが多い。単数が望ましいと考える人もいるが、公式のパッケージにも複数形のパッケージはある。この辺りはチームで開発する場合どうするのか明確に決めておく必要があるだろう。
各クラスの実装
Input
Usecaseが実体となり、外に位置するクラスが呼び出す際のインターフェースのみ実装。 ./inputs/set_input.go
package inputs
import (
"api_stub/dtos"
)
type SetInput interface {
Handle(dtos.SetDto) error
}
Output
Usecaseが外にデータを出力するインターフェースのみ実装。 ./outputs/set_output.go
package outputs
import (
"api_stub/vo"
)
type SetOutput interface {
Success(vo.Id)
}
DTO
DTOはInput(実体はUsecase)に渡される。ここでControllerからの入力を検査してしまうようにした。
package dtos
import (
"api_stub/dtos/validations"
"api_stub/vo"
)
type SetDto interface {
GetId() vo.Id
GetData() string
}
type setDto struct {
id vo.Id
data string
}
func NewDummySetDto() SetDto {
return &setDto{id: vo.NewDummyId(), data: ""}
}
func NewSetDto(params map[string]interface{}) (SetDto, error) {
id, err := validations.GetSureId(params)
if err != nil {
return NewDummySetDto(), err
}
data, err := validations.GetSureData(params)
if err != nil {
return NewDummySetDto(), err
}
return &setDto{id: id, data: data}, nil
}
func (dto *setDto) GetId() vo.Id {
return dto.id
}
func (dto *setDto) GetData() string {
return dto.data
}
Usecase
UsecaseはDTOを受け取り、Repositoryを操作してOutput(実体はPresenter)に必要な値を返す。
package usecases
import (
"api_stub/dtos"
"api_stub/outputs"
"api_stub/repositories"
)
type SetUsecase struct {
repos map[string]interface{}
out outputs.SetOutput
}
func NewSetUsecase(repos map[string]interface{}, out outputs.SetOutput) *SetUsecase {
return &SetUsecase{repos: repos, out: out}
}
func (uc *SetUsecase) Handle(dto dtos.SetDto) error {
id := dto.GetId()
data := dto.GetData()
repo := uc.repos[repositories.Message].(repositories.MessageRepository)
err := repo.Push(id, data)
if err != nil {
return err
}
uc.out.Success(id)
return nil
}
Presenter
PresenterはUsecaseから値を受け取り、インターフェースに合わせた形式で出力する。この場合はhttp.ResponseWriterに出力して終了するが、HTTPレスポンスの出力層にもカスタマイズしうる。
package presenters
import (
"api_stub/vo"
"encoding/json"
"fmt"
"net/http"
)
type setPresenter struct {
w http.ResponseWriter
}
func NewSetPresenter(w http.ResponseWriter) *setPresenter {
return &setPresenter{w: w}
}
func (p *setPresenter) Success(id vo.Id) {
res := map[string]string{"id": id.Tos()}
bytes, _ := json.Marshal(res)
fmt.Fprint(p.w, string(bytes))
}
Repository
Usecaseに提供するインターフェースのみ実装。
package repositories
import (
"api_stub/vo"
)
type MessageRepository interface {
Push(vo.Id, string) error
...
}
RedisRepository
Redis Clientを操作して永続データの読み書きを行う、Repositoryの実体を実装。
package redis_repositories
import (
"api_stub/exceptions"
"api_stub/repositories"
"api_stub/vo"
"context"
"github.com/go-redis/redis/v9"
)
type redisMessageRepository struct {
ctx context.Context
db *redis.Client
}
func NewRedisMessageRepository(ctx context.Context, db *redis.Client) repositories.MessageRepository {
return &redisMessageRepository{ctx: ctx, db: db}
}
func (repo *redisMessageRepository) Push(id vo.Id, s string) error {
err := repo.db.RPush(repo.ctx, id.Tos(), s).Err()
if err != nil {
return exceptions.NewDatabaseException(exceptions.DatabaseExceptionDefault)
}
return nil
}
...
Back to prev page