💬

goのSlackアプリで設計勉強導入

2025/01/29に公開

はじめに

アーキテクチャの勉強がしたかったのでgoでSlackアプリ作ってみた
まず適当に実装してみる→これができなくて困る→修正
この思考の流れを書いて整理していくので備忘録的なものになるかと思います。
個人的には勉強なので間違ってるかな?とか、もっといい書き方ないかな?と思ってもまず進める!
その後修正してみて失敗してこっちの方がいいんだ!と体で覚えていくスタイルで。

参考
https://techbookfest.org/product/9a3U54LBdKDE30ewPS6Ugn?productVariantID=itEzQN5gKZX8gXMmLTEXAB

環境構築

メインじゃないので参考として

環境構築
  • Dockerfile作る
Dockerfile
FROM golang:1.23.5

RUN go install github.com/air-verse/air@v1.61.7

WORKDIR /go/src/app

ENV GO111MODULE=on
COPY go.mod /go/src/app/go.mod
COPY go.sum /go/src/app/go.sum
RUN go mod download
  • docker-compose作る
docker-compose.yml
version: "3.9"

name: chat-bot
services:
  app:
    container_name: chat-bot-app
    env_file:
      - ./app/.env.local
    image: chat-bot-app
    build:
      context: ./app
      dockerfile: Dockerfile
    depends_on:
      mysql:
        condition: service_healthy
    tty: true
    ports:
      - 8080:8080
    volumes:
      - ./app/:/go/src/app
    command: ["air", "-c", "./main/.air.toml"]
    networks:
      - chat-bot-network

  mysql:
    container_name: chat-bot-mysql
    image: mysql:8.0
    ports:
      - 3306:3306
    volumes:
      - ./mysql/data:/var/lib/mysql
      - ./mysql/my.cnf:/etc/mysql/conf.d/my.cnf
      - ./mysql/initdb.d:/docker-entrypoint-initdb.d
    environment:
      TZ: "Asia/Tokyo"
      MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
    healthcheck:
      test: mysqladmin ping -h 127.0.0.1 -uroot -p
    networks:
      - chat-bot-network

networks:
  chat-bot-network:
    external: true
  • air
    sampleほぼそのまま
    変えたところはcmdだけ
.air.toml
# Config file for [Air](https://github.com/air-verse/air) in TOML format

# Working directory
# . or absolute path, please note that the directories following must be under root.
root = "."
tmp_dir = "tmp"

[build]
# Array of commands to run before each build
pre_cmd = ["echo 'hello air' > pre_cmd.txt"]
# Just plain old shell command. You could use `make` as well.
cmd = "go build -o ./tmp/main ./main/main.go"
# Array of commands to run after ^C
post_cmd = ["echo 'hello air' > post_cmd.txt"]
# Binary file yields from `cmd`.
bin = "tmp/main"
# Customize binary, can setup environment variables when run your app.
full_bin = "APP_ENV=dev APP_USER=air ./tmp/main"
# Add additional arguments when running binary (bin/full_bin). Will run './tmp/main hello world'.
args_bin = ["hello", "world"]
# Watch these filename extensions.
include_ext = ["go", "tpl", "tmpl", "html"]
# Ignore these filename extensions or directories.
exclude_dir = ["assets", "tmp", "vendor", "frontend/node_modules"]
# Watch these directories if you specified.
include_dir = []
# Watch these files.
include_file = []
# Exclude files.
exclude_file = []
# Exclude specific regular expressions.
exclude_regex = ["_test\\.go"]
# Exclude unchanged files.
exclude_unchanged = true
# Follow symlink for directories
follow_symlink = true
# This log file is placed in your tmp_dir.
log = "air.log"
# Poll files for changes instead of using fsnotify.
poll = false
# Poll interval (defaults to the minimum interval of 500ms).
poll_interval = 500 # ms
# It's not necessary to trigger build each time file changes if it's too frequent.
delay = 0 # ms
# Stop running old binary when build errors occur.
stop_on_error = true
# Send Interrupt signal before killing process (windows does not support this feature)
send_interrupt = false
# Delay after sending Interrupt signal
kill_delay = 500 # nanosecond
# Rerun binary or not
rerun = false
# Delay after each execution
rerun_delay = 500

