Goの勉強 テスト

Goの勉強をやり始めたのでメモ。
プログラミング言語 Go」を読んでる。

go test ツール

  • go test はテストドライバ。
  • _test.go で終わるファイルは go build ではビルド対象には含まれず、 go test ではビルド対象に含まれる。
  • _test.go には以下のものを書くことができる。
    • Test で始まる関数はテスト関数
    • Benchmarkで始まる関数はベンチマーク関数
    • Exampleで始まる関数はコード例関数
  • go test を実行するとこれらの関数を探し、一時的なmainパッケージを生成して実行する。

Test関数

func TestName(t *testing.T) {  
        // ...  
}  
  • Name は省略可能である。
  • パラメータtはテストの失敗を報告するメソッドを提供する。
    • t.Errorまたはt.Errorfを使用する。
  • go test -v で実行時間を表示できる。
  • go test -run で実行するテストを正規表現で指定できる。
  • Goのテストでは表駆動を使用する場合が多い。
  • テストが失敗しても後続のテストは実行される。
    • 本当にテストを止めたい場合には t.Fatalt.Fatalfを使用する。
  • テストの作成者はテストの失敗の原因を究明しなければいけないプログラマに役立つように努めるべきである。
  • テストの都合でパッケージ循環が起きるケースがあり、その場合のために外部テストパッケージがある。
    • 外部パッケージはそのパッケージの名前に _test をつけた名前になる。
  • go list を使うことで、ビルド時にそのパッケージに含まれるファイルを調べることができる。
    • go list -f={{.GoFiles}} package_name でビルド時に含まれるファイルのリストを出力する。
    • go list -f={{.TestGoFiles}} package_name でテストファイルのリストを出力する。
    • go list -f={{}.XTestGoFiles} package_name で外部テストファイルのリストを出力する。
  • 外部テストパッケージでその対象パッケージの非公開要素にアクセスするために、_test.goに公開要素のみを書くことがある。慣習的に export_test.go という名前を付ける。

カバレッジ

  • テストはその性質上、バグが存在しないことを証明できない。
  • テストがどの程度テスト対象のパッケージを動かしたかをカバレッジと呼ぶ。
  • カバレッジ定量化できず、あくまでカバレッジが有効であるというのは経験則である。
  • ステートメントカバレッジはテスト中に1回は実行される文のソース中での割合である。
  • go test -run=FuncName -coverprofile=c.out PackageNameカバレッジを収集できるようにし、
  • go tool cover -html=c.out で結果をブラウザで見ることができる。

Benchmark 関数

  • ベンチマークは固定した負荷のもとでプログラムの性能を測定することである。
  • ベンチマーク関数は関数名を Benchmark ではじめ、 *testing.B を引数にとる
func BenchmarkMethod(b *testing.B) {  
    for i := 0; i < b.N; i++ {  
        Method()  
    }  
}  
> go test -bench=.  
goos: darwin  
goarch: arm64  
pkg: pkg/name  
Method-10         6738589               160.8 ns/op  
PASS  
ok      pkg/name       1.854s  
  • 以下を示している。
    • GOMAXPROCS が 10 だった。
    • 関数の呼び出しを6738589 回実行した結果の平均が 168.0 ns だった。
  • -benchmem オプションを付けるとメモリ割り当ての情報も表示できる。

プロファイル

  • プロファイリングとは、プロファイルイベントをサンプリングし、そのイベントから性能を推定することである。
  • プロファイリングは重要なコードを特定するために用いられる。
  • イベントの統計情報をプロファイルと呼ぶ

  • Go では以下の種類のプロファイルをサポートしている。

    • CPUプロファイル: CPU時間を最も必要とした関数を特定する。OSからの割り込みから再開された際にイベントを作成する。
    • ヒーププロファイル: 最も多くのメモリを割り当てた文を特定する。512KBのメモリ割り当てにつきイベントを作成する。
    • 待ちプロファイル: 最もゴルーチンを待たせたものを特定する。ゴルーチンが待たされるごとにイベントを作成する。
  • 以下のコマンドでプロファイルを収集できる。
    • go test -cpuprofile=cpu.out
    • go test -blockprofile=block.out
    • go test -memprofile=mem.out
  • プロファイルオプション付きでテストが実行されると、実行ファイルが末尾.testで保存される
  • プロファイルの解析にはpprofが必要である。
