ポエム:『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の技術的プラクティスについては前提知識として踏まえておいたほうがいいだろう。

Goの勉強 ゴルーチンとチャネル

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

ゴルーチン

  • とりあえずスレッドみたいなもの(厳密には違うらしい)
  • main関数を呼び出すゴルーチンはメインゴルーチンと呼ぶ
  • 新たなゴルーチンは go 文で生成できる
  • main関数が終了するとすべてのゴルーチンも終了する
  • mainから戻ることとプログラムを終了させること以外に他のゴルーチンを終了させる方法はない
    • ゴルーチンの停止をリクエストするために他のゴルーチンと通信する方法はある

チャネル

  • チャネルはゴルーチン間の接続
    • ひとつのゴルーチンから他のゴルーチンへ値を送ることができる
  • チャネルはマップと同様に make で作成する。
    • つまり参照になる。
  • 同じ型のチャネルは == で比較可能である。
  • チャネルは「送信」と「受信」の操作を持ち、まとめて「通信」と呼ばれる。
  • 他に、クローズという操作もある。
ch <- x // 送信  
x = <-ch // 受信  
close(ch) // クローズ  

バッファなしチャネル

  • バッファなしチャネルは送信と受信を同期させるために使用する。
  • チャネルに送信されるメッセージのことをイベントと呼ぶ。
  • 同期のみが目的の場合、チャネルの型は struct{} にすることが一般的である。

パイプライン

  • チャネルを使って複数のゴルーチンを接続することをパイプラインと呼ぶ。
  • チャネルは使用した後にすべて閉じる必要はない。
    • 到達不能なチャネルはガベージコレクタがその資源を回収する。

一方向チャネル

  • 送信用、受信用のチャネルを型で宣言することができる。
  • 引数でチャネルを渡すと暗黙的に送信用、受信用に型変換される。
  • 双方向チャネルから一方向チャネルに型変換することはできるが、逆はできない。
chan<- int // 送信用  
<-chan int // 受信用  

バッファありチャネル

ch = make(chan string, 3)  

とすることで容量が3のバッファありチャネルを作成することができる。

  • チャネルがいっぱいになるまで受信を待たずに送信することができる。
  • チャネルが空になるまで送信を待たずに受信することができる。
  • チャネルが空でもいっぱいでもなければ、送信・受信ともにすることができる。

select での多重化

受け取ったチャネルによって処理を変更したいときに select を使う。

select {  
case <-ch1:  
        // ....  
case x := <-ch2:  
        // ....  
case ch3 <- y:  
        // ....  
default:  
        // ....  
}  
  • select{} は永久に待ち続ける。
  • 複数のチャネルから受信可能であれば、どのチャネルが選ばれるかはランダムに選択される。

以下のような処理はポーリングと呼ばれる。

for {  
        select {  
                case <- abort:  
                        return  
                default:  
                        // 何もしない  
        }  
}  

「開発速度が遅い」は誰にとっての問題なのか?

3連休も終わりですね。
連休だったので帰省していたのですが、なんと実家にあの有名な古典の「ライト、ついてますか」があり、思わず一気読みしてしまいました。
この本は問題解決の本で、「エンジニアにきいたオススメの1冊」みたいなのにも取り上げられるような本です。

www.kyoritsu-pub.co.jp

さて、この本いわく、問題を解決する際(または問題について考える際)に、「それは誰にとって問題なのか?」を考えるべきだと書かれています。(つまり、誰がその問題で困っているのか、誰にとっての問題を解決したいのか、ということ。)
例えば本の冒頭では、ビルのエレベーターが遅いという問題に対して、入居者にとっての問題であると見たときと、ビルオーナーにとっての問題であると見たときでは、問題の解決方法が変わってくるよね、ということが書いてあります。

「たしかになぁ・・・」と思いつつ、ふと身近な問題で考えてみようと思い、このブログ記事を書いてます。
僕らエンジニアでよくある問題といえば、「開発速度が遅い」というやつでしょう。

問題定義

問題はもちろん「開発速度が遅い」です。
ここでウォーターフォールについて頭を巡らせるのも楽しそうではありますが、せっかくなので、ベンチャー企業でBtoCの自社サービスを開発していて、アジャイルを、何ならスクラムを採用しているイケイケなチームについて考えてみましょう。

このチームにとって、「開発速度が遅い」という問題は誰にとっての問題なのか、そしてその問題はどのように解決するべきなのかについて考えていきます。

