エナジードリンクから学ぶインターフェース

# はじめに

こんにちは、皆さんエナドリ飲んでますか。

「エナジードリンクなんて、カフェインの塊じゃないか」
「そんなもの毎日飲んでたら身体に悪いぞ」
「依存症になるんじゃないのか？」

大丈夫、私はもうカフェイン依存症です☕️

今日は皆さんに、**カフェイン依存症**と**プログラミング**の意外な関係性についてお話しします。

実はこの2つには驚くほど共通点があります。例えば：

- どちらも「適度に」じゃないと危険
- 一度ハマると抜け出すのが難しい
- 周りから「それ、やりすぎじゃない？」と心配される
- 本人は「まだいける！」と思っている

最後に、

**適切に設計しないと、とんでもないことになる**

「依存」と「設計」の深いい関係に、ダイブしていきましょう。

その前に、エナジードリンク補給してきます。

**ｶｼｭｳｯ!!（缶を開ける音）**

# カフェイン依存症

おk、注入しました😊
本題に入っていきましょう。

まず、私が最初にハマったエナジードリンク、そう、**BlueBull**のことから話します。

BlueBullって知ってます？ほら、あの青いパッケージの...いやいや、架空のドリンクですよ。もう...

最初は「たまには飲もうかな」程度だったのが、気づいたら毎日飲んでる。そんな生活を送っていました。それと同じように、プログラムも特定のパッケージに依存していきました。

```go
package main

import (
    "fmt"
    "example.com/bluebull"
)

type Person struct {
    caffeineLevel int
    tolerance     int
}

func (p *Person) drinkBlueBull() {
    drink := bluebull.New()
    p.caffeineLevel += drink.CaffeineContent()
    p.tolerance += drink.ToleranceIncrease()
    fmt.Printf("%sを飲みました！カフェイン摂取量: %dmg, 耐性: %d\n", drink.Name(), p.caffeineLevel, p.tolerance)
}

func main() {
    me := &Person{caffeineLevel: 0, tolerance: 0}
    for i := 0; i < 7; i++ {
        me.drinkBlueBull()
    }
}
```

**ｶｼｭｳｯｯ!!（缶を開ける音）**

「よっしゃ、今日も一日頑張るぞ！」

そう意気込んで、いつものようにBlueBullを片手にコードを書いていたある日のこと...

```bash
$ go build
go: example.com/bluebull@v1.2.3: module example.com/bluebull is deprecated: 
We are no longer maintaining this package. Please switch to example.com/RockinStar.
```

「え...？」

目の前が真っ暗になる私。そう、使っていた `bluebull` パッケージが突如 `deprecated` になったのです。

「まさか、BlueBullが製造中止に...！？ カフェイン切れで死んじゃう...」

こんな状況、想像したこともありませんでした。だって、BlueBullはエナジードリンクの代名詞。それがなくなるなんて...

# 生殺与奪の件を生産者に握らせるな！

まずは、冷静になってコードをじっくり見直してみます。

```go
package main

import (
    "fmt"
    "example.com/bluebull"
)

type Person struct {
    caffeineLevel int
    tolerance     int
}

func (p *Person) drinkBlueBull() {
    drink := bluebull.New()
    p.caffeineLevel += drink.CaffeineContent()
    p.tolerance += drink.ToleranceIncrease()
    fmt.Printf("%sを飲みました！カフェイン摂取量: %dmg, 耐性: %d\n", drink.Name(), p.caffeineLevel, p.tolerance)
}

func main() {
    me := &Person{caffeineLevel: 0, tolerance: 0}
    for i := 0; i < 7; i++ {
        me.drinkBlueBull()
    }
}
```

`drinkBlueBull` メソッドで `bluebull.New()` を直接呼んでいる点が気になります。

これらの問題は、プログラミングにおける「密結合」と呼ばれる状態を引き起こしています。つまり、コードの一部が変更されると、他の部分にも大きな影響を与えてしまうのです。

カフェインで例えるなら、「BlueBullのカフェインでしか目が覚めない体質」になってしまっているようなものです。これでは、BlueBullが製造中止になったら、私たちのプログラム（そして私自身）は動けなくなってしまいます。

以下のように、`Caffeinated` インターフェースを追加して、それを使用するようにしましょう。

```go
type Caffeinated interface {
    Name() string
    CaffeineContent() int
    ToleranceIncrease() int
}

type Person struct {
    caffeineLevel int
    tolerance     int
}

func (p *Person) drink(d Caffeinated) {
    p.caffeineLevel += d.CaffeineContent()
    p.tolerance += d.ToleranceIncrease()
    fmt.Printf("%sを飲みました！カフェイン摂取量: %dmg, 耐性: %d\n", d.Name(), p.caffeineLevel, p.tolerance)
}
```

この変更で、私たちのPersonは「BlueBull」という特定のドリンクではなく、「`Caffeinated`」というインターフェースへ依存するようになりました。

これって、「BlueBull依存症」から「一般的なカフェイン依存症」に"進化"したようなものですね（病名を発見したと言った方がいいかもしれない）。

この変更によって：

