年末にThinkPad X1 Carbonを手に入れ、環境を作っていました。これまではホストOSをWindowsに、開発などではゲストOSをLinuxにしていましたが、今回はホストをLinuxにしようとしていろいろ試してみました。結論としては、これまでと変わらずホストWindowsになってしまったのですが、それまでの経緯や調べたことなどを記録しておきます。誰かの参考になるかもしれないので、役に立ったリンクも張っておきます。

3行まとめ

  • ホストLinuxでは新しめのハードウェアに苦労する
  • GUIアプリの操作感は、ゲストOSはだいぶ不利
  • それ以外のパフォーマンスは、ゲストOSでも遜色ない

やったこと

新マシンのスペックはこんな感じです。

Windowsプレインストールですが、回復メディアを作ったらさっさと初期化、まずはUbuntu Desktop 14.04を入れてみました。そこから、Ubuntu上で暮らしていけるかの確認(主にハードウェア)と、Windowsをゲストにして作業が快適か(主にOfficeを使う)を、調べていきます。

ハードウェアについては、自分にとってはいきなり致命的だったのが、WiGigドックとBluetoothマウスの問題でした。WiGigドックはLinuxでの情報が見つからず(「数年以内にはサポートするんじゃない?」とか )、断念。

マウスはロジクールMX Anywhere 2Bluetooth接続ですが、Ubuntu 14.04ではPINなしのペアリングに対応してないとのこと。回答の中に「DON'T DO THIS!」とあったりもしたので、こちらも諦める方向(実のところPPA追加だけなので、大した騒ぎではないのですけど)。

また指紋認証も、fprintのサポート外デバイスらしく、使えるようにできませんでした。 (こちらのリンクの138a:0090に該当。)

Windowsゲストの導入

気を取り直して、Windows 10をゲストでインストールします。qemu+kvmVirtualBoxをそれぞれ試しました。Windows10はインストールUSBディスクを作っておいたものの、VMをUSBブートさせる方法がわからず、isoイメージから。基本的には問題なかったものの、引っかかった点をいくつか。

Windows上ではブラウザやIntelliJ IDEAを操作してみましたが、もっさり感があるのと、キーリピートが不自然に中断されるなどの微妙な使いにくさがありました。(これは対策したけど変わらず)

ベンチマーク(要約)

その後、新マシンにあらためてWindowsを(ホストとして)新規インストールして、さらにVirtualBoxHyper-VそれぞれでUbuntu Desktop 14.04を入れ、PCMark8や、pythonベンチマークを取ってみました。ここでは要約だけで、より詳しくは下の方に載せておきます。

PCMark8はWindows専用で、ブラウジングなど一般的なアプリケーション利用の操作感のスコアを出してくれる、ようです。ホストの時は2644、ゲストの時は1250前後となり、半分くらいでした。ただし項目ごとに見ると、差があるのは一部の項目だけで、そこまでの差はないようです。

Pythonでは総合的なベンチマークを取りました。こちらの結果は、傾向としてLinuxホスト>Linuxゲスト>Windowsホスト>Windowsゲストという、わかりやすい順位になりました。

ベンチマークのスコアを見る限りでは、どちらがホストでも極端な差は出ないようです。ただしアプリケーションの操作感は、Linuxホスト/Windowsゲストだとどうも使い辛く感じます。気のせい、プラセボ、思い込みかもしれません。

当面の結論

なにより、使いたいハードウェアを使えないという点で、WindowsをホストOSにして(もう少し)生きていくことにしました。ただし開発はゲストのUbuntu Desktop上で、IntelliJ IDEAも含めて使っていくつもりです(Windows上のPythonは、慣れたけど、やっぱりツライ)。

環境にかける情熱がだいぶ減っているなあというのも発見でした。なんとかなりそうでも、すぐ面倒くさくなる。いったん解決できても、今後のことやまたトラブル出たらと、思ってしまう。前にやった解決方法とかも、毎回忘れてるし。

そういえばゲストWindowsでOfficeを使ってみれば良かったなあ(面倒なので諦めた)。

ベンチマーク結果

PCMark8のベンチマークWindows専用です。無料で使えるHome Conventional 3.0を実行します。結果、ゲストではホストの約半分のスコアでした(2644に対し1250前後)。qemu(1259)とVirtualBox(1235)では、目立った差はありませんでした。

しかし項目別に見ると、Casual GamingとVideo Chat encodingが極端に悪いものの、それ以外ではそこまでの差はないように見えます。(いずれも、ホストの場合を1.0とした相対値。[s][ms]は時間なので長い方が遅く、[fps]は短い方が遅い)

ゲストを実際に操作すると、もっさりというか、引っかかるというか、使い辛い気がするんですが、ユーザーの操作に対する反応性だけであって、総合的なパフォーマンスは悪くないようです。

pythonのパフォーマンスはPython benchmark suiteというものを使いました。様々なベンチマーク(純粋なPythonの処理から、Web、DB、jsonxmlなどまで)をまとめて実行してくれるものです。画面表示・描画はありません。
グラフはそれぞれのかたまりがベンチマークの種類に対応し、その中の7本のバーが左から、以下の順序になっています。長さは実行時間で、長い方が遅くなります。いずれのベンチマークでも、Windowsゲストを1.0とした相対値になっています。

  1. Windowsゲスト(VirtualBox上)
  2. Windowsホスト
  3. Linuxゲスト(Hyper-V上)
  4. Linuxゲスト(VirtualBox上)
  5. Linuxホスト
  6. X240sでのWindowsホスト(参考値)
  7. X240sでのLinuxゲスト(Debian。参考値)