[log]
# Show log time
time = false
# Only show main log (silences watcher, build, runner)
main_only = false
# silence all logs produced by air 
silent = false

[color]
# Customize each part's color. If no color found, use the raw app log.
main = "magenta"
watcher = "cyan"
build = "yellow"
runner = "green"

[misc]
# Delete tmp directory on exit
clean_on_exit = true

[screen]
clear_on_rebuild = true
keep_scroll = true

[proxy]
# Enable live-reloading on the browser.
enabled = true
proxy_port = 8090
app_port = 8080

実装

Slackインストール処理実装

  1. まず書いてみる

mainではentrypointの設定とechoサーバの起動をする

main.go
func main() {
	e := echo.New()
	e.Use(middleware.Logger())

	e.GET("/slack/install", slack.Install)

	e.Start(":8080")
}

ハンドラーでSlackインストール処理を作る

slack/handler.go
package slack

import (
	"fmt"
	"log"
	"net/http"

	"github.com/caarlos0/env/v11"
	"github.com/labstack/echo/v4"
	"github.com/slack-go/slack"
)

type InstallRequest struct {
	Code string `query:"code"`
}

type EnvConfig struct {
	Slack Slack
}

type Slack struct {
	ClientID     string `env:"SLACK_CLIENT_ID,notEmpty"`
	ClientSecret string `env:"SLACK_CLIENT_SECRET,notEmpty"`
}

func Install(c echo.Context) error {
	var cfg EnvConfig

	err := env.Parse(&cfg)
	if err != nil {
		log.Fatal(err)
	}

	// クエリパラメータからcodeを取得
	var params InstallRequest
	err = c.Bind(&params)
	if err != nil {
		return err
	}

	// OAuth2認証
	response, err := slack.GetOAuthV2ResponseContext(
		c.Request().Context(),
		http.DefaultClient,
		cfg.Slack.ClientID,
		cfg.Slack.ClientSecret,
		params.Code,
		fmt.Sprintf("https://%s/slack/install", c.Request().Host),
	)
	if err != nil {
		return err
	}

	return c.JSON(http.StatusOK, response)
}
.env.local
ENV=local
SLACK_CLIENT_ID=xxxxx
SLACK_CLIENT_SECRET=xxxxx
  1. apiの処理の中で環境変数の読み込みを書いてるので、api追加の度に同じ処理を書くことになる
    じゃあ共通化したいからレシーバにしてみる

環境変数の読み込みを切り出す

config.go
package config

import (
	"github.com/caarlos0/env/v11"
)

type EnvConfig struct {
	Slack slack
}

type slack struct {
	ClientID     string `env:"SLACK_CLIENT_ID,notEmpty"`
	ClientSecret string `env:"SLACK_CLIENT_SECRET,notEmpty"`
}

func New() (*EnvConfig, error) {
	var cfg EnvConfig

	err := env.Parse(&cfg)
	if err != nil {
		return nil, err
	}

	return &cfg, nil
}

apiから呼び出せるよう環境変数contextを作る

context.go
package context

import (
	"chat-bot/internal/config"
)

type Context struct {
	Config *config.EnvConfig
}

func New(config *config.EnvConfig) *Context {
	return &Context{
		Config: config,
	}
}

Slack用のapiハンドラを切り出してインストール処理をレシーバで実装する

slack/handler.go
package slack

import (
	"chat-bot/internal/context"
	"fmt"
	"net/http"

	"github.com/labstack/echo/v4"
	"github.com/slack-go/slack"
)

type SlackHandler struct {
	ctx *context.Context
}

func NewSlackHandler(ctx *context.Context) *SlackHandler {
	return &SlackHandler{
		ctx,
	}
}

func HandleSlack(e *echo.Group, ctx *context.Context) {
	handler := NewSlackHandler(ctx)
	g := e.Group("/slack")
	g.GET("/install", handler.Install)
}

type InstallRequest struct {
	Code string `query:"code"`
}

