アクターの諸刃の剣:非同期のメッセージパッシングと状態隔離対障害モデル、そしてそれを克服するAkka Streamの可能性

ScalaのアクターライブラリであるAkkaでは今までHTTPがIOパッケージでサポートされてきませんでした。HTTPのサポートはAkka Streamの開発とともになされたのです。なぜアクターモデルとHTTPをすぐ統合せずにStreamの登場を待つことになったのか?この疑問に答えることはアクターの問題を考えるよい題材です。

Sprayの革新とその限界

SprayはAkkaのアクターモデルとHTTPを統合して成功したフレームワークです。SprayはJVMベースのフレームワークの中で最高のパフォーマンスを誇りました。SprayはServletがもつがスレッドの高速性と、NodeJSがもつイベントループによる大量のリクエストを効率的に捌くことの特性の両立を、アクターによって可能にしたのです。

ところがSprayはアクターモデルならではの問題とも戦うことになります。Reactive Manifestoが定義する特性に対障害性とメッセージ駆動があります。アクターでは対障害モデルにより、互いアクターの状態は完全に隔離され、その状態を知る唯一の手段は非同期のメッセージパッシングしかないという特性です。これが諸刃の剣となったのです。

すべてを非同期なメッセージパッシングにすることの弊害:メッセージやリソース消費速度調整の困難さ

問題を理解するためにメッセージを送ったらHTTPリクエストを送るアクターを作ります。リクエストを受けるサーバーは少し待ってからレスポンスを返すようにしておきます。このアクターにメッセージを送り続けると、後半のリクエストがタイムアウトし続ける現象がおきます。タイムアウトを起こしている箇所はここです

import akka.io.IO
import akka.util.Timeout
import spray.can.Http
import spray.client.pipelining._

trait RequestProvider { this: Actor =>
  import context.system
  import context.dispatcher

  implicit val timeout = Timeout(60 seconds)

  val requestUrl = "http://some.slow.server"

  lazy val pipeline = {
    sendReceive(IO(Http)(system)) ~> unmarshal[String]
  }

  def request(): Future[String] = pipeline(Get(requestUrl))
}

class SprayClientActor extends Actor with RequestProvider {
  import context.dispatcher

