0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

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))
    }
}

実践的メリットまとめ

  1. 拡張性の向上

    • 新しいデータソース(ファイル、APIなど)を追加する際、既存のサービスコードを変更せずに実装可能
    type ItemAPIRepository struct{ /* API実装 */ }
    func (r *ItemAPIRepository) FindAll() ([]models.Item, error) {
        // APIからデータ取得
    }
    
  2. テスト容易性

    • 実際のデータベースに依存せず、モック実装でサービス層をテスト可能
    • テスト実行速度の向上
  3. 依存関係の逆転

    サービスが具体的な実装ではなく抽象(インターフェース)に依存

  4. コードの見通し向上

    • 各コンポーネントの責務が明確化
    • ビジネスロジック(サービス層)とデータアクセス層が分離
  5. 段階的なリファクタリング

    • 最初にメモリ実装でプロトタイプ作成
    • 後からデータベース実装に差し替え可能
    • システム進化に伴う変更コスト最小化

効果的な適用パターン

  1. ストレージ層

    • データベース/メモリ/ファイルストレージの切り替え
  2. 外部サービス連携

    • 実際のAPIクライアントとモック実装の切り替え
  3. アルゴリズム切り替え

    • ソートや検索アルゴリズムの差し替え
  4. ロギングシステム

    • コンソール出力/ファイル出力/クラウドロギングの切り替え
type Logger interface {
    Info(msg string)
    Error(msg string)
}

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?