func (h *SlackHandler) Install(c echo.Context) error {
	// クエリパラメータからcodeを取得
	var params InstallRequest
	err := c.Bind(&params)
	if err != nil {
		return err
	}

	// OAuth2認証
	response, err := slack.GetOAuthV2ResponseContext(
		c.Request().Context(),
		http.DefaultClient,
		h.ctx.Config.Slack.ClientID,
		h.ctx.Config.Slack.ClientSecret,
		params.Code,
		fmt.Sprintf("https://%s/slack/install", c.Request().Host),
	)
	if err != nil {
		return err
	}

	return c.JSON(http.StatusOK, response)
}
main.go
package main

import (
	"chat-bot/internal/app/slack"
	"chat-bot/internal/config"
	"chat-bot/internal/context"
	"log"

	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
)

func main() {
	config, err := config.New()
	if err != nil {
		log.Fatalln(err)
	}

	ctx := context.New(config)

	e := echo.New()

	e.Use(middleware.Logger())

	g := e.Group("")
	slack.HandleSlack(g, ctx)

	e.Start(":8080")
}
  1. apiハンドラが増えるたびにmain関数が肥大化していくので切り出したい
routers.go
package routers

import (
	"chat-bot/internal/app/slack"
	"chat-bot/internal/context"

	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
)

func New(ctx *context.Context) *echo.Echo {
	e := echo.New()

	e.Use(middleware.Logger())

	g := e.Group("")
	slack.HandleSlack(g, ctx)

	return e
}
main.go
package main

import (
	"chat-bot/internal/app/rest/routers"
	"chat-bot/internal/config"
	"chat-bot/internal/context"
	"log"
)

func main() {
	config, err := config.New()
	if err != nil {
		log.Fatalln(err)
	}

	ctx := context.New(config)

	e := routers.New(ctx)

	e.Start(":8080")
}

accessTokenをDBに保存したい

  1. DBの接続情報を環境変数に追加する

ここに追加していくだけでapi側からも呼び出せるようになるので修正範囲が少なくてよくなってる

config.go
package config

import (
	"github.com/caarlos0/env/v11"
)

type EnvConfig struct {
	Slack slack
	DB    db
}

type slack struct {
	ClientID     string `env:"SLACK_CLIENT_ID,notEmpty"`
	ClientSecret string `env:"SLACK_CLIENT_SECRET,notEmpty"`
}

type db struct {
	User     string `env:"DB_USER_NAME,notEmpty"`
	Password string `env:"DB_USER_PASSWORD,notEmpty"`
	Host     string `env:"DB_HOST,notEmpty"`
	Port     string `env:"DB_PORT,notEmpty"`
	Name     string `env:"DB_NAME,notEmpty"`
}

func New() (*EnvConfig, error) {
	var cfg EnvConfig

	err := env.Parse(&cfg)
	if err != nil {
		return nil, err
	}

	return &cfg, nil
}
  1. DBの接続を作ってcontextに追加する(apiから呼び出せるように)
db.go
package mysql

import (
	"chat-bot/internal/config"
	"fmt"

	"github.com/jmoiron/sqlx"
)

func Open(config *config.EnvConfig) (*sqlx.DB, error) {
	dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?parseTime=True&loc=Local",
		config.DB.User,
		config.DB.Password,
		config.DB.Host,
		config.DB.Port,
		config.DB.Name,
	)

	conn, err := sqlx.Open("mysql", dsn)
	if err != nil {
		return nil, err
	}

	if err := conn.Ping(); err != nil {
		return nil, err
	}

	return conn, nil
}
context.go
package context

import (
	"chat-bot/internal/config"

	"github.com/jmoiron/sqlx"
)

type Context struct {
	Config *config.EnvConfig
	DB     *sqlx.DB
}

func New(conn *sqlx.DB, config *config.EnvConfig) *Context {
	return &Context{
		Config: config,
		DB:     conn,
	}
}
main.go
package main

import (
	"chat-bot/internal/app/rest/routers"
	"chat-bot/internal/config"
	"chat-bot/internal/context"
	"chat-bot/internal/infra/mysql"
	"log"
)