全体的な傾向としては、前述のように 5>4>3>2>1の順に速く(グラフは短く)なっています。項目によっては、Windowsのほうが速かったりと、異なる傾向のものもあります。(なお、極端な差が出たり、環境によって実行できなかった項目は、除いています。) Linuxゲストは、Hyper-VよりVirtualBoxのほうが成績が良いようですが、厳密に同じ設定になっていないかもしれないので、ここは参考程度に。

なおPython benchmark suiteで利用しているGitHub - vstinner/perf: Toolkit to run Python benchmarksというモジュールが現在Windowsで動かず、パッチを作って使いました。うまくいけばそのうち取り込まれると思います。

一歩ずつ進める

あなたが解きたい問題には、簡単なものもあれば難しいものもあります。
単純なものもあるし複雑なものもあります。とても簡単でとても単純な問題だったら、一発で解決できるかもしれません。しかしもう少し難しかったりもう少し複雑だったりすると、一発では解決できなくなってきます。こうした問題は少しずつ、一手ずつ解いていくことになります。

大きくて複雑な問題を一息で解決できれば、カッコいいかもしれません。
しかしそのやり方にはリスクがあります。一息でやっつけようとして、それが上手くいかないと、振り出しに戻ってしまい一歩も先へ進めません。ことによっては、最初よりも状況が悪化してしまうかもしれません。カッコよさよりも、問題解決という目的を大切にして、確実で効果のある進め方が必要です。小さな問題でも大きな問題でも、自分に合った歩幅で一歩ずつ進めましょう。ひとくちサイズずつ食べるという言い方もします。

一歩ずつ進めるアプローチにはメリットがたくさんあります。一歩で考えるのは小さな問題、元の問題から切り出した一部分になります。そうすると同時に考えないといけないことが減って、比較的簡単に解決できるようになります。問題はわかりやすく、解決方法もシンプルで、善し悪しも客観的に評価しやすくなります。解決方法が正しいかどうか、判断も容易です。考慮漏れや見落としが入り込む可能性も小さくなります。さらにより良い解決方法を求めて、方法自体を洗練させたり、他の案を検討したりもできます。

例題:
  • 皿洗い問題 汚れたお皿が10枚あります。洗って拭いて食器棚にしまわないといけません。
    • どういうやりかたをしますか?
    • 100枚だったらどうですか? 10000枚だったら?

自分の歩幅

一歩ずつ進めるといっても、どんなふうに一歩を決めればいいでしょうか。Webアプリの開発を例にすると、たとえば次のように6つに区切るという手があります(あくまで一例です)。

  1. 画面を実装して入力できるようにする
  2. 入力したデータをどう保持するか設計する
  3. 入力と既存のデータを組み合わせて保存する実装をする
  4. ユーザーが使いやすいよう画面を修正する
  5. 全体が矛盾なく整合しているかテストする
  6. 実行時間が遅くなったりしないか確認する

1ステップごとに、プログラムが少しずつ増え、完成に近づいていきます。2番は設計を検討するだけですし、5番や6番はテストやレビューが中心で、コードは書かないようなステップもあります。

こうした「一歩の大きさ」は様々です。ベテランならば1番から6番までまとめて一歩で片付けてしまうかもしれません。新人だったら、もっと細かく分けないとうまく進めないかもしれません。あるいはベテランでも、1画面に300項目あるような複雑な機能なら、念入りに小さな一歩で進めるでしょう。Rubyならまとめてできる人でも、C++では設計だけ先に考えるということもあります。問題の性質、人の経験やスキル、解決に使う道具やテクノロジーによって、妥当な一歩の大きさが変わってきます。自分が自信を持てる、これなら踏み外したり、うっかり間違えたりしないと思えるような一歩を、そのときどきで選んでください。

プログラミングの世界ではよく、設計・実装・テストという作業をしますが、こうした「作業の分解」も一歩ずつ進める一つの方法です。

  • 設計: 対象の問題全体を解決する方法を決める
  • 実装: 設計で決めた方法を具体化する
  • テスト: 問題が解決したか確かめる
例題:
  • 普通の一歩問題 普通に歩いているときの、一歩の大きさを測ってください。そのうえで、「自分が自信を持てる」一歩の大きさは、どのくらいだと思いますか?
    • 平坦な道路では?急な山道だったら?ツルツルに凍った氷の上なら?
    • 裸足のとき、履き慣れた靴のとき、ビーチサンダル、下駄、竹馬ではどうでしょう?
    • 空中100メートルで綱渡りをするとしたら、一歩の大きさはどのくらいですか?一歩でも進めますか?
  • 間違えない問題 プログラムを書くとき、何文字、あるいは何行までなら絶対に一箇所も間違えないで書けますか?
    • 確かめてください。
    • 自分で自信があると感じる度合いと、実際に間違えてしまう割合は、合っていますか?

前に進まない一歩

一歩ずつ着実に確実に前に進むという考え方ではあるのですが、前に進まない一歩というものもあります。進めていくうちに、わからないところが見つかることがあります。最初の想定と違う箇所が出てくることもあるし、今までやったことが間違っていたとわかることもあります。単に、自信が持てなくなることだってあります。そうしたときは、前進するのをいったんやめて、考え直さないといけません。状況を再確認したり、方針を見直したり、情報を収集したりするのも、前に進まない感じがありますがこれもまた一歩です。

