Go言語のインターフェースと構造体を効果的に活用すると、拡張性の高いシステム設計が可能になります。
ユースケースに沿ったサンプルコード学習の
備忘録まとめです。
基本構造:インターフェースと実装の分離
// repositories/item_repository.go
package repositories
import "gin-fleamarket/models"
// インターフェース: 商品リポジトリの契約
type IItemRepository interface {
FindAll() ([]models.Item, error)
FindByID(id uint) (*models.Item, error)
}
// 構造体: メモリ実装
type ItemMemoryRepository struct {
items []models.Item
}
// コンストラクタ
func NewItemMemoryRepository(items []models.Item) IItemRepository {
return &ItemMemoryRepository{items: items}
}
// メソッド実装
func (r *ItemMemoryRepository) FindAll() ([]models.Item, error) {
return r.items, nil
}
func (r *ItemMemoryRepository) FindByID(id uint) (*models.Item, error) {
for _, item := range r.items {
if item.ID == id {
return &item, nil
}
}
return nil, errors.New("Item not found")
}
メリット1: 実装の差し替えが容易
データベース実装を追加
// repositories/item_db_repository.go
package repositories
import (
"database/sql"
"gin-fleamarket/models"
)
// 構造体: データベース実装
type ItemDBRepository struct {
db *sql.DB
}
func NewItemDBRepository(db *sql.DB) IItemRepository {
return &ItemDBRepository{db: db}
}
func (r *ItemDBRepository) FindAll() ([]models.Item, error) {
rows, err := r.db.Query("SELECT id, name, price FROM items")
if err != nil {
return nil, err
}
defer rows.Close()
var items []models.Item
for rows.Next() {
var item models.Item
if err := rows.Scan(&item.ID, &item.Name, &item.Price); err != nil {
return nil, err
}
items = append(items, item)
}
return items, nil
}
func (r *ItemDBRepository) FindByID(id uint) (*models.Item, error) {
row := r.db.QueryRow("SELECT id, name, price FROM items WHERE id = ?", id)
var item models.Item
if err := row.Scan(&item.ID, &item.Name, &item.Price); err != nil {
return nil, err
}
return &item, nil
}
メリット2: サービスの実装がインターフェースに依存
// services/item_service.go
package services
import (
"gin-fleamarket/models"
"gin-fleamarket/repositories"
)
type ItemService struct {
repo repositories.IItemRepository // インターフェースに依存
}
func NewItemService(repo repositories.IItemRepository) *ItemService {
return &ItemService{repo: repo}
}
func (s *ItemService) GetAllItems() ([]models.Item, error) {
return s.repo.FindAll()
}
func (s *ItemService) GetItemByID(id uint) (*models.Item, error) {
return s.repo.FindByID(id)
}
メリット3: 実行時に実装を選択可能
// main.go
package main
import (
"database/sql"
"fmt"
"gin-fleamarket/repositories"
"gin-fleamarket/services"
"log"
_ "github.com/go-sql-driver/mysql"
)
func main() {
// 環境に応じて実装を切り替え
var repo repositories.IItemRepository
if config.UseMemoryDB {
// メモリ実装
initialItems := []models.Item{
{ID: 1, Name: "Item A", Price: 1000},
{ID: 2, Name: "Item B", Price: 2000},
}
repo = repositories.NewItemMemoryRepository(initialItems)
} else {
// データベース実装
db, err := sql.Open("mysql", "user:password@/dbname")
if err != nil {
log.Fatal(err)
}
defer db.Close()
repo = repositories.NewItemDBRepository(db)
}
// サービスの初期化(実装の詳細を知らない)
itemService := services.NewItemService(repo)
// 同じインターフェースで操作
items, err := itemService.GetAllItems()
if err != nil {
log.Fatal(err)
}
fmt.Println("Items:", items)
}
メリット4: テストの容易性
// services/item_service_test.go
package services_test
import (
"testing"
"gin-fleamarket/models"
"gin-fleamarket/repositories"
"gin-fleamarket/services"
)
// モックリポジトリ実装
type MockItemRepository struct {
items []models.Item
}
func (m *MockItemRepository) FindAll() ([]models.Item, error) {
return m.items, nil
}
func (m *MockItemRepository) FindByID(id uint) (*models.Item, error) {
for _, item := range m.items {
if item.ID == id {
return &item, nil
}
}
return nil, errors.New("not found")
}
func TestItemService_GetAllItems(t *testing.T) {
// モックデータ準備
mockItems := []models.Item{
{ID: 1, Name: "Test Item 1", Price: 100},
{ID: 2, Name: "Test Item 2", Price: 200},
}
// モックリポジトリ作成
mockRepo := &MockItemRepository{items: mockItems}
// サービス初期化
service := services.NewItemService(mockRepo)
// テスト実行
items, err := service.GetAllItems()
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
if len(items) != 2 {
t.Errorf("Expected 2 items, got %d", len(items))
}
}
実践的メリットまとめ
-
拡張性の向上
- 新しいデータソース(ファイル、APIなど)を追加する際、既存のサービスコードを変更せずに実装可能
type ItemAPIRepository struct{ /* API実装 */ } func (r *ItemAPIRepository) FindAll() ([]models.Item, error) { // APIからデータ取得 }
-
テスト容易性
- 実際のデータベースに依存せず、モック実装でサービス層をテスト可能
- テスト実行速度の向上
-
依存関係の逆転
サービスが具体的な実装ではなく抽象(インターフェース)に依存
-
コードの見通し向上
- 各コンポーネントの責務が明確化
- ビジネスロジック(サービス層)とデータアクセス層が分離
-
段階的なリファクタリング
- 最初にメモリ実装でプロトタイプ作成
- 後からデータベース実装に差し替え可能
- システム進化に伴う変更コスト最小化
効果的な適用パターン
-
ストレージ層
- データベース/メモリ/ファイルストレージの切り替え
-
外部サービス連携
- 実際のAPIクライアントとモック実装の切り替え
-
アルゴリズム切り替え
- ソートや検索アルゴリズムの差し替え
-
ロギングシステム
- コンソール出力/ファイル出力/クラウドロギングの切り替え
type Logger interface {
Info(msg string)
Error(msg string)
}
// 使用例
func ProcessOrder(service PaymentService, logger Logger) {
logger.Info("処理開始")
// ...
}