コンテンツにスキップ

Go/メソッドチェイン

出典: フリー教科書『ウィキブックス(Wikibooks)』
< Go


メソッドチェイン

[編集]

メソッドチェインとは

[編集]

メソッドチェイン(Method Chaining)は、複数のメソッド呼び出しを連続して繋げる設計パターンです。各メソッドが同じ型のレシーバーを返すことで、ドット記法を使って連続的にメソッドを呼び出すことができます。

result := object.Method1().Method2().Method3()

この記法により、コードがより流暢(fluent)で読みやすくなり、一時的な変数の使用を減らすことができます。

メソッドチェインの基本構造

[編集]

レシーバーを返すメソッド

[編集]

メソッドチェインの核心は、各メソッドがメソッドを持った型(典型的には自分自身)を返すことです。

type StringBuilder struct {
    buffer []byte
}

func (sb *StringBuilder) Append(s string) *StringBuilder {
    sb.buffer = append(sb.buffer, []byte(s)...)
    return sb // 自分自身を返す
}

func (sb *StringBuilder) AppendLine(s string) *StringBuilder {
    sb.buffer = append(sb.buffer, []byte(s+"\n")...)
    return sb
}

func (sb *StringBuilder) String() string {
    return string(sb.buffer)
}

使用例

[編集]
func main() {
    sb := &StringBuilder{}
    
    result := sb.Append("Hello, ").
                Append("World!").
                AppendLine("").
                Append("Go is awesome!").
                String()
                
    fmt.Println(result)
    // 出力:
    // Hello, World!
    // Go is awesome!
}

実践的な例:HTTPクライアントビルダー

[編集]

より実践的な例として、HTTPリクエストを構築するビルダーパターンを実装してみましょう。

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "time"
)

type HTTPClient struct {
    url     string
    method  string
    headers map[string]string
    body    []byte
    timeout time.Duration
}

func NewHTTPClient() *HTTPClient {
    return &HTTPClient{
        method:  "GET",
        headers: make(map[string]string),
        timeout: 30 * time.Second,
    }
}

func (c *HTTPClient) URL(url string) *HTTPClient {
    c.url = url
    return c
}

func (c *HTTPClient) Method(method string) *HTTPClient {
    c.method = method
    return c
}

func (c *HTTPClient) Header(key, value string) *HTTPClient {
    c.headers[key] = value
    return c
}

func (c *HTTPClient) JSON(data interface{}) *HTTPClient {
    body, err := json.Marshal(data)
    if err != nil {
        // エラーハンドリングは後述
        return c
    }
    c.body = body
    c.Header("Content-Type", "application/json")
    return c
}

func (c *HTTPClient) Timeout(d time.Duration) *HTTPClient {
    c.timeout = d
    return c
}

func (c *HTTPClient) Send() (*http.Response, error) {
    client := &http.Client{Timeout: c.timeout}
    
    var bodyReader io.Reader
    if c.body != nil {
        bodyReader = bytes.NewReader(c.body)
    }
    
    req, err := http.NewRequest(c.method, c.url, bodyReader)
    if err != nil {
        return nil, err
    }
    
    for key, value := range c.headers {
        req.Header.Set(key, value)
    }
    
    return client.Do(req)
}

使用例

[編集]
func main() {
    // GETリクエスト
    resp1, err := NewHTTPClient().
        URL("https://api.example.com/users").
        Header("Authorization", "Bearer token123").
        Timeout(10 * time.Second).
        Send()
    
    // POSTリクエスト
    userData := map[string]string{
        "name":  "John Doe",
        "email": "john@example.com",
    }
    
    resp2, err := NewHTTPClient().
        URL("https://api.example.com/users").
        Method("POST").
        JSON(userData).
        Header("Authorization", "Bearer token123").
        Send()
    
    if err != nil {
        fmt.Printf("Error: %v\n", err)
        return
    }
    
    defer resp1.Body.Close()
    defer resp2.Body.Close()
}

エラーハンドリング

[編集]

メソッドチェインでのエラーハンドリングは特別な考慮が必要です。

エラーを蓄積する方式

[編集]
type QueryBuilder struct {
    query string
    args  []interface{}
    err   error
}

func NewQuery() *QueryBuilder {
    return &QueryBuilder{}
}

func (q *QueryBuilder) Select(fields string) *QueryBuilder {
    if q.err != nil {
        return q
    }
    q.query += "SELECT " + fields
    return q
}