大きな問題を少しずつ解決するのは、長い道のりを一歩ずつ進むようなものです。一歩ずつ進んでいると、先行きがわからなくなったり、見失ったりしやすくなります。全体として問題解決に近づいているかどうか、ときどき周りを見渡し、行く手を確認したほうがよいでしょう。本当に前進しているか、解決に近づいているか、事実の確認と仮説の検証をしながら進んでください。道を間違えていると困るので、ときどき行く手を探索して情報収集しましょう。むしろ、一歩ずつ進んでいるからこそ、問題点や不明点に気づくことができます。

コラム 前に進まない楽しい仕事

プログラミングというのは楽しいものです。 楽しいことをしていると、つい時間を忘れて没頭しがちです。 やっていることが面白くなって、もともとの目的を忘れてしまうことも、またありがちです。 コードの美しさにこだわったり、アルゴリズムのチューニングに熱中したり、モデリング中毒になったり、アニメーションの気持ちよさを追求したりと、ハマりどころはたくさんあります。

そういう点ももちろん大事なので、かけるべき時間はかけましょう。しかし今やらないといけないのか、どれくらい時間をかけるべきなのか、意識して確認しましょう。先にやるべきことは先にやる、後に回せることは後にするのが大事です。

いま、なんのために、どんな一歩を踏んでいるところなのかを忘れずに。

解としての一歩ずつ

そもそもプログラミングとは、問題を解くために、コードを1行ずつ書くことです。プログラムはまさしく、一歩ずつの積み重ねでできているわけです。アルゴリズムを勉強していると、解説はたいてい、1ステップずつの処理に分けて書いてあります。分割統治との組み合わせで、様々なアルゴリズムにおいて、扱いやすい一部分だけを取り出して処理するというアプローチが利用されています。問題解決の方法と進め方はどちらも、一歩ずつ進めるのと相性がいいのです。

一歩ずつのアプローチは、後から他の人が見たときも理解しやすくなります。理解するほうも、やはり一歩ずつ理解を進められるためです。一歩ずつの考え方を反映したプログラムやシステムの構造も、同様です。

例題
  • 問題分解問題 スマホアプリに入力したデータをサーバで集計して、運用者がGoogle Spreadsheetで見られるようにしたいとします。
    • まずこの問題を3つの問題に分解してください。
    • 3つに分けたそれぞれを、さらに小さな問題に分解してみてください。ここでも一歩ずつ進めましょう。一気にうんと小さくしようとせず、2つか3つくらいずつ、段階を追って分解してください。

https://github.com/yattom/text_for_programmer/wiki/%E4%B8%80%E6%AD%A9%E3%81%9A%E3%81%A4%E9%80%B2%E3%82%81%E3%82%8B

抽象化

問題を解決するには、まず問題を把握しなくてはなりません。 しかし一歩ずつ進めるうえでの最初の一歩から悩むこともあります。 たくさんの要素を同時に考えないといけなかったり、多くの事柄が複雑に関連していて、小さな部分に切り出すのが難しいことがあります。 分割統治するための分割が、難しいわけです。 いちどに把握して検討できるものの数には限度があって、それ以上になるとまとめて考えることができなくなり、見落としや考慮漏れが発生します。

例として、週末にパーティーを開くために買い物をすることになったと考えてみましょう。 ほしいものを考えながら買い物リストを作っていたら、ものすごく長くなってしまいました。 全部で100個はありそうです。 近所のスーパーで買えるものもあれば、駅前の商店街で買いたいもの、デパートまで行かないといけないものもあります。 これを整理しないで買い物に出かけたら、スーパーと商店街を何回も往復したり、せっかくデパートまで行っても買い忘れをしたりしそうです。

そこで、買い物リストを分割することにします。 どこで買うかによって、リストを3つに分けます。 これで、スーパーの買い物リスト、商店街の買い物リスト、デパートの買い物リストの3つのリストができました。 これで買い物を無駄なく、忘れ物もなくできそうです。

いまの例では、100個以上の項目がある「買い物リスト」を、「どこで買うか」という観点で整理して3つの買い物リストにしました。 整理する前の問題は「リストを見ながらどこで買うか考えながら買い物をして回る」というもので、移動と買い物を同時に考える必要がありました。 100個以上も頭の中だけで整理していたら、移動の無駄が起きたり、買い忘れが起きてしまいます。 整理して3つの買い物リストに分けたおかげで、今度は問題も分割できました。 まず「買い物する場所に行く」という問題、そして「その場所で買い物をする」という問題の2つです。

「買い物リスト」を「どこで買うか」という観点で見直す、これが抽象化の一例です。 大きく複雑なひとかたまりの問題を、観点によって整理し、簡単に解けるいくつかの問題に分割するのです。 細かい点を見ないようにしたり、差異を無視して同じものと扱ったり、似た点があるものをグループ化したりするのが、抽象化です。

「抽象的」は「具体的」の反対の意味です。 抽象化とは、具体的の反対にすることなので、必ず情報が失われます。 このことから「抽象」は「捨象」とも言います。 情報を捨てるという意味です。 いま着目したい、重要視したいところだけを取り出し、それ以外の情報は無視して考える。 それが抽象化です。

