株式会社クリアコード > ククログ

ククログ

タグ:

Groongaの管理画面の改良案

クリアコードは今年もインターンシップを実施します。インターンシップをどのように実施するかを3行で説明すると次の通りです。

  • 目的: インターンがクリアコードで実践している開発方法を体験する
  • 内容: クリアコードのメンバーと一緒にフリーソフトウェアを開発する
  • 対象: プログラミングが好きでフリーソフトウェアに関心がある16歳以上の方(学生、社会人、休職中などは問わない)

詳細はインターンシップページを確認してください。過去に実施したインターンシップの詳細はGitHubにあるメモを参照してください。興味のある方はぜひ応募してください。

今回は、インターンシップで開発する題材として使えそうな開発内容を久しぶりに紹介します。

現在のGroongaの管理画面

Groongaという全文検索エンジンがあります。HTTPサーバーとして全文検索機能を提供できます。GroongaをHTTPサーバーとして使うと、Webブラウザーで管理画面を使えます。管理画面では、データベースのスキーマを定義したり、登録されている文書を確認することができます。

どのようなUIになっているかは groongaでブラウザベースの管理ツールを使うには - Qiitaを参照してください。

この管理画面を改良する、というのが今回紹介する題材です。どのように改良するかを紹介する前に現状のよいところを整理します。

よいところ

現在の管理画面のよいところは次の通りです。これらは改良後も維持したい点です。

  • すべてクライアント側で動作する実装*1である。
  • 全文検索機能(selectコマンド)の動作をWebブラウザー上で確認できる。

少し補足します。

まず、すべてクライアント側で動作する点についてです。これにより、別途Webアプリケーションを用意する必要がなく、簡単にインストールできます。これが魅力的です。

次に、全文検索機能の確認についてです。Webブラウザー上で確認できると、手軽に試行錯誤できます。これは開発時に魅力的な機能です。

改良したいこと

前述の現在のよいところを維持した上で、管理画面を次のように改良したいです。

  • 国際化対応
  • 全文検索機能の結果をわかりやすく表示
  • トークナイズ結果の確認機能を追加
  • プラグインで追加したトークナイザーやノーマライザーに対応
  • よい見た目とよい使い勝手

それぞれの項目の詳細は[groonga-dev,02321] 管理画面を改良したい!を参照してください。

実現方法案

現在の管理画面はjQueryとjQuery UIで実装していますが、それをAngularJSを使って実装するのはどうかという案があります。AngularJSにはいくつか国際化対応のためのライブラリーもあるよう*2なので、改良したいことを実現しやすい雰囲気があります。

AngularJSベースにする場合は、一気に現在の管理画面を置き換えるのではなく、別のサブパスに配置して、現在の管理画面と共存しながら徐々に機能を移していく方法をとります。現在の管理画面は意外と機能が多く、一気に書き換えると途中で挫けてしまうかもしれないからです。

関連技術

この開発内容では次の技術を使うことになるでしょう。

  • HTML5
  • CSS3
  • JavaScript
  • AngularJS(もしかしたら使わないかもしれない。実装時に相談して決める。)
  • C(プラグインで追加したトークナイザーやノーマライザーに対応するなら。)

まとめ

久しぶりにインターンシップの題材になりそうな開発内容を紹介しました。プログラミングが好きでフリーソフトウェアに関心がある方はぜひ応募してください。

*1 HTML + CSS + JavaScriptで実装している。

*2 メッセージそのものを翻訳時のキーとして使うという点からangularjs-i18nがよさそうな印象です。

つづき: 2015-01-07
2014-06-17

インターンの作業を進めやすくするための工夫

はじめに

リーダブルコードの解説やつくばインターンシップコンソーシアムのマッチングイベントをきっかけに、インターンシップへの応募が2件あり、今年2回目のインターンシップを9/2~9/6の1週間実施しました。インターンシップの記録はメモに残して公開しています。今回もインターンシップを通じてたくさんのことを学ぶことができました。

今回のインターンシップでは、2人のインターンがるりまプロジェクトにおける異なる2つの作業を行いました。1人はBitClustへの機能追加、もう1人はるりまのドキュメント改善です。一方のメンターも2名配置しました。1人はるりまプロジェクトのメンバーです。インターンへの作業内容の説明やインターンからの質問に対応しながら、るりまプロジェクトに関する作業も並行して行いました。もう1人は非エンジニアである筆者で、インターンシップで掲げたインターン、クリアコード双方の目的が達成できるよう調整する役を務めました。このような体制でインターンシップを実施したところ、インターンの作業が進まなかったり、違う方向に進んでしまうといった問題が発生しました。そこで今回はインターンシップで生じた問題と、それぞれの問題を解決するために試した工夫を紹介します。

問題を共有する

インターンシップ開始当初、メンターがインターンに作業内容を説明した上で、インターンに作業を進めてもらい、疑問などが生じればメンターに相談してもらうことにしていました。しかし実際はインターンが質問することを遠慮したり、質問しないで自分で解決しようとした結果、相談する機会をもつことがあまりできませんでした。インターンが質問や相談することをトリガーとして問題を共有し解決しようとするやり方はうまくいきませんでした。しかしながら問題を解決するためには、まずインターンが抱えている問題を共有しなければなりません。そこで有効だったのが次に紹介する1日のふりかえりです。

1日のふりかえり

1日のふりかえりは日々の作業状況と問題点をインターンとメンターが共有し、問題への解決策と翌日の作業内容を一緒に考えることを目的としていました。1日のふりかえりはクリアコードで行なっている1ヶ月のふりかえりを参考に、毎日夕方5時、1時間から1時間半をかけて行いました。

1日のふりかえりの手順

1日のふりかえりは以下の手順で行いました。

  • 朝の段階で決めていた作業予定をインターンが報告する
  • 作業結果(やったこと)をインターンが報告する
  • 問題点をインターンとメンターが報告する
  • 問題に対する解決策を話し合う
  • 解決策をふまえて翌日の作業内容を決める

報告内容はホワイトボードに書き、それを見ながら話をしました。また、ふりかえりの内容は議事メモに残して、合意内容に認識のズレがないかメンバーで確認しました。

1日のふりかえりの効果

1日のふりかえりを行った結果、インターンが作業を進める上での問題点を洗い出し、解決策をみんなで考えることができました。問題から解決策を検討するには問題を整理しなければなりません。問題が生じた時のインターンの作業をたどって、どこでうまくいかなくなったか、またそのときどのような判断をしたのかをふりかえることで、問題が整理され、どこが原因だったかがわかりました。インターンシップの前半では問題への解決策はメンターが提案していましたが、後半になると問題を整理することを手伝えば、インターン自ら解決策を提案するようになりました。

個別の問題への対応策

1日のふりかえりで共有した問題に対して、解決策を決めて、翌日の作業で試してみました。ここからは、1日のふりかえりで見つかった問題に対する対応策で効果のあったものをいくつか紹介します。

作業単位ではなく、時間単位で状況を共有する

インターンシップの1日目、2日目と予定通りに作業が進みませんでした。作業完了に必要だと見積もった時間が適切でなかったことが原因の1つでした。作業時間を見積もることは経験のない人にとって困難なことです。同じ作業を繰り返し行うのであれば見積もることもできるようになりますが、インターンシップでは次々と新しいことに取り組むので、経験のないインターンが作業時間を見積もることはできませんでした。そこでメンターがこの作業なら3時間もあればできるだろうと判断し、3時間で作業を完了させるよう指示していました。しかし、結果は3時間経過しても完了せず、インターンは作業が完了するまで作業を続けていました。 メンターが見積もった時間はあくまで目安でしかなく、インターンが作業を完了させるのに必要な時間を適切に見積もったとはいえませんでした。メンターがインターンの作業時間を見積もることも難しいことがわかりました。そこで解決策として、作業時間を決めて、その作業時間が経過した時点でインターンがメンターに作業状況を報告することにしました。これによって、インターンが予定時間を超えて作業を続けることはなくなりました。またその時点で作業が完了していない場合は、作業状況をメンターが確認し、作業完了に向けてどう進めていくか相談することができました。

1人で悩む時間を制限する

作業着手時には想定していなかった問題が発生し、その問題に対処するためにどうするかインターンが1人で長時間悩むことがありました。相談する前に、自分で調べられるところは何とかしようという意識が強かったこともあり、なかなか相談できなかったようです。 そこで、悩んでいる時間を制限するため、悩んでいる時間が一定時間を超えたらメンターに相談することにしました。前述の時間の区切りがだいたい1時間だったので、15分に設定しました。その結果、インターンからメンターへの相談が増え、悩んで手が止まる時間は短くなりました。メンターもインターンの手が止まっていれば、早めに声を掛けるようになりました。

まとめ

今回はインターンシップで学んだことから、インターンの作業を進めやすくするための工夫を紹介しました。こまめに状況を共有し、問題を一緒に解決するような仕組みを用意することによって、インターンの作業が進めやすくなりました。また経験の少ないインターンにとって、相談することは難しく、いいアドバイスをもらうための相談の仕方にあるように準備をして相談できるようになるためには練習が必要です。1日のふりかえりや相談を増やす取り組みは、相談するときの手順にある今の状態と目指している状態を整理する機会になりました。これらの工夫はインターンシップだけでなく、新しい社員を迎えるときにも活用できそうです。

