Goのインターフェースと構造体の実践例


Go言語の**インターフェース**と**構造体**を効果的に活用すると、拡張性の高いシステム設計が可能になります。

ユースケースに沿ったサンプルコード学習の
備忘録まとめです。

### 基本構造：インターフェースと実装の分離
```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: 実装の差し替えが容易
#### データベース実装を追加
```go
// 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: サービスの実装がインターフェースに依存
```go
// 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: 実行時に実装を選択可能
```go
// 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: テストの容易性
```go
// 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))
    }
}
```

### 実践的メリットまとめ

1. **拡張性の向上**
   - 新しいデータソース（ファイル、APIなど）を追加する際、既存のサービスコードを変更せずに実装可能
   ```go
   type ItemAPIRepository struct{ /* API実装 */ }
   func (r *ItemAPIRepository) FindAll() ([]models.Item, error) {
       // APIからデータ取得
   }
   ```

2. **テスト容易性**
   - 実際のデータベースに依存せず、モック実装でサービス層をテスト可能
   - テスト実行速度の向上

3. **依存関係の逆転**
   ```mermaid
   graph TD
       A[Service] -->|依存| B[Interface]
       C[Memory実装] -->|実装| B
       D[DB実装] -->|実装| B
       E[API実装] -->|実装| B
   ```
   サービスが具体的な実装ではなく抽象（インターフェース）に依存

4. **コードの見通し向上**
   - 各コンポーネントの責務が明確化
   - ビジネスロジック（サービス層）とデータアクセス層が分離

5. **段階的なリファクタリング**
   - 最初にメモリ実装でプロトタイプ作成
   - 後からデータベース実装に差し替え可能
   - システム進化に伴う変更コスト最小化

### 効果的な適用パターン

1. **ストレージ層**
   - データベース/メモリ/ファイルストレージの切り替え

2. **外部サービス連携**
   - 実際のAPIクライアントとモック実装の切り替え

3. **アルゴリズム切り替え**
   - ソートや検索アルゴリズムの差し替え

4. **ロギングシステム**
   - コンソール出力/ファイル出力/クラウドロギングの切り替え

```go
type Logger interface {
    Info(msg string)
    Error(msg string)
}

// 使用例
func ProcessOrder(service PaymentService, logger Logger) {
    logger.Info("処理開始")
    // ...
}
```