例題:
  • 最寄り駅から家までの地図を書いてください。書き終わったら地図を観察して、どんな情報が載っているか、どんな情報は捨てたのか書き出してください。なぜその情報を選んだのか、考えてみてください。
  • 会社で泊まりがけの合宿をすることになり、宿泊場所の候補を探してほしいと頼まれてしまいました。いくつかの宿泊施設を選んで、それぞれの特徴や情報を整理して一覧表にしないといけません。どういう情報を載せますか?
    • 家族や仲間との遊びの旅行だったら、載せる情報は変わりますか?それはなぜですか?
  • ブラウザで適当なWebページを開いてください。そのページの構成や内容について、抽象化して説明してください。

抽象化のレベル

整理した3つのリストを、3枚のメモ用紙に分けて書いて、机の上に置いてみましょう。 ここからちょっと離れてみてください。 2メートルくらい離れると(すごく目のいい人なら5メートルくらい)、もうリストになにが書いてあるかはわからなくなるはずです。 それでもリストが3つあることはわかります。 左がスーパー、真ん中が商店街、右がデパートで買うもののリストです。 これくらい離れると、買うもの個々のことは気にせず、どこに買い物に行くかだけ考えられます。 デパート→商店街→スーパーの順に回るか? 商店街とスーパーは仕事帰りに寄って、デパートは土曜日にするか?

行く場所が決まったら、また机のところに戻りましょう。 そしてこれから最初に行く場所のメモを1枚取って、目の前に持ってきてください。 何を買うのかよく見えるようになったので、買い物の内容をチェックできます。 飲み物は3本で足りるか?料理の材料に忘れ物はないか?花を飾ろうか、いらないだろうか? このとき、他のリストは見えなくなっています。 一箇所の買い物だけに集中できて、その後どこへ行くのか気にしなくてすみます。

物事を抽象化すると、それぞれ異なる抽象化のレベルが見つかります。 この例では「リストを選ぶレベル」と「買い物で使うレベル」の2つできました。 それぞれのレベルでは、それぞれの問題に集中できます。 そしてあるレベルで考えたり判断したことは、他のレベルに影響しません。 いまは「スーパー」「商店街」「デパート」の3つに整理していますが、たとえば商店街のどの店で買うのか、デパートのどのフロアに行くのかを考えたくなるかもしれません そうすればレベルが増えて、商店街を回る順番や、デパートで寄るフロアを考えることもできるようになります。

具体的に近い方を「低レベル」、抽象度の高い方を「高レベル」と呼ぶこともあります。 これは誰かをほめたり、けなしたりしているわけではなく、単に抽象化の度合いを表している言葉です。 高い低いを、空を飛ぶ鳥や飛行機のような高度でイメージしてみてください。 地面に近い、低いところでは、いろいろなものが詳細に、具体的に見えます。 地面から遠く、高い高度まで上がっていくと、広い範囲が見渡せるようになります。 低レベルでは詳細度が上がりますが視野が狭まり、高レベルでは詳細は見えませんが視野が広くなります。 どちらが大事とか、偉いとかいう話ではありません。 それぞれのレベルで見えるものが違い、考えられることも、解決できることも違うという考え方です。

問題のどの部分を、どのレベルから解決していくのか意識すると、問題全体にどうアプローチしていくのか作戦が立てられるようになります。 そうすると抽象レベルを高く上がったり低く下がったり、行ったり来たりしながら問題に取り組むことになります。 自分がいまどのレベルで考えているのか、そこでどういう問題を解こうとしているのか、忘れないようにしましょう。

例題:
  • Googleマップで自宅周辺の地図を表示して、拡大・縮小してみましょう。4つくらいのレベルを決めて、それぞれ比較してください。(スクリーンショットを保存して見比べるとよいです。)
    • それぞれのレベルでは、何が見え、何が見えないですか?どんなことがわかりますか?
    • それぞれのレベルの地図は、どんな使い道に向くと思いますか?
    • 同じことを、衛星写真(Earth)でやったり、交通状況を切り替えたりして、比較してください。
  • ふだん使っているソフトウェアやアプリについて、4つの抽象レベルで、それぞれ100文字程度で説明してください。
    1. 今日(または最後に使ったとき)の具体的なやったこと、使い方
    2. 自分がなんのためにそのソフトウェアを使うのか、その目的
    3. 類似するソフトウェアと比較した特徴
    4. 90歳のおばあちゃん(または3歳の子供)にそのソフトウェアを説明する
    • それぞれの説明の違いはなにか、なぜそのように変えたのか、整理してください
    • 他にどんな抽象レベルがあるか、考えてみてください
      • 例: そのソフトウェアの開発者なら?その会社の社長なら?株式アナリストなら?ライバルなら?

観点が重要

ここまでの例や例題で見てきたように、抽象化はソフトウェア開発に限らず、あらゆる場面で使われています。 もちろんソフトウェア開発やシステム開発でも、問題解決のためさまざまな抽象化がおこなわれています。 システムは一般的に複雑なものなので、抽象化も様々な観点が必要となります。 設計書、仕様書などのドキュメントはほとんどが、なんらかの観点からシステムを抽象化して記述したものです。 (なおドキュメントの呼称や内容は、会社や現場、人によって大きく違うので、知っているのと違うと思うかもしれません。 あくまで一例です。)

  • ワイヤーフレーム
    • 観点は画面の種類や数と、表示する主要な情報、遷移の流れなど
  • フローチャート
    • 一連の処理がどこから始まりどこで終わるか、どう流れていくかという観点
  • 詳細仕様書
    • 対象としている範囲に限った、プログラミングに必要な情報をすべて含む観点
  • システム概要
    • システムに登場するもの最大粒度で表現する観点で、システムの境界を定め、対象と対象外を区別するのに使う