つづき: 2014-01-09
2013-09-26

インターンシップで学んだこと4:何をテストするか

前回は3日目に3つ学んだことの中の2つめ「テストを整理する方法」についてまとめました。今回は3日目に学んだことの最後、3つめである「何をテストするか」についてまとめます。

このまとめはインターンシップ時に書いたメモを読み返しながら書いています。数日前にこのURLのパスを変えました。当初は「/2013/...」と開催年を使っていたのですが、「/2/...」と通算何回目のインターンシップかを使うことにしました。理由は2013年に2回インターンシップを実施することになったため、開催年がインターンシップを識別するユニークな情報ではなくなったからです。

どこまでテストするか

テストを書いた方がよいことはわかった、テストを整理する方法もわかった、どんどんテストを書いていける。そんな状態でテストを書き始めると、たくさんテストを書いてしまいます。テストがたくさんあることはよいことのように聞こえますが、必ずしもよいこととは限りません。テストがたくさんあると以下のようなことが起きます。

  • テストが遅くなる。
  • テストのメンテナンスが大変になる。

どちらも「テストはいらない」と感じる原因になります。

テストが遅くなるとテストを実行することが面倒になります。面倒になるとテストを実行しなくなります。テストを実行しないと「テストがあっても意味ないね」と思うようになり、「テストはいらない」と感じるようになります。

APIの変更などでテストを変更する必要があったとき、テストが多いとテストの修正が大変になります。テストのメンテナンスが大変になるということです。テストのメンテナンスが大変になるとテストの作成・変更が開発の足をひっぱるようになり、機能の追加や修正作業に影響がでます。そうすると、自分達は「機能の追加や修正をしたいはずなのにテストばかりに時間を使っている、これでは本末転倒じゃないか」と思うようになり、「テストはいらない」と感じるようになります。

補足しておくと、どちらもテストがたくさんあることだけが原因ではありません。原因の1つというだけです。例えば、データベースに接続しているために遅くなる場合もありますし、テストが整理されていないためにメンテナンスが大変になる場合もあります。データベースに接続して遅くなっているなら、スタブを使って速くすることができますし、テストを整理することでメンテナンスを簡単にすることもできます。ここでは、テストの多さに注目するというだけです。

せっかくテストの恩恵を得るためにたくさんテストを書いたのに、「テストいらないかも…」と感じるようになってはもったいありません。そうならないために、適切な量のテストだけ書きましょう。

適切な量のテスト

適切な量のテストとは「実質的に同じこと」を含まないテストです。「同じこと」ではなく「実質的に同じこと」です。

例えば、以下の2つのアサーションは「同じこと」を確認しています。

1
2
assert_equal(11, 2 + 9)
assert_equal(11, 2 + 9)

以下の2つのアサーションは「実質的に同じこと」を確認しています。

1
2
assert_equal(11, 2 + 9)
assert_equal(12, 3 + 9)

以下ようにすると「実質的に違うこと」を確認しています。

1
2
assert_equal(11, 2 + 9)
assert_equal(-7, 2 + -9)

実質的に同じかどうかを判断するポイントは、入力値がどの分類に属しているかです。入力値が同じ分類なら「実質的に同じ」です。

2 + 9」を「正の整数 + 正の整数」と考えると、「2」も「3」もどちらも同じ「正の整数」という分類に入るので、「2 + 9」も「3 + 9」も実質的に同じです。

一方、「-9」は「負の整数」という分類になるので、「2 + 9」と「2 + -9」は実質的に違います。

分類をどう考えるかは「何を基準にするか」で変わってきます。たとえば、偶数か奇数かという基準にすれば「2」と「3」は実質的に違います。

どうやって基準を見つけるか

どうやって適切な基準を見つければよいか、そのやり方はまだうまくまとめられていません。テストを書いているときは、基準を考えて、分類し、実質的に違うことだけ確認しようとしているので、何かしらやり方をもっているような気がしますが、それを他の人に説明するところまではいっていません。「境界値を見つける」など説明できることはありますが、それだけではない気がしています。もっと何か別のやり方を持っている気がします。それらについてもうまく説明できるようになることは今後の課題です。

ただ、具体的にこの場合はどうするか?ということには答えることができます。1つ紹介します。

以下のようなテストがありました。

1
2
3
4
5
6
7
8
9
10
def test_title
  epub_book_doc_all = EPUB::Parser.parse(fixture_path('empty_contributors_single_spine.epub'))
  @document_all = EPUBSearcher::EPUBDocument.new(epub_book_doc_all)

  epub_book_doc_11_12 = EPUB::Parser.parse(fixture_path('single_contributors_multi_spine.epub'))
  @document_11_12 = EPUBSearcher::EPUBDocument.new(epub_book_doc_11_12)

  assert_equal("groongaについて", @document_all.title)
  assert_equal("groongaについて", @document_11_12.title)
end

@document_all@document_11_12の違いはコントリビューターの数とspineの数です。タイトルは同じです。

このときは、テスト対象の「タイトル」を基準に分類します。タイトルに違いがないなら、コントリビューターの数が違ってもspineの数が違っても同じ分類と考えます。同じ分類なら実質的に同じです。

書いてみて気づきましたが、テストで何に注目しているかを考えることが基準を見つけるやり方のひとつのような気がしますね。まとめてよかったです。

まとめ

インターンシップで説明した何をテストするかについてまとめました。インターンシップのときはうまく説明できなかったのですが、こうしてまとめてみたら整理された気がします。よかったです。

2013-09-05

インターンシップで学んだこと3:テストを整理する方法

インターン募集を開始したのが半年前の2月で、6月に開催されたRubyKaigi 2013までは1件も応募がありませんでした。RubyKaigi 2013に参加したところ1件応募があり、6月後半から7月にかけて実施しました。ここ最近まとめているインターンシップで学んだことはこの時期に実施したインターンシップで学んだことです。

その後、さらに2件の応募がありました。クリアコードのインターンシップは学生に限定せず働いている人でも働いていない人でも対象としていますが、3件ともすべて学生の方です。学生の方からの応募は夏休みの時期の方が多いようです*1

2件の応募のうち、1件は「インターンシップ実施企業と学生が話をできる場を提供するイベント」がきっかけでした。もう1件はリーダブルコードの解説を読んでクリアコードを知ったことがきっかけだということでした。解説を書いてよかったです*2

なお、この2件の応募に対して9月にインターンシップを実施する予定です。9月のインターンシップに向けて、前回のインターンシップで学んだことを早くまとめて活かしたいところです。が、間に合わなそうです。17日あるうちのまだ3日目です。

さて、前回は3日目に3つ学んだことの中の1つ「1人で開発しているときにtypoとどうつきあっていくか」についてまとめました。今回は3日目に学んだことの2つめである「テストを整理する方法」についてまとめます。

冗長なテスト

3日目の時点でのテストは以下のように50行にも満たない小さなものでした。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# -*- coding: utf-8 -*-

require 'test-unit'

require 'epub/parser'
require 'epub-searcher/epub-document'

class TestEPUBDocument < Test::Unit::TestCase
  def setup
    # groonga_doc_all.epub ... spine を一つしか含まない EPUB ファイル
    # 本文は groonga ドキュメント 1 章 が全て入っている
    epub_book_1 = EPUB::Parser.parse(fixture_path('groonga_doc_all.epub'))
    @document_1 = EPUBSearcher::EPUBDocument.new(epub_book_1)

    # groonga_doc_11_12.epub ... spine を二つ含む EPUB ファイル
    # 本文は groonga ドキュメント 1.1 と 1.2 が入っている
    epub_book_2 = EPUB::Parser.parse(fixture_path('groonga_doc_11_12.epub'))
    @document_2 = EPUBSearcher::EPUBDocument.new(epub_book_2)
  end

  def test_extract_contributors
    assert_equal([], @document_1.extract_contributors)
    assert_equal(["groongaコミュニティ A", "groongaコミュニティ B", "groongaコミュニティ C"], @document_2.extract_contributors)
  end

  def test_extract_creators
    assert_equal(["groonga"], @document_1.extract_creators)
    assert_equal(["groongaプロジェクト"], @document_2.extract_creators)
  end

  def test_extract_title
    assert_equal("groongaについて", @document_1.extract_title)
    assert_equal("groongaについて", @document_2.extract_title)
  end

  def test_extract_xhtml_spine
    assert_equal(["OEBPS/item0001.xhtml"], @document_1.extract_xhtml_spine)
    assert_equal(["item0001.xhtml", "item0002.xhtml"], @document_2.extract_xhtml_spine)
  end

  private
  def fixture_path(basename)
    File.join(__dir__, 'fixtures', basename)
  end
end

しかし、いくつか冗長な点が見えてきています。例えば変数名です。

1
2
3
4
5
6
7
8
9
10
11
def setup
  # groonga_doc_all.epub ... spine を一つしか含まない EPUB ファイル
  # 本文は groonga ドキュメント 1 章 が全て入っている
  epub_book_1 = EPUB::Parser.parse(fixture_path('groonga_doc_all.epub'))
  @document_1 = EPUBSearcher::EPUBDocument.new(epub_book_1)

  # groonga_doc_11_12.epub ... spine を二つ含む EPUB ファイル
  # 本文は groonga ドキュメント 1.1 と 1.2 が入っている
  epub_book_2 = EPUB::Parser.parse(fixture_path('groonga_doc_11_12.epub'))
  @document_2 = EPUBSearcher::EPUBDocument.new(epub_book_2)