func main() {
	config, err := config.New()
	if err != nil {
		log.Fatalln(err)
	}

	conn, err := mysql.Open(config)
	if err != nil {
		log.Fatalln(err)
	}

	ctx := context.New(conn, config)

	e := routers.New(ctx)

	e.Start(":8080")
}
  1. インストール処理にDB保存処理を追加する
slack/handler.go
package slack

import (
	"chat-bot/internal/context"
	"fmt"
	"net/http"
	"time"

	_ "github.com/go-sql-driver/mysql"
	"github.com/labstack/echo/v4"
	"github.com/slack-go/slack"
)

type SlackHandler struct {
	ctx *context.Context
}

func NewSlackHandler(ctx *context.Context) *SlackHandler {
	return &SlackHandler{
		ctx,
	}
}

func HandleSlack(e *echo.Group, ctx *context.Context) {
	handler := NewSlackHandler(ctx)
	g := e.Group("/slack")
	g.GET("/install", handler.Install)
}

type InstallRequest struct {
	Code string `query:"code"`
}

func (h *SlackHandler) Install(c echo.Context) error {
	// クエリパラメータからcodeを取得
	var params InstallRequest
	err := c.Bind(&params)
	if err != nil {
		return err
	}

	// OAuth2認証
	response, err := slack.GetOAuthV2ResponseContext(
		c.Request().Context(),
		http.DefaultClient,
		h.ctx.Config.Slack.ClientID,
		h.ctx.Config.Slack.ClientSecret,
		params.Code,
		fmt.Sprintf("https://%s/slack/install", c.Request().Host),
	)
	if err != nil {
		return err
	}

	_, err = h.ctx.DB.ExecContext(c.Request().Context(),
		`INSERT INTO slack (team_id, access_token, access_token_expires_at, refresh_token) VALUES (?,?,?,?) ON DUPLICATE KEY UPDATE
		access_token=?, access_token_expires_at=?, refresh_token=?`,
		response.Team.ID, response.AccessToken, time.Now().Unix()+int64(response.ExpiresIn),
		response.RefreshToken, response.AccessToken, time.Now().Unix()+int64(response.ExpiresIn), response.RefreshToken)
	if err != nil {
		return err
	}

	return c.JSON(http.StatusOK, nil)
}

Slackインストール処理のテスト実装

ここまででインストール処理は完成したのでテストを実装したい
まずインストール処理を整理するとパラメータ取得→OAuth認証→DB保存
パラメータは取得するのみで整形したりしないので、テストは不要そう
OAuth認証も外部APIの結果なので不要
じゃあDB保存のところだけテストできればいいかな
ただ現状のインストール処理だとOAuth認証があるせいでテストが書けないので
この部分を切り出してテストの時はモックを利用するようにする

  1. OAuth認証を切り出す

外部接続なのでinfraに切り出す

infra/slack/oauth.go
package slack

import (
	"chat-bot/internal/config"
	"context"
	"net/http"

	"github.com/slack-go/slack"
)

type SlackOAuth interface {
	OAuth(c context.Context, code string, redirectURL string) (*slack.OAuthV2Response, error)
}

type slackOAuth struct {
	config *config.EnvConfig
}

func NewSlackOAuth(config *config.EnvConfig) SlackOAuth {
	return &slackOAuth{
		config,
	}
}

func (s *slackOAuth) OAuth(c context.Context, code string, redirectURL string) (*slack.OAuthV2Response, error) {
	return slack.GetOAuthV2ResponseContext(
		c,
		http.DefaultClient,
		s.config.Slack.ClientID,
		s.config.Slack.ClientSecret,
		code,
		redirectURL,
	)
}
  1. インストール処理も切り出す

usecaseとして切り出す

usecase/slackinstall.go
package usecase

import (
	ctx "chat-bot/internal/context"
	"chat-bot/internal/infra/slack"
	"context"
	"time"
)

type SlackInstall interface {
	Install(c context.Context, code string, redirectURL string) error
}

type slackInstall struct {
	ctx        *ctx.Context
	slackOAuth slack.SlackOAuth
}