go test pprof -text -nodecount=10 ./package.test ./cpu.log  

Example関数

  • Example で始まる関数はコード例を示す。
  • コード例はコンパイル時に検査されるため、古くなりにくい。
  • ExampleFunctionName は関数のコード例を示し、 Example はパッケージ全体のコード例を示す
  • コード例の最後に // Output: のコメントがある場合、その関数が標準出力に出力した内容とコメントの内容が一致するかが検査される。

Goの勉強 パッケージとGoツール

Goの勉強をやり始めたのでメモ。
プログラミング言語 Go」を読んでる。

  • Goではパッケージが導入されていて、パッケージを通して再利用可能になっている。
  • 公開されたパッケージは http://godoc.org で確認できる。

導入

  • パッケージの目的は、他のパッケージとは独立に、関連する機能を、変更や理解が容易にできる単位にまとめることである。
    • モジュール性という。
  • 個々のパッケージは独自の名前空間を定義するため、他のパッケージの名前と衝突しない。
  • パッケージは可視性を利用してカプセル化を提供する。
  • ファイルを変更すると、そのファイルの含まれるパッケージ全体を再コンパイルしないといけない。
  • Goのコンパイラは他の言語のコンパイラよりも速い
    • import文が先頭にあることで、ファイルをすべて読み込まなくても依存関係がわかるため。
    • 循環がないため並列にコンパイルできるため。
    • コンパイルされたオブジェクトファイルが依存先の情報も記録しているため。

インポートパス

  • import 宣言にあるインポートパスによってパッケージを一意に特定できる。
  • 言語仕様上、インポートパスの決定方法を定義しておらず、各ツールに任せている。
  • この章ではgoツールを前提にする。
  • 標準ライブラリ以外のパッケージは名前衝突を避けるために、パッケージ名をインターネットドメイン名から始めるべきである。

パッケージ宣言

  • package 宣言はすべてのGoファイルの先頭に必須である。
  • 慣習的にパッケージ名はインポートパスの最後の部分になる。
  • ただし、以下は例外である。
    • mainパッケージ。
    • ファイル名が_test.goで終わる場合、パッケージ名も_testで終わることができる。
      • つまりディレクトリに2つのパッケージを置くことができる。
    • インポートパスにバージョン番号がある場合、パッケージ名はそのバージョン番号を取る。

インポート宣言

  • Goファイルではpackage宣言の直後に0個以上のimport宣言を書くことができる。
  • 同じ名前を持つパッケージをインポートする場合、改名インポートが使用される。
    • 改名インポートはそのファイルにのみ影響を与える。同じパッケージの別ファイルには影響を与えない。
import (  
        "crypto/rand"  
        mrand "math/rand"  
)  
  • 改名インポートは衝突がない場合でも役立つ。
    • 自動生成されたコードのパッケージ名が扱いづらいとき。
    • パッケージと同じ名前のローカル変数名を多く持ちたいとき。

ブランクインポート

  • パッケージをインポートする必要はあるが、そのパッケージを呼び出さない場合、ブランクインポートをする必要がある。
import _ "image/png"  
  • これはパッケージで定義されている変数の初期化やinit関数を実行するために使用される。
  • 例えば image.Decodeはデコードできる形式を登録する必要がある。これはimage/jpegimage/pngのinit関数で行っている。
  • 同じように、database/sqlで使用できるドライバは各ライブラリのinit関数で登録される。