- 新しいカフェイン飲料が出ても、すぐに試せるようになりました
- 好みのエナジードリンクが製造中止になっても、すぐに別のドリンクに切り替えられます

また、以下のようにして「カフェイン含有量が100mg以下のドリンクだけ」や「1日のカフェイン摂取量を制限する」といった制御も簡単に追加できます。

```go
func (p *Person) drinkSafely(d Caffeinated) error {
    if d.CaffeineContent() > 100 {
        return fmt.Errorf("カフェイン含有量が多すぎます: %dmg", d.CaffeineContent())
    }
    if p.caffeineLevel + d.CaffeineContent() > 400 {
        return fmt.Errorf("1日のカフェイン摂取量の上限を超えています")
    }
    p.drink(d)
    return nil
}
```

つまり、特定のブランドに縛られることなく、より自由にカフェインを補給できるようになったわけです。

**ｶｯｼｭｳｯｯ!!（缶を開ける音）**

あ、これは**RockinStar**ですね。まあ、どっちもCaffeinatedインターフェースを実装してるはずだから問題ないでしょう。

```go
package main

import (
    "fmt"
    "example.com/caffeinateddrink"
    "example.com/rockinstar"
)

type Caffeinated interface {
    Name() string
    CaffeineContent() int
    ToleranceIncrease() int
}

// Person 構造体とその drink メソッド
type Person struct {
    caffeineLevel int
    tolerance     int
}

func (p *Person) drink(d Caffeinated) {
    p.caffeineLevel += d.CaffeineContent()
    p.tolerance += d.ToleranceIncrease()
    fmt.Printf("%sを飲みました！カフェイン摂取量: %dmg, 耐性: %d\n", d.Name(), p.caffeineLevel, p.tolerance)
}

func main() {
    me := &Person{caffeineLevel: 0, tolerance: 0}
    
    // BlueBull が使えなくなったので、RockinStar に切り替え
    rockinstar := rockinstar.New()
    me.drink(rockinstar)
}
```

# おまけ (カフェインを徐々に減らす)

ここまでで私たちは「BlueBull依存症」から「一般的なカフェイン依存症」へと"進化"しました。でも、本当にこれで大丈夫だろうか、だって所詮素人の自己診断...

怖くなった私は **Martin Fowler** 医師に相談したところ、「急に全てのカフェインを断つのは危険です。徐々に減らしていきましょう」と言われ、**BranchByAbstraction** パターンを処方されました。

```Go
package bluebull

type BlueBull struct{}

func New() *BlueBull {
	return &BlueBull{}
}

func (b *BlueBull) Name() string {
	return "BlueBull"
}

func (b *BlueBull) CaffeineContent() int {
	return 80 // 80mg of caffeine
}

func (b *BlueBull) ToleranceIncrease() int {
	return 5
}

```

```Go
package rockinstar

type RockinStar struct{}

func New() *RockinStar {
	return &RockinStar{}
}

func (r *RockinStar) Name() string {
	return "RockinStar"
}

func (r *RockinStar) CaffeineContent() int {
	return 120 // 120mg of caffeine
}

func (r *RockinStar) ToleranceIncrease() int {
	return 7
}

```

```Go
package main

import (
	"context"
	"fmt"
	"log"
	"math/rand"
	"time"

	ffclient "github.com/thomaspoignant/go-feature-flag"
	"github.com/thomaspoignant/go-feature-flag/ffcontext"
	"github.com/thomaspoignant/go-feature-flag/retriever/fileretriever"

	"example/bluebull"
	"example/rockinstar"
)

type CaffeinatedDrink struct {
	rockinStar  *rockinstar.RockinStar
	blueBull    *bluebull.BlueBull
	useNewDrink bool
}

func NewCaffeinatedDrink(rock *rockinstar.RockinStar, bull *bluebull.BlueBull) *CaffeinatedDrink {
	return &CaffeinatedDrink{
		rockinStar:  rock,
		blueBull:    bull,
		useNewDrink: false,
	}
}

func (c *CaffeinatedDrink) SetUseNewDrink(useNew bool) {
	c.useNewDrink = useNew
}

func (c *CaffeinatedDrink) Name() string {
	if c.useNewDrink {
		return c.rockinStar.Name()
	}
	return c.blueBull.Name()
}

func (c *CaffeinatedDrink) CaffeineContent() int {
	if c.useNewDrink {
		return c.rockinStar.CaffeineContent()
	}
	return c.blueBull.CaffeineContent()
}

func (c *CaffeinatedDrink) ToleranceIncrease() int {
	if c.useNewDrink {
		return c.rockinStar.ToleranceIncrease()
	}
	return c.blueBull.ToleranceIncrease()
}

type Person struct {
	caffeineLevel int
	tolerance     int
}

type Caffeinated interface {
    Name() string
    CaffeineContent() int
    ToleranceIncrease() int
}
func (p *Person) drink(d Caffeinated) {
	p.caffeineLevel += d.CaffeineContent()
	p.tolerance += d.ToleranceIncrease()
	fmt.Printf("%sを飲みました。カフェイン摂取量: %dmg, 耐性: %d\n", d.Name(), p.caffeineLevel, p.tolerance)
}

func (p *Person) die(userID string) bool {
	if p.caffeineLevel > 1000 {
		fmt.Printf("%sはカフェインオーバードーズで力尽きました... 💀\n", userID)
		return true
	}
	return false
}

var users = make([]*Person, 0)

func main() {
	err := ffclient.Init(ffclient.Config{
		PollingInterval: 3 * time.Second,
		Context:         context.Background(),
		Retriever: &fileretriever.Retriever{
			Path: "./flags.yaml",
		},
	})
	if err != nil {
		panic(err)
	}
	defer ffclient.Close()

	drink := NewCaffeinatedDrink(rockinstar.New(), bluebull.New())

	for i := 0; i < 100; i++ {
		users = append(users, &Person{caffeineLevel: 0, tolerance: 0})
	}

	for userID, user := range users {
		u := ffcontext.NewEvaluationContext(fmt.Sprintf("user%d", userID))
		useNewDrink, err := ffclient.BoolVariation("drink", u, false)
		if err != nil {
			log.Printf("フラグの取得エラー: %v\n", err)
			continue
		}
		drink.SetUseNewDrink(useNewDrink)

		// ランダムな回数エナドリを飲む
		n := rand.Intn(20) + 1
		fmt.Printf("%s「%d回飲もーっと😊」\n", fmt.Sprintf("user%d", userID), n)
		isDead := false
		for i := 0; i < n; i++ {
			user.drink(drink)
			isDead = user.die(fmt.Sprintf("user%d", userID))
			if isDead {
				break
			}
		}

		if !isDead {
			fmt.Printf("%sは生き残った💪\n", fmt.Sprintf("user%d", userID))
		}
	}

}
```
```yaml
-- flags.yaml --

drink:
  variations:
    new-drink: true
    old-drink: false
  defaultRule:
    percentage:
      new-drink: 50
      old-drink: 50
```