end

単にepub_book@documentとするのではなく、epub_book_1@document_2としています。epub_book_@document_が重複しています。

同様に、テストメソッド名にも冗長な点があります。すべてのテストメソッド名がextract_を含んでいます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def test_extract_contributors
  # ...
end

def test_extract_creators
  # ...
end

def test_extract_title
  # ...
end

def test_extract_xhtml_spine
  # ...
end

このように、重複した情報によりテストが冗長になってきています。

流れ的には「冗長なテストをすっきりさせるにはどうしたらよいか」という話にいくのですが、その前に、どうして冗長なテストをすっきりさせなければいけないかを考えてみましょう。

冗長なテストをすっきりさせなければいけない理由

実装のコードはすっきりさせないといけないと言われています。そのための技術としてリファクタリングという名前もついているぐらいです。では、テストのコードはどうでしょうか。テストのコードもすっきりさせないといけないのでしょうか。もし、テストのコードもすっきりさせないといけないなら、テストのコードにもリファクタリングという技術を使えるのでしょうか。

結論を先にいうと、テストのコードもすっきりさせなければいけません。しかし、リファクタリングという技術は使えません。テストのコードをすっきりさせるには別のやり方を使わなければいけません。

テストのコードをすっきりさせなければいけない理由は、実装のコードをすっきりさせなければいけない理由と同じです。コードがすっきりしていないと、新しくコードを追加したり、既存のコードを修正したりといったコードを変更することが難しくなるからです。

では、テストのコードを変更しやすくするのはどうしてでしょうか。それは、実装のコードを変更しやすくするためです。テストがあれば、実装のコードを整理しても整理したことによって実装が壊れていないことを簡単に確認できます。同様に、新しく機能を追加したときも既存の機能が壊れていないことを簡単に確認できます。テストのコードが変更しづらくなって、テストを追加しなくなっていくと、実装のコードが壊れてしまったかどうかを確認することが難しくなります。そうすると、こわくて実装のコードを変更しづらくなります。

テストのコードが変更しやすいと以下のように開発できます。

実装変更 + テスト追加 → テスト実行 → 問題なし →
実装変更 + テスト追加 → テスト実行 → 問題なし →
実装整理              → テスト実行 → 問題なし →
実装変更 + テスト追加 → テスト実行 → 問題なし →
…

しかし、テストのコードの変更がしづらいと以下のようになります。

実装変更(テスト追加なし) → テスト実行 → たぶん問題なし →
実装変更(テスト追加なし) → テスト実行 → たぶん問題なし →
実装整理                   → テスト実行 → たぶん問題なし →
実装変更(テスト追加なし) → テスト実行 → たぶん問題なし →
実装変更(テスト追加なし) →
テスト少ないし実行しなくてもいいよね →
問題ないといいなぁ →
実装変更(したくないなぁ) →
…

まとめると、テストのコードを変更しやすくしておくことは、実装を変更しやすくしておくことにつながるので重要です。

それでは、テストのコードをすっきりさせるにはどうしたらよいでしょうか。残念ながらリファクタリングという技術は使えません。なぜなら、リファクタリングをするにはテストが必要だからです。テストをリファクタリングするにはテストのテストが必要になります。これではいつまでたってもテストのリファクタリングができません。

テストをすっきりさせるにはどのようなやり方がよいかを考えてみましょう。

それぞれのテストを独立させる

テストのコードを整理するときは、変更が他のテストに影響を与えていないことを手動で確認する必要があります。テストのテストがないため自動で確認することができないからです。

手動で確認するので、多くのことを確認しなければいけない状況は避けましょう。ミスが多くなります。この状況を避けるためには、それぞれのテストの影響範囲を小さくしておくことが有効です。変更の影響を確認する範囲が小さくなります。

具体的には以下のようにしてそれぞれのテストを独立させます。

  • 1つのテストで1つのことを確認する。
  • テスト対象ごとにテストをグループ化する。(さまざまなテスト対象のテストを1つのグループにまとめない。)
1つのテストで1つのことを確認

1つのテストで1つのことを確認するというのは、ざっくりというと1つのテストの中で1つのアサーションを使うということです*3

今回のケースでは、以下のようにtest_extract_contributorsという1つのテストの中で、「spineを1つしか含まないケース」と「spineを2つ含むケース」の2つのことを確認していました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class TestEPUBDocument < Test::Unit::TestCase
  def setup
    # groonga_doc_all.epub ... spine を一つしか含まない EPUB ファイル
    # 本文は groonga ドキュメント 1 章 が全て入っている
    epub_book_1 = EPUB::Parser.parse(fixture_path('groonga_doc_all.epub'))
    @document_1 = EPUBSearcher::EPUBDocument.new(epub_book_1)

    # groonga_doc_11_12.epub ... spine を二つ含む EPUB ファイル
    # 本文は groonga ドキュメント 1.1 と 1.2 が入っている
    epub_book_2 = EPUB::Parser.parse(fixture_path('groonga_doc_11_12.epub'))
    @document_2 = EPUBSearcher::EPUBDocument.new(epub_book_2)
  end

  def test_extract_contributors
    assert_equal([], @document_1.extract_contributors)
    assert_equal(["groongaコミュニティ A", "groongaコミュニティ B", "groongaコミュニティ C"], @document_2.extract_contributors)
  end
end

これは、以下のように別のテストにわけると1つのテストで1つのことを確認するようになります。

1
2
3
4
5
6
7
8
9
10
11
12
13
class TestEPUBDocument < Test::Unit::TestCase
  def setup
    # ...
  end

  def test_extract_contributors_with_spine
    assert_equal([], @document_1.extract_contributors)
  end

  def test_extract_contributors_with_spines
    assert_equal(["groongaコミュニティ A", "groongaコミュニティ B", "groongaコミュニティ C"], @document_2.extract_contributors)
  end
end

こうすると、@document_1を変更しても2つめのテスト(..._spinesの方)には影響がありません。影響範囲が狭くなりましたね。

なお、1つのテストで1つのことを確認することはテストが失敗したときのデバッグのしやすさにもつながるので、テストの変更のしやすさ以外の観点からも有用です。

テスト対象ごとにテストをグループ化

個々のテストではなくもう少し大きな単位で考えてみましょう。影響範囲を小さくする別の方法があります。具体的に言うと、「テストをグループ化した単位」です。xUnitの場合はテストケースです。

具体的なコードで考えてみましょう。以下のコードには1つのテストケースの中に2つのテストがあります。それぞれのテストではsetupで用意したのに使っていない変数があります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class TestEPUBDocument < Test::Unit::TestCase
  def setup
    # groonga_doc_all.epub ... spine を一つしか含まない EPUB ファイル
    # 本文は groonga ドキュメント 1 章 が全て入っている
    epub_book_1 = EPUB::Parser.parse(fixture_path('groonga_doc_all.epub'))
    @document_1 = EPUBSearcher::EPUBDocument.new(epub_book_1)

    # groonga_doc_11_12.epub ... spine を二つ含む EPUB ファイル
    # 本文は groonga ドキュメント 1.1 と 1.2 が入っている
    epub_book_2 = EPUB::Parser.parse(fixture_path('groonga_doc_11_12.epub'))
    @document_2 = EPUBSearcher::EPUBDocument.new(epub_book_2)
  end

  def test_extract_contributors_with_spine
    assert_equal([], @document_1.extract_contributors)
  end

  def test_extract_contributors_with_spines
    assert_equal(["groongaコミュニティ A", "groongaコミュニティ B", "groongaコミュニティ C"], @document_2.extract_contributors)
  end
end

@document_1は1つめのテストでしか使われていません。@document_2は2つめのテストでしか使われていません。setupで準備したのに使われていない変数があるということは、そのテストケースでは様々な種類のテストをグループ化しているということです。必要な変数を使うテスト毎にグループを細かくすることで影響範囲を小さくすることができます。

「spineの数」でさらにグループ化しましょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class TestEPUBDocument < Test::Unit::TestCase
  class TestSingleSpine < self
    def setup
      epub_book = EPUB::Parser.parse(fixture_path('groonga_doc_all.epub'))
      @document = EPUBSearcher::EPUBDocument.new(epub_book)
    end

    def test_extract_contributors
      assert_equal([], @document.extract_contributors)
    end
  end

  class TestMultipleSpines < self
    def setup
      epub_book = EPUB::Parser.parse(fixture_path('groonga_doc_11_12.epub'))
      @document = EPUBSearcher::EPUBDocument.new(epub_book)
    end

    def test_extract_contributors
      assert_equal(["groongaコミュニティ A", "groongaコミュニティ B", "groongaコミュニティ C"], @document.extract_contributors)
    end
  end
end

「spineの数」でグループ化したことにより、テストケース名(TestSingleSpine)で「どのようなspineに注目しているか」ということが表現できるようになりました。そのため、もともとは以下のように「spineの数は1つだよ」と書いていたコメントを削除しました。

1
2
3
4
5
6
def setup
   # groonga_doc_all.epub ... spine を一つしか含まない EPUB ファイル
   #                          本文は groonga ドキュメント 1 章 が全て入っている
   epub_book_1 = EPUB::Parser.parse(fixture_path('groonga_doc_all.epub'))
   @document_1 = EPUBSearcher::EPUBDocument.new(epub_book_1)