func (q *QueryBuilder) From(table string) *QueryBuilder {
    if q.err != nil {
        return q
    }
    if q.query == "" {
        q.err = fmt.Errorf("SELECT must be called before FROM")
        return q
    }
    q.query += " FROM " + table
    return q
}

func (q *QueryBuilder) Where(condition string, args ...interface{}) *QueryBuilder {
    if q.err != nil {
        return q
    }
    q.query += " WHERE " + condition
    q.args = append(q.args, args...)
    return q
}

func (q *QueryBuilder) Build() (string, []interface{}, error) {
    return q.query, q.args, q.err
}

使用例

[編集]
func main() {
    query, args, err := NewQuery().
        Select("name, email").
        From("users").
        Where("age > ?", 18).
        Build()
    
    if err != nil {
        fmt.Printf("Query build error: %v\n", err)
        return
    }
    
    fmt.Printf("Query: %s\n", query)
    fmt.Printf("Args: %v\n", args)
}

ファンクショナル・オプションパターンとの組み合わせ

[編集]

メソッドチェインはファンクショナル・オプションパターンと組み合わせて使うことができます。

type Server struct {
    host    string
    port    int
    timeout time.Duration
    logger  interface{}
}

type ServerOption func(*Server)

func WithHost(host string) ServerOption {
    return func(s *Server) {
        s.host = host
    }
}

func WithPort(port int) ServerOption {
    return func(s *Server) {
        s.port = port
    }
}

func WithTimeout(timeout time.Duration) ServerOption {
    return func(s *Server) {
        s.timeout = timeout
    }
}

type ServerBuilder struct {
    server *Server
}

func NewServerBuilder() *ServerBuilder {
    return &ServerBuilder{
        server: &Server{
            host:    "localhost",
            port:    8080,
            timeout: 30 * time.Second,
        },
    }
}

func (sb *ServerBuilder) With(opts ...ServerOption) *ServerBuilder {
    for _, opt := range opts {
        opt(sb.server)
    }
    return sb
}

func (sb *ServerBuilder) Host(host string) *ServerBuilder {
    sb.server.host = host
    return sb
}

func (sb *ServerBuilder) Port(port int) *ServerBuilder {
    sb.server.port = port
    return sb
}

func (sb *ServerBuilder) Build() *Server {
    return sb.server
}

使用例

[編集]
func main() {
    // メソッドチェインスタイル
    server1 := NewServerBuilder().
        Host("0.0.0.0").
        Port(9000).
        Build()
    
    // ファンクショナル・オプションとの組み合わせ
    server2 := NewServerBuilder().
        With(
            WithHost("127.0.0.1"),
            WithPort(8080),
            WithTimeout(60*time.Second),
        ).
        Build()
    
    fmt.Printf("Server1: %+v\n", server1)
    fmt.Printf("Server2: %+v\n", server2)
}

メソッドチェインのベストプラクティス

[編集]

設計原則

[編集]
  1. 不変性の維持: 可能な限り、メソッドチェインでは元のオブジェクトを変更せず、新しいインスタンスを返すことを検討する
  2. メソッド名の一貫性: 動詞を使った明確で一貫性のあるメソッド名を使用する
  3. エラーハンドリング: エラーが発生する可能性がある場合は、適切なエラーハンドリング戦略を実装する

不変オブジェクトの例

[編集]
type ImmutableString struct {
    value string
}

func NewImmutableString(s string) *ImmutableString {
    return &ImmutableString{value: s}
}

func (is *ImmutableString) Append(s string) *ImmutableString {
    return &ImmutableString{value: is.value + s}
}

func (is *ImmutableString) Prepend(s string) *ImmutableString {
    return &ImmutableString{value: s + is.value}
}

func (is *ImmutableString) ToUpper() *ImmutableString {
    return &ImmutableString{value: strings.ToUpper(is.value)}
}

func (is *ImmutableString) String() string {
    return is.value
}

使用例

[編集]
func main() {
    result := NewImmutableString("hello").
        Append(" ").
        Append("world").
        ToUpper().
        String()
    
    fmt.Println(result) // "HELLO WORLD"
}

パフォーマンスの考慮事項

[編集]

メモリアロケーション

[編集]

メソッドチェインでは、各メソッド呼び出しで新しいオブジェクトを作成する場合、メモリアロケーションが頻繁に発生する可能性があります。