```bash
...

RockinStarを飲みました。カフェイン摂取量: 120mg, 耐性: 7
RockinStarを飲みました。カフェイン摂取量: 240mg, 耐性: 14
RockinStarを飲みました。カフェイン摂取量: 360mg, 耐性: 21
RockinStarを飲みました。カフェイン摂取量: 480mg, 耐性: 28
RockinStarを飲みました。カフェイン摂取量: 600mg, 耐性: 35
RockinStarを飲みました。カフェイン摂取量: 720mg, 耐性: 42
RockinStarを飲みました。カフェイン摂取量: 840mg, 耐性: 49
RockinStarを飲みました。カフェイン摂取量: 960mg, 耐性: 56
RockinStarを飲みました。カフェイン摂取量: 1080mg, 耐性: 63
user96はカフェインオーバードーズで力尽きました... 💀
user97「5回飲もーっと😊」
BlueBullを飲みました。カフェイン摂取量: 80mg, 耐性: 5
BlueBullを飲みました。カフェイン摂取量: 160mg, 耐性: 10
BlueBullを飲みました。カフェイン摂取量: 240mg, 耐性: 15
BlueBullを飲みました。カフェイン摂取量: 320mg, 耐性: 20
BlueBullを飲みました。カフェイン摂取量: 400mg, 耐性: 25
user97は生き残った💪
user98「7回飲もーっと😊」
RockinStarを飲みました。カフェイン摂取量: 120mg, 耐性: 7
RockinStarを飲みました。カフェイン摂取量: 240mg, 耐性: 14
RockinStarを飲みました。カフェイン摂取量: 360mg, 耐性: 21
RockinStarを飲みました。カフェイン摂取量: 480mg, 耐性: 28
RockinStarを飲みました。カフェイン摂取量: 600mg, 耐性: 35
RockinStarを飲みました。カフェイン摂取量: 720mg, 耐性: 42
RockinStarを飲みました。カフェイン摂取量: 840mg, 耐性: 49
user98は生き残った💪
user99「15回飲もーっと😊」
RockinStarを飲みました。カフェイン摂取量: 120mg, 耐性: 7
RockinStarを飲みました。カフェイン摂取量: 240mg, 耐性: 14
RockinStarを飲みました。カフェイン摂取量: 360mg, 耐性: 21
RockinStarを飲みました。カフェイン摂取量: 480mg, 耐性: 28
RockinStarを飲みました。カフェイン摂取量: 600mg, 耐性: 35
RockinStarを飲みました。カフェイン摂取量: 720mg, 耐性: 42
RockinStarを飲みました。カフェイン摂取量: 840mg, 耐性: 49
RockinStarを飲みました。カフェイン摂取量: 960mg, 耐性: 56
RockinStarを飲みました。カフェイン摂取量: 1080mg, 耐性: 63
user99はカフェインオーバードーズで力尽きました... 💀

```

結論: エナドリは飲みすぎない方がいい

## 資料

https://x.com/fuwasegu/status/1621325166746959873

https://www.youtube.com/watch?v=Ksc8Qu33se4

https://bliki-ja.github.io/BranchByAbstraction

https://100go.co/ja/

https://gofeatureflag.org/