このように観点しだいで抽象化の結果も目的も変わってきます。 どういう観点を選べばいいのか、どういう問題にはどういう観点を使えばいいのか、これは知識と経験が必要になってきます。 また観点の選び方が悪いと、問題解決の役に立たないどころか、かえって事態を混乱させてしまうこともあります。

買い物リストの話で考えてみましょう。 どこで買うかという観点でリストを整理し、3つのリストを作って、「どこに行くか」と「その場所で何を買うか」を分離して考えられるようにしました。 ところで、もし花を買うとしたら、どこで買うといいでしょうか? 花はデパートでも買えるし、商店街にも花屋がありそうですし、スーパーでも売っています。 花を実際に見て値段も確認してから買おうと思ったら、どのリストに入れておくといいでしょうか。 もし3箇所見比べたかったら、「どこに行くか」を考えるとき考慮しないといけなくなります。 こうなると、せっかくリストを整理したのに、「花をどこで買うか」という新たな問題が発生してしまいます。

また別の観点もあるかもしれません。 たとえば、予算が決まっていて、ほしいものは全部買えないかもしれません。 リストが1つだけだったら、リストの中身を必要な順に並べ替えて、予算がなくなるまで上から順に買えばいいでしょう。 しかしリストが3つに分かれていると、どのリストをどこまで買うべきなのかわからなくなってしまいます。 全体の予算があると、「どこで買うか」という観点の抽象化は不適切なわけです。 先に予算の問題を解決しないと、適用できない観点だということになります。

観点と抽象化レベルによって、もともとの大きな問題を分割してそれぞれ個別に解いていけます。 しかし、分離しきれない問題が残ることもあって、これはレベルをまたいで考える必要があります。 観点が間違っていて、問題が複雑化してしまうこともあります。 もともとの問題を全部理解して把握できていれば、あとからこんなことに気づいたりしないですむかもしれません。 しかしそもそも、問題は複雑で理解も把握も難しいから、いろいろ工夫をしているわけです。 「考えればわかること」は「考えたからわかる」ので、最初からわかっていれば苦労しません。

そこで、抽象化がうまく働いているか、問題がきれいに分割できているか、進めながら確かめる必要が出てきます。 高レベルで問題を解決したら、それが低レベルでもちゃんと解決できているか調べます。 また、他のレベルに不要な影響を与えていないかも、要確認です。 こうした確認を通じて、設定した観点と抽象レベルが妥当なものだったかわかってくるのです。 抽象レベルを自由自在に行き来しながら問題を解決し検証するというのは、とても高度なスキルですが、これができるようになるとプログラマとしての問題解決能力が大きく広がります。

コラム: アーキテクトも実装する

ソフトウェア開発ではよく「上流の設計が、下流で矛盾を生む」という事態が起きます。 その理由のひとつが、こうした抽象化の誤りです。(理由は他にもたくさんありますが。) 上流SEや、アーキテクト、プロジェクトリーダーといった立場の人が、高レベルのことだけ考えて低レベルでの確認を怠ると、実装時に設計を考え直さないといけなくなったり、実装できない設計になったりしてしまいます。

『組織パターン』には「アーキテクトも実装する」というパターンがあり、全体の方向性を決定するアーキテクトもプログラマーと一緒にコードを書くことの重要性を説いています。 また『人月の神話』の「第3章 外科手術チーム」では、生産性の非常に高い10名の開発チームには「執刀医」に当たるチーフプログラマーがいて、「自ら機能と性能を定義し、プログラムをデザインし、コーディングし、テストして文書を書く」と述べています。 ソフトウェア開発において実装というのはかなり具体的な活動、低レベルなものですが、ここまで下りてこないと本当には確認できない問題というのがたくさんあるのです。 (ちなみに、さらに低く、OSやメモリ、通信プロトコルや物理マシンといったレベルまで下がっていくこともできます。)

例題:
  • 何人かのグループを男女に分けるという場面をよく目にすると思います。どんな状況があるか、男女に分けることでどんな問題を解決しようとしているのか、いくつか書き出してください。
    • 男女に分けるという問題解決が、どういった観点からもたらされたのか、考えてください。
    • 男女に分けたせいで問題が起きることもあります。どんな観点だと、かえって状況が悪化すると思いますか。
  • いま利用しているプログラミング言語を、他の言語と比較しながら、なぜ自分がいまそちらを使っているのか説明してください。できれば他の言語は2つ以上選んでください。

https://github.com/yattom/text_for_programmer/wiki/%E6%8A%BD%E8%B1%A1%E5%8C%96

C言語でTDDを学ぶ?TDDでC言語を学ぶ?

Facebook上で議論になったのですが、C言語でTDDを勉強していると、「よりよい設計」になりにくいという話になりました。

例として整数の区間の最初のほうの課題を考えてみます。

課題1-1
下端点と上端点を与えて閉区間を生成しよう
区間から下端点と上端点を取得しよう

TDDで実装したコードとテスト

    // プロダクションコード
    int upper_point;
    int lower_point;
    
    // テストコード
    TEST(Range, Endpoint) {
        lower_point = 3;
        upper_point = 8;
        EXPECT_EQ(3, lower_point);
        EXPECT_EQ(8, upper_point);
    }

問題:どうしたら関数を導入できるでしょうか?