func NewSlackInstall(ctx *ctx.Context) SlackInstall {
	return &slackInstall{
		ctx:        ctx,
		slackOAuth: slack.NewSlackOAuth(ctx.Config),
	}
}

func (s *slackInstall) Install(c context.Context, code string, redirectURL string) error {
	response, err := s.slackOAuth.OAuth(c, code, redirectURL)
	if err != nil {
		return err
	}

	_, err = s.ctx.DB.ExecContext(c,
		`INSERT INTO slack (team_id, access_token, access_token_expires_at, refresh_token) VALUES (?,?,?,?) ON DUPLICATE KEY UPDATE
		access_token=?, access_token_expires_at=?, refresh_token=?`,
		response.Team.ID, response.AccessToken, time.Now().Unix()+int64(response.ExpiresIn),
		response.RefreshToken, response.AccessToken, time.Now().Unix()+int64(response.ExpiresIn), response.RefreshToken)
	if err != nil {
		return err
	}

	return nil
}
  1. slackOAuthモック化したテストを書く

localと同じDB接続してるとか環境変数用のcontextの名前がわかりにくくなってるし、context作る処理も冗長なので修正したいが一旦進める

usecase/slackinstall_test.go
package usecase

import (
	"chat-bot/internal/config"
	"chat-bot/internal/infra/mysql"
	"context"
	"testing"

	ctx "chat-bot/internal/context"

	"github.com/slack-go/slack"
)

func Test_slackInstall_Install(t *testing.T) {
	cfg, err := config.New()
	if err != nil {
		panic(err)
	}
	conn, err := mysql.Open(cfg)
	if err != nil {
		panic(err)
	}
	ctx := ctx.New(conn, cfg)
	s := &slackInstall{
		ctx: ctx,
		slackOAuth: &slackOAuthTest{
			config: cfg,
		},
	}

	context := context.Background()
	defer func() {
		defer ctx.DB.Close()
		_, err := ctx.DB.ExecContext(context, "DELETE FROM slack WHERE team_id=?", "1")
		if err != nil {
			panic(err)
		}
	}()

	type args struct {
		code string
	}
	tests := []struct {
		name string
		args args
	}{
		{
			name: "slackインストール",
			args: args{
				code: "",
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := s.Install(context, tt.args.code, "")
			if err != nil {
				t.Errorf("slackInstall.Install() error = %v", err)
			}
		})
	}
}

type slackOAuthTest struct {
	config *config.EnvConfig
}

func (s *slackOAuthTest) OAuth(c context.Context, code string, redirectURL string) (*slack.OAuthV2Response, error) {
	return &slack.OAuthV2Response{
		AccessToken:  "AccessToken_1",
		RefreshToken: "RefreshToken_1",
		ExpiresIn:    5000,
		Team: slack.OAuthV2ResponseTeam{
			ID:   "1",
			Name: "tema_1",
		},
	}, nil
}

ORMを変更

ここでORMをGORMに変更しみて修正箇所がどれぐらいになるのか見てみる

  • mysql/db.go
  • context.go
  • slackinstall.go
  • slackinstall_test.go
    の修正が必要になる
  1. DBの操作をしてるところ、今で言うとusecaseが増えると修正箇所が増えてしまうので修正したい

まずDB操作を切り出す

mysql/repository/slack.go
package repository

import (
	"chat-bot/internal/app/slack/repository"
	"context"

	"github.com/jmoiron/sqlx"
)

type slackRepository struct {
	db *sqlx.DB
}

func NewSlackRepository(db *sqlx.DB) repository.SlackRepository {
	return &slackRepository{
		db: db,
	}
}

func (r *slackRepository) Store(
	ctx context.Context,
	teamID string,
	accessToken string,
	expiresAt int64,
	refreshToken string,
) error {
	_, err := r.db.ExecContext(ctx,
		`INSERT INTO slack (team_id, access_token, access_token_expires_at, refresh_token) VALUES (?,?,?,?) ON DUPLICATE KEY UPDATE
		access_token=?, access_token_expires_at=?, refresh_token=?`,
		teamID, accessToken, expiresAt,
		refreshToken, accessToken, expiresAt, refreshToken)
	if err != nil {
		return err
	}

	return nil
}