end

使っている変数に注目してテストケースをわけたため、それぞれのテストは独立するようになりました。TestSingleSpineの中を変更しても、もう一方のテストケースには影響を与えません。

今は変数に注目してテストケースをわけましたが、メソッド名に注目してわける方法もあります。メソッド名の一部のtest_extract_contributorsに注目するとこうなります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class TestEPUBDocument < Test::Unit::TestCase
  class TestExtractContributors < self
    def test_empty
      assert_equal([], extract("groonga_doc_empty.epub"))
    end

    def test_multiple
      assert_equal(["groongaコミュニティ A", "groongaコミュニティ B", "groongaコミュニティ C"],
                   extract("groonga_doc_multiple_contributors.epub"))
    end

    private
    def extract(path)
      epub_book = EPUB::Parser.parse(fixture_path(path))
      document = EPUBSearcher::EPUBDocument.new(epub_book)
      document.extract_contributors
    end
  end

  def test_XXX
    # ...
  end

  # ...
end

extract_contributorsという情報をテストケース名にもっていって、テスト名からは抜きました。これで、このテストケース内の変更は他のテストケースには影響を与えません。

extract_contributorsがsnake_caseからExtractContributorsというCamelCaseになって気持ち悪いと感じる人もいるでしょう。その場合はsub_test_caseを使えば、snake_caseのまま書けます*4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class TestEPUBDocument < Test::Unit::TestCase
  sub_test_case("extract_contributors") do
    def test_empty
      # ...
    end

    def test_multiple
      # ...
    end

    private
    def extract(path)
      # ...
    end
  end

  def test_XXX
    # ...
  end

  # ...
end

テストのグループ化についてはテストをすっきり書く方法として過去にまとめていて、インターンシップでもこのエントリーを読んでもらいました。

まとめ

インターンシップで説明した、テストを整理する方法についてまとめました。今回説明したやり方は以下の2つです。

  • 1つのテストで1つのことを確認する。
  • テスト対象ごとにテストをグループ化する。

過去にこれらのやり方についてエントリーにまとめていたので、インターンシップではまずエントリーを読んでもらい、概要を把握してもらったあとに説明しました。知見を文章としてまとめておくと有用であることがわかりました。過去に説明したものはまとめておいて、次に説明する機会があるときに参照しながら説明する、ということは今後も続けたいです。

作業をしていてなにかあったら相談して欲しいということは伝えていましたが、それだと曖昧すぎるようです。それよりも、「名前に困ったら相談するタイミング」というより具体的な基準を設定するほうが実行しやすいということがわかりました。今回のケースでは_1_2がでていますが、連番は適切な名前が思いつかなかったときに使われることが多いです*5。このように、この基準はわかりやすいため、これからも使えそうです。

*1 そりゃそうだろうなぁという感じですね。

*2 リーダブルなコードに関する講演依頼はお問い合わせフォームからどうぞ。

*3 複数のアサーションで1つのことを確認しているならそれでもOKです。厳密に1つのアサーションにしろということではなく、1つのことを確認しているかがわかりやすい表現なだけです。

*4 ただし、いつものRubyの書き方ではなくなります。この書き方を知っている人にはわかりやすいかもしれませんが、Rubyだけを知っている人には馴染みにくいでしょう。どちらを選ぶかはトレードオフを判断して決めてください。

*5 連番が適切なときもありますが、配列などで表現することもできるのでそんなに多くはありません。

2013-08-27

インターンシップで学んだこと2:1人で開発しているときはていねいに開発を進める

インターンシップ1日目と2日目ではコメントに注目するとよいということを学びました。インターンシップ3日目のメモを読み返すと3日目は3つ学んだことがありました。今回はそのうちの1つ「1人で開発しているときにtypoとどうつきあっていくか」です。

背景

インターンシップは基本的にメンターがいつもインターンのそばにいて一緒に開発を進めていく予定でしたが、打ち合わせなど他の予定が入ってしまうこともありました。そのようなときはインターンが1人で開発を進めていました。メンターは他の予定から戻ってきたときに「どうだった?」「詰まっているところはない?」などと聞きながらコミットメールを読んでいました。

コミットメールを読めばだいたいどんなことをやっていたかはわかりますが、「変なことをしていないか?」という視点で読んでいるわけではないので細かいミスすべてに気づくわけではありません。どちらかというと時間を越えてペアプログラミングしている気分で読んでいます。これは、コードレビューでの読み方とは違う読み方です。

そんなわけで、2日目に追加したテスト名にtypoがあり、テストとして認識されていないことに気づいていませんでした*1。typoを直してテストとして認識されるようにしたところテストが失敗していました。せっかくテストを追加しながら開発していたのですが活かせていませんでした。

typoはよくあることですし、すぐに直せることなので、typoしてしまうことが悪いことだとは思いません。typoを防ぐことよりもtypoしてもすぐに気づいて直せるようにした方が現実的でしょう。ということで、「1人で開発しているときにtypoとどうつきあっていくか」についてです。どうつきあっていくかを最初に言うと、ていねいに開発を進めてつきあっていく、です。

開発を継続するためのテスト

開発するときはテストを書きますが、それは開発を駆動するためではないのでテスト駆動開発という言葉を使うことはあまりありません。「テストを書きながら開発をするやり方」をざっくりと説明するために便利なのでテスト駆動開発という言葉を使うくらいです。テストを書くのは開発を駆動するためではなく、開発を継続するためです。

いくつもフリーソフトウェアの開発に関わっていると、数カ月ぶりにコミットするということがあります。そんなときはコードの隅々まで覚えているという状態ではありません。そんな状態でも安心してコードを変更できるようにするためにテストを書いておきます。他にも、開発を長く続けていると新しいプラットフォームや新しいライブラリーに対応したりする場合にテストがあると安心してコードを変更できます。つまり、いつまでも安心してコードを変更していけるように、開発を継続していけるように、そのためにテストを書いています。

開発を駆動するためにテストを書いているわけではないので、テストファーストで開発を進めるときもあればそうでない場合もあります。入力のパターンがわかっている場合は1つずつテストファーストで開発を進めていきますが、さぐりさぐりで開発しているときはある程度実装が見えてきてからテストを書きます。

このように、あまりテスト駆動開発を取り入れていませんが、テスト駆動開発の中にていねいに開発を進めるためによいやり方があるので、それは取り入れています。

最初にテストを失敗させる

テスト駆動開発の中にあるていねいに開発を進めるためのよいやり方とは「最初にテストが失敗させ、ちゃんとテストが実行されていることを確認する」やり方です。これは、1人で開発しているときでもなるべく早くtypoに気づけるようになるからです。具体的にどうやっているかというと、最初は期待する値として"XXX"という必ず失敗する値を指定して実行しています。別に"XXX"でなくてもよいのですが、コードのコメントの中で「なんか気をつけて!」くらいの意味合いで「XXX」が使われるので、失敗する値としても"XXX"を使っています。

1
2
3
def test_plus
  assert_equal("XXX", 1 + 2)
end

このコードをコミットすることはないのでこのように開発していることを知っている人はほとんどいないでしょう。インターンシップのときも伝え忘れました。単に、「テストを書いたら最初に失敗させてテストが実行されていて確認するといいよ」と伝えたくらいだった気がします。

テストでは補完機能を使わない

typoを早く見つけるために気をつけていることがもう1つありました。それは、テストコードの方ではあまりエディターの補完機能を使わないということです。テストコード固有のところでは使いますが、テスト対象のコードを書くとき(開発した機能をテストの中で使っているとき)には使いません。これは、補完機能を使ってしまうと開発しているコードの中にあるtypoをそのままテストでも使ってしまうからです。そのまま使ってしまうとtypoに気づくことができません。

テストコードの中では開発しているコードを使う側の気持ちになって書きたいので、エディターの補完機能に頼らず実際に入力しています。

このやり方もインターンシップ中には伝え忘れた気がします。

まとめ

メインで開発している人が自分だけの場合は、typoに気づきづらいものですが、ていねいに開発を進めれば早いうちに気づけるものですよ、ということをまとめました。普段、意識せずにやっていて、インターンシップのときもそれほどまとまった形で伝えられなかったことが、こうしてインターンシップのメモをまとめ直すことでまとまった形にできてよかったです。

*1 こういうときはdef test_XXXではなく、test "..." {}でテストを定義したほうがよいのではないかという気がしてきます。

2013-08-13

インターンシップで学んだこと1:コメントを書きたくなるときはコードを見直す機会

RubyKaigi 2013で発表したらインターンシップの応募があり、6/17から7/29まで1ヶ月半くらい週3日でインターンシップを実施しました。インターンシップでやったことはメモに残して公開していました。ただメモに残すだけではなく、まとめておき、次のインターンシップや新しくクリアコードのメンバーになった人に伝えるために役立てようと試みています。

クリアコードではインターンシップをクリアコード側もいろいろ学ぶ機会として捉えています。今回のインターンシップでクリアコードが得られたものは以下のようなものです。やった価値がありました。

  • 自分たちがクリアなコードを書くために大事にしていることのいくつかを明文化できた。
  • こうすれば伝わりやすいのではないかと考えた方法のうち、効果があるものとないものがわかった。
  • 新しい人とコミュニケーションをとりやすくする方法を試して、効果があることがわかった。

