0
0

【Golang】Functional Option Patternで実現する関数のデフォルト引数(オプション引数)の設定方法

Last updated at Posted at 2024-06-08

はじめに

  • Golangには、関数のデフォルト引数(後述します)がありません
  • これに対するアプローチの1つとして「Functional Option Pattern」があります
  • 本記事ではこの手法について説明します

対象読者

  • 現在Golangを学ばれている方
  • これからGolangを学ばれる方

関数のデフォルト引数とは?

説明のし易さのため、関数のデフォルト引数があるTypeScriptで説明させてください(TypeScriptが分からない方でも雰囲気は伝わるはずです)。

以下はオブジェクト「Greet」を生成し、そのプロパティ「str1」と「str2」を出力するサンプルのソースコードです。

sample.ts
class Greet {
    str1: string;
    str2: string;

    // コンストラクタ
    constructor(str1: string, str2: string = "") {
        this.str1 = str1;
        this.str2 = str2;
    }
}

// メイン処理
const greet1 = new Greet("hello");
console.log(greet1.str1 + " " + greet1.str2); // "hello"

const greet2 = new Greet("hello", "world");
console.log(greet2.str1 + " " + greet2.str2); // "hello world"

順番に見ていきましょう。まず、メイン関数に注目すると、greet1生成時の引数は"hello"のみを渡しているのに対して、greet2生成時は"hello"と"world"を渡していますね。

const greet1 = new Greet("hello");
const greet2 = new Greet("hello", "world");

次に、コンストラクタでは第二引数「str2」のでデフォルト値として空文字を指定しています。これによって、コンストラクタ(関数)呼出し時に第二引数の指定が省略された場合、str2には空文字が設定されるようになります(この場合、str1は省略できません)。

// コンストラクタ
constructor(str1: string, str2: string = "") {

このように、指定を省略した場合に、デフォルト値が設定される引数のことを、「デフォルト引数」(または、オプション引数)と言います。デフォルト引数があることによって、関数呼び出し時の引数の個数は可変にできるのですね。

NGパターン

冒頭でも触れたように、Golangには関数のデフォルト引数がありません。
では、先ほどのサンプルのような出力をGolangで実現するにはどうすれば良いでしょうか?

1つの案としては、「省略できないなら設定すれば良いじゃん!」と、greet1生成時の第二引数に空文字を指定するパターンです。

main.go
package main

import "fmt"

type Greet struct {
	Str1 string
	Str2 string
}

// コンストラクタ
func NewGreet(str1 string, str2 string) *Greet {
	// デフォルト値
	greet := &Greet{
		Str1: str1,
        Str2: "",
	}

	return greet
}

// メイン処理
func main() {
	greet1 := NewGreet("hello", "")
	fmt.Println(greet1.Str1, greet1.Str2) // "hello"
 
	greet2 := NewGreet("hello", "world")
	fmt.Println(greet2.Str1, greet2.Str2) // "hello world"
}

デフォルト値も設定できて、一応問題無く動くのですが、第三引数、第四引数...と引数が増える度に冗長となってしまいます。使わない値であるのに何かしら値を指定しなければいけないのはあまり美しくありません。

// 引数が3つに増えた場合
greet1 := NewGreet("hello", "", "")
greet2 := NewGreet("hello", "world", "")
greet3 := NewGreet("hello", "world", "!!!")

じゃあどうすれば?

そこで登場するのが「Functional Option Pattern」という手法です。これは、Option型の関数によってオブジェクトのプロパティ値を設定する方法です。

百聞は一見に如かずということで、 実際に修正後のソースコードを見て見ましょう。

main.go
package main

import "fmt"

type Greet struct {
	Str1 string
	Str2 string
}

type Option func(*Greet)

func WithStr2(str2 string) Option {
	return func(greet *Greet) {
		greet.Str2 = str2
	}
}

// コンストラクタ
func NewGreet(str1 string, options ...Option) *Greet {
    // デフォルト値
	greet := &Greet{
		Str1: str1,
        Str2: "",
	}

	for _, option := range options {
		option(greet)
	}

	return greet
}

// メイン処理
func main() {
	greet1 := NewGreet("hello")
	fmt.Println(greet1.Str1, greet1.Str2) // "hello"
 
    greet2 := NewGreet("hello", WithStr2("world"))
	fmt.Println(greet2.Str1, greet2.Str2) // "hello world"
}

注目のメイン処理部分ですが、greet1生成時の引数が"hello"のみになっています!
greet2生成時の第一引数は"hello"、第二引数に"world"を引数に指定した後述する関数「WithStr2」を指定しています。

// メイン処理
func main() {
    greet1 := NewGreet("hello", WithStr2("world"))
	fmt.Println(greet1.Str1, greet1.Str2) // "hello world"
	
	greet2 := NewGreet("hello")
	fmt.Println(greet2.Str1, greet2.Str2) // "hello"
}

順番に見ていきましょう。まず、Greetのメソッドを「Option型」と定義します。

type Option func(*Greet)

次に、関数「WithStr2」を定義します。メイン処理で呼び出されていましたね。
これは少しややこしいですが、Greetメソッドを戻り値として返す関数です。そのGreetメソッドの中身は、Greetオブジェクトのプロパティ「str2」をセットするという内容ですね。

注意点としては、関数「WithStr2」は定義であって、その戻り値であるGreetメソッドが実行されて初めてプロパティ「str2」がセットされます。

func WithStr2(str2 string) Option {
	return func(greet *Greet) {
		greet.Str2 = str2
	}
}

最後に、コンストラクタです。第一引数に「str1」(サンプルに合わせて必須パラメータとする意図)、第二引数に可変長のOptionを受け取リます。
そしてfor文によって、受け取ったGreetメソッドを順に実行していきます。

// コンストラクタ
func NewGreet(str1 string, options ...Option) *Greet {
	// デフォルト値
	greet := &Greet{
		Str1: str1,
        Str2: "",
	}

	for _, option := range options {
		option(greet)
	}

	return greet
}

ここでメイン処理の内容を振り返ります。greet2生成時の第二引数は「WithStr2("world"))」でしたね。この関数を渡すことでコンストラクタ内のfor文内で関数「WithStr2」が実行 -> 関数「WithStr2」戻り値のGreetメソッドが実行 -> Greetオブジェクトのプロパティ「str2」に"world"が設定、という動作になっていたわけです。

Functional Option Patternのメリット

個人的に感じたFunctional Option Patternを使うことのメリットを3点挙げます。

  • 可読性の向上
    • Optionの有無によってどのプロパティがセットされるのかが直感的に分かりやすいです
  • 保守性の向上
    • Option型の関数はそれぞれが独立しているため、Option型の指定順序が変わっても正しく機能します
  • 柔軟性の向上
    • プロパティの追加/変更/削除、必須プロパティかどうかを簡単に設定できます

まとめ

  • Golangには、関数のデフォルト引数が無く、これに対するアプローチの1つとして「Functional Option Pattern」があります
  • Functional Option PatternはOption型の関数によってオブジェクトのプロパティを設定する方法です
  • Functional Option Patternを使うことで、可読性、保守性、柔軟性の向上が期待できます

参考

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