それをusecaseから呼び出したいが直接NewSlackRepositoryしてしまうと結局DBの情報をusecaseでもつことになってしまって依存度が高くなってしまうのでinterfaceを使う

slack/repository/slack.go
package repository

import "context"

type SlackRepository interface {
	Store(ctx context.Context, teamID string, accessToken string, expiresAt int64, refreshToken string) error
}

このinterfaceを通してusecaseからDB操作を呼び出す

slack/usecase/slackinstall.go
package usecase

import (
	"chat-bot/internal/app/slack/repository"
	repository2 "chat-bot/internal/infra/slack/repository"
	"context"
	"time"

	_ "github.com/go-sql-driver/mysql"
)

type SlackInstall interface {
	Install(c context.Context, code string, redirectURL string) error
}

type slackInstall struct {
	slackOAuth      repository2.SlackOAuth
	slackRepository repository.SlackRepository
}

func NewSlackInstall(slackOAuthrepository repository2.SlackOAuth, slackRepository repository.SlackRepository) SlackInstall {
	return &slackInstall{
		slackOAuth:      slackOAuthrepository,
		slackRepository: slackRepository,
	}
}

func (s *slackInstall) Install(c context.Context, code string, redirectURL string) error {
	response, err := s.slackOAuth.OAuth(c, code, redirectURL)
	if err != nil {
		return err
	}

	err = s.slackRepository.Store(c, response.Team.ID, response.AccessToken, time.Now().Unix()+int64(response.ExpiresIn), response.RefreshToken)
	if err != nil {
		return err
	}

	return nil
}

その際にusecaseからもNewSlackRepositoryしてしまうと依存度が高くなっちゃうので
routersでnewして→handler→usecaseに渡すようにする

routers.go
package routers

import (
	"chat-bot/internal/app/slack"
	"chat-bot/internal/context"
	"chat-bot/internal/infra/mysql/repository"
	repository2 "chat-bot/internal/infra/slack/repository"

	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
)

func New(ctx *context.Context) *echo.Echo {
	e := echo.New()

	e.Use(middleware.Logger())

	slackRepository := repository.NewSlackRepository(ctx.DB)
	slackRepository2 := repository2.NewSlackOAuth(ctx.Config)

	g := e.Group("")
	slack.HandleSlack(g, slackRepository2, slackRepository)

	return e
}
slack/handler.go
package slack

import (
	"chat-bot/internal/app/slack/repository"
	"chat-bot/internal/app/slack/usecase"
	repository2 "chat-bot/internal/infra/slack/repository"
	"fmt"
	"net/http"

	"github.com/labstack/echo/v4"
)

type SlackHandler struct {
	slackInstall usecase.SlackInstall
}

func NewSlackHandler(slackOAuthRepository repository2.SlackOAuth, slackRepository repository.SlackRepository) *SlackHandler {
	return &SlackHandler{
		slackInstall: usecase.NewSlackInstall(slackOAuthRepository, slackRepository),
	}
}

func HandleSlack(e *echo.Group, slackOAuthRepository repository2.SlackOAuth, slackRepository repository.SlackRepository) {
	handler := NewSlackHandler(slackOAuthRepository, slackRepository)
	g := e.Group("/slack")
	g.GET("/install", handler.Install)
}

type InstallRequest struct {
	Code string `query:"code"`
}

func (h *SlackHandler) Install(c echo.Context) error {
	// クエリパラメータからcodeを取得
	var params InstallRequest
	err := c.Bind(&params)
	if err != nil {
		return err
	}

	h.slackInstall.Install(c.Request().Context(), params.Code, fmt.Sprintf("https://%s/slack/install", c.Request().Host))

	return c.JSON(http.StatusOK, nil)
}
  1. これで処理を切り分けれたのでテストも修正していく