元のお題はオブジェクト指向を想定しているところがあって、「生成しよう」でオブジェクトを作り、「取得しよう」でオブジェクトから値を取り出すという形になるんですが、C言語では「値を与えて取り出す」のならば、変数があればいいよねと。とりあえずグローバルでも構わない、というのは、お題(要求仕様)に何も書いてないから。(externがないのでモジュール内に閉じていますけれど。逆に、プロダクションコードとテストコードを別モジュールにしたら、グローバルになってしまいますね。)

TDDのやり方に律儀に従うなら、新たなテストによって現在の(グローバル変数だけで生成は関数不要という)設計の限界が明らかになり、よりよい、シンプルな設計が導かれます。区間のお題の続きに「閉区間が別の閉区間と等しいか」があるので、複数の区間を扱うにはグローバル変数では困るよね……と思いきや、こうなります。

    // プロダクションコード
    int upper_point;
    int lower_point;
    
    int range_equal(int lower_point2, int upper_point2) {
        return lower_point == lower_point2 && upper_point == upper_point2;
    }

    // テストコード
    TEST(Range, Endpoint) { /* 略 */ }

    TEST(Range, Equal) {
        lower_point = 3;
        upper_point = 8;
        EXPECT_TRUE(range_equal(3, 8)); // [3,8]と[3,8]は等しいはず
        EXPECT_FALSE(range_equal(1, 9)); // [3,8]と[1,9]は等しくないはず
    }

うん、グローバルに設定したものとパラメータで渡すものを比較すればいいよね……間違ってはいない。

TDDでシンプルな解を追求する

TDDには(ひいてはXPには)「シンプルさ」という価値があります。TDDではシンプルな解をよしとします。ここには「(与えられた問題や制約をすべて充足するなかでもっとも)シンプル」という条件が隠れています。テストコードを眺めて、「比較対象を片方は変数に、片方はパラメータで渡すのは、シンプルでない」と思えば、以下のように書き直せます。

    // プロダクションコード
    int upper_point;
    int lower_point;
    
    int range_equal(int lower_point1, int upper_point1,
                    int lower_point2, int upper_point2) {
        upper_point = upper_point1;
        lower_point = lower_point1;
        return lower_point == lower_point2 && upper_point == upper_point2;
    }
    
    // テストコード
    TEST(Range, Endpoint) { /* 略 */ }
    
    TEST(Range, Equal) {
        EXPECT_TRUE(range_equal(3, 8, 3, 8)); // [3,8]と[3,8]は等しいはず
        EXPECT_FALSE(range_equal(3, 8, 1, 9)); // [3,8]と[1,9]は等しくないはず
    }

しぶといな、グローバル変数

TDDが自動的に良い設計を保証するわけではありません。「TDDを使ってよりよい設計に到達する」のと「TDDを使えばよりよい設計になる」の間には、深い深い溝があります。TDDを使うと設計品質が悪くなるという調査もあります。少なくとも、何が「良い設計」であるか、言語やライブラリやフレームワークのどんな特性を利用すれば「良い設計」になるか、知識がなければ設計として実現できません。

元々のお題にはありませんが、このようなグローバル変数を使った設計に対しては、別の課題を設定してあげるという手を思いつきました。グローバル変数を避けるべき理由として「いつの間にか値が書き換わって危険」という知識があります。range_equal()を呼ぶと書き換わってしまうので、自明でない挙動をする。そこで「比較しても値が書き換わらないようにする」という課題が考えられます。それをテストで表現すれば、TDDの流れで設計を改善できます。

    // テストコード ※失敗する
    TEST(Range, Equal_DoesNotChange) {
        lower_point = 3;
        upper_point = 8;
        range_equal(0, 5, 3, 8);
        EXPECT_EQ(3, lower_point);  // range_equal()を呼んでも変化しないはず
        EXPECT_EQ(8, upper_point);
    }

このテストは失敗するので、グリーンになるように実装を直します。目指すのはグローバル変数の除去ですが、そうすると実装を直す過程で上端・下端を生成したり取得したりというテストが、成立しなくなってしまいます。おまけに今書いたばかりのテストも、中身がなくなってしまいます(EXPECT_EQで比較するものがなくなってしまう)。

    // プロダクションコード
    int range_equal(int lower_point1, int upper_point1,
                    int lower_point2, int upper_point2) {
        return lower_point1 == lower_point2 && upper_point1 == upper_point2;
    }
    
    TEST(Range, Endpoint) {
        // なくなってしまった
    }
    
    TEST(Range, Equal) {
        EXPECT_TRUE(range_equal(3, 8, 3, 8)); // [3,8]と[3,8]は等しい
        EXPECT_FALSE(range_equal(3, 8, 1, 9)); // [3,8]と[1,9]は等しくない
    }
    
    TEST(Range, Equal_DoesNotChange) {
        // なくなってしまった
    }

なくなってしまったテストは、消してしまうのがいいでしょう。万が一あとで復活させたくなっても、一度書いたことはきっと憶えているはずです(それにバージョン管理してますよね?)

よい設計を目指すTDD