先に結論

というよりも、この記事では結論を出しません。というか僕もよくわかりません。
解決方法を提案する記事ではなく、問題についてぼんやりと考えてみる記事なのです。
もしも結論や解決方法を知りたければ、スクラムフェス三河にでも行って、そこらへんのスクラムマスターに質問してみるのが一番早いと思います。
(質問する前に「『それはチームによって違います』なんて(役に立たない)回答はしないでくださいね」と付け加えておくと、あなたが欲しい結論が得やすいかもしれません。)

誰にとっての問題か?

さて、話を戻しましょう。「開発速度が遅い」のは誰にとっての問題なのでしょうか。
本に倣って候補を挙げてみましょう。

こんなものでしょうか。
候補を細かく見ていきましょう。

まず、この問題はスクラムマスターにとっての問題であると言って間違いないでしょう。
スクラムマスターはプロセスの権威であり、開発速度の遅さはプロセスの問題であることがほとんどだからです。
スクラムの有用性についての責任もあり、開発速度は有用性を測る指標のひとつにもなるでしょう。
開発速度が低いままだと「お前は仕事をしていないじゃないか」と減給されてしまうかもしれませんね。

逆に、開発者にとってこれは問題になりづらいでしょう。
開発者にとって、開発速度が遅いことは(目に見える範囲)で問題になりづらいので、問題として認識されないように思われます。
開発速度が遅いとどのような悪いことが起きるのでしょうか。遠い将来のことを除けば、悪いことは起きないですよね。
なぜならチームの(遅い)速度をベースに見積もりをし、計画を立てているからです。
どちらかというと開発者が問題として認識するのは、開発速度が遅いときではなく、遅くなったときでしょう。
速度が遅くなった場合はプロダクトオーナーと連携を取る必要が出てきますし、その結果スプリント中のトレードオフが発生する可能性もあります。スプリントレビューのシナリオを考え直さないといけないかもしれません。そしてレトロスペクティブでベロシティが下がったことが議題として挙がり、解決策が考えられるでしょう。
ただ、今回問題にしたいのは、「普段よりも遅くなった」ことではなく「普段から遅い」ことなのです。

一番候補から外れやすいのはユーザーでしょう。
「開発速度が遅いとユーザーに機能を届けるのが遅くなってしまう。そうするとユーザーは困ってしまうぞ。」と言う人がたまにいますが、そんなケースは非常にレアです。
なぜなら多くの機能は、ユーザーに事前告知なくリリースされるからです。
「俺はよく知らない機能がなかなかリリースされなくて困っているんだ!」なんて人はいないでしょう。 (そもそもリリースした機能がユーザーに認知されているのかも怪しいです。)
ユーザーにとって開発速度が問題になるときは、バグ修正のリリースや、すでにわかっている機能のリリースなどの場合(例えばそのユーザーが大口顧客で、機能の追加をしないと契約しないぞと言った場合など)に限られます。

ステークホルダーにとっては問題にはなり得ますが、あまり深刻な問題としては捉えていないかもしれません。
例えば営業の場合、既存の機能をもとに営業をかけることのほうが多いからです。
その企業が新たな機能がないと契約してくれない場合でも、他の代案を提案したり、契約を待ってもらうなど、回避策を用意することができる場合がほとんどです。
ただし、開発速度が遅いことによって自分たちの仕事が増えているので、解決できるなら解決したいと思うでしょう。

経営者にとっては深刻な問題になる可能性があります。
例えばVCに「今季中にシングルサインオンと権限管理の機能をリリースする」などと約束している場合などです。
出資取り止めになることはどうしても避けたいので、何としてでもこの問題を解決しないといけません。

プロダクトオーナーにとっては少し複雑な問題です。
開発速度の遅さは直接的にはプロダクトオーナーの問題ではありません。
どんなに開発速度が遅くても責められるのは開発者やスクラムマスターであり、プロダクトオーナーは開発速度に責任を持たなくていいはずです。
一方で、開発速度は「どのような戦略をとるか」ということに関係してきます。
「もっと開発速度が速ければ、こういう戦略も取れたんだけどなぁ」と心の中で思うプロダクトオーナーは多いのではないでしょうか。
また、自分の考えたものがなかなか形にならず世に出ないということは、心にもやもやを抱えることになりそうです。
このもやもやを解消したいと思うのであれば、これはプロダクトオーナーにとっての問題なのかもしれません。