パッケージと命名

  • パッケージ名は
    • 簡潔でかつ意味がとおるようにする。
    • 説明的でかつ曖昧にならないようにする。
    • 単数形にする。
    • 他の意味を持つ名前を避ける。
      • 例: "temp": temperature, temporary
    • ローカル変数で使われる名前を避ける。
  • パッケージのメンバー名は
    • パッケージ名合わせて意味が通るようにする。
    • 単一の型を提供する単一型パッケージrand.Randなど重複しないように注意する

Goツール

  • goツールは
    • パッケージマネージャである。
    • ビルドシステムである。
    • テストドライバである。

ワークスペースの構成

パッケージのダウンロード

  • go get を使うことでインターネット上のパッケージもインストールできる。
  • go getGitHubなどの有名なホスティングサイトをサポートしており、Gitのようなバージョン管理システムをサポートしている。
    • つまり、単なるファイルコピーではなくクライアントである。
  • インポートパスとホスティングされているファイルのドメイン名は必ずしも一致しない。
    • インポートパスにHTTPアクセスをするとHTMLが返り、その中の <meta name="go-import">ホスティングの情報が書かれている。
    • 例: <meta name="go-import" content="golang.org/x/net git https://go.googlesource.com/net">

パッケージのビルド

  • go build
    • 引数に渡されたパッケージをコンパイルできる。
      • パッケージがライブラリ(main以外)の場合はコンパイル結果は破棄される。
      • パッケージがmainの場合はリンカが呼ばれ、実行ファイルが作成される。
      • パッケージはインポートパスや . で始まる相対パスで指定できる。
      • カレントディレクトリに実行ファイルが生成される。
    • 引数でファイル名を指定することもできる。
      • go build をしてそのファイルを実行するためのコマンド go run がある。
      • .go で終わっていない引数はプログラムへの引数として解釈される。
  • go install
  • go buildgo install は1度実行されると、それ以降変更されていない場合はコンパイルを実行しない。
  • net_linux.go などOSやアーキテクチャ名の入っているファイルは、それをターゲットにしている場合のみコンパイルされる。
  • 以下のようなビルドタグをパッケージ宣言の前に書くことでビルドを制御できる
// +build linux darwin  
// +build ignore  

パッケージのドキュメント化

  • パッケージのAPIにはドキュメンテーションコメントを書く慣習がある。
  • コメントは宣言名から始まる要約文になる。
  • コードと同様にドキュメンテーションも簡潔性と単純さが好まれる。
  • パッケージのドキュメンテーションコメントはdoc.goという名前のファイルに書かれることが多い。
  • go docを使うとドキュメントを参照できる。
    • 完全なインポートパスや大文字小文字を必要としない。(最後の例)
go doc time  
go doc time.Slice  
go doc time.Duration.Seconds  
go doc json.decode  
  • godoc コマンドを使うとドキュメントを表示するWebサーバを立ち上げることができる。
godoc -http :8000  

インターナルパッケージ

  • インポートパスにinternalが含まれている場合、go buildはそのパッケージを特別に扱う
  • 例えば、net/http/internal/chunkednet/http/httputilnet/httpからは参照できるがnet/urlからは参照できない。

パッケージの問い合わせ

  • go list を使うと利用可能なパッケージを調べることができる。
  • ワイルドカード ... を使用できる。
  • -jsonJSON形式で出力できる。
  • -f を使うと template/text のテンプレートで出力形式を定義できる。
go list -f '{{join .Deps " "}}' strconv  

認定スクラムマスターを取得しました

認定スクラムマスターを取得しました!やったー!

背景

去年知ってビックリしたのは、アジャイルスクラムに詳しくない人の中には、スクラムマスターを資格の名前だと思っている人がまぁまぁいるということ。
ギャップを埋めるために「資格の名前じゃないんですよ」と説明することもできますが、自分が資格を取ってしまうのもアリだなということで、重い腰をあげて資格取得に動きました。

研修選び

認定スクラムマスターを取得するためには研修を受ける必要がありますが、僕はアトラクタさんの研修を選びました。
前提知識なしで選んでいたので、ネームバリューというか、有名な方が多くいらっしゃったことが主な理由です。
また、最近はオンライン研修が多く、現地参加型の研修が少なかったというのもあります。
オンラインよりも現地のほうが楽しそうですし、学びも大きそうだなと感じています。(実際にそうだったように思います。)

