さくら学院のデータを管理するライブラリRubyGems::sakura_gakuinを公開した

さくら学院のメンバーやクラブ活動のデータを取得出来るライブラリ、sakura_gakuinをRubyGemsで先日公開した。
いつか作りたいと思っていたAcmeモジュール。(Rubyだけど)
 
sakura_gakuin | RubyGems.org | your community gem host
https://rubygems.org/gems/sakura_gakuin
 
yoshida-eth0/ruby-sakura_gakuin
https://github.com/yoshida-eth0/ruby-sakura_gakuin
 
install

gem install sakura_gakuin

 
シンプルな中からゲスさが見え隠れするRubyらしい綺麗なコードが書けたと自負してる。
アイドルがテーマのgemだしネタ扱いされて見向きもされないんだろうと勿体無く思う位には気に入ったコードが書けた。
なのでこのライブラリのお気に入りポイントを挙げてみる。
 

module SakuraGakuin::YamlLoadable

メンバーやクラブ活動のデータは全てconfig/*.ymlに格納してある。
このYAMLファイルを使う各実装クラスは SakuraGakuin::YamlLoadable をincludeする。
このモジュールは以下の処理をする汎用的な共通モジュール。
 
YAMLファイルを読み込んでObjectの配列へ変換しクラス変数へキャッシュする。
・プライマリキーの生成。
・カラムに対してindexを作成。
・indexを用いた検索。
・Enumerableの実装。
 
SakuraGakuin配下に置かなくてもいいんじゃないかって位汎用的。
今ライブラリの肝になるモジュール。
 

YAMLでのデータバインディング

データの読み込みは、module SakuraGakuin::YamlLoadable の yaml_load メソッドにて行う。
 
このYAMLファイルでデータバインディングの為の記述をしてあって、ファイルを読み込むと同時にオブジェクトに変換してくれる。
Memberクラス/ClubMemberクラス/Clubクラスを読み込むタイミングでYAMLファイルを読み込み、クラス変数にキャッシュするようにした。
 
YAMLのデータバインディングは、セッターを介さずに直接インスタンス変数に突っ込まれる。
なのでクラスにセッターを定義しなくても問題なく動く。
シンプルで綺麗。
 

クラスとリレーション

クラスのインスタンスを相互にリレーション出来るようにした。
これらは全てO(1)での検索が可能。(後述)
O(1)に対応していないリレーションは割愛。
結構数があるので、ソースかドキュメントを参照。
 
クラスとリレーションは以下の通り。
 

SakuraGakuin::Member

さくら学院のメンバー。
名前や身長等のデータが含まれる。
 
・Member.club_members
 インスタンスメソッド。
 Memberに紐付くClubMemberの配列を返す。
 

SakuraGakuin::Club

さくら学院のクラブ活動。
クラブ活動の名前、グループ名のデータが含まれる。
 
・Club.club_members
 インスタンスメソッド。
 Clubに紐付くClubMemberの配列を返す。
 

SakuraGakuin::ClubMember

MemberとClubを多対多で紐付けるクラス。
ActiveRecordで言う所の、through で指定するクラスみたいなものだが、
ここにはクラブ活動固有の名前(YUIMETALや堀内研究員等)や入部した日、退部した日のデータも含まれる。
 
・ClubMember.club
 インスタンスメソッド。
 ClubMemberに紐付くClubを返す。
 
・ClubMember.member
 インスタンスメソッド。
 ClubMemberに紐付くMemberを返す。
 

SakuraGakuin::YamlLoadable

・YamlLoadable.[]
 特異メソッド。
 includeしたクラスのidに紐づくインスタンスを返す。
 

ID系のデータにindexを張ってO(1)で検索・リレーション出来るようにした

具体的には、以下のデータにindexを張った。
・Member.id
・Club.id
・ClubMember.id
・ClubMember.club_id
・ClubMember.member_id
 

idの生成

idというカラムはYAMLファイルには記述していない。
これは module SakuraGakuin::YamlLoadable の primary_key メソッドにカラム名を1つ以上渡して、IDカラムを生成する。
例えば、Member.idは、firstname_enとlastname_enを合わせてIDとしている。
 
YamlLoadable関連の処理は、クラス定義の先頭に書きたい。
ActiveRecordでhas_manyを先頭に書きたいのと同じで、気分的にヘッダー情報として上の方に書きたい。
しかし yaml_load してすぐに primary_key しようとすると、firstname_en等のゲッターがまだ記述されていない為NoMethodErrorが起きてしまう。
その為、idは必要になったタイミングで遅延評価して生成するようにした。
module SakuraGakuin::YamlLoadable の primary_key メソッド内で define_method(:id, lambda) している。
これでidカラムの問題は解決した。
 

id以外のindex作成

id以外のindexを張るのは、module SakuraGakuin::YamlLoadable の create_index メソッドを使う。
この中でindex生成用のlambdaと、そのlambdaによって生成されたデータをそれぞれ持つようにした。
これで実際に検索が行われるまで何もしないようになり、ゲッター定義前にcreate_index出来るようになった。
 
index生成用のlambdaをこう定義される。

@indexes[indexに使うカラム] = -> (indexの値){}

 

indexを用いた検索

Rubyのlambdaは、[]メソッドで実行出来るのでとても都合がいい。
indexを用いた検索は以下で行う。

@indexes[:club_id][:baton_club]

これが呼ばれた時、初回1回だけ :club_id に対するindexを作成し、2回目以降はO(1)で引っ張ってこれる。
まるで多段Hashを操作しているような感覚で、遅延実行されたデータを取得する事が出来る。
実際の検索は、module SakuraGakuin::YamlLoadable の index_search メソッド等で行われている。
 
遅延評価をさらっとソースに埋め込むのはモテコードだと噂で聞いた事がある。
おそらくこれはそのモテコードに準ずるものなのではないかと。
 
しかし、O(1)で動作するリレーションには時間の情報が含まれない。
例えば以下コードは、現在在籍中のメンバーではなく歴代メンバー全員を返す。
もし在籍中のメンバーのみを取得したい場合は select(&:active?) 等を使う。

irb(main):014:0> SakuraGakuin::Club[:cooking_club].club_members.count
=> 6
irb(main):015:0> SakuraGakuin::Club[:cooking_club].club_members.select(&:active?).count
=> 3

 

クラブ活動でのメンバーの名前

重音部と科学部については、メンバーの名前が変わる。
その為、ClubMemberに名前を持たせ、クラブ活動特有の名前が存在しない場合はMember.nameを見るようにした。

irb(main):017:0> SakuraGakuin::ClubMember[:cooking_club_yui_mizuno].name
=> "水野由結"
irb(main):018:0> SakuraGakuin::ClubMember[:heavy_music_club_yui_mizuno].name
=> "YUIMETAL"

 
クッキング部ではメンバーカラーが存在するが、それは追加していない。
メンバーカラーのない他のクラブ活動でabstractのような状態になってしまう為、ポリモーフィズムを優先させる。
 

Timecopで「今」を変える

さくら学院が成長期限定ユニットだからこその問題がある。
前述のとおり、時間の変化に伴い返るべき値が変わる。
その為デフォルト引数を Date.today としているメソッドが多く存在している。
しかし毎度毎度日付を指定するのは非常に面倒。
 
そこで登場するのがTimecop。
以下のコードで2011年度卒業式当日のメンバーと当時の年齢の一覧を出力出来る。
すなわち、会長が引っ張って来てくれて、それを支えてる三吉がいて、隣で笑ってるあいりーんがいる時代のメンバーの一覧である。

irb(main):019:0> Timecop.travel(Date.new(2012, 3, 25)) do
irb(main):020:1*   SakuraGakuin::Member.select(&:active?).map do |member|
irb(main):021:2*     "#{member.name}(#{member.age})"
irb(main):022:2>   end
irb(main):023:1> end
=> ["武藤彩未(15)", "三吉彩花(15)", "松井愛莉(15)", "中元すず香(14)", "堀内まり菜(13)", "飯田來麗(13)", "杉崎寧々(13)", "佐藤日向(13)", "水野由結(12)", "菊地最愛(12)", "田口華(12)", "磯野莉音(11)"]

 

TODO

以下余力があったらやる事リスト。
 

生徒会長、トーク委員長、部長等の役職の追加。

これらも時間の変化に対応する必要があり、且つ複数掛け持つ可能性も考えなければならない。
生徒会長については、年度の切り替わり時期に不在となる。
 

その他

テストケース作成。
RDocコメント記述。
 

これ系のライブラリ

アイドル系は、モー娘、AKB48ももクロの3つ。
Ruby実装はプリキュア1つ。
探した感じだとこんな感じ。
今回は以下のライブラリを参考にしつつも、1から作った。
 

Perl

Acme::MorningMusume リリース - delirious thoughts
http://blog.kentarok.org/entry/20050404/1112585090
 
Acme::PrettyCure - JPerl Advent Calendar 2010 Acme Track
http://perl-users.jp/articles/advent-calendar/2010/acme/6
 
Acme::AKB48 - JPerl Advent Calendar 2010 Acme Track
http://perl-users.jp/articles/advent-calendar/2010/acme/7
 
第一回ももクロハッカソンに参加して Acme::MomoiroClover リリースしました - 2nd life
http://secondlife.hatenablog.jp/entry/20110904/1315129581
 

PHP

Acme_MorningMusume \ パッケージ \ Openpear
http://openpear.org/package/Acme_MorningMusume
 
Acme_IdolMaster \ パッケージ \ Openpear
http://openpear.org/package/Acme_IdolMaster
 

Ruby

Rubyプリキュアを作った #cure_advent - くりにっき
http://sue445.hatenablog.com/entry/2013/12/16/000011
 

あとがき

コントリビュータ募集中

特に水野由結ちゃん卒業後(2015年4月以降)にアップデートしていくかは現状定かではなく、メンテナンスされない可能性もあります。
お気軽にPullRequestください。
 

英語

Gemの説明文を英語で色々考えたけど、正しいかは不明。
間違ってるよ!とかもっといい言い回しあるよ!と思った方はご指摘 or PullRequestください。

All about a limited growth period only Japanese idol Sakura Gakuin.
All about Sakura Gakuin, the Japanese idol limited for growth period.
All about a Japanese idol Sakura Gakuin, that consists of girls of up to junior high.
All about Sakura Gakuin, that this Japanese idol consists of girls of up to junior high.
All about Sakura Gakuin, that this Japanese idol consists of girls of growth period.
All about Japanese idol "Sakura Gakuin", that consists of limited to growth period girls only.