これらは大きな項目なので、少しずつもう少し細かい単位でまとめていきます。今回まとめるのは、明文化できたクリアなコードを書くために大事にしていることの1つです。

背景

インターンシップ1日目では、作ろうとしているWebアプリケーションが技術的に簡単に実現できそうかどうかを試しました。具体的にいうと、コアとなる機能を使ったコマンドラインツールを作りました。コマンドラインツールを作ることで簡単に実現できそうだということがわかりました。

作ったコマンドラインツールを見ると気になるところがあったので、インターンシップ2日目は気になるところを説明しながら整理しました。その中で明文化できたクリアなコードを書くために大事にしていることの1つが「コメントを書きたくなるときはコードを見直す機会とすること」でした。

コードの中のコメント

クリアコードの人が書くプログラムにはコメントが少ないです。これは、「コメントで書いていることを本当にコードで表現できないか」を考えてコードを書いているからです。多くの場合はコードで表現することができるので、コメントを書くことが少なくなります。

コメントが少ないからといって「絶対コメントを書くな!」と思っているわけではありません。コメントを書くことでコードだけよりも理解が進むと判断したらコメントを書きます。

例えば、groongaで高速な位置情報検索をしているコードには以下のようなコメントを書いています。リンク先の記事で「例なので、実際の処理よりも大雑把に説明します。」と書いていることの「実際の処理」の部分です。

groonga/lib/geo.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/*
  b: base_point
  x: geo_base
  0-83: sub meshes. 0-83 are added order.

j: -5  -4  -3  -2  -1   0   1   2   3   4
  +---+---+---+---+---+---+---+---+---+---+
  |74 |75 |76 |77 |78 |79 |80 |81 |82 |83 | 4
  +---+---+---+---+---+---+---+---+---+---+
  |64 |65 |66 |67 |68 |69 |70 |71 |72 |73 | 3
  +---+---+---+---+---+---+---+---+---+---+
  |54 |55 |56 |57 |58 |59 |60 |61 |62 |63 | 2
  +---+---+---+---+---+---+---+---+---+---+
  |48 |49 |50 |  b    |       |51 |52 |53 | 1
  +---+---+---+       |       +---+---+---+
  |42 |43 |44 |       |x      |45 |46 |47 | 0
  +---+---+---+-------+-------+---+---+---+
  |36 |37 |38 |       |       |39 |40 |41 | -1
  +---+---+---+  base meshes  +---+---+---+
  |30 |31 |32 |       |       |33 |34 |35 | -2
  +---+---+---+---+---+---+---+---+---+---+
  |20 |21 |22 |23 |24 |25 |26 |27 |28 |29 | -3
  +---+---+---+---+---+---+---+---+---+---+
  |10 |11 |12 |13 |14 |15 |16 |17 |18 |19 | -4
  +---+---+---+---+---+---+---+---+---+---+
  | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -5
  +---+---+---+---+---+---+---+---+---+---+
                                            i
*/

1日目に書いたスクリプトにはいくつかコメントがありました。これを見て、「自分たちはコメントを書くときにこのコメントは本当にコードで表現できないかを考えているなぁ」と気づきました。ということで、一緒にコメントを1つずつ確認し、コードで表現できないか考えることにしました。

コードと同じ内容のコメントか

最初に確認したコメントは以下のコメントです。

1
2
3
4
5
def show_html_content(io)
  # Nokogiri でパースし、内部テキストを出力する
  content = Nokogiri::HTML(io)
  puts content.text
end

このコメントはメソッド内の処理していることそのものについて書いています。つまり、コードの内容とコメントの内容が同じです。コードと同じ内容のコメントは削除します。

1
2
3
4
def show_html_content(io)
  content = Nokogiri::HTML(io)
  puts content.text
end

削除する理由は以下のとおりです。

  • 重複した情報なので必要ないから。
  • 「コメントに書くくらいだから何か特別な情報が入っているのではないか?」と読む人に勘違いさせないため。

後者の「読む人に勘違いさせないため」ということを少し補足します。「書いた人は読む人の助けとなるようにコードもコメントも書いているはず」という前提にたって読んでいる人は、本当は意味のないコメントにも「一見意味がなさそうだが、実はなにか意味があるのではないか?」という視点で読みます。そのため、「本当は必要のないコメント」を「必要なコメントではないか?」と勘違いしてしまい、コメントの必要性を探ってしまいます。そして、読んだ後に「なんだ、必要のないコメントだったのか」と気づきます。

参考:

処理の塊を説明しているコメントはメソッド名で表現する

次に検討したコメントは以下のコメントです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def open_epub(filename)
  uri_array = Array.new
  epub_book = EPUB::Parser.parse(filename)
  metadata = epub_book.metadata

  # xhtml ドキュメントがどの順番で出てくるか記録する
  epub_book.each_page_on_spine do |item|
    if item.media_type == "application/xhtml+xml"
      uri = item.href
      uri_array << uri.to_s
    end
  end
  # ... 
end

このコメントは処理の塊で何をしているかを説明しています。コメントの内容の方が少し抽象度の高い説明になっており、コードと同じ内容というわけではありません。このように処理の塊に抽象度の高い説明をつけている場合は処理をメソッドに切り出してコメントで表現していたことをメソッド名で表現します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def extract_xhtml_uri_array(epub_book)
  uri_array = Array.new

  epub_book.each_page_on_spine do |item|
    if item.media_type == "application/xhtml+xml"
      uri = item.href
      uri_array << uri.to_s
    end
  end

  uri_array
end

def open_epub(filename)
  epub_book = EPUB::Parser.parse(filename)
  metadata = epub_book.metadata

  uri_array = extract_xhtml_uri_array(epub_book)
  # ... 
end

「どの順番で出てくるか」という情報が失われているので、コメントの情報をすべてをメソッド名で表現できていませんが、処理の塊にコメントで説明していた情報をメソッド名で表現するということはこういうことです。

まとめ

インターンシップでクリアコードが学んだことをまとめはじめました。今回は、自分たちがクリアなコードを書くために「コメントを書きたくなるときはコードを見直す機会」と捉えていることについてまとめました。

今回でてきたコードはコメントにだけ着目したもので、その後、他のところも整理されよりクリアなコードになっていきました。気になる人はranguba/epub-searcherを確認してみてください。

2013-08-05

クリアコードがインターンシップを実施する理由

こんにちは。初代社長の南です。クリアコードでは開発以外のあらゆる業務を担当しています。

はじめに

2013年6月18日にTAMA協会主催のキャリア体験フェア2013が開催されました。このイベントではTAMA地域の学生さんと企業がインターンシップのマッチングを行いました。クリアコードはインターンを受け入れる側の企業として初めて参加しました。クリアコードの他には、IT関連や金融、製造業など幅広い業種の12社が参加していました。学生さんは100名近く参加していたようです。

マッチングでは、クリアコードのインターンシップに興味をもってくれた学生さんに対して、業務内容とインターンシップの概要を説明しました。面談は1回あたり15分で合計5回行ったのですが、参加している学生さんのほとんどが経済学部や文学部といった文系の方*1で、IT業界には興味はあるけれどプログラミングの経験がほとんどない方ばかりでしたので、クリアコードのインターンシップの応募条件を満たす方には出会えませんでした。

そのような状況でしたので、最初2回は業務内容とインターンシップの説明をしましたが、3回目と4回目はなぜクリアコードはインターンシップを行うのかという話、5回目は就職活動の事例紹介として、筆者の就職活動の話やクリアコードで働くまでの経緯を紹介しました。合計5回の面談を通じて、学生さんの反応が良かった*2のはなぜインターンシップを行うのかという話と筆者の就職活動の話でした。

そこで今回は、学生さんがインターンシップ先を検討するときのアドバイスも兼ねて、なぜクリアコードがインターンシップを行うのかを紹介します。

なぜインターンシップを行うのか

インターンシップを実施する企業は、なんらかの目的を達成するためにインターンシップという手段を選んでいることでしょう。クリアコードは、4つの目的を達成するためにインターンシップを実施します。

インターンシップを実施する4つの目的

クリアコードが掲げている目的は次の4つです。クリアコードは、クリアなコードやフリーソフトウェアを大切にしていて、それらにつながった目的になっています。

  • 採用につなげる
    • インターンシップを通じてクリアコードのことを知ってもらい、クリアコードを就職先として考えてもらいたい。クリアコードの開発者は業務としてフリーソフトウェアを開発するので、クリアコードの開発者が増えるとフリーソフトウェアへの貢献につながります。
  • フリーソフトウェア開発者を増やす
    • インターンがインターンシップ終了後もフリーソフトウェア開発を続けるようになってほしい。インターンがフリーソフトウェア開発者になれば、フリーソフトウェアへの貢献につながります。
  • よいコードを書く人を増やす
    • 自分達が出会うコードがきれいであってほしい。インターンがよいコードを書くスキルを身につけ、それを実践するようになれば、クリアなコードが増えます。こうしてクリアなコードをさらに広めていきたいです。
  • 伝える技術を磨く
    • クリアコードが持っているノウハウや技術を効率よく伝える方法を確立したい。インターンから伝え方に対するフィードバックを積極的にもらえれば、よりうまく伝える方法を見つけられそうです。

この4つの目的のうち、1つでも多くの目的を達成できそうな方をインターンに採用したい考えです。