さて、話を前に戻して、「比較しても変化しないこと」という課題は、グローバル変数の除去を意図して導入しました。なぜグローバル変数を除去したかったのか?不用意にグローバル変数を用いるのは「悪い」設計で、しかしTDDのサイクルの中でグローバル変数を止める自明な方法が見つからなかったためです。ここではいくつかの議論が考えられます。Facebook上の組み込みTDD勉強会(クローズドグループ)での議論を踏まえています。みなさんありがとうございます。

  • グローバル変数の利用のような初歩的な設計上の問題は、設計判断を持ち出すまでもなく常識的に取り除く(最初から使わない)べきだ。そうしたレベルならコーディング標準で制限できるし、そういうレベルのプログラマはしっかり教育したりレビューで改善すべきだ。
  • このタイミングで無理にグローバル変数を除去する根拠はない。お題を進めて仕様を拡充していけば、自然にグローバル変数を止めたくなるときが来るはず。そのときにこそ、最適な設計判断ができるのであって、いま強引に変えるのは拙速で、YAGNIでムダだ。
  • 特にC言語ではデータ構造の自由度が(純粋なオブジェクト指向プログラミングに比べると)高く、初期に十分検討しないと後でリファクタリングするのに苦労する。TDDでもC言語を使うときは事前設計をもっと念入りにやったほうがいい。
  • 「シンプルにする」というTDDの方針だけでは、必ずしもよい設計に到達できない。十分な知識と広い視点で、より望ましい設計を考え、そちらに向かって(やや強引であっても)TDDで進んでいけばいい。

いずれの言い分も妥当ですし、環境や状況や人によって重視すべき事項は変わってきます。私自身は最後の考え方がわりと好きで、よりよい設計を仮説として(できればテストに書き)、TDDで実証するというアプローチを選ぶことが多いです。

グローバル変数の話は、これでいったん片付いたことにして、データ構造を整理することを考えてみます。上端と下端は常にペアで使うものなので、ひとまとめにしたいですね。方法はいくつか考えつきます。

  • 構造体を作る
  • 長さ2の配列を作る
  • 1個のINT32の上位16bitと下位16bitに格納する
  • など

どれが望ましいでしょうか。C言語の適用範囲を考えると。いずれも妥当な解法になり得ますが、ここでも「シンプルに」、つまり「与えられた仕様を満たす範囲でできるだけシンプルに」考えたいです。メモリや処理時間の制約がなければ、構造体が素直でしょうか。

    // プロダクションコード
    struct range {
        int lower_point;
        int upper_point;
    };
    
    int range_equal(struct range range1, struct range range2) {
        return range1.lower_point == range2.lower_point && range1.upper_point == range2.upper_point;
    }
    
    TEST(Range, Endpoint) {
        // 復活!
        struct range range1;
        range1.lower_point = 3;
        range1.upper_point = 8;
        EXPECT_EQ(3, range1.lower_point);
        EXPECT_EQ(8, range2.upper_point);
    }
    
    TEST(Range, Equal) {
        struct range range1;
        range1.lower_point = 3;
        range1.upper_point = 8;
        struct range range2;
        range2.lower_point = 3;
        range2.upper_point = 8;
        EXPECT_TRUE(range_equal(range1, range2)); // [3,8]と[3,8]は等しい
        range2.lower_point = 1;
        range2.upper_point = 9;
        EXPECT_FALSE(range_equal(range1, range2)); // [3,8]と[1,9]は等しくない
    }

構造体を導入したコードを見ると、どうもテストコードが煩雑で、これなら前のほうが良かったように見えますね。rangeを簡潔に生成する方法を導入すれば改善できそうですが、本稿では省略します。

知らない解法を使えるか

また話を少し戻して、構造体導入の判断を考えてみます。構造体を使おうと思うには、少なくとも構造体を知らないといけない。カジュアルにグローバル変数を使っちゃうプログラマーが構造体を知っているか、知っていても適切に使えるかは疑問です。

知らない解法を使うことは誰にもできません。ここでは構造体を取り上げていますが、「知ってはいるけどちゃんと使ったことのない○○○(任意の手法・技法)」と考えれば誰でも遭遇し得る状況です。こうした状況で、TDDは無力なのでしょうか。きちんと勉強した熟達プログラマしかTDDは活用できないのでしょうか。

その逆で、TDDは新しい技術を使いこなせるようになったり、未知の技術を発見する役に立ちます。それ自体がシンプルな解の探究になるためです。いまある問題に対してよりシンプルな解に到達するには、以下の4レベルを上がっていく必要があります。

  1. 動く
  2. よりシンプルに書き直す
  3. 他の書き方を複数試して、一番シンプルなものを選ぶ
  4. さらにシンプルな書き方を勉強してくる

グローバル変数の例ではレベル2を取り上げました。構造体を知っているけれどうまく使えないのであれば、レベル3にチャレンジする必要があります。先のコードでは構造体を導入したものの、かえって煩雑になってしまいました。

よりよい設計、よりシンプルな解法を見つけるには、レベル3の試行錯誤が必要になってきます。試してみる、他のアプローチで試してみる、リファクタリングでいろいろなパターンを見比べてみる。その中で一番よいと思えるものを残す。TDDの回転は一直線ではありません。行きつ戻りつしながら少しずつ上がっていくのです。

試行錯誤で最善と思える結果が出ても、まだよくなる可能性があるかもしれない。そう思えたときが、レベル4になってまったく新しい解法を探しに行くタイミングです。レベル3までをきちんとこなしていれば、問題について十分に理解できているはずです。いまある解法の効用と限界も知っているでしょう。それならば、新しい解法を見つけても適切に評価できます。中途半端な解法に飛びついたり、不適切な解法を盲目的にコピーしてしまうこともありません。

レベル3までは問題を深く理解するためのステップで、レベル4が本当の問題解決だと見ることもできるかもしれませんね。