// 効率が悪い例(毎回新しいsliceを作成)
type inefficientSlice struct {
    data []int
}

func (s *inefficientSlice) Add(n int) *inefficientSlice {
    newData := make([]int, len(s.data)+1)
    copy(newData, s.data)
    newData[len(s.data)] = n
    return &inefficientSlice{data: newData}
}

// 効率的な例(既存のsliceを拡張)
type efficientSlice struct {
    data []int
}

func (s *efficientSlice) Add(n int) *efficientSlice {
    s.data = append(s.data, n)
    return s
}

ベンチマーク例

[編集]
func BenchmarkInefficient(b *testing.B) {
    for i := 0; i < b.N; i++ {
        s := &inefficientSlice{}
        for j := 0; j < 100; j++ {
            s = s.Add(j)
        }
    }
}

func BenchmarkEfficient(b *testing.B) {
    for i := 0; i < b.N; i++ {
        s := &efficientSlice{}
        for j := 0; j < 100; j++ {
            s = s.Add(j)
        }
    }
}

実際のライブラリでの使用例

[編集]

データベースクエリビルダー

[編集]
type User struct {
    ID    int    `db:"id"`
    Name  string `db:"name"`
    Email string `db:"email"`
    Age   int    `db:"age"`
}

type UserQuery struct {
    conditions []string
    args       []interface{}
    limit      int
    offset     int
    orderBy    string
}

func NewUserQuery() *UserQuery {
    return &UserQuery{}
}

func (q *UserQuery) WhereNameLike(pattern string) *UserQuery {
    q.conditions = append(q.conditions, "name LIKE ?")
    q.args = append(q.args, "%"+pattern+"%")
    return q
}

func (q *UserQuery) WhereAgeGreaterThan(age int) *UserQuery {
    q.conditions = append(q.conditions, "age > ?")
    q.args = append(q.args, age)
    return q
}

func (q *UserQuery) OrderBy(field string) *UserQuery {
    q.orderBy = field
    return q
}

func (q *UserQuery) Limit(n int) *UserQuery {
    q.limit = n
    return q
}

func (q *UserQuery) Offset(n int) *UserQuery {
    q.offset = n
    return q
}

func (q *UserQuery) BuildSQL() (string, []interface{}) {
    sql := "SELECT id, name, email, age FROM users"
    
    if len(q.conditions) > 0 {
        sql += " WHERE " + strings.Join(q.conditions, " AND ")
    }
    
    if q.orderBy != "" {
        sql += " ORDER BY " + q.orderBy
    }
    
    if q.limit > 0 {
        sql += fmt.Sprintf(" LIMIT %d", q.limit)
    }
    
    if q.offset > 0 {
        sql += fmt.Sprintf(" OFFSET %d", q.offset)
    }
    
    return sql, q.args
}

使用例

[編集]
func main() {
    sql, args := NewUserQuery().
        WhereNameLike("John").
        WhereAgeGreaterThan(18).
        OrderBy("name ASC").
        Limit(10).
        Offset(0).
        BuildSQL()
    
    fmt.Printf("SQL: %s\n", sql)
    fmt.Printf("Args: %v\n", args)
    
    // 出力:
    // SQL: SELECT id, name, email, age FROM users WHERE name LIKE ? AND age > ? ORDER BY name ASC LIMIT 10 OFFSET 0
    // Args: [%John% 18]
}

まとめ

[編集]

メソッドチェインは、Goにおいて流暢で読みやすいAPIを作成するための強力なパターンです。主な利点として:

  • 可読性の向上: コードが自然言語に近い形で読める
  • 一時変数の削減: 中間結果を保存する変数が不要
  • APIの一貫性: 統一されたインターフェースを提供

ただし、以下の点に注意が必要です:

  • エラーハンドリング: 適切なエラー処理戦略を実装する
  • パフォーマンス: 不要なメモリアロケーションを避ける
  • デバッグの困難さ: 長いチェインはデバッグが困難になる場合がある

メソッドチェインを効果的に使用することで、より保守性が高く、使いやすいGoライブラリやアプリケーションを構築することができます。

演習問題

[編集]

問題1: 基本的なメソッドチェイン

[編集]