インターンシップに応募する学生さんの目的

クリアコードは前述の通りの目的を達成したいと考えています。一方、クリアコードのインターンに応募する学生さんもインターンシップで達成したい目的があるはずです。インターンシップを通して学生さんが目的を達成できれば、学生さんにとってこのインターンシップはやってよかったといえるでしょう。学生さんにとって有意義なインターンシップになるどうかは、クリアコードのインターンシップの内容が学生さんの目的達成につながるかにかかっています。クリアコードのインターンシップでは、学生さんは次の目的を達成できるでしょう。

  • プログラマーの仕事を体験したい
    • クリアコードのメンバーと一緒にフリーソフトウェアを開発します。クリアコードの場合、フリーソフトウェア開発も業務ですので、プログラマーの仕事を体験することになります。
  • フリーソフトウェア開発に関わりたい
    • インターンシップではフリーソフトウェアを開発します。フリーソフトウェアですので、インターンシップ終了後も開発に関わり続けることができます。
  • よいコードを書けるようになりたい

これらのほかにも、学生さんの目的を教えてもらえればそれをインターンシップを通じて達成できないか検討します。

インターンの選考でお互いの目的を達成できるかどうか面談で確認

このように、インターンシップを実施する企業、インターンシップに応募する学生それぞれに、インターンシップによって達成したい目標があります。そのため、インターンの選考では、インターンシップを通じてお互いの目的を達成できそうであることを確認する必要があります。そこで、クリアコードのインターンシップの面談では、クリアコードの目的と応募者の目的をお互いに説明し、インターンシップを通じて目的を達成できるか確認することにしています。先日の面談でもこのプロセスを通じて、お互いの目的が達成できることを確認しました。

インターンシップ先を検討する際のアドバイス

このようなプロセスがインターンシップを実施する他の企業でも行われているのではないかと想像しています。もちろん、クリアコードのように明示的に行うところもあれば、書類選考や面接を通じて企業側の視点から企業の目的を達成できるかどうかを判断しているところもあるでしょう。これからインターンシップ先を検討される方は、インターンシップを実施する企業の目的を理解し、また自分がインターンシップで達成したい目的を明確にした上で、お互いに目的を達成することができるかどうか確認してみてはいかがでしょうか。自分に適したインターンシップ先かどうかの判断基準になるはずです。

まとめ

クリアコードがインターンシップを実施する目的と、目的が達成できるか確認するために面談をすることを紹介しました。インターンシップ先を検討する際に参考にしていただけるとうれしいです。もちろん、クリアコードのインターンシップを検討するときは、是非お互いの目的を達成できそうか考えてみてください。

また、7月11日につくばインターンシップ・コンソーシアム主催の2013夏休みインターンシップマッチングフェアに参加します。クリアコードのインターンシップに興味のある方は是非お越しください。

*1 筆者も経済学部出身で、普通に文系就職し、新人のころ3年ほど新卒採用に関わっていましたので、文系の学生さんとお話するのはとても懐かくて楽しかったです。

*2 筆者と学生さんが楽しく会話できて、かつ学生さんからよい感想をもらえたという基準です。

2013-06-20

GtkIMCocoaの動作状況

はじめに

今回は、以前紹介したMac OS XのCocoa版GTK+で日本語入力を行うためのgtkimmodule(GtkIMCocoa)の開発の続きです。

前回の記事に対してはいくつかの反応を頂きましたが、中でも興味深かったのは、Xamarin Studio(旧MonoDevelop)でも日本語入力ができるようになるかもしれないという記事でした。筆者は、現時点ではCocoa版GTK+に対応したバイナリを配布しているアプリケーションはほとんどないと認識していたので、これは意外な反応でした。この記事をきっかけに知ったのですが、Mac版MonoDevelopでは何年も前からCocoa版GTK+を採用しており、当初からユーザーの間では日本語入力の問題が取り沙汰されていたようです。MonoDevelopはメジャーなゲームエンジンの一つであるUnityにも採用されており、潜在的なユーザーは意外と多いのではないか?ということに気づきました。

そこで今回は、いくつかのGTK+アプリケーションにGtkIMCocoaを組み込んでみて、現時点のGtkIMCocoaがどの程度動作するのかを確認した結果を紹介致します。問題点を把握することで開発を加速させることが目的であり、現時点での実用性をアピールするものではありませんので、その面での期待はしないでください。

Xamarin Studioへの適用方法と動作検証結果

前回の記事でも紹介した通り、GtkIMCocoaはGTK+本体を修正すること無く組み込むことができるように設計されています。このため、バイナリ形式で配布されているGTK+に対しても、GtkIMCocoaのモジュールファイルを追加して、設定ファイルを更新するだけで適用することができます。

GtkIMCocoaの対応バージョン

2013年5月22日現在、Xamarin Studioに同梱されているGTK+は、正式にMac OS Xに対応した3.0系ではなく、2.0系にCocoa対応パッチを当てたバージョンが採用されています。これに対して、GtkIMCocoaは主にGTK+3をターゲットとしており、前回の記事でリリースしたgtkimcocoa-0.0.0も、GTK+3のみをビルド対象としています。

しかし、その後の修正でGTK+2に対してもビルドできるように対応しておりますので、GTK+2アプリケーションに組み込みたい場合は、GitHubから最新のソースコードを取得してください。

Xamarin Studioに対するGtkIMCocoaのビルド方法

GtkIMCocoaの基本的なビルドおよびインストール方法は前回の記事を参照してください。

Xamarin Studioに同梱されているGTK+に対するビルドも、基本的にはこれと同じ方法で行います。ただし、インストールされているパスやGTK+バイナリの対応アーキテクチャに合わせて、configureスクリプト実行時に環境変数を調整する必要があります。GTK+が/Library/Frameworks/Mono.framework/Versions/2.10.12にインストールされている場合のインストール例を以下に示します*1。前回と同様にjhbuildのshellで実行します。

$ ~/.local/bin/jhbuild shell
$ export GTK_PATH=/Library/Frameworks/Mono.framework/Versions/2.10.12
$ git clone git://github.com/ashie/gtkimcocoa.git
$ cd gtkimcocoa
$ ./autogen.sh
$ ./configure ¥
  PATH="$GTK_PATH/bin:$PATH" ¥
  PKG_CONFIG_PATH="$GTK_PATH/lib/pkgconfig" ¥
  CFLAGS="-arch i386" ¥
  LDFLAGS="-arch i386"
$ make
$ sudo make install
$ $GTK_PATH/bin/gtk-query-immodules-2.0 | sudo tee $GTK_PATH/etc/gtk-2.0/gtk.immodules

Xamarin Studioに含まれるGTK+はi386用のバイナリですが、x86_64環境で普通にビルドしてしまうとx86_64用のバイナリが生成されてしまうため、CFLAGSおよびLDFLAGSにオプションを追加して適切なバイナリを生成するようにしています。

動作検証結果

前述の方法でGtkIMCocoaを組み込んだXamarin Studio 4.0.1を実行したところ、トップ画面の検索エントリで日本語を入力できることを確認しました。

Xaramain Studioの検索エントリでの日本語入力の様子

次にテキストエディタで入力を試みたところ、こちらでは問題が発生しました。

  1. 未確定文字列が表示されない
  2. Enterでの確定時に、改行が挿入されてしまう
  3. 日本語入力OFF時に文字が2重に入力されてしまう
  4. 日本語入力OFF時に、前回の未確定文字列が常に表示されてしまう

Xaramain Studioのテキストエディタウィジェットでの日本語入力の様子

GTK+が標準で用意しているテキスト入力ウィジェットについては、前回の記事である程度の動作は確認できています。しかし、アプリケーションの中にはテキスト入力ウィジェットを独自実装して提供しているものもあります。このようなウィジェットでは、テキスト入力処理の動作がGTK+標準のウィジェットとは微妙に異なることによって、不具合が発生することもしばしばあります。このため、immoduleの開発にあたってはさまざまなアプリケーションと組み合わせての動作検証や調整が必要であり、時にはアプリケーション側の修正も必要となります。Xamarin Studioのテキスト入力ウィジェットもアプリケーション独自のウィジェットであるため、このケースに当たってしまったようです。

原因を究明するにあたり、まずはUbuntu上のXamarin Studioでも簡単に動作確認を行ってみました。UbuntuではGtkIMCocoaは動作しませんので、Ubuntu標準のibusというimmoduleで動作を検証しています。これにより、問題がアプリケーション側にあるのか、immodule側にあるのかを大雑把に切り分けることができます。

結果としては、

  • 1.の問題はUbuntu上のXamarin Studioでも再現するため、アプリケーションの問題である*2
  • その他の問題は再現しないため、GtkIMCocoaの問題である

ということになりました。

この検証結果を受けてGtkIMCocoaには修正を入れ、2.〜4.の問題については解決しています。しかし、1.の問題については着手できておらず、今後の調査が必要です。

Sylpheedでの動作検証

一つのアプリケーションだけでは問題を把握しきれない可能性が高いため、Xamarin Studioに加えて、Sylpheedでも動作検証を行いました。

Sylpheedのビルド方法

SylpheedについてはMac OS X用のバイナリは配布されていませんので、ソースからビルドする必要があります。筆者の場合は、GTK+2をjhbuildでインストールし、GtkIMCocoaとSylpheedもjhbuildのshell上でビルドしました。