まずusecase slackinstallのテストとしては認証、DB保存の各処理の結果はどうでもよくて
認証が成功していれば、Store処理を呼べることだけ確認すればいいのでモック使ってテストできる
本来はビジネスロジックが入ってくるはずなのでそこを確認するテストになればいい

usecaseslackinstall_test.go
package usecase

import (
	"context"
	"testing"

	"github.com/slack-go/slack"
)

func Test_slackInstall_Install(t *testing.T) {
	repo := NewSlackInstall(&slackOAuthTest{}, &slackRepository{})
	context := context.Background()
	type args struct {
		code string
	}
	tests := []struct {
		name string
		args args
	}{
		{
			name: "slackインストール成功",
			args: args{
				code: "code123456789",
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := repo.Install(context, tt.args.code, "")
			if err != nil {
				t.Errorf("slackInstall.Install() error = %v", err)
			}
		})
	}
}

type slackRepository struct {
}

func (r *slackRepository) Store(
	ctx context.Context,
	teamID string,
	accessToken string,
	expiresAt int64,
	refreshToken string,
) error {
	return nil
}

type slackOAuthTest struct {
}

func (s *slackOAuthTest) OAuth(c context.Context, code string, redirectURL string) (*slack.OAuthV2Response, error) {
	return &slack.OAuthV2Response{
		AccessToken:  "AccessToken_1",
		RefreshToken: "RefreshToken_1",
		ExpiresIn:    5000,
		Team: slack.OAuthV2ResponseTeam{
			ID:   "1",
			Name: "tema_1",
		},
	}, nil
}
  1. ここまでできたのでGORMに変更を考えると修正対象が下記になる
  • mysql/db.go
  • context.go
  • mysql/repository配下
    そんなに変わってないように見えるが切り分けたことによってDB操作の部分だけ修正すればよくなった
    テストも影響あるのでDB操作部分だけになる
    ビジネスロジックとかその他に影響がない(結果を受け取ってるだけなので)からよさそう
package mysql

import (
	"chat-bot/internal/config"
	"fmt"

	"gorm.io/driver/mysql"
	"gorm.io/gorm"
)

func Open(config *config.EnvConfig) (*gorm.DB, error) {
	dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?parseTime=True&loc=Local",
		config.DB.User,
		config.DB.Password,
		config.DB.Host,
		config.DB.Port,
		config.DB.Name,
	)

	conn, err := gorm.Open(mysql.Open(dsn))
	if err != nil {
		return nil, err
	}

	return conn, nil
}
package context

import (
	"chat-bot/internal/config"

	"gorm.io/gorm"
)

type Context struct {
	Config *config.EnvConfig
	DB     *gorm.DB
}

func New(conn *gorm.DB, config *config.EnvConfig) *Context {
	return &Context{
		Config: config,
		DB:     conn,
	}
}
package repository

import (
	"chat-bot/internal/app/slack/repository"
	"context"

	"gorm.io/gorm"
)

type slackRepository struct {
	db *gorm.DB
}

func NewSlackRepository(db *gorm.DB) repository.SlackRepository {
	return &slackRepository{
		db: db,
	}
}

func (r *slackRepository) Store(
	ctx context.Context,
	teamID string,
	accessToken string,
	expiresAt int64,
	refreshToken string,
) error {
	r.db.Exec(
		`INSERT INTO slack (team_id, access_token, access_token_expires_at, refresh_token) VALUES (?,?,?,?) ON DUPLICATE KEY UPDATE
		access_token=?, access_token_expires_at=?, refresh_token=?`,
		teamID, accessToken, expiresAt,
		refreshToken, accessToken, expiresAt, refreshToken)

	return nil
}

おわりに

今回作ったslackアプリだとビジネスロジックもないしわかりにくいところも多かったが

  • 疎結合になることによってテストが書きやすくなった(モックを使えることによってテストの依存度が少なくなった
  • フレームワークやDBなど外部接続が必要な箇所が切り分けられたので、影響範囲が少なくできた
    ここら辺が実感できたのはよかった
    本当はもっと色々改善できる箇所があると思うが、実際に経験して設計って重要だし面白いなと思えたのでもっと勉強していきたいです

Discussion