学習

学習としては、スクラムガイドを読み直したのと、SCRUM MASTER THE BOOK を購入して読みました。
あとはRSGT2024への参加も学習の一環として行ったのですが、これは認定スクラムマスターの勉強の域をだいぶ越えていそうでした。(が、それはそれとして面白く、学びも多かったです。 RSGT2024参加レポート

研修

研修は箱根の研修施設で2泊3日で行われました。
研修の内容はもちろん書けませんが、自分の知りたかったことを多く学べたし、自分の気付いていない視点も重要であることがわかったので非常に満足でした。
他の参加者の方と広くお話できたのも良かったです。これはオンラインだと難しそう。


(まるで素材のような我々)

試験

試験は日本語で受けましたが、少し難しかったです。
もしかして英語だとわかりやすいのか?と思う問題もチラホラ。
結果は92/100点でした。
どの問題を間違えたかは教えてくれるのですが、正答は教えてくれず・・・
学習のために教えてほしい!!

まとめ

ということで、無事に資格を取れました。
資格を取れたということも嬉しいし、そこまでの過程での学習も実のあるものになって良かったです。
SEUちゃんと計上していこう。

『偶然同じ』という奇跡は頻繁に起こるのか?

ポエム。
フロントエンドは事情が違いそうなので、バックエンドの話と捉えてください。

DRYの原則について調べるとたいてい出てくる『偶然同じ』処理に注意せよという話がいっぱい出てくる。
つまり、何かをまとめようとしたときにそれはまとめるべきではなく、『偶然同じ』処理な場合があると言う話だ。

この話自体にはもちろん完全同意なのだが、一方でこの考えを免罪符のように使ってないだろうか?

自分の作ってるプロダクトを思い浮かべてほしい。あなたのプロダクトには、この「たまたま『偶然同じ』だった」という奇跡が何個あるだろうか?(つまり、プロダクトコードにある重複コードの数だ。)

「世の中って奇跡にあふれてるんだなぁ」みたいな考え方になることもできるが、僕はどちらかというと「これは本当に奇跡なんだろうか?」と疑うようにしている。
そしてじっくりと考えてみると、「『偶然同じ』処理だった」ではなく「抽象化に失敗していた」というケースが多い。
抽象度をひとつ上げて考えたり、リフレーミングしてみると解決できたりする。
またはそれがドメインに由来するものなら、ドメイン知識を持っている人と議論したほうが良いかもしれない。ドメイン知識を蒸留できるチャンスだ。

ということで、「『偶然同じ』処理には気をつけよう。それが本当に『偶然同じ』なのか、抽象化の失敗なのかはしっかりと考えたほうがいい。」というのを最近思っている。

Regional Scrum Gathering Tokyo 2024 に参加してきた

掲題のとおり、通称RSGT2024に参加してきました。

2024.scrumgatheringtokyo.org

色々と衝撃的なイベントだったので、まとめようと思います。

チケットが手に入らない

発売日の2日後に現地チケットを購入しにいったら売り切れてました。
オンラインチケットは売っていたのでオンラインチケットを購入。

このままオンラインで観るかーと思ってたところ、開催2日前に「現地に行けなくなりチケットを譲りたい」という方がいて、譲っていただきました。ありがとうございます!


ということで、幸運で現地に行くことができました。来年はチケット争奪戦がんばろうと思います。

day0の繋がり

開催前日にオンラインのイベント?があり、知り合いを増やすワークショップに参加しました。
後述するRSGTの性質上、自分のようなソロ初参加勢は知り合いがおらず、当日のギャザリングの壁が高いのでとてもありがたかったです。

開催2日目にday0メンバーに声をかけ、一緒にランチをさせてもらいました。

ここから、一緒にセッションをきいたり、終わった後の雑談などもしやすくなったので、本当にday0に感謝です。

ギャザリングへの情熱がすごい

当日の朝、会場に行って一番驚いたのは、すでにみんな受付のまわりでワイガヤと参加者同士でお喋りしてるということ。
そして会場のほうは閑散としていて、誰も席に座っていません。
自分が参加してきた今までのカンファレンスだとあさイチでお喋りしてる人なんて少数で、みんな会場の席に座ってパソコン開いている人ばかりだったので衝撃的でした。
そして当日の朝に限らず、みんな「隙あらば会話」といった感じで人と話しているのです。すごい!
これは2日目の昼間に撮った写真ですが、廊下の奥までみんな会話しているのがわかると思います。

自分はひとりで初参加だったので知り合いも多くないので「誰かと会話する」というのは少しハードルが高かったのですが、勇気を持って話しかけるとみなさんフレンドリーに接してくれ、みなさんの暖かさに感動しました。

そしてもちろん、夜の懇親会でもギャザリングしてたわけですが、discordのテキストチャットやボイスチャットでもギャザリングがされており、深夜帯まで無限に動いていて凄かったです。

セッションのレベルが高い

色々なセッションに参加してきましたが、どれもレベルが非常に高かったです。
スケジュールに「Begginer Session」とあっても、全然ビギナーレベルじゃない感じでした。

全体的に「価値」や「大規模スクラム」に関するセッションが多かったように思います。

レベルは高いですが、得るものや考えたいことが多くあり、非常にタメになるお話が多かったです。
特にキーノートの2セッションはもう一度振り返りたいです。

そしてもちろん、セッションだけでなくギャザリングの内容もレベルが高かったです。
いくつかその中で気づきもあったので良かったです。

夜が長い

これは他のカンファレンスも同じですが。
これは僕が開催場所である御茶ノ水or秋葉原を去った時間です。

  • 1日目: 23:50
  • 2日目: 22:40
  • 3日目: 28:30

そして、2日目以外は途中抜けしていますし、2日目も終わった後に他と合流しに行った人もいたそうです。すごい。

イベントによって夜の長さは違います。RSGTは初参加だったのでとりあえず同じ3日間開催のPHPerKaigiと同じくらいだろうと見積もっていましたが、だいたい同じくらいでした。
ただ最終日の夜に「First Time Attendee でこの時間までいるのは変人ですよ」と言われました。そんなことないです、ただの一般人です。

まとめ

まとめると「最高だった」ということです。
来年も行きたいですし、今度はスクフェスにも参加してみたい!

Goの勉強 共有された変数による並行性

Goの勉強をやり始めたのでメモ。
プログラミング言語 Go」を読んでる。

競合状態

  • あるゴルーチン内のイベントxと別のゴルーチン内のイベントyがあり、xとyのどちらのイベントが先に発火するか不明な場合、xとyは並行であると言う。
  • ある関数が2つ以上のゴルーチンから同期なく呼び出しても正しく動作を続ける場合、その関数は並行的に安全と言う。
  • ドキュメントで型が並行的に安全であると述べられている場合に限り、並行に変数へアクセスすべきである。
    • そうでない場合は単一のゴルーチンに閉じ込めるか、相互排他の高度な不変式を維持する必要がある。
  • パッケージレベルの変数は並行的に安全であることが期待される。
  • データ競合は、2つのゴルーチンが同じ変数へ並行にアクセスし、かつ書き込みがある場合に発生する。
var x []int  
go func() { x = make([]int, 10) } // *1  
go func() { x = make([]int, 100000) } // *2  
x[99999] = 1  

とした場合、1 でスライスのポインタが設定され、2でスライスの長さが設定されるかもしれない。
この場合、基底配列は10の長さしかないため、関係のないメモリを更新してしまう可能性がある。
これを 未定義な振る舞い と言う。

データ競合を回避する方法は3つある。

  • データを変数へ書き込まない
  • 複数のゴルーチンからの変数へのアクセスを避ける
    • ゴルーチンに閉じ込められた変数へのアクセスを仲介するゴルーチンのことをモニターゴルーチンと言う
    • パイプラインを用いて変数を共有した際に、次のステージへ渡した後にその変数にアクセスしないのであれば、アクセスが逐次的になる。この方法を順次閉じ込めと言う。
    • 複数のゴルーチンからの変数へのアクセスを許可するが、一度に単一のゴルーチンだけにアクセスさせる。
      • 相互排他と言う。

相互排他:sync.Mutex

  • 同時実行を制御するためのチャネルを計数セマフォと呼び、その中でも1までしか数えないセマフォバイナリセマフォと言う。
  • このパターンの実装として sync.Mutex がある。
var (  
    mu      sync.Mutex  
    balance int  
)  

func Deposit(amount int) {  
    mu.Lock()  
    balance = balance + amount  
    mu.Unlock()  
}  
  • LockUnlock の間の領域をクリティカルセクションと言う。
  • 上記のように関数・ミューテックスロック・変数を扱うことをモニターと言う。
  • sync.Mutex は再入可能ではないため、二重に呼び出すことはできない。
  • 対応策として、公開関数と内部関数に処理を分ける方法がある。
func Deposit(amount int) {  
    mu.Lock()  
    defer mu.Unlock()  
    deposit(amount)  
}  

func deposit(amount int) {  
    balance += amount  
}  

func Withdraw(amount int) bool {  
    mu.Lock()  
    defer mu.Unlock()  
    deposit(-amount)  
    if balance < 0 {  
        deposit(amount)  
        return false  
    }  
    return true  
}  

リード/ライトミューテックス:sync.RWMutex

  • sync.Mutex のロックを使用する場合、読み込み同士でもロック待ちが発生してしまう。
  • sync.RWMutex.RLock() を使用することで、共有ロックを取得でき、読み込み同士であればロック待ちが発生しない。
  • sync.Mutex.Lock() でロックが取得されている間は sync.RWMutex.RLock() は待たされることになる。

メモリの同期

うーん、わからんかった。

  • 同期をすることにより、タイミングだけでなくメモリの状態も同期することができる。
  • 同期をしない場合、一方で設定した値をもう一方で取得できるとは限らない。
    • CPUコアが複数ある場合など、メモリの持ち方や参照のされ方はハードウェアに依存する。

遅延初期化: Sync.Once

  • コストの高い初期化ステップを必要になるまで遅延させることがある。
  • 一方でそういった処理は並行的に安全に書くコストが高い。
  • Goでは Sync.Once を使って初期化処理を簡単に書くことができる。
var loadIconsOnce sync.Once  
var icons map[string]image.Image  

func Icon(name string) image.Image {  
        loadIconsOnce.Do(loadIcons)  
        return icons[name]  
}  

競合検出器

  • 並行性のミスを発見するための動的解析ツールである競合検出器がGoランタイムにある。
  • --race オプションを付けることで使用できる。
  • 競合検出器はデータ競合をレポートするが、実行時に発生したデータ競合しか検出できない。

ゴルーチンとスレッド

伸長可能なスタック

  • OSのスレッドはたいてい2MBのスタックと呼ばれるメモリ領域を確保する。
    • スタックは実行中の関数呼び出しやローカル変数が保存されている活動領域。
  • 2MBの領域は大きすぎるし、小さすぎる。
    • 簡単な計算しかしないものについては2MBは大きすぎる。
    • ネストの深い複雑な関数を呼び出しているものについては2MBは小さすぎる。
  • ゴルーチンはたいてい2KBのスタックで活動する。
  • ただし、必要に応じて領域を拡張したり縮小したりできる。
  • ゴルーチンのスタックの大きさの制限は1GBである。

ゴルーチンのスケジュール

  • OSスレッドはOSカーネルによってスケジュールされる。
    • 定期的にハードウェアタイマーがプロセッサに割り込み、スケジューラを起動させる。
    • スケジューラは現在のスレッドを一時停止させ、次に実行されるスレッドを調べる。
    • 別のスレッドを実行する必要がある場合は、メモリ上にレジスタの情報を保存し、実行するスレッドのレジスタ情報をメモリから読み込む。
  • OSスレッドは頻繁にメモリの読み書きが発生する。
  • Goのランタイムはm個のゴルーチンをn個のOSスレッドで多重化する。(m:nスケジューリング
    • 時間単位ではなく、 time.Sleep やチャネルの操作があるとスケジューリングを行う。

GOMAXPROCS

  • 能動的に実行できるOSスレッド数を GOMAXPROCS で設定できる。
    • デフォルトはCPU数。
    • ちなみに僕のマシンのCPUは10個でした。
  • runtime.GOMAXPROCS関数を使うか、環境変数を設定することによりこの値を変更できる。
    • 引数に-1を渡すと設定を変えずに現在の値を見ることができる。
  • 以下のゴルーチンはGOMAXPROCSの管理に含まれない。
    • スリープしていたり、通信で待たされているゴルーチン。
      • こちらはそもそもOSスレッドを必要としない。
    • I/Oや他のシステムコール、Goで書かれていない関数呼び出しをしているゴルーチン。

ゴルーチンは識別子を持たない

  • 一般的なマルチスレッドサポートのプログラミング言語では、スレッドに識別子を持たせている
  • これをキーにしたグローバルなマップをスレッドローカル領域と呼び、スレッドごとに独立した値を保存している
  • Goではこの識別子をプログラマが知る手段を提供していない
    • なぜなら乱用されがちだから

ポエム:『YAGNIで思考停止しない』で思考停止しない

最近、設計界隈で『YAGNIで思考停止しない』という言葉をよく耳にする。
だが一方で、YAGNIについてあまり理解せずにこの言葉が使われているのではないかと思うことがある。
YAGNIを手抜きの手段だと捉え、そこで思考停止しているから、『YAGNIで思考停止しない』という言葉が出てくるのではないだろうか?
そんな嫌らしい邪推が頭から離れないので、文章にしてスッキリしようというのがこの記事の目的である。

復習:YAGNIとは?

YAGNIとは、「今必要ないものを作らない」ということだ。
YAGNIを理解するための重要なポイントは、「早さ」と「作り込みすぎない」という2点にある。

まず「早さ」について。
YAGNIで思考停止せずに3日間、うんうんと設計を考えて"素敵な"設計が浮かぶとしよう。
YAGNIを採用するということは、この素敵な設計を作るよりも、3日早く実装をしたほうが良いということだ。3日早くお客さんやビジネスサイドに動くものを提供したほうが良いということだ。3日早くリリースして、ユーザに価値を提供することもできる。もし作っているものがサブスクリプション型のサービスであれば、本来よりも3日分の売上を上げることもできる。つまり、YAGNIを採用するならYAGNIで思考停止すべきだ。思考停止せずにうんうんと考える時間がもったいない。
3日早めるということは、3日早くフィードバックが返ってくるということだ。この早さというものはビジネス上で非常に重要だ。3日前に知っていれば別の選択肢を取れたが、3日後だとその選択肢を取れなくなっていることは本当によくある。
また、フィードバックは知識だ。フィードバックを取り入れることにより、より良いものを作れるようになる。先延ばしにした設計が本当に必要になったときに、最初に設計したときよりも知識が多い状態で設計できる。逆に言うなら、最初にすべてを設計しようとすると、知識が欠乏した状態で設計することになる。

次に「作り込みすぎない」ことについて。
エンジニアは得てして作り込みをしすぎてしまう傾向にある。より抽象的に、より汎用的により応用の効くように作ってしまう傾向にある。それ自体は良いことだ。問題なのは、汎用性が不要なものですら汎用的に作ってしまうことにある。
「現状はS3をストレージとして使っているが、他のストレージに置き換えられるようにインターフェースを切っておこう」これはよくある話だ。だがよく考えてほしい。S3から別のストレージに置き換えることはあるんだろうか?その別のストレージが登場するまで、このコードは読みやすいんだろうか?この設計判断を知らない人がコードを読んだときに、理解しやすいだろうか?
特にバグ調査において、無駄に抽象的なコードほど厄介なものはない。スタックトレースとコードからコンテキストを理解するのに時間がかかるようになる。エラーが発生した行で呼び出されている Storage というインターフェースにどの実装が入っているのか調べる必要がある。だが、 S3Storage という実装から直接呼び出されているのであれば、そんな無駄な労力はかけなくて済む。将来の汎用性のために今現在にコストをかけるのは、非常に苦痛に感じるものである。

勘違いされていそうだが、手抜きの設計はYAGNIではない。YAGNIでは必要になったときに設計をするが、その設計を妥協していいわけではない。同様に「あとでリファクタリングする」もYAGNIの考えから離れている。リファクタリングが必要なのであれば、今するべきだ。YAGNIは品質に使われる言葉ではない。

YAGNIを適用するとき、しないとき

YAGNIはすべての開発に適用できるわけではない。
もしあなたがアジャイル開発をしているのであれば、YAGNIを適用できる。むしろ適用すべきだし、おそらくすでにYAGNIで物事がまわっているのではないだろうか。

もしあなたがアジャイルな開発ではなくアジャイル「で」開発している場合や、ウォーターフォールなどの規範的プロセスで開発している場合、YAGNIを適用するべきではない。というよりもYAGNIを実践できない環境にある。
繰り返しになるが、YAGNIとは「今必要ないものを作らない」ということだ。逆に言うなら「必要になったときには作らないといけない」ということだ。ものごとをフェーズで区切るようなプロセスを採用している場合、必要になったタイミングで作ることができないか、コストが多くかかってしまう。もしこのような環境で他のメンバーがYAGNIを提案してきたときには「YAGNIで思考停止しない」ではなく「うちはYAGNIを使えない環境なんですよ」と言うべきだろう。

YAGNIを適用するときに注意するポイント

アジャイルな開発をしている場合、毎日実装をし、毎日デプロイしているだろう。つまり「必要になったら」のタイミングが毎日来る。よって当然、毎日設計しなければいけない。少しでも最適な設計から外れてしまうとYAGNIによる恩恵は下がってしまうので、気を緩めないことが大事だ。少しでも設計をしやすいようにXPのプラクティスである「コードの縮小」は実践したほうがいいだろう。

YAGNIを適用する際に一番難しいのは見積もりだと思う。
今までの見積もりとかなり見積もり方が変わるだろう。なぜなら、簡単な機能追加であっても、最適な設計にさせるまでにコストがかかることがあるからだ。

  1. 初めて機能を実装するときは、価値を体現する必要があるのでまま時間がかかる
  2. 似た機能を実装するときは、抽象化をする必要があるので、そこそこ時間がかかる
  3. さらに似た機能を実装するときは、すでに抽象化されているので時間はかからない

最初は難しいかもしれないが、感覚を掴めばわりといける。

YAGNIを適用しないときに注意するポイント

YAGNIを適用せず、つまり「もし〇〇したいと思ったときに」という設計をする場合、以下のポイントに注意する必要がある。

  1. 「〇〇したいと思ったとき」が来なかった場合にその設計はわかりやすいか
  2. 設計をしてからその「〇〇したいと思ったとき」が来るまでの間、その設計はわかりやすいか
  3. 「〇〇したいと思ったとき」が来たときに、システムの前提が今と変わらないか

まとめ

  • YAGNIは「今必要ないものを作らない」ということ
  • YAGNIでは早さと作り込みの防止が重要である
  • アジャイルな開発を行っていない場合、YAGNIを使ってはいけない

YAGNIアジャイルラクティスを発祥とする考え方だ。よってYAGNIを理解するためにはアジャイルな考え方を理解する必要がある。特にXPの技術的プラクティスについては前提知識として踏まえておいたほうがいいだろう。