GTK+2のインストール

前回の記事を元にGTK+3をインストール済みの環境の場合、追加で以下を実行することでGTK+2をインストールすることができます。

$ ~/.local/bin/jhbuild build meta-gtk-osx-core
GtkIMCocoaのインストール

GtkIMCocoaについては、GTK+2用のimmoduleを追加するために、最新のソースコードを取得して、前回と同様の手順でビルドおよびインストールします。

$ ~/.local/bin/jhbuild shell
$ git clone git://github.com/ashie/gtkimcocoa.git
$ cd gtkimcocoa
$ ./autogen.sh
$ ./configure
$ make
$ make install
$ gtk-query-immodules-2.0 > ~/gtk/inst/etc/gtk-2.0/gtk.immodules
Sylpheedのインストール

Sylpheedのサイトからソースコードをダウンロードして、同様の手順でビルドします。

$ tar xvfj sylpheed-3.4.0beta3.tar.bz2
$ cd sylpheed-3.4.0beta3
$ ./configure --prefix=~/gtk/inst
$ make
$ make install
Sylpheedでの動作検証結果

SylpheedではGTK+標準のウィジェットしか使われていないため、一見すると普通に動いている様にも見えます。

Sylpheedでの日本語入力の様子

しかしながら、よくよく確認してみると、以下のような問題があることがわかりました。

  1. キー操作で入力モードを切り替えると、フォーカスウィジェットが勝手に切り替わってしまう
  2. フォーカスが当たっているウィジェットと、実際に入力されるウィジェットが異なる場合がある
  3. 注目文節(2重下線部)の表示が間違っている*3

その後の修正で2.の問題については解決していますが、1.および3.の問題については今後の調査が必要です。

まとめ

今回の検証では、当初の設計意図通り、バイナリ配布されているCocoa版GTK+アプリケーションに対して、GtkIMCocoaのモジュールファイルを一つ追加するだけで日本語入力が可能となることは確認できました*4。しかしながら、細かい挙動ではまだまだ課題も多く、引き続き地道な検証および調整作業が必要なこともわかりました。また、Xamarin Studioについてはアプリケーション側の修正が必要な事もわかりました。

引き続き弊社インターンシップの題材としても挙げていますので、修正にチャレンジしてみたいと思われる方はご応募頂ければ幸いです。

*1  パス中のバージョン番号はインストールされているXamarin Studioによって異なると思われますので、その場合は適宜読み替えてください。

*2  未確定文字列をマウスで選択状態にして反転させると表示されることから、GTK+のスタイル定義の問題である可能性もあります。

*3  このスクリーンショットの状態では本来は「件名」の部分が注目文節になっているのですが、2重下線は「本当は」の部分に描画されています。

*4  現在判明している主な問題点を解決出来た際には、GtkIMCocoaのコンパイル済みバイナリを配布することも検討しています。

2013-05-23

るりまをより便利にするために開発したい機能

こんにちは。クリアコードで組込み機器向けのサイネージシステムの開発やRubyでのmilter*1開発などを担当している沖元です。プライベートでは、るりまプロジェクトなどで活動しています。

先日このブログで紹介したインターンシップ制度では、クリアコードのメンバーが開発したいと考えているフリーソフトウェアの中から「これは」というものをインターンが選択します。本エントリで紹介する開発したいフリーソフトウェアはBitClust*2です。BitClustに以下の機能を追加してるりまをより便利にしたいと考えています。

  • RDocへのリンクを表示する機能
  • サンプルコードの実行結果をデータベース生成時に埋め込む機能

各機能の開発内容の詳細を述べます。

RDocへのリンクを表示する機能

概要

Ruby-Doc.orgへのリンクを表示できるようにします。RDocの各クラスやメソッドへのリンクは機械的に計算することができるはずなので、そんなに難しくないと考えています。

この機能を追加するのは、るりまの内容とRDocの内容をウェブブラウザ上で簡単に比較できるようにするためです。また、次のような効果も期待しています。

  • るりまのページからRDocの内容をすばやく確認できると正しい情報に到達しやすくなる。
  • るりまの内容とRDocの内容を簡単に比較できるようになるので、RDocに不足がある場合でもRDocへのフィードバックがしやすくなる。
開発の流れ(現時点での想定)
  1. チケットを読む。
  2. RDocの各クラスやメソッドへのリンクの作成方法を検討する。(RDocのソースコードを読む必要があるかもしれません。)
  3. クラス名やメソッド名からRDocへのリンクを生成するヘルパーメソッドをBitClustに追加する。
  4. 上で追加したヘルパーメソッドを使ってRDocへのリンクを表示する。
開発に必要な要素技術や知識分野
  • Ruby一般の知識
  • HTML一般の知識
挑戦ポイント

特に挑戦ポイントはありません。RDocへのリンクを正しく生成する方法さえ見つけることができれば、あとは簡単です。

サンプルコードの実行結果をデータベース生成時に埋め込む機能

概要

現在、るりまプロジェクトではサンプルコードの埋め込みは手で実行した結果をコピー&ペーストしています。しかし、すべてのサンプルコードの実行結果が正しいことを確認できていないので、ときどき、プロジェクトのITS*3に「サンプルコードが動きません」や「サンプルコードが間違っています」のような報告があります。また、Rubyのバージョンアップによって挙動が変化したものへの追従も手動で行なっているため、完全に追従できているわけではありません。

人力ですべてのサンプルコードをRubyのバージョンごとに実行し結果をコピー&ペーストするのは、現実的ではありません。我々はプログラマなので、自動化できるところは自動化したいと考えています。

そこで、インターンシップの期間を用いて以下の機能を開発したいと考えています。

  • サンプルコード専用の記法を導入し、コードブロックを実行したり別ファイルに書かれたサンプルコードをインクルードしつつ実行し、結果を埋め込む機能。
  • 読む人の使っているRubyとサンプルコードを実行したRubyはバージョンが同じでもプラットフォームやパッチレベルが違う可能性があるのでそのサンプルコードを実行したときのruby -vも埋め込む機能。

この機能を開発することにより、以下のことを期待しています。

  • すべてのサンプルコードが自動的に実行されるため公開されているドキュメントに間違いが入りづらくなる。
  • サンプルコードの実行結果も自動で埋め込まれるのでドキュメントを書くコストが下がる。
  • Rubyの新しいバージョンがリリースされたときでもドキュメントを再生成するだけなのでメンテナンスコストが下がる。
開発の流れ(現時点での想定)
  1. チケットを読む。
  2. サンプルコード用の記法の仕様を検討する。
  3. 上で検討した結果を元にしてBitClustを変更する。
  4. 作成した記法をいくつかのドキュメントに導入して動作を確認する。
開発に必要な要素技術や知識分野
  • Ruby一般の知識
  • HTML一般の知識
挑戦ポイント

BitClustはほとんど外部ライブラリを使用せずに開発されているのでブラックボックスになっている部分がありません*4。そのため外部ライブラリをあまり使用せずにRubyを用いてアプリケーションを開発するときの参考になるでしょう。機能を実現するのに既存のツールを使用するか自前で実装するのか決める必要があります。

BitClustの既存の記法と衝突しないように設計する必要があります。また、その記法が本当にそれでいいのかコミュニティと合意を形成する必要があります。

まとめ

インターンシップで開発したいるりまをより便利にする機能を2つ挙げました。これ以外にも色々と開発したい機能はあるのですが、それについてはまた別の機会に書きます。この中に「挑戦してみたい!」と思える機能があれば、インターン募集ページから是非ともご応募下さい。

*1 milter managerを使うとRubyでmilterを開発することができます。

*2 るりまプロジェクトで使っているドキュメント生成や検索機能を提供するツール。

*3 Issue Tracking System

*4 ウェブアプリケーション部分の実装でRackを使っているくらいです。

つづき: 2013-06-10
2013-04-30

GaaS: Groonga as a Service

4年ぶりにインターンシップを開始することを2月にお知らせしました。そこで予告していた通り、4月からインターンシップを実施します。インターンシップをどのように実施するかを3行で説明すると以下の通りです。

  • 目的: インターンがクリアコードで実践している開発方法を体験する
  • 内容: クリアコードのメンバーと一緒にフリーソフトウェアを開発する
  • 対象: プログラミングが好きでフリーソフトウェアに関心がある16歳以上の方(学生、社会人、休職中などは問わない)

詳細はインターンシップページを確認してください。

現時点で紹介している一緒に開発するフリーソフトウェアは以下の通りです。

1つめはJavaScriptやFirefoxに関連した開発になります。2つめはC、Objective-C、GTK+に関連した開発になります。

今回紹介するフリーソフトウェアはRubyやgroongaに関連した開発になります。

これまで紹介したフリーソフトウェアも含めて、ぜひクリアコードの開発方法を体験しながら開発したいと感じた方はインターンシップページにある方法で応募してください。

GaaS: Groonga as a Service

自分でセットアップしなくてもgroongaを使えたらステキではありませんか?groongaをサービスとして提供する、Groonga as a Service。略してGaaS(ガース)。そんなサービスを実現するためのフリーソフトウェアをインターンシップで開発します*1

GaaSを使えば、Herokuアドオンとしてgroongaを提供することもできます。夢が広がりますね*2