文字列操作を行う TextProcessor 構造体を作成し、以下のメソッドを実装してください:

  • ToUpper(): 文字列を大文字に変換
  • ToLower(): 文字列を小文字に変換
  • Trim(): 前後の空白を削除
  • Replace(old, new string): 文字列を置換
  • String(): 最終的な文字列を返す

問題2: エラーハンドリング付きチェイン

[編集]

数値計算を行う Calculator 構造体を作成し、エラーハンドリングを含むメソッドチェインを実装してください。ゼロ除算やオーバーフローをチェックする機能を含めてください。

問題3: 設定ビルダー

[編集]

アプリケーション設定を構築する ConfigBuilder を実装し、様々な設定オプションをメソッドチェインで設定できるようにしてください。バリデーション機能も含めてください。

遅延評価を使用するシーケンス

[編集]
lazy.go
package main

import (
	"fmt"
	"iter"
	"strings"
)

// Seq は遅延評価を使用するシーケンスを表す型
type Seq[T any] iter.Seq[T]

// 新しいSeqを作成する関数
func NewSeq[T any](elements ...T) Seq[T] {
	return func(yield func(T) bool) {
		for _, el := range elements {
			if !yield(el) {
				break
			}
		}
	}
}

// Map はSeqの各要素に対して関数を適用
func (seq Seq[T]) Map(fn func(T) T) Seq[T] {
	return func(yield func(T) bool) {
		seq(func(v T) bool {
			return yield(fn(v))
		})
	}
}

// Filter はSeqの各要素を条件でフィルタリング
func (seq Seq[T]) Filter(fn func(T) bool) Seq[T] {
	return func(yield func(T) bool) {
		seq(func(v T) bool {
			if fn(v) {
				return yield(v)
			}
			return true
		})
	}
}

// Reduce はSeqの各要素を累積的に処理
func (seq Seq[T]) Reduce(fn func(T, T) T, initial T) T {
	accumulator := initial
	seq(func(v T) bool {
		accumulator = fn(accumulator, v)
		return true
	})
	return accumulator
}

// Drop はSeqの先頭からn個の要素を削除
func (seq Seq[T]) Drop(n int) Seq[T] {
	return func(yield func(T) bool) {
		count := 0
		seq(func(v T) bool {
			if count < n {
				count++
				return true
			}
			return yield(v)
		})
	}
}

// Take はSeqの先頭からn個の要素を取得
func (seq Seq[T]) Take(n int) Seq[T] {
	return func(yield func(T) bool) {
		count := 0
		seq(func(v T) bool {
			if count < n {
				count++
				return yield(v)
			}
			return false
		})
	}
}

// Concat は二つのSeqを結合
func (seq1 Seq[T]) Concat(seq2 Seq[T]) Seq[T] {
	return func(yield func(T) bool) {
		seq1(yield)
		seq2(yield)
	}
}

// ForEach はSeqの各要素に対して指定された関数を実行
func (seq Seq[T]) ForEach(fn func(T)) {
	seq(func(v T) bool {
		fn(v)
		return true
	})
}

// ToSlice はSeqをスライスに変換(JSのtoArray()相当)
func ToSlice[T any](seq Seq[T]) []T {
	var result []T
	seq(func(v T) bool {
		result = append(result, v)
		return true
	})
	return result
}

// ToArray はSeqをスライスに変換(JSのtoArray()と同等)
func (seq Seq[T]) ToArray() []T {
	return ToSlice(seq)
}

// Every はすべての要素が条件を満たすか確認(JSのevery()相当)
func (seq Seq[T]) Every(fn func(T) bool) bool {
	result := true
	seq(func(v T) bool {
		if !fn(v) {
			result = false
			return false // 一つでも条件を満たさない要素があれば中断
		}
		return true
	})
	return result
}

// Some は少なくとも一つの要素が条件を満たすか確認(JSのsome()相当)
func (seq Seq[T]) Some(fn func(T) bool) bool {
	result := false
	seq(func(v T) bool {
		if fn(v) {
			result = true
			return false // 一つでも条件を満たす要素があれば中断
		}
		return true
	})
	return result
}

// Find は条件を満たす最初の要素を見つける(JSのfind()相当)
func (seq Seq[T]) Find(fn func(T) bool) (T, bool) {
	var found T
	foundAny := false

	seq(func(v T) bool {
		if fn(v) {
			found = v
			foundAny = true
			return false // 条件を満たす要素を見つけたら中断
		}
		return true
	})

	return found, foundAny
}

