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) }
メソッドチェインのベストプラクティス
[編集]設計原則
[編集]- 不変性の維持: 可能な限り、メソッドチェインでは元のオブジェクトを変更せず、新しいインスタンスを返すことを検討する
- メソッド名の一貫性: 動詞を使った明確で一貫性のあるメソッド名を使用する
- エラーハンドリング: エラーが発生する可能性がある場合は、適切なエラーハンドリング戦略を実装する
不変オブジェクトの例
[編集]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] }