問題解決のむずかしさ

さて、「それは誰にとっての問題なのか?」を考えてみると、なぜこの「開発速度が遅い」という問題が多くの場所で起こり、そしてそれがなかなか解決されないかの一端が見えてくるように思います。

この問題を解決するのは誰なのか、それはきっと開発者でしょう。
開発者が今までのやり方を変えるなり、認識を改めるなり、残業をするなり、何かしらのアクションをしないと開発速度は上がらないでしょう。
一方で、前述したようにこれは開発者にとっての問題ではないのです。
この「問題の当事者と問題の解決者が一致しない」ということが、解決が難しい原因のように思います。

当事者と解決者が一致しない場合、問題を解決するためにはこの問題を開発者の問題にする必要があります。(「今年中にここまでリリースしないと給料を下げるぞ」といったように。)
アジャイル風に言うと「問題を売り込む」でしょうか。
この売り込み方が、非常に難しいように思います。
アジャイルなチームなのであれば、自己組織化をしなければいけません。
つまり、開発者の代わりに問題を解決したり、解決方法を押し付けるようなことは避けるべきです。
では、どんな方法で問題を売り込めばいいんでしょうか。

さて、ここからはまだ考えがまとまっていません。
たとえば

  • 「このままだとVCから投資されなくなってクビにしないといけない」と脅す
  • 「開発速度が速いほうがみんな幸せになるよ」と開発者の良心に訴える

みたいなのはすぐに思いつきますが、あまり良い方法ではなさそうですね。

もしこの問題に興味があって良い売り込み方法が思いついた方がいれば、スクラムフェス三河で登壇してみてはいかがでしょうか。
(7月末までプロポーザル出せるっぽいよ!)

(宣伝記事みたくなってしまいましたが、僕はスクラムフェス三河とは何ら関係はありません。)

Goでスペースっぽい文字を見つける

(空文字列ではない)

Golangでスペースっぽい文字を見つけようとすると、次の3つの方法がありそうです。

この3つについて、自分はいまいち使い分けがわかっていなかったので、調べてみました。
ずらっとリストにしてみます。(たいていは知らない文字だけど)

文字名 わかりやすく言うと unicode の番号 unicode.IsSpace \s \pZ
Character Tabulation タブ文字(\t) U+0009 o o x
End Of Line 改行文字(\n) U+000A o o x
Line Tabulation 垂直タブ(\v) U+000B o x x
Form Feed \f U+000C o o x
Carriage Return \r U+000D o o x
Space 半角スペース U+0020 o o o
Next Line U+0085 o x x
No-Break Space U+00A0 o x o
Ogham Space Mark U+1680 o x o
En Quad U+2000 o x o
Em Quad U+2001 o x o
En Space U+2002 o x o
Em Space U+2003 o x o
Three-Per-Em Space U+2004 o x o
Four-Per-Em Space U+2005 o x o
Six-Per-Em Space U+2006 o x o
Figure Space U+2007 o x o
Punctuation Space U+2008 o x o
Thin Space U+2009 o x o
Hair Space U+200A o x o
Line Separator U+2028 o x o
Paragraph Separator U+2029 o x o
Narrow No-Break Space U+202F o x o
Medium Mathematical Space U+205F o x o
Ideographic Space 全角スペース U+3000 o x o

まとめると次のような感じなんでしょうか。

分類 文字
どれでも判定できる 半角スペース
unicode.IsSpace でしか判定できない 垂直タブ・Next Line
\pZ では判定できない 制御文字・Next Line
\s では判定できない いっぱいある

結論としては、 unicode.IsSpace を使えば良さそう という感じでしょうか。

Goの勉強 インターフェース

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

契約としてのインターフェース

  • GoはStructualTypingを採用している。
    • インターフェースを明示的に宣言していなくても、シグネチャを満たしていればそのインターフェースだと見做せる。

インターフェース型

// インターフェースの宣言  
type InterfaceName interface {  
    MethodName(args int) bool  
}  

// 既存のインターフェースの流用  
type InterfaceName2 interface {  
    InterfaceName  
    io.Writer  
}  

インターフェースを満足する

interface{} は空インターフェースと呼ばれ、どんな値でも入れることができる。

var any interface{}  
any = 1  
any = 'r'  
any = "string"  
any = map[string]int{"one", 1}  
any = new(bytes.Buffer)  