  def receive: Receive = {
    case "request" => request().onComplete {
      case Success(s) => println("response", s)
      case Failure(e) => println("error", e.getMessage)
    }
  }
}
[ERROR] [09/10/2015 19:21:44.934] [SprayClient-akka.actor.default-dispatcher-25] [akka://SprayClient/user/$a] failed
akka.pattern.AskTimeoutException: Ask timed out on [Actor[akka://SprayClient/user/IO-HTTP#-302794656]] after [60000 ms]
    at akka.pattern.PromiseActorRef$$anonfun$1.apply$mcV$sp(AskSupport.scala:334)
    at akka.actor.Scheduler$$anon$7.run(Scheduler.scala:117)
    at scala.concurrent.Future$InternalCallbackExecutor$.unbatchedExecute(Future.scala:599)
    at scala.concurrent.BatchingExecutor$class.execute(BatchingExecutor.scala:109)
    at scala.concurrent.Future$InternalCallbackExecutor$.execute(Future.scala:597)
    at akka.actor.LightArrayRevolverScheduler$TaskHolder.executeTask(Scheduler.scala:467)
    at akka.actor.LightArrayRevolverScheduler$$anon$8.executeBucket$1(Scheduler.scala:419)
    at akka.actor.LightArrayRevolverScheduler$$anon$8.nextTick(Scheduler.scala:423)
    at akka.actor.LightArrayRevolverScheduler$$anon$8.run(Scheduler.scala:375)
    at java.lang.Thread.run(Thread.java:745)

まずこの現象をアクターの非同期性から理解します。アクターのメールボックスにメッセージを送るのは非同期に行われます。なのでメッセージを送ると制御はすぐに返ります。さらにSprayのHTTPクライアントはノンブロッキングIOです。なのでリクエストを送ると制御はすぐに返ります。これらはスレッドプールからスレッドをつかめたときに行われ、処理時間は一瞬です。 一方HTTPリクエストが返ってくるのには時間がかかります。またHTTPの仕様によりリクエストが返ってくるまで次のリクエストは送れません。その結果HTTPコネクションが開放されて利用可能になる回転率はスレッドよりもはるかに悪くなります。

Sprayはこの問題を軽減するためにホストごとに複数のHTTPコネクションをプールしたり、HTTP pipeliningでリクエストが返る前に次のリクエストを送れるようにしています。しかしHTTPコネクションは外部のサービスだと張れる数に限界がありますし、HTTP pipeliningもサーバーはリクエストを受けた順にレスポンスを返さなければならないという制約がある以上、1つでもレスポンスが遅くなるとすべてレスポンスが遅れてしまいます。

ところでHTTPリクエストを行うスレッドをレスポンスが返るまでブロックすれば、たしかにスレッドとコネクション両者の消費速度は一致します。これはブロッキングIOを使ったレガシーなクライアントが行ってきたことです。しかしこれではアクターの性能を発揮できないばかりでなく、デッドロックの危険性を高めます。

根本的に問題を解決するには、スレッドとコネクションという特性の異なる2つのリソース、これらの消費速度を非同期に調整する必要があります。

状態隔離が引き起こす最悪の事態:メモリーの枯渇によるVMのクラッシュ

アクターの対障害性を支える状態隔離も問題を難しくしています。

上記の問題はIOにかかわらず速い生産者と遅い消費者が混在する以上起こりえます。その場合遅い消費者のメールボックスがメッセージで溢れかえることになります。

これは時間が経過するごとにメールボックス中にメッセージが貯まり続けることを引き起こし、いずれメモリーの枯渇を招きます。OutOfMemoryErrorは致命的なエラーなので、アクターシステムは対処を諦め、VM自体のクラッシュに至ります。

またこれは悪意ある過剰なリクエストでVMを落とせる可能性があることを意味します。

これはとても皮肉なことです。状態を隔離しそれを知ることをできなくすることで高めていた対障害性が、最悪の事態であるVMのクラッシュに導く危険性を高めているのですから。

状態が隔離されたアクターはどうやって相手に自分の状況を伝えるといいのでしょうか?それはメッセージパッシングで伝えるしかありません。消費が追いついていないので、相手にメッセージを送るスピードを下げてほしいというメッセージを送るのです。

Akka Streamという解決策

Akka Streamは目標の1つとして、"No more OutOfMemoryErrors"をかかげ、この問題に対処してきました。 Akka Streamはメッセージの消費速度をアクター間で調整する仕組みをもつことで、これらの問題を解決しています。生産者であるActorPublisherと消費者であるActorSubscriberのコードを以下に示します。receive関数の中に注目してください。ActorPublisherは Request(n: Int)メッセージを受け取ることで下流のアクターがいくつメッセージを要求するのか教えてもらいます。ActorSubscriberはOnNext(element: Any)メッセージで処理するメッセージを受け取り、OnCompleteメッセージで上流のアクターのメッセージの生成が完了したことを受信し、OnError(cause: Throwable)メッセージで上流でエラーがあったことを知ることができます。この仕組によって互いの消費スピードを自動調整するのです。Reactive Extensionに似たAPIですね。

ActorPublisher

class TweetPublisher extends ActorPublisher[Tweet] {
  import akka.stream.actor.ActorPublisherMessage._
  import TweetPublisher._

  var buffer = Vector.empty[Tweet]

  def receive: Receive = {
    case Request(_) => deliverBuffers()
    case Cancel => context.stop(self)
    case Post(tweet) => {
      buffer = buffer :+ tweet
      deliverBuffers()
    }
  }

  @annotation.tailrec
  final def deliverBuffers(): Unit = {
    if (totalDemand > 0 && !buffer.isEmpty) {
      val (head, tail) = (buffer.head, buffer.tail)
      buffer = tail
      onNext(head)
      deliverBuffers()
    }
  }
}

object TweetPublisher {
  case class Post(tweet: Tweet)
  def props = Props(new TweetPublisher)
}

ActorSubscriber

class TweetSubscriber extends ActorSubscriber with ActorLogging {
  import ActorSubscriberMessage._

  val requestStrategy = ZeroRequestStrategy

  request(1)

  def receive: Receive = {
    case OnNext(Tweet(message, hashTags)) => {
      log.info(s"tweet `$message` with tags ${hashTags.mkString(", ")}")
      request(1)
    }
    case OnComplete => log.info("OnComplete: This actor will be stopped")
    case OnError(e) => log.error(e, "OnError: This actor will be stopped")
  }
}

object TweetSubscriber {
  def props = Props(new TweetSubscriber)
}

Akka Streamのこのメッセージの消費速度の自動調整、背圧制御はAkka HTTPにも応用され、スレッドとコネクションという2つのリソース消費速度の自動調整を可能にしています。

Akka Streamを使わずに問題に対処するには

地道に対処するしかなさそうです。

Akka Streamのアクターのコードで見たようにメッセージパッシングを使って背圧制御を実現できます。これを自分で実装するとよいでしょう。

またアクターの最適化では遅い消費者に対して別のスレッドプールを割り与えます。こうして遅い消費者に必要な分のスレッドを与えかつ他の部分と独立に処理を進められるようにします。これにより各アクターのメールボックスの量のバランスをとり、リソースが許す限り滞留メッセージが均等に0に近くなるように調整することができます。

もはやメモリーの制約をも超えるAkka Persistence Query

少し未来の話をします。

Akka Persistenceはメッセージを永続化することによりVMごとクラッシュしてもメッセージをイベントソーシング経由で復元することを可能にします。

Akka Persistenceではメッセージの書き込みはできたのですが、書き込んだメッセージを検索して取得することはできませんでした。これはコマンドクエリー責務分離に従っているからです。

Akka Persistence QueryはAkka Persistenceの読み込み専用のAPIです。Akka Persistence Queryが革命的な点は、それがAkka Streamベースに作られていることです。これがどういうことを意味するかというと、背圧制御により下流から要求されるまでデータをディスクからメモリーに読み込まないということです。これは実質的にメモリーの制約を取り払います。もはやOutOfMemoryErrorとはお別れです。Akka Persistence Queryの登場によりSQSのようなキューサーバーとアクターのユースケースの差異が無くなる未来が来るかもしれません。

まとめ

リアクティブシステムを実現するための最適解であるアクターは、非同期メッセージパッシングと状態隔離によりメッセージやリソースの消費速度の調整が困難でありメモリーの枯渇の危険があるという問題がありました。これはアクターに背圧制御を取り入れたAkka Streamが解決します。またAkka StreamはAkka Persistence Queryなどの新たな技術の核になり、次世代のアクタープログラミングを牽引していくでしょう。

テストを書くことを文化にするために、テスト駆動開発合宿をやった

テストを書くことを文化にするために、テスト駆動開発合宿をやった

テストを普及したく休日を割いて開発合宿をやってみた。

動機

テストを書くことの重要性はさんざん主張してきた。テスト自動化の環境も作った。けどほとんどのメンバーはテストを1つも書かなかった。彼らのいい分では、テストは重要なことは分かったけど、どうやればいいか分からないとのこと。

そこで1日割いて、テスト駆動開発を実際にやってみる合宿をやった。

内容

  1. なぜテストを書くのか by @frontainer
  2. テストを書いてテストの利点を実際に体験してみる https://github.com/TanUkkii007/daily-lives-with-tdd
  3. テストがすでに書いてある状況で、テストを通しながらアプリケーションをつくる https://github.com/TanUkkii007/tddworkshop

テストを書くことを文化にするためには?

当日メンターをやっていただいた@frontainer先生に、合宿の計画などのフィードバックをもらった。

@frontainer先生が人に教えるときに実践しているステップを教えてくれた。

  1. 重要性を説く(把握)
  2. 体感(失敗含む)する(体験)
  3. 一度振り返り、反省する(思考)
  4. 再度少しレベルを上げて挑戦する(復習)
  5. 実ケースにあるような無茶ぶりをする(実践)
  6. ここまでの経験を活かして無事対応できる(成功体験)

特に6番へ全員が至るかどうかが文化となるか否かのラインだとのこと。

2 の”体感(失敗含む)する”が今まで足りていなかったもの。2-5までを合宿でカーバーした。

1. なぜテストを書くのか by @frontainer

最初に@frontainer先生の「なぜテストを書くのか」という内容をディスカッション。

テストの種類を挙げて見よう

など。

現時点で社内でやっているテストは何テスト?

会社全体でテストにどれぐらいコストはらっている?

開発者がテストに費やす時間 × 開発者の単価  +  デバッガーがテストに費やす時間 × デバッガーの単価

ただし誰も把握してない テストコードを書くことを批判する人は、テストコードを書くコストしか見てない。組織全体のテストコストを最適化しよう。

自分のテストのコストは?

普段の開発でテストしてる時間の比率: 3-4割

開発人生の3-4割を占めるテストをおろそかにしていいの?

そのテストは誰のため?

テストしないのは無防備、自分を守れてないということ

どのぐらいテストを書くの?

ユニットテストは自分のため -> じゃあ自分の生活を守れる粒度で書く

テストの学習コスト

  • jasmineだとtoBeとtoEqualという2つの関数で8割は完結
  • たった2つの関数を覚えるだけ
  • 難しいことはあとで

自動化しよう

テスト仕様書スプレッドシートにテスト項目を埋めるのも手段の1つ テストコードを書いて自動化するのとどっちがいい?

ここでみんなのテストに対するモチベーションはMaxに。

2. テストを書いてテストの利点を実際に体験してみる

資料: https://github.com/TanUkkii007/daily-lives-with-tdd

  1. 重要性を説く(把握)
  2. 体感(失敗含む)する(体験)

に相当する内容。

この資料の06と07の間に「郵便番号7桁ハイフン含むをチェックする関数を作り、テストする」という課題を出す。4. 再度少しレベルを上げて挑戦する(復習)に相当。

関数の実装もひとそれぞれ違ったけど、テスト項目も人それぞれで結構面白かった。

終わったあとの各自の感想:

  • テストコードを書いた方が情報量が多い
  • テストコードはもっと難しいと思っていたけど、意外と簡単
  • テストはコードだから人と共有できてよい
  • 簡単にいろんな入力を与えることができる
  • リファクタリングもやりやすい

3.テストがすでに書いてある状況で、テストを通しながらアプリケーションをつくる

資料: https://github.com/TanUkkii007/tddworkshop

 5. 実ケースにあるような無茶ぶりをする(実践)に相当。

定番のTodoリストを作ってもらった。内容は少し難しめ。2人1組のチームで、2時間ぐらいで65あるテストのうち、20-60のテストを通していた。

全員で実践し、普及し、必要不可欠なものへ

テストを書くことの重要性も、やり方も分かってくれた。あとは6. 「ここまでの経験を活かして無事対応できる(成功体験)」を残すのみだ。合宿に参加してくれたメンバーなら、実際に業務で実践し、ものにしていってくれるはず。

残念ながら参加者はメンバーの全員ではなかった。でも参加してくれたメンバーならもうテストを教えられるはず。残りのメンバーに普及していって欲しい。文化は1人では成らず。テストが文化になるには、君たちの力が必要だ。

Isomorphic Javascriptのジレンマ

@axrossがIsomorphic Javascriptを実現すべく、社内にNode.jsを啓蒙する発表の場に僕はいた。僕は2ヶ月前までWebフロントエンドエンジニアであり、今はサーバーサイドエンジニアをやっている。その2つの立場から@axrossの発表を聞いて、自分の考えを書いておく。

社内事情的な側面もあるので、この記事を読んでからの方が理解しやすいと思います。

フロントエンド側の動機

僕もIsomorphic Javascriptの未来を見たい。@axrossが社内でこのようなきっかけをつくってくれたのは嬉しい。これを機にフロントエンドとサーバーサイドが歩み寄るといい。

サーバーとAPIとの統合時に、型の違いなどの衝突が起こることがある。レスポンスのJSONの中で、数値であるべき値が文字列であったり、もっと酷いと空の配列がnullになっていたりする。PHP連想配列SQLドライバの問題なんだろうけど、これがかなりの頻度で起きる。その都度サーバーサイドの人に修正してもらうのだが、コミュニケーションコストをともなう。

コミュニケーションをあきらめてしまう人もいる。コードレビューのとき異常な量のバリデーションやキャストを見つけたとき指摘をすると、そのような理由を聞く。この問題がより深刻なのはWebよりもネイティブクライアントで、キャストエラーでアプリがクラッシュする。それを回避するためにネイティブ側でレスポンスの形式が正しいかどうかテストを書いている人がいる。(本来それはサーバーサイドが書くべき受け入れテストなのにね。生産性の高いことを理由にPHPを採用したのに、生産性の低いネイティブコードでテスト書いてては意味ないだろ!)

これは数ある問題の1つでしかない。

Isomorphicな開発を採用できれば、モデルやバリデーション、ユニットテストなどを共有できる。書くコードを共有でき生産性が上げられるのみでなく、コミュニケーションコストも大幅に下がるだろう。axrossもこのようなフラストレーションのすえにisomorphismの提案に踏み切ったのだろう。

ただし僕らがIsomorphic Javascriptを実現するまでには、まだ長い道のりが待っている。

Isomorphic Javascriptの前に

@axrossの発表の質疑で、社内ですでにNode.jsを使ってチャットサービスをやっているチームからNode.jsの欠点の指摘があった。

  • 安全なデプロイメントの構築が難しい
  • APIの更新が激しく、バージョンを上げにくい

ただしこれらはNode.jsへの直接的な批判ではなくて、その根底に社内の柔軟性のないインフラに問題があると僕は思う。オンプレミスのサーバーを使っており、1台の性能が非常に高い。なので複数のサービスが1つの物理マシンに乗っていたりする。これではNode.jsのバージョンを上げることは危険だし、デプロイもリロードに頼るしかない。

この問題はまさに今僕が取り組んでいる問題だ。今僕はScalaのAkkaを使って開発しているのだけど、Node.jsと同じくシングルプロセスでイベントループが回っているのでリロードは避けたい。なのでBlue-Green Deploymentができるようにしている。これが当たり前になればNode.jsを導入する障壁も下げられるだろう。

他にも冒頭で紹介したAPIの型が安定しない問題に関しては、通信部分のモデル層をサーバー側からクライアント側に自動生成することで、クライアントサイドのモデル実装のコストや統合のコストは下げられる。両者が異なった言語でもこの手法は適用できる。

要はIsomorphic Javascriptのような両領域が高度に統合された難しいものでなくても、単純にお互いが開発しやすいようにしてあげればいいだけで問題が解決することもある。Isomorphic Javascriptの前にやれることはたくさんある。

もちろんIsomorphic Javascriptの前にやらなければならないこともある。社内ではTDDやコードレビューは、サーバーサイドもフロントエンドもまだ十分にできていない。これができていない状態でIsomorphic Javascriptに踏み込んでも、十分な恩恵は受けられないし、僕たちが見たかった未来とは違う結末に陥る危険性がある。

Isomorphic Javascriptのジレンマ

今所属している会社ではインフラを始めもろもろの問題があり、isomorphicな開発を実現するまでに解決すべきことがいくつもある。

これらの問題を見て「それは君の会社のサーバーサイドやインフラエンジニアがやることをやってないからだね。」と言うのは待ってほしい。僕もそう思いたい。しかしそう思ったときに、自身の矛盾にぶつかる。

Isomorphic Javascriptは究極的にサーバーサイドとフロントエンドの境界をなくしていくというものだ。Isomorphic Javascriptを実現したいフロントエンドエンジニアが「それはサーバーサイドの仕事」と境界を作ると議論は進まなくなる。Isomorphic Javascriptができないのは社内のサーバーサイドのせいだと言ってしまうと、自己矛盾に陥ることになるのだ。

この「isomorphismに従って相手の領域との境界を取り払ったら自分の範疇の問題が急激に増えて手に負えなくなる。だからといってそれをその領域の専門家に任せるとisomorphismに反する」問題をIsomorphic Javascriptのジレンマと呼ぼう。

Isomorphic Javascriptを実現するには

サーバーサイドにとってみればJavascriptという言語は数ある選択肢の1つでしかない。一方でフロントエンドではAltJSとJavascriptしか選択肢がない。さらにサーバーサイドレンダリングをしようとすると、サーバー側にJavascript処理系が必要になる。Isomorphic Javascriptを提案するということは、フロントエンドが解決できる問題が増える一方で、サーバーサイドの選択肢を奪うことになる。

このようにお互いの利害が非対称なので、フロントエンド側の提案でIsomorphic Javascriptを実践するなら、フロントエンド側はその利点を説明した上で、サーバーサイド側も含めた開発にある程度責任を持たなければならない。フロントエンド側はフロントエンドに加えてサーバーサイド側の知識も持っていないといけないということになる。このハードルはかなり高い。

Isomorphic Javascriptな開発を実現するまず第一歩として、サーバーサイド側の数ある選択肢から技術を選ぶ際に、フロントエンドの意見を取り入れる余地をつくらなければならない。実際うちのフロントエンドのメンバーが最も怒っているのはこの部分だ。つまり両者の統合領域が広がりつつある今、サーバーサイドの技術を決める際にフロントエンドが意見する機会すら与えられていないことだ。最終的にそれがPHPだろうがNode.jsだろうが、お互いに選定理由に合意していれば衝突はおきないだろう。

Webフロントエンドの未来

Isomorphic Javascriptが描く未来では、サーバーサイドとフロントエンドの境界がなくなる。これを押し進めるもう1つの流れにExtensible Webがある。Extensible Webによって低レベルAPIがブラウザにやってくる。極論ブラウザでもそれらの低レベルAPIを使ってサーバーサイド同じぐらい高度なことができてしまうわけだ。

この2つの大きな流れの帰結として、フロントエンドエンジニアもサーバーサイドの知識が必要ということになる。

で実際問題として、これができるフロントエンドエンジニアってどれぐらいいるのだろうと疑問に思う。

例えば今回の社内の問題では、Node.jsを導入しやすくするにはインフラまで手を入れる必要があった。この問題につい最近までWebフロントエンドエンジニアだった僕が取り組んだのだけど、やっぱり難しい。幸い同じ問題意識をもっていた社内のインフラエンジニアが協力してくれたからできたけど。今後フロントエンドだけでは解決できない問題がたくさん出てくると思う。フロントエンドエンジニアの定義も変わるだろう。

未来のことは誰にもわからない。だから僕は今僕ができることをサーバーサイドからやっていく。@axrossのような開拓者が突き進めるように、そして自分がフロントエンドエンジニアに戻ったときに快適にIsomorphic Javascriptに取り組めるように、荒野状態の社内のサーバーサイドの領域を耕しておく。

テストを書き始めた僕が伝えたいテストの恩恵

2ヶ月前から、僕はテストを書き始めました。とりあえず早く作れという開発にうんざりしていた毎日から抜け出すために。とりあえず実践テスト駆動開発を読んで、そして実際に実践してみました。

実践する前は、テストの一番の利点はバグを早期に発見できることかなと思っていました。しかし実際に実践してみると、もっと重要な恩恵をテストはもたらしてくれるのだと気づいたのです。

2ヶ月前の僕みたいにテストを書き始めようとしている人や、テストを書け書けってうるさく言ってる人(今の僕みたいに)がいるのだけど何でだろうと思っている人に、この記事が役に立てれば幸いです。

※この記事でのテストはユニットテストを指します。

テストを書くメリット

テストを書くメリットの中で、特に重要だと思うものだけを挙げよと言われたら、僕だったら以下の3つを挙げます。僕が重要だと思う順番に並べました。

  1. 適切な粒度、再利用可能でスケーラブル、YAGNIな設計
  2. 高速なフィードバックサイクル
  3. 書いたコードへの自信、それを変更する自信、そして複雑な技術に対峙できる自信

それぞれを解説します。

1. 適切な粒度、再利用可能でスケーラブル、YAGNIな設計

テスト駆動開発ではプロダクションコードを 書く前にテストコードを書きます。そしてこれには意味があるのです。

プロダクションコードを書く前に書くテストは、これから実装する機能が外部から使いやすいかどうかを実装者に考えさせます。

簡単な例を示した方が理解しやすいですね。「あるアイテムの最終更新日時を取得して表示する機能を作ってほしい」という要求に応えるとしましょう。この機能を要求単位で作るとこんな感じになったとします。

function showLastUpdatedDate(id) {
  var $dateBox = $("#dateBox");
  $.get("/updatedDate", {id: id}, function success(data) {
    var today = new Date.now();
    var lastUpdatedDay = Date.parse(data.lastUpdated);
    var diff = (today - lastUpdatedDay) / 24*60*60;
    $dateBox.text(diff + "日前");
  });
}

このshowLastUpdatedDate関数をload時に呼べば確かに最終更新日時を表示できます。しかしこの設計には問題があるのです。

何が問題なのかを回答する前に、このコードをテストしてみましょう。「テストは設計の問題を浮き彫りにする」ので、このコードのテストケースを書くことで設計の問題が分かりそうです。

describe("showLastUpdatedDate", function() {
  beforeEach(function(done) {
    showLastUpdatedDate("someId");
    setTimeout(function() {done();}, 5000);
  });
  it("showLastUpdatedDate(id)は/updatedDateから取得したデータのlastUpdatedキーから、現在から何日前に更新されたのかをdateBoxに表示する", function() {
    expect($("#dateBox").text()).toBe("3日前");
  });
});

このテストはいくつかの無理をしています。

  • showLastUpdatedDateがいつ完了するかが分からないので、setTimeoutでとりあえず5秒後には表示されているだろうと過程している。しかし実装では5秒後に処理が完了することは保証してはいない。
  • このテストはshowLastUpdatedDateが最終更新日時を取得する方法を知っていることを示している。この例では/updatedDateというURLからGETで取得するのだけど、このサーバーが利用可能でないと動作しない。
  • サーバーのレスポンスが適切な形式である必要がある。この場合はlastUpdatedというキーがあるオブジェクトでないといけない。
  • かつ"someId"というIDをもつアイテムが存在し、その更新日時が”今日”から3日前でなければならない。”今日”が何日なのかによってテストの成否が左右されてしまう。
  • "dateBox"というIDをもつHTML要素が存在する必要がある。

さあもうshowLastUpdatedDate関数の設計の問題が浮き彫りになったと思います。何が問題だったのかというと、showLastUpdatedDate関数の役割が多すぎるのです。

showLastUpdatedDate関数は1. 最終更新日時の取得、2. 現在からの経過日の計算、3. 計算結果の表示、という3つの役割をもっています。でもshowLastUpdatedDate関数のメインの役割は2.の「現在からの経過日の計算」でしょう。最終更新日時をどう取得するかはどうでもいいのです。例えば最終更新日時をlocalStorageから取得しても目的は満たせるかもしれません。そして計算結果の表示場所も同様に固定しない方が汎用的に使えます。

テストが複雑になってしまいました。テストを後に書いたからですね。実装を書く前にテストから書いたらもっと簡単に書けそうなのに。

ということでテストから書いてみましょう。テストが書きやすいように「現在からの経過日の計算」の機能に集中します。

describe("diffDate", function() {
  it("diffDateは2つの日付の差を日単位で計算する", function() {
    var today = new Date("Sun, 25 Jan 2015 04:01:02 GMT");
    var lastUpdatedDay = new Date("Thu, 22 Jan 2015 04:01:02 GMT");
    expect(diffDate(lastUpdatedDay, today)).toBe(3);
  });

});

今度はdiffDateという2つの日付の差を計算することに特化した関数に対するテストケースを記述しました。これは単に2つのDateオブジェクトを受け取り、その差を日単位で計算して返しているだけです。最終更新日時の取得や、計算結果の表示の機能は排除してあります。これによってテストは単純になりました。さらに”今日”という日付も外から渡すことによって、いつ実行しても再現がとれるようになっています。

今回設計したdiffDate関数は、showLastUpdatedDate関数よりもどの点において優れているでしょうか。

  • 最終更新日時をどうやって取得したかに依存しない。ajaxから取得しようがストレージから取得しようがメモリー上のオブジェクトから取得しようが使うことができる。
  • 最終更新日時の形式に依存しない。showLastUpdatedDate関数はlastUpdatedというキーに最終更新日時が格納されていることを仮定していた。
  • 最終更新日時の計算用途に限定されない。計算しているものは2つの日付の差なので、例えば「残り何日」という計算にも使えるだろう。
  • どこに表示するかに関して無関心。どのDOM要素に表示しようがコンソールに表示しようが自由だ。もちろん表示に使わなくてもよい。

これらを一言でいうと、showLastUpdatedDateに比べてdiffDateは再利用可能だということです。

再利用可能性の実例を見せましょう。最終更新日時が1日を切ったときに、○時間前、○分前というように表示したほうがよいという要求が来ることは想像に難くないですね。

例によってテストから書きます。

describe("diffDate", function() {
  var today = new Date("Sun, 25 Jan 2015 04:01:02 GMT");
  
  it("diffDateは2つの日付の差を日単位で計算する", function() {
    var lastUpdatedDay = new Date("Thu, 22 Jan 2015 04:01:02 GMT");
    expect(diffDate(lastUpdatedDay, today)).toBe(3);
  });
  
  it("diffHourは2つの日付の差を時間単位で計算する", function() {
    var lastUpdatedDay = new Date("Thu, 22 Jan 2015 01:01:02 GMT");
    expect(diffHour(lastUpdatedDay, today)).toBe(3);
  });
  
  it("diffMinutesは2つの日付の差を分単位で計算する", function() {
    var lastUpdatedDay = new Date("Thu, 22 Jan 2015 04:00:02 GMT");
    expect(diffMinutes(lastUpdatedDay, today)).toBe(1);
  });

});

実装は例えば以下のようになるでしょう。

function diffDate(a, b) {
  return diffHour(a, b) / 24;
}

function diffHour(a, b) {
  return diffMinutes(a, b) / 60;
}

function diffMinutes(a, b) {
  return (b - a) / 60;
}

既存のコードをうまく再利用することができました。

再利用可能であると同時にスケーラブルとも言えます。これらの基本的な機能を組み合わせてもっと複雑なこともできるでしょう。よい設計は将来のユーザーの要求に対応するためにも非常に重要です。

さらに便利にしてみましょう。年単位、月単位、秒単位で差を計算できるようにしてみましょう。もちろんテストから書き始めましょう。

.....

めんどくさいですね。そう面倒なのです。はたして年単位、月単位、秒単位などという表示は使うのでしょうか?一度も使われない機能を実装するのは時間の無駄です。そしてバグの温床になるという危険もあります。

テストから書けば過剰な機能追加を思いとどまることができます。つまりYAGNI (You ain't gonna need it!: あなたはそれを必要としない)設計にできるのです。

2. 高速なフィードバックサイクル

テスト駆動開発では開発サイクルを高速に回すことができます。

テスト駆動開発へのよくある批判に「テストを書く工数が発生し開発を遅らせる」というものがあります。しかしこれは間違いです。

たしかにテストコードは書く必要があります。しかしそれはとても単純なものです。1つのテストケースを書くのに要する時間はたった数分です。もしテストがあまりに複雑になる場合は、前述したように設計に問題がある可能性があります。

そのテストコードを書くことに使った時間は、その後の高速なフィードバックサイクルで裕にとりかえすことができます。テストを実行すると、特定の機能の振る舞いが要件を満たしているかがチェックされ、瞬時にその結果の一覧を教えてくれます。そして失敗しているテストをパスするようにプロダクションコードを修正していきます。このサイクルはとても短いのです。

このテスト駆動開発サイクルを、デバッグプリントやブレイクポイントを使って実際にアプリケーションを実行して動作を確かめる従来の開発サイクルと比較してみてください。ブラウザでアクセスしフォーム入力などいくつもの操作して動作を確かめる、リクエストを投げてデバッグ情報を書き込んだレスポンスを見る、ブレイクポイントを張って動作を止めて関数の返り値を確かめる。これらの操作はアプリケーションを丸ごと実行し、人間の操作を伴う時間のかかる作業です。さらに確かめたい機能以外の複雑な部分も目に入ってきます。

最近のフレームワークは本当に複雑です。僕はAngularJSやReactなどを使いましたが、それぞれ固有の実行ライフサイクルをもっており、自分のコードがいつどのように実行されるのかを知ることは困難です。

また今はAkkaを使った並列分散処理アプリケーションを作っています。このアプリケーションでは処理が複数のスレッド、複数JVM複数のサーバー上で実行されるので、アプリケーションを実行してデバッグするスタイルでは処理の結果や状態を把握しきれません。

このような複雑なアプリケーションの開発下でも、雑音を断ち切り特定の機能の振る舞いの結果を瞬時に取得できる。それがユニットテストの利点なのです。

3. 書いたコードへの自信、それを変更する自信、そして複雑な技術に対峙できる自信

開発者が自分のコードに自信をもつことは非常に重要です。

テストをパスしていれば、あなたのコードは少なくとも用意したテストケースに対して確実に動作します。そして将来の変更に対して準備ができた設計になっているはずです。このコードへの自信はプロダクトへの自信につながります。

さらには先端技術に対する前向きな姿勢につながります。

コードレビューに対しておっくうなチームのメンバーがいました。彼女はAngularJSを使ったアプリケーションのレビューを控えていましたが、AngularJSの理解に苦しんでいました。そこで僕がテストを書き、彼女が実装するというやり方をやってみました。AngularJSの複雑さの中で、目的の情報だけを瞬時に取り出せるユニットテストに彼女は感動を覚えたようです。彼女はチームの中でテストの信者第1号になってくれました。

またユニットテストの力を信じていなければ、僕はAkkaという並列分散フレームワークの導入に舵を切らなかったでしょう。プロジェクトの開始時にその技術の全容を知るには複雑すぎる相手です。ユニットテストは複雑な全体からより単純な機能を切り出し、その状態を瞬時に伝えてくれます。ユニットテストの力を信じることで、僕たちは現代の複雑な技術と自信を持って対峙することができるのです。

まとめ

テストを実践してみてはじめに実感した恩恵は次の3つです。

  1. 適切な粒度、再利用可能でスケーラブル、YAGNIな設計
  2. 高速なフィードバックサイクル
  3. 書いたコードへの自信、それを変更する自信、そして複雑な技術に対峙できる自信

今の僕はこの3つを伝えたいのです。今後テストへの理解が深まるとともに、僕の考えをまた発信していこうと思います。

追伸:テストと設計の関係についてよく質問されるのですが、「テストが設計を導く」ことを説明することがかなり難しいです。この記事では簡素な例しか思いつきませんでした。よいサイトとかあれば教えて頂きたいです!

エンジニアブログ始めました

同僚のaxrossが「実力あるのだから、情報発信しなよ」と言ってくれた。

そして短い間だったけど一緒に仕事をして転職していった仲間たちからも、もっと僕の勉強会を聞きたいと辞め際に言ってくれた。

社内ではよく勉強会で発表したりしているけど、やっぱり外の世界に発信しないとね。

ということでエンジニアブログ始めました。