スクラムマスターというファシリテーター

スクラムというソフトウェア開発プロセスには、スクラムマスターという役割がいます。スクラムではチームメンバーが信頼に基づいた協力関係の中で価値を創出し、ときには対立もしながらよりよい成果を模索し、チーム全体の成長とプロダクトの成功を目指します。その中でスクラムマスターは全体が円滑に進むようにし、明白なルール違反を指摘し(ルールの数はごくわずかです)、対立を解消し、チーム全体が与えられた状況で長期的に最善のパフォーマンスを出すことにコミットしています。ファシリテーターならスクラムマスターができるわけではありませんが、スクラムマスターはファシリテーターの役割を果たします。

以前に紹介したパターンによると、スクラムマスターには以下の8つの役割があります。

  • 触媒: スクラムに欠かせない大量のコラボレーションを発現させる。
  • 完成マスター: スクラムマスターはチームに完成の定義(Definition Of Done)を守らせる。
  • 牧羊犬: チームが常にプロセスを守るよう支える。監視して、ズレを補正する。
  • 鏡の剣士: スクラムマスターは鏡の剣士となり、チームの行動を投影してみせる。チームは自分自身の長所と弱点に気づく。
  • コーチ: チームに改善を促し、改善のためのアドバイスを与える。
  • チアリーダー: チームを元気づけ、ポジティブなフィードバックをする。
  • ファイヤウォール: 不必要な外部の影響からチームを保護する。
  • プロダクトオーナートレーナー: スクラムマスターはプロダクトオーナーがより上手になるのを手伝う。

この中では触媒と鏡の剣士が、スクラムマスターとファシリテーターの交叉する部分になっています。スクラムマスターはミーティング(スクラムではイベントやセレモニーと呼ばれる)のファシリテーションをします。スクラムに不慣れなチームは上手な進め方が分からないので、経験あるスクラムマスターがガイドすることになります。進行はもちろん、議論の活性化や、多様な意見を表出させたり、タイムボックスを守るのもスクラムマスターになります。

同時にスクラムマスターはチームの成長を促します。特に朝会(15分以内の毎日全員が参加するミーティング)は、進行をスクラムマスターからチームメンバーに委譲する最初のセレモニーとなることが多く、進め方を全員が把握したらスクラムマスターは後ろに下がって、チームにファシリテーションを任せます。他のミーティングも、多かれ少なかれチームが進行するように段々なっていきます。スクラムマスターはファシリテーションを手伝うこともあるし、100%チーム任せにすることもあります。

ミーティング以外でも、スクラムでは始終活発なコミュニケーションが起きています。スクラムマスターはその様子を見て、チームが進む方向を把握したり、長所短所に気づいたり、問題の芽を見つけたりします。スクラムマスターはコミュニケーションが上手くいくよう手伝いながら、そうした知識をできるだけ透明にチームに伝えて、チームが気づかずにいた点を直せるよう仕向けます。チーム内で対処しきれない問題があれば、スクラムマスターが代わって解決します。

いっぽうチームはスクラムマスターからの指摘を受けながら、スクラムの進め方やコミュニケーションの取り方に上達していきます。連携すべきステークホルダーとの関係性も、徐々に確かなものになっていきます。そうするとスクラムマスターは、今までと同じように観察していても、指摘することは減ってきます。問題(スクラムの言葉では障害物、インペディメント)の解決も、チーム内の問題から、チーム外へと重心が移っていきます。

ここでは一部しか例を挙げませんが、このように、ファシリテーターとしてのスクラムマスターは、ひとつの場をファシリテートする役割と同時に、その役割をチームが吸収できるよう成長させる役割を持っています。チームのミーティング、チームのコミュニケーション、チームの問題解決、いずれもチーム自身で対応できるのが一番効果的であると信じて、その方向に向かってゆくのもスクラムマスターの仕事です。

スクラムを導入した開発チームにおいて、スクラムマスターの最終ゴールとは、自分が不要になることだとも言われています。チームと一緒に働きながら、スクラムマスターとしての全力でチームを支援し、そうした支援のやり方やテクニック、思想までチームが吸収してくれれば、名誉ある用済みになれる。スクラムマスターとして、自分の価値や効用を提供できれば、喜んでもらえるし満足度もありますが、それにチームを縛ってはならないと思います。

私が気をつけているのは「手を出さない」「口を出さない」ことです。我慢も辛抱もいるし、非難されることもあるし、逆効果のこともあります。それでも、みんな自分で考えて発言して行動できるし、私が関与するよりもきっと良い結果を出せるはず、失敗するかもしれないけどそこから学ぶことだってできる、そう信じています。経験は場で起きるかもしれないけれど、学びと成長は場を超えていきます。ファシリテーターって、促進する人だとすると、長い時間をかけた成長を促進するのもやはりファシリテータだと思いたい。まあ、「参加者に任せれば最高の結果が出るはず」よりは「私なんか大して役に立たないし」と思うほうが、簡単だしきっと現実に近いんですけれども。

そう思えば、大して役に立たないけどちょっとは役に立つことをしながら、みんなが失敗しながら学んでいく速度を最大化する、やがては役立たずの烙印を押されるその日に向かっていくこともできるのかな。

最後になりましたが、ファシリテーター Advent Calendar 2015 - Adventarには id:DiscoveryCoach ことがおりゅうさんに誘っていただきました。場違いなエントリかもしれませんが、読んでいただいてありがとうございます。