以下のようにインターフェースとの関係性を明示的に示すことができる。

var _ io.Writer = (*bytes.Buffer)(nil)  

インターフェース値

インターフェース値は

  • 動的な型(具象型)
  • 動的な値(その型の値)

の2つを持っている。

コード
var w io.Writer nil nil
w = os.Stdout *os.File os.Stdoutのコピー
w = new(bytes.Buffer) *bytes.Buffer 新たな Buffer へのポインタ
w = nil nil nil
  • インターフェース値の動的な型が何であるかはコンパイル時にはわからない。
  • インターフェース値は動的な型が比較可能であれば比較できるし、比較不可能であれば比較できない
    • 他の型と違い、比較可能かどうかが型のみで判定できない
  • nil ポインタを含むインターフェースは nil ではない

アサーション

以下のように書くことで、型が別の型がどうか、あるいは満足するかを確認できる。

var w io.Writer = os.stdout  
f, ok := w.(*os.File)  

型switch

型による分岐を以下のように書くことができる。

switch x := x.(type) {  
case nil : //...  
case int, uint : //...  
case bool : //...  
case string : //...  
default : //...  
}  

ちょっとした助言

Goでは、インターフェースは2つ以上の具象型により満足されている場合にだけ使われる。

PHPerKaigi2023にいくと登壇しなくてもアウトプットが増える

PHPerKaigi2023 にいってきました。
PHPerKaigiは2,3年ぶりの参加です。
そして、初の一般参加です。
僕は登壇したわけではないですが、なぜかアウトプットが増えました。
不思議です。

🍺の力でアウトプットがはえ

PHPerKaigiといえば🍺です。
🍺しか飲んでいない気もします。
LTあたりから会場にバドワイザーが提供され2本くらいあけ、クロージングをした後は当然のように居酒屋にいって🍺を飲みます。
そんなに飲んでいれば他の人との会話は弾んで色々な話をきけるし、飲みすぎで寝付きが悪くなるわけです。

寝れないから暇なんで、飲みながら話した内容をなんとなくスライドにするわけです・・・

なぜか500以上ものViewが付き、自分の公開資料の中で3位になりました。
このスライドで発表してないんですけどね。

不具合によってアウトプットがはえ

blog.phperkaigi.jp

forteeからキャンペーンアイコンをダウンロードできたらしいです。
らしいです。

・・・自分は表示されなかったのですが。。。

調べてみると、どうやら500エラーになっているようです。

エラーログをいっぱい出したのが運営に届いたらしく、forteeからフレーム画像をダウンロードできるようになっていて、「自分で合成してね!」とのことでした。

でもそんな画像編集ソフトなんてないしなぁ・・・

ということで、フレームを合成するPHPのプログラムを作りました。ここはPHPerKaigiですからね。

github.com

最終日でしたが、僕も無事にフレームを付けることができました!

みんなもPHPerKaigiに参加してアウトプットを増やそう

PHPerKaigiは参加するだけでアウトプットが増える素敵なカンファレンスです!
ぜひみなさん来年もPHPerKaigiに参加してアウトプットを増やしましょう!

Goの勉強 メソッド

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

メソッド宣言

type Point struct{ X, Y float64 }  
func (p Point) Distance(q Point) float64 {  
    // ...  
}  

は次と一緒

<?php  

class Point  
{  
    public float $x  
    public float $y  
    
    public function distance(self q)  
    {  
        //...  
    }  
}  
  • Go では thisself のような表現は使用しない。
  • フィールドと同じ名前のメソッドを宣言することはできない。

ポインタレシーバを持つメソッド

  • Go では引数はすべてコピーされるため、大きい引数や更新を行いたい場合はポインタを引数とする必要がある。
  • 慣習的に、ひとつでもポインタレシーバを持つメソッドがあるのであれば、その型のすべてのメソッドはポインタレシーバを持つようにする。

メソッド値とメソッド式

次はメソッド値

p := Point{1, 2}  
q := Point{4, 6}  

distanceFromP := p.Distance  
fmt.println(distanceFromP(q)) // 5  

次はメソッド式

p := Point{1, 2}  
q := Point{4, 6}  

distance := Point.Distance  
fmt.println(distance(p, q)) // 5  

カプセル化

  • 他の言語と違い、Goの可視性はパッケージ単位でしか行えない。
  • Getter を定義する場合、慣習的に Get プレフィックスは付けない。
    • Setter の場合は Set を付ける。