GaaSの構想は数年前からありましたが、groonga本体の開発などに注力しており未着手です。しかし、GitHub上にリポジトリを作成済みだったり、どんな構成にするのがよいかをスケッチしたりと実現に向けた準備は進めています。

GaaSの機能

GaaSは以下の機能を提供します。

  • APIでgroongaサーバーを追加・削除する機能
    • 負荷の増大・縮小に動的に対応できる
    • 追加時は既存のgroongaサーバーと同じデータを持つ
  • 同じデータに対して、複数のgroongaサーバーで独立して検索サービスを提供する機能
    • 負荷の増大に対応できる
  • 複数のgroongaサーバーがいるときの更新処理はすべてのgroongaサーバーで実行する機能
    • レプリケーション機能
  • 認証機能
  • groongaサーバー毎に使用可能なリソース量を制限する機能
  • groongaサーバーで更新されたデータをリアルタイムでバックアップするAPI

一方、以下の機能は提供しません。

  • 同じデータを複数のgroongaサーバーに分散して配置し、分散して1つのクエリを実行する機能
    • シャーディングはできない
    • 分散して検索した結果をマージできない
  • データの永続性を保証する機能
    • groongaサーバーを落としたらそのgroongaサーバーが持っていたデータは消える

このことからGaaSは以下のようなケースで有用です。

  • るりまサーチのようにドキュメントを全文検索するケース
    • マスターデータが別にあるのでデータの永続性がなくても困らない(作りなおせばよい)
    • 検索数が増えたらgroongaサーバーを追加すれば対応できる
  • ブログの全文検索機能
    • マスターデータが別にあるのでデータの永続性がなくても困らない(作りなおせばよい)
    • 検索数が増えたらgroongaサーバーを追加すれば対応できる
  • 中規模(数十万レコード)くらいまでのECサイト
    • groongaサーバーを追加して負荷を分散できる
    • groongaはすぐに更新結果を検索に反映できるので、品切れになった商品はすぐにヒットしなくなる
    • バックアップAPIで更新データのバックアップをとっておけば、groongaサーバーがすべて落ちても復旧可能
    • 複数のgroongaサーバーの管理を任せられる

シャーディング機能がないためレコード数(データ量)が多いケースには対応できませんが、レコード数がそれほど多くない場合は運用が楽になるため有用です。

GaaSの実現方法案

GaaSの以下のそれぞれの機能の実現方法案を説明します。まだ作っていないので、実際に作ったらここから大きく方向が変わる可能性があります。

  • APIでgroongaサーバーを追加・削除する機能
  • 同じデータに対して、複数のgroongaサーバーで独立して検索サービスを提供する機能
  • 複数のgroongaサーバーがいるときの更新処理はすべてのgroongaサーバーで実行する機能
  • 認証機能
  • groongaサーバー毎に使用可能なリソース量を制限する機能
  • groongaサーバーで更新されたデータをリアルタイムでバックアップするAPI
APIでgroongaサーバーを追加・削除する機能

まずは、APIでgroongaサーバーを追加・削除する機能の実現方法案を説明します。

簡単のために、groongaサーバーを追加したい時、空いているサーバーはすでにあるとします*3。問題は初期データをどうするかです。初期データは指定したURLからHTTPでダウンロードすることにします。こうすることで、GaaS側では初期データを管理する必要がなくなります。また、追加するサーバーは常に最新の初期データを参照できるので、どのgroongaサーバーの初期化処理でも手順が同様になりgroongaサーバーの追加が簡単です。

起動時の流れは以下のようになります。

  1. 初期データをHTTPでダウンロード
  2. 初期データをgroongaに投入
  3. groongaサーバー起動
  4. サービス開始

groongaサーバーの削除は、groongaサーバーを終了して関連ファイルを削除するだけです。

APIは作ればよいだけなので省略します。

同じデータに対して、複数のgroongaサーバーで独立して検索サービスを提供する機能

検索サービスを複数のgroongaサーバーで分担して提供する機能の実現方法案を説明します。

groongaはHTTPサーバーとして検索APIを提供できます。そのため、すでにあるHTTP関連の技術を使うことができます。クライアントからのリクエストを複数のgroongaサーバーに振り分ける機能はリバースプロキシを使って実現できるでしょう。

複数のgroongaサーバーがいるときの更新処理はすべてのgroongaサーバーで実行する機能

レプリケーション機能の実現方法案について説明します。

fluent-plugin-groongaを使うことにより、1つのgroongaサーバーで実行した更新処理を他のサーバーでも実行することができます。これによりレプリケーション機能を実現できます。

残る問題は、クライアントからのリクエストが更新リクエストか検索リクエストかを判断する方法です。更新リクエストの場合はfluent-plugin-groongaで処理しなければいけませんが、検索リクエストの場合は別々のgroongaサーバーで処理したいです。これは、以下のような構成にすることで実現可能ではないかと考えています*4

まず検索時の構成案を示します。

検索時
                                    +--> fluentd -----> groonga
クライアント --> リバースプロキシ --+--> fluentd -----> groonga
                                    +--> fluentd -----> groonga

検索時は、fluentdは単にデータを中継します。

更新時は、fluentdが更新クエリを全てのgroongaに配布します。以下は、真ん中のfluentdに更新リクエストが飛んだ場合です。同様に、他のfluentdに更新リクエストが飛んだ場合もすべてのgroongaサーバーに変更が反映されます。

更新時
                                         fluentd   +--> groonga
クライアント --> リバースプロキシ -----> fluentd --+--> groonga
                                         fluentd   +--> groonga
認証機能

1つのGaaSシステムで複数のユーザーをホストするためには、他のユーザーのデータが読めないように認証機能が必要です。認証機能はHTTPの仕組みを使えばどうとでもなりそうです。

groongaサーバー毎に使用可能なリソース量を制限する機能

1つのGaaSシステムで複数のユーザーをホストした場合、1人のユーザーがリソースを使いすぎて他のユーザー用のサービスが提供できなくなることは問題です。そのため、groongaサーバー毎に使用可能なリソース量を制限し、制限を超えた場合は自動で再初期化したり、サービスを停止したりするような仕組みが必要です。GodBluepillなどリソース監視も備えたプロセス監視システムはいくつかあるので、それらを使用すれば実現できるでしょう。あるいは、単に、ulimitとプロセス監視システムの組合せでもよいかもしれません。

groongaサーバーで更新されたデータをリアルタイムでバックアップするAPI

groongaサーバーで更新されたデータをリアルタイムでバックアップするAPIがあれば、初期データを随時更新して、groongaサーバーを追加した時に使う初期データを最新のデータにしておくことができます。

GaaSのレプリケーション機能はfluent-plugin-groongaで実現するので、fluentdのcopy outputプラグインとforward outputプラグイン機能を使ってこの機能を実現できます。

まとめ

groongaをサービスとして提供する仕組みGaaS(Groonga as a Service。ガース。)の構想とそれの実現方法案を紹介しました。

GaaSを一緒に開発しながらクリアコードの開発方法を体験したいという方はインターンシップページを確認の上、応募してください。

他のインターンシップ対象のフリーソフトウェアを再掲します。

*1 ここでは「GaaS」という単語をgroongaの機能を提供するサービスおよびそのサービスを実現するソフトウェアの両方を示すものとして使っています。サービスだけを提供するのではなく、サービスを提供するソフトウェアも提供することで、自分の環境にサービスを構築することもできるようにします。データを外部に出したくない場合でも使えるようになります。

*2 groongaをHerokuアドオンとして提供することに(インターンシップとは関係なく)興味のある方はお問い合わせフォームからご連絡ください。一緒に実現させましょう。

*3 Amazon EC2などを使えば追加したい時に動的にサーバーを追加することはできるので、このあたりはそんなに困らないはず。groongaサーバーを設定するためのChefのcookbookを作っていたりします。

*4 要検討

つづき: 2013-06-10
2013-04-01

2008|05|06|07|08|09|10|11|12|
2009|01|02|03|04|05|06|07|08|09|10|11|12|
2010|01|02|03|04|05|06|07|08|09|10|11|12|
2011|01|02|03|04|05|06|07|08|09|10|11|12|
2012|01|02|03|04|05|06|07|08|09|10|11|12|
2013|01|02|03|04|05|06|07|08|09|10|11|12|
2014|01|02|03|04|05|06|07|08|09|10|11|12|
2015|01|02|03|04|05|06|07|08|09|10|11|12|
2016|01|02|03|04|05|06|07|08|09|10|11|12|
2017|01|02|03|04|05|06|
タグ:
RubyKaigi 2015 sponsor RubyKaigi 2015 speaker RubyKaigi 2015 committer RubyKaigi 2014 official-sponsor RubyKaigi 2014 speaker RubyKaigi 2014 committer RubyKaigi 2013 OfficialSponsor RubyKaigi 2013 Speaker RubyKaigi 2013 Committer SapporoRubyKaigi 2012 OfficialSponsor SapporoRubyKaigi 2012 Speaker RubyKaigi2010 Sponsor RubyKaigi2010 Speaker RubyKaigi2010 Committer badge_speaker.gif RubyKaigi2010 Sponsor RubyKaigi2010 Speaker RubyKaigi2010 Committer
SapporoRubyKaigi02Sponsor
SapporoRubyKaigi02Speaker
RubyKaigi2009Sponsor
RubyKaigi2009Speaker
RubyKaigi2008Speaker