はじめに
こんにちは、皆さんエナドリ飲んでますか。
「エナジードリンクなんて、カフェインの塊じゃないか」
「そんなもの毎日飲んでたら身体に悪いぞ」
「依存症になるんじゃないのか?」
大丈夫、私はもうカフェイン依存症です☕️
今日は皆さんに、カフェイン依存症とプログラミングの意外な関係性についてお話しします。
実はこの2つには驚くほど共通点があります。例えば:
- どちらも「適度に」じゃないと危険
- 一度ハマると抜け出すのが難しい
- 周りから「それ、やりすぎじゃない?」と心配される
- 本人は「まだいける!」と思っている
最後に、
適切に設計しないと、とんでもないことになる
「依存」と「設計」の深いい関係に、ダイブしていきましょう。
その前に、エナジードリンク補給してきます。
カシュウッ!!(缶を開ける音)
カフェイン依存症
おk、注入しました😊
本題に入っていきましょう。
まず、私が最初にハマったエナジードリンク、そう、BlueBullのことから話します。
BlueBullって知ってます?ほら、あの青いパッケージの...いやいや、架空のドリンクですよ。もう...
最初は「たまには飲もうかな」程度だったのが、気づいたら毎日飲んでる。そんな生活を送っていました。それと同じように、プログラムも特定のパッケージに依存していきました。
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を片手にコードを書いていたある日のこと...
$ 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はエナジードリンクの代名詞。それがなくなるなんて...
生殺与奪の件を生産者に握らせるな!
まずは、冷静になってコードをじっくり見直してみます。
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
インターフェースを追加して、それを使用するようにしましょう。
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日のカフェイン摂取量を制限する」といった制御も簡単に追加できます。
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インターフェースを実装してるはずだから問題ないでしょう。
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 パターンを処方されました。
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
}
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
}
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))
}
}
}
-- flags.yaml --
drink:
variations:
new-drink: true
old-drink: false
defaultRule:
percentage:
new-drink: 50
old-drink: 50
...
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はカフェインオーバードーズで力尽きました... 💀
結論: エナドリは飲みすぎない方がいい
資料