// FlatMap は各要素にマッピング関数を適用し、結果をフラット化(JSのflatMap()相当)
func (seq Seq[T]) FlatMap(fn func(T) Seq[T]) Seq[T] {
	return func(yield func(T) bool) {
		seq(func(v T) bool {
			innerSeq := fn(v)
			continueOuter := true

			innerSeq(func(innerV T) bool {
				if !yield(innerV) {
					continueOuter = false
					return false
				}
				return true
			})

			return continueOuter
		})
	}
}

// Stringerインターフェースを実装
func (seq Seq[T]) String() string {
	slice := []string{}
	seq(func(v T) bool {
		slice = append(slice, fmt.Sprintf("%v", any(v)))
		return true
	})
	return fmt.Sprintf("{%s}", strings.Join(slice, ", "))
}

// GoStringerインターフェースを実装
func (seq Seq[T]) GoString() string {
	slice := []string{}
	seq(func(v T) bool {
		slice = append(slice, fmt.Sprintf("%v", any(v)))
		return true
	})
	return fmt.Sprintf("NewSeq(%s)", strings.Join(slice, ", "))
}

func main() {
	// Seqを作成
	seq1 := NewSeq(1, 2, 3)
	seq2 := NewSeq(4, 5)

	// Seqを結合
	seq3 := seq1.Concat(seq2)

	// 結果をToSliceで表示
	fmt.Printf("Concatenated Seq: %s(%#v)\n", seq3, seq3) // {1, 2, 3, 4, 5}(NewSeq(1, 2, 3, 4, 5))

	// Mapを使って各要素を2倍にする
	doubledSeq := seq3.Map(func(x int) int {
		return x * 2
	})
	fmt.Println("Doubled Seq:", doubledSeq) // {2, 4, 6, 8, 10}

	// Filterを使って偶数を抽出
	evenSeq := seq3.Filter(func(x int) bool {
		return x%2 == 0
	})
	fmt.Println("Even Seq:", evenSeq) // {2, 4}

	// ForEachを使って各要素を表示
	seq3.ForEach(func(x int) {
		fmt.Println("ForEach element:", x)
	})

	// Reduceを使って合計を計算
	sum := seq3.Reduce(func(acc, x int) int {
		return acc + x
	}, 0)
	fmt.Println("Sum:", sum) // 15

	seq := NewSeq(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

	// Dropのテスト
	droppedSeq := seq.Drop(3)
	fmt.Println("Dropped Seq:", droppedSeq) // {4, 5, 6, 7, 8, 9, 10}

	// Takeのテスト
	takenSeq := seq.Take(5)
	fmt.Println("Taken Seq:", takenSeq) // {1, 2, 3, 4, 5}

	// DropとTakeの組み合わせ
	combinedSeq := seq.Drop(2).Take(3)
	fmt.Println("Combined Seq:", combinedSeq) // {3, 4, 5}

	// 新しく追加したメソッドのテスト

	// Everyのテスト
	allLessThan11 := seq.Every(func(x int) bool {
		return x < 11
	})
	allEven := seq.Every(func(x int) bool {
		return x%2 == 0
	})
	fmt.Println("All less than 11:", allLessThan11) // true
	fmt.Println("All even:", allEven)               // false

	// Someのテスト
	hasEven := seq.Some(func(x int) bool {
		return x%2 == 0
	})
	hasNegative := seq.Some(func(x int) bool {
		return x < 0
	})
	fmt.Println("Has even numbers:", hasEven)         // true
	fmt.Println("Has negative numbers:", hasNegative) // false

	// Findのテスト
	firstEven, foundEven := seq.Find(func(x int) bool {
		return x%2 == 0
	})
	firstBig, foundBig := seq.Find(func(x int) bool {
		return x > 100
	})
	fmt.Println("First even number:", firstEven, "Found:", foundEven)    // 2 true
	fmt.Printf("First number > 100: %v Found: %v\n", firstBig, foundBig) // 0 false

	// FlatMapのテスト
	poweredSeq := seq.Take(3).FlatMap(func(x int) Seq[int] {
		return NewSeq(x, x*x)
	})
	fmt.Println("FlatMapped powered sequence:", poweredSeq) // {1, 1, 2, 4, 3, 9}

	// ToArrayのテスト
	array := seq.Take(4).ToArray()
	fmt.Println("ToArray result:", array) // [1 2 3 4]
}

脚註

[編集]