RailsをRedisで「効率よく」高速化してみる(+おまけ)
仕事でコードを書く時間が減ると、別なところでコードを書きたくなるもので、
久々にチェンクロパーティーシミュレーター(以下ccpts)の
システム部分をいじっていました...φ(・ω・`)
以前react化したり、Rails5に置き換えたりしたわけですが・・・
・・・どうしても「遅い」ってのが問題でして(´-ω-)
react化=表示自体を全てJavaScriptで制御となると、
JSを解釈するまでメイン部分が描画されないわけで、
どうしても体感で遅くなってしまいます
「部分的react化」により、見かけ上の描画を早くすることは可能ですが、
私のスキルが追いついていないので、それはいったん置いておいて・・・Σ(・ω・ノ)ノ
私にできるのは「APIの速度改善」かな・・・ということで、
APIの速度を高速化してみたわけです
Redisのキャッシュを「効率よく」使う
APIが遅い原因はわりとシンプルで、たいていの場合は「(同期的な)I/O」です
ある程度並列処理が許される計算処理に対し、
同一のリソースに触るI/Oは排他制御等の「待ち」が発生します
I/Oといっても「ファイル」とか「ネットワーク」とかいろいろありますが、
「ネットワークでアクセスするファイルI/O」が遅いのは間違いなく、
端的にいえば「DBはどうしても遅い」ということに(´-ω-)
以前だと、DBのデータをメモリに全部乗せるとかいろいろありましたが、
KVS(現在はほぼRedis)の登場により
「高速で非同期アクセスが可能なネットワークストレージ」が登場しました
DBに対して処理時間のオーダーが文字通り「桁違い」なので、
これをキャッシュ的に生かせば、DBへのアクセスが減り、速くなる・・・はずです
だからといって、何でもかんでもRedisに突っ込めばいいわけではありません
Redisもネットワークストレージなわけで、
自分のメモリ上で処理が完結するならそっちの方が速いわけです
この記事は非常にすばらしく、参考になります(`・ω・´) b
そもそも、「Key-Value Store」という名の通り、
「Keyに紐付いたデータ」を取り出すのは非常に高速ですが、
「Keyのリスト」とか取り出そうとした時点で破綻しますΣ(゚Д゚)ガーン
実際、私も以前MongoDBで作ったシステムをMariaDBで作り直しています
用途が増え、串刺しな検索する機能が増えた結果、
RDBのような構造化された構造の方が圧倒的に速くなったわけです
つまり、当たり前のことですが・・・
- 参照が圧倒的に多い
- 結果取得に計算時間がかかる
- 頻繁に内容が更新されない
- ライフサイクルを明確に定義できる
- Key-Valueという単純な構造に落とし込める
- 「Keyのリスト」にアクセスする必要がない
- 非同期アクセスしても問題ない
・・・(Redisに限らず)こういう条件を満たせなければ、
キャッシュを生かせないか、無駄に複雑化してバグの温床になるだけです(´・ω・`)
ccptsでRedisキャッシュを使う
これをふまえてccptsのAPIを高速化していくわけですが、
ccptsのAPIは概ね「検索に対してアルカナデータのリストを返す」という構造をしています
以前はクエリに対してDBを検索し、アルカナデータを構築して返していたのですが、
一つのアルカナに関連するテーブルがたくさんあり、
効率的に取得してもどうしても遅くなるわけです(´-ω-)
しかし、「アルカナのデータ」は「データの更新」をしない限り一意で、
だからこそブラウザ側でもキャッシュさせたりしているので、
「アルカナのID - アルカナデータ」という構造をキャッシュしようと
つまり、「DBで検索してDBからデータを取得」から、
「DBでIDだけ取り出して、IDに対応するアルカナデータをRedisから取得」に変えるわけです
ここで重要なのは、検索自体はDBにやらせることです
「構造化されたデータの検索」は圧倒的にDBが高速ですからね・・・
他にも、「検索画面用に返しているマスターデータ」とか、
初期表示に使う「最近追加されたアルカナリスト」とか、
決め打ちで返せるものもキャッシュしてしまいます
問題は「キャッシュの生存期間」とか「更新」についてですが、
今回の場合「データの取り込み時」にキャッシュも更新してあげれば解決です
古いキャッシュにアクセスされる可能性も、
「Keyにデータバージョンを含む」ことで回避できます(`・ω・´)
以上を実装して、先日リリースしたのがこちらです
redisのキャッシュで高速化 · parrot-studio/cc-pt-viewer@42947de · GitHub
<Redisキャッシュ対応前>
<Redisキャッシュ対応後>
<Redisキャッシュ対応後(ブラウザで時間測定)>
文字通り、桁が違いましたΣ(゚Д゚;≡;゚д゚)
まあ、元々は仕事でもりもり設計していた部分ではあるのですが、
仕事のコードは持ち出せなくても、ノウハウを自分のアプリに反映しておけば、
別なところに使い回しできますからね・・・
ちなみに、お仕事の場合はメモリキャッシュ(Railsのmemstore)も使ってまして、
扱うデータに応じてメモリかRedisか選んで格納するようにしています*1
あと、キャッシュはあくまでキャッシュであって、
「消えてもDBから"必ず"復元できる」というのも徹底しています
Redisを信用してないわけではないですが、データの安全性はRDBが圧倒的ですしね
assetsの取り扱いを変える
これで体感でもそこそこ速くなったので、
Google先生のパフォーマンスチェックサイトで計測してみたのですが・・・
testmysite.thinkwithgoogle.com
・・・モバイル/PCでそれぞれ40点とさんざんでしたΣ(゚Д゚)ガーン
アドバイスに「データを圧縮して返せ( ゚Д゚)y─~~」と書かれてまして、
そういえばRailsのprecompileに圧縮版があったな・・・と思い出しました
今まで非圧縮版にアクセスさせていたので、
ガイドの通りnginxを設定し、圧縮版にアクセスさせたところ・・・
今までassetsを圧縮版で返してなくて、Google先生のツールで「遅い! 40点!!(ノ゚Д゚)ノ彡┻━┻ 」と怒られていたので、nginxの設定を変えたらこれであるΣ(゚Д゚)ガーン pic.twitter.com/dJ0iBltQVs
— ぱろっと (@parrot_studio) 2016年10月29日
・・・ここまで変わりました(; д ) ゚ ゚
ここからモバイルアクセスの点を上げるには、
レンダリングをブロックしているJS/CSSを改善しないといけないらしいです
Remove Render-Blocking JavaScript | PageSpeed Insights | Google Developers
とはいえ、完全reactの状態では難しく、
ファーストビューだけでもサーバサイドでレンダリングする必要があるのですが、
その問題自体は認識していて、今後の課題ですかね・・・
こういうことが「さらっと」(=私の手の届く範囲で)できるといいのですが(´-ω-)
まとめ
- Redisキャッシュを使えばいいというわけではなく、使い方をちゃんと考える
- Railsのガイドはちゃんと読む(´・ω・`)
- 今後の課題:完全reactから一部をサーバサイドに差し戻す
アンドロイドはアイドルの夢を見るか(恋するハッカソン〜君色に染まるアイドル〜を解いた件)
ということで、8回目のPOHなのですが・・・
・・・前回あたりから「ゲーム」としてPRしていたり、
今回から会員登録しないとダメだったりと、
そろそろいいかな・・・とも思いまして(´-ω-)
しかも、先月は死ぬほど忙しくて、
普段飲まないエナジードリンクを飲みながら仕事していたレベルで、
こういう遊びをやっている余裕もなくてですね・・・
でも、仕事もやっと一段落して、そもそもそのエナジードリンクは、
以前のPOHで当たったやつだったりしたので、
会員登録くらいはまあいいかな・・・とかΣ(・ω・ノ)ノ
ただ、外部サイト連携で登録しようとしたら全然うまくいかなくて、
結局ID/PASSで登録するはめになったはちょっと・・・
何がおかしかったのは不明です
そんなこんなで先日挑戦しまして、
数時間で(現時点である問題を)全てクリアしました∠( ゚д゚)/
まだロックされているドレスは、
「お仕事」(イベントシーン)をあと20回くらい見ないと解放されないやつで、
面倒なので放置しています( ゚Д゚)y─~~
今回もほとんどは楽勝だったのですが、
水着問題はパフォーマンス的にやや難易度が高め(雑に書くと何秒もかかる)で、
浴衣問題と制服問題がBOSS・・・という感じでしょうか
(それでも初期の頃の問題に比べれば瞬殺のレベルですが)
一応、全部のコードがこちらに(´・ω・)っ
https://gist.github.com/parrot-studio/d657f61ce60969685995b6e7f22bf119
ということで、今回は浴衣問題と制服問題の話だけ書いていきます
浴衣問題
冷蔵庫の電気料金に関する問題です
設定温度より高いと2円/h、設定温度で維持できるなら1円/h
1日に何度か開け閉めすると、そのタイミングで温度が上昇
1日で電気料金はいくら・・・という問題です
この、「状態が変化する」系の問題は、
クロージャーの出番でございます(`・ω・´)
# クロージャー生成 count = 0 counter = lambda do count += 1 end # クロージャーを呼び出す counter.call puts count # => 1 counter.call puts count # => 2 3.times { counter.call } puts count # => 5
クロージャー(今回の場合はRubyのProcオブジェクト*1)は、
作られた時点での外部環境(countという変数)を参照しているので、
こういうことができるわけですね
今回の問題はこれを使えば簡単で、
クロージャーの中に問題の仕様をそのまま織り込んでしまい、
1時間ごとにcallするだけで終わりです
与えられているのは開け閉めする時刻なので、
その時刻まで状態を進めて、開け閉めした時点で温度を加算して、
また次まで進めて・・・というわけです
これを真面目に解こうとすると結構面倒だと思いますが、
仕様をそのまま書けば解けてしまうというのが面白いですね(`・ω・´) b
制服問題
ざっくり言うと、「52人で大富豪をやって、全員の順位を決めろ」という問題です
先ほどの浴衣問題が「業務系にありがちな問題」だとすれば、
こちらは「ゲーム系にありがちな問題」という感じでしょうか
まず面倒なのが、「カードの強弱」です
大富豪は3が一番弱く、10->J->Q->K->A->2という感じで強さが決まっており、
これを毎回比較するのはコストが高いです(´-ω-)
なので、データを読み込む時点で、3を0、4を1・・・2を12というように、
全部強さの順に数値化してしまい、単純な大小比較に持ち込みました
でもこれはたいした話ではなく、最大の問題は「場が流れる条件判定」です
問題通りだと、ある人がカードを出したあと、その人の次まで誰も出せなければ、
次の人が自分の手札を出せる・・・となっていますが、
そのまんま実装したら明らかに計算が無駄です(´・ω・`)
要は、「その人が出したカードが現時点で最強なら場を流す」ということなのですが、
そうなると「その人の出したカードが最強かどうかをどう判定するか?」という問題が
最初のうちは「2」が最強なので、2を出したら場が流れます
しかし、4枚2を出されたら、次の最強はAですし、
刻々と最強のカードは変わっていくわけです
これをどうしたものかな・・・と思ったのですが、
わりとシンプルに「使ったカードを排除していく」という手段をとりました
(コード内のuse!メソッド)
あとは「場が流れた」ことを-1で表現して、次の人が確実に出せるように単純化したとか、
ランキングを配列で表現したとか、いつものレベルの最適化でして、
52枚と枚数が決まっているため、計算量もほぼ一定で、パフォーマンス的にも難しくありません
まとめ
ということで、現時点ではこの2問がやや難しいといった感じで、
他は効率よく書けるかはともかくとして、「書けない」ということはないはずです
見た感じあと2問、まだ解放されていない問題があるようなので、
これがさっきの2問より難しいようなら、またこの記事に追記します...φ(・ω・`)
Rails4.2からRails5.0(RC1)に移行する際に修正したポイント
昨年の秋あたりから、お仕事の関係で監視していたRailsの開発状況ですが、
お仕事が関係なくなっても、なんとなく毎日チェックしておりまして
どうしてもβを採用するのは怖いので、(仕様が固まる)RC版を待っていたところ、
先日RC版がリリースされました(`・ω・´)
Riding Rails: This Week In Rails 💯: RailsConf recap & Rails 5.0 RC 1 is out!
ということで、手持ちの「チェンクロパーティーシミュレーター」を、
Rails4.2からRails5.0RC1に移行したわけです
移行する際に書き換えたコードがこれになりますが・・・
Rails5(RC1)に移行 / 入手先検索のバグを修正 / viewのコードを修正 / データの誤りを修正 · parrot-studio/cc-pt-viewer@9f3e715 · GitHub
・・・全体を見直したことによるバグfixも含むとはいえ、
なんとなくどこが変わったのかはつかめるんじゃないかと
以下、引っかかった点や変更点をざっくりと...φ(・ω・`)
modelをApplicationRecordからの継承に変更
以前は「ActiveRecord::Base」から直に継承していたmodelですが、
Rails5から「ApplicationRecord」というアプリ層クラスを経由するようになりました
この変更はとてもいいので、Rails4.2で進めている今のお仕事でも取り込んでいます(`・ω・´) b *1
例えば、複数のmodelを触るトランザクションを張る場合に、以前だと・・・
# Railsのクラスを直に触っている ActiveRecord::Base.transaction do # なにか更新処理 end # これが嫌なら、自分のクラスを経由するけど、なにか気持ち悪い(´-ω-) Arcana.transaction do # 実際はArcana以外のmodelも更新している end
・・・こんな感じで書くしかありませんでした
しかし、一つクラスをはさんだことで・・・
ApplicationRecord.transaction do # 明確な親クラスでトランザクションを使える end
・・・こう書くことができて、とてもすっきりしますヽ(`・ω・´)ノ
トランザクション以外にも、アプリ全体に処理を仕込みたい場合、
全部「ApplicationRecord」に書けばいいので、ルールとしてわかりやすいです
Relationの仕様変更にはまる
Railsのバージョンアップをする際、だいたい鬼門になるのはActiveRecordです
今回も見事に仕様変更の罠を踏み抜きました(lll゚Д゚)
チェンクロのアルカナ情報を表すクラスとしてArcanaクラスを用意して、
その下に細かい情報クラスをぶら下げておりました
# Rails4.2のコード class Arcana < ActiveRecord::Base # 中略 belongs_to :first_skill, class_name: 'Skill' belongs_to :second_skill, class_name: 'Skill' belongs_to :third_skill, class_name: 'Skill' belongs_to :first_ability, class_name: 'Ability' belongs_to :second_ability, class_name: 'Ability' belongs_to :weapon_ability, class_name: 'Ability' # 後略 end
しかし、Rails5に移行したら、データのimport処理で引っかかりまくりまして、
吐き出されたエラーメッセージを読んだところ、
「結びついたデータがないぞ(#゚Д゚)」といったことが書いてありました
以前はこれで動いていたわけで、なにかおかしい・・・と思ったところ、
Relationの存在チェックするバリデーションが、Rails5からデフォルトで有効になっていたようです
関連を定義したのだから、その先の情報もチェックするのが当然・・・という思想はわかるのですが、
全てのアルカナが二つ以上スキルを持っているわけでもなく、
アビリティに至っては持ってないケースもあるので、これは困ります(´-ω-)
そこで、このようにチェックを無効化する必要があったのです
# Rails5のコード class Arcana < ApplicationRecord # 中略 belongs_to :first_skill, class_name: 'Skill' belongs_to :second_skill, class_name: 'Skill', optional: true belongs_to :third_skill, class_name: 'Skill', optional: true belongs_to :first_ability, class_name: 'Ability', optional: true belongs_to :second_ability, class_name: 'Ability', optional: true belongs_to :weapon_ability, class_name: 'Ability', optional: true # 後略 end
これで以前と同じ動作になります(`・ω・´)
(first_skillだけは必ず持っているはずなので、逆にチェックを増やした状態になってます)
schema.rbがすっきり&MariaDB関連のパッチが採用
移行するついでに、migrationを統合してしまおうと思いまして、
schema.rbの内容をそのままコピペしたmigrationを作ろうとしたのですが、
だいぶいろいろ変わっておりました
ActiveRecord::Schema.define(version: 20160507003347) do create_table "abilities", force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin" do |t| t.string "name", limit: 100, null: false t.string "explanation", limit: 500 t.string "weapon_name", limit: 100 t.datetime "created_at", null: false t.datetime "updated_at", null: false t.index ["name"], name: "index_abilities_on_name", unique: true, using: :btree end # ... end
まず、create_tableのオプションに、MariaDB(MySQL)固有のオプション情報が付加されています
あと、indexの生成が「add_index」ではなく、「t.index」という形でcreate_tableに統合されています
後者はとてもいいですね(`・ω・´) b
しかも、migration関連の処理がえらい速くなりました
単にmigrationを統合したから、という感じではない速度です
デフォルトAPサーバがpumaに変更されたが・・・
ActionCableが追加され、WebSocketが採用されたために、
Webrickからpumaに変更されたのですが、
これがいろいろ問題を引き起こしているようで・・・(´-ω-)
configレベルで誤りがあって起動しないって場合に、
Webrickなら例外を吐いて止まってくれるのですが、
pumaだと落ちずにずっとフリーズしたままで、他のコンソールからkillしないといけません
特にコンソールに例外を吐いてくれないのが困ります
どこを直せばいいのかわかりませんし(´・ω・`)
他にもreload周りのissueが挙がっているようですし、
まだまだ不安定な感じがしますね・・・
まあ、本番で使うわけではないですし、コードの本体をいじる際には問題ないのですが
ArelでOR文の書き方変更
Rails5より前だと、ActiveRecordでOR文を書く際、
やや面倒な書き方をしなければいけませんでした(´-ω-)
def skill_search(category, cost, sub, ef) return [] if (category.blank? && cost.blank?) arel = SkillEffect.all arel.where!(category: category) unless category.blank? arel.where!(subcategory: sub) unless sub.blank? arel = arel.joins(:skill).where(skills: { cost: cost }) unless cost.blank? unless ef.blank? efs = [ef].flatten.uniq.compact arel.where!( SkillEffect.where( subeffect1: efs ).where( subeffect2: efs ).where( subeffect3: efs ).where( subeffect4: efs ).where( subeffect5: efs ).where_values.reduce(:or) ) end arel.pluck(:skill_id) end
「where_values.reduce(:or)」って書き方は美しくないので、
「or(条件)」という書き方が採用されております(`・ω・´) b
そこまではいいのですが、ANDとORが混在する場合のルールがいまいちよくわからなくて、
whereとorの順番を変えるだけで、いろいろ変わってしまいまして・・・
先にwhereを書いてからorを書くと、whareの条件と混線してしまうので、
orを先に書いてしまってからwhereを書くことでなんとか
def skill_search(category, cost, sub, ef) return [] if (category.blank? && cost.blank?) arel = SkillEffect.all # 先にOR条件を構築 unless ef.blank? efs = [ef].flatten.uniq.compact arel = arel.where(subeffect1: efs) .or(SkillEffect.where(subeffect2: efs)) .or(SkillEffect.where(subeffect3: efs)) .or(SkillEffect.where(subeffect4: efs)) .or(SkillEffect.where(subeffect5: efs)) end # そのあとAND条件を付加 arel = arel.where(category: category) unless category.blank? arel = arel.where(subcategory: sub) unless sub.blank? arel = arel.joins(:skill).where(skills: { cost: cost }) unless cost.blank? arel.pluck(:skill_id) end
このあたりは生成されるSQLを眺めて慎重に進めるしかないのですが、
やはりドキュメントが欲しいですね(´-ω-)
キャッシュの制御がフレームワーク化
処理効率化のため、Rails.cacheを使うってのはよくあるはなしですが、
開発環境では邪魔だし、かといってテストもできないと困るので、
以前は自前のconfigにON/OFFフラグを記述して制御していました
def with_cache(name, &b) return unless (name && b) # configの情報を見て利用の有無を制御 return b.call unless ServerSettings.use_cache? Rails.cache.fetch(name, &b) end
これがRails5では不要になりまして、
「tmp/caching-dev.txt」の有無で制御できるようになってます
(実際の操作は「rails dev:cache」か「rake dev:cache」するだけです)
# config/environments/development.rb # Enable/disable caching. By default caching is disabled. if Rails.root.join('tmp/caching-dev.txt').exist? config.action_controller.perform_caching = true config.cache_store = :memory_store config.public_file_server.headers = { 'Cache-Control' => 'public, max-age=172800' } else config.action_controller.perform_caching = false config.cache_store = :null_store end
こういうコードがdevelopment.rbに自動生成されるおかげなので、
他のバージョンでもこれをコピペすれば同じことができます(`・ω・´) b
既存のWebアプリをReactで書き換えた話(+ES6の困った点)
本当は1ヶ月前に下書きを書いた記事なのですが、
忙しくて見直す時間がなく、だいぶ遅くなってしまいました(´・ω・`)
その間にバグfixを進め、一応安定動作していますし、
それもふまえた内容に書き換えていく方向で・・・
相変わらずメンテと拡張を続けている
「チェンクロパーティーシミュレーター」(以下「ccpts」)ですが、
先日Reactでほぼ全体を書き換えました(`・ω・´)
ちょうど1年ほど前、私がReactを初めて意識した際に、
一度やろうとしたことはあるのですが、
いろいろな理由*1があって諦めてました
しかし、昨年から仕事でNode.jsを扱うように*2なり、
先日はccptsのロジックをFRPで整理するのに成功したりと、
今ならいけるんじゃないか・・・と思い、今回試してみたわけです
React(+flux)についてはいくらでも素晴らしい解説が見つかりますので、
今回は「すでに動いているものをReact化するまでの段取り」と
「その過程で考えたこと」を書いていきます...φ(・ω・`)
全体の段取り
React化に以前失敗しているのもあり、事前に全体の段取りを立てて臨みました
(というより、その段取りが見えたから今回進めたというか)
まず、react-railsですが、これを使うとjsxのサーバサイドレンダリングが可能になります
初期の段階ではerbで書かれたHTMLの構造を、ほぼそのままjsxにコピーしていき、
サーバでレンダリングさせることで、
(jQuery等から見たときの)HTMLの構造はほぼそのままになります*3
erbをjsxに書き換えただけにはなりますが、初期の段階ではそれでも十分ですし、
サーバでのレンダリングをやめることが最終目標になるわけです
ccptsのメインロジックはブラウザで実装されており、
erbで動的な出力は最低限になってはいますが、
それでもReactを前提にするといろいろいじる必要は出てきます
(余談ですが、元のviewがHTML埋め込みのerbだったからこそ使えた手で、
haml等を使っていたら全部書き直しでしたね・・・
別にhamlが悪いわけではなく、結果的に、ですけども)
react-railsのおかげで、部分的に置き換えていくというのが容易になったので、
動的制御のない部品をどんどんjsxに置き換え、
画面で動作確認する・・・というフローが可能になりました
以前は上から作っていったので、終わりが見えずに心が折れましたが、
下から部品単位で置き換えていけば、「動く状態」を維持できるため、
「いつでもリリースできる」という安心感もあります(`・ω・´) *4
同様に、flux等のstate管理フレームワークは考えませんでした
そういった仕組みを導入すると、また上から全部考え直しになるので、
どんな形であれReact化してから考えればいいかな・・・と
(flux実装の一つであるreduxを見て今回のReact化を検討したり、
ES6の使用を決めたのは事実ですが・・・)
実際の進め方
1. ほぼコピペでjsxにコードを移動
先ほども書いたように、erbにロジックはほぼ入ってなかったので、
ガンガンとコピペしていくだけです( ゚д゚)o彡゚
コピペで引っかかるのは「class="hoge"」を「className」に変えるとか、
「for="piyo"」と「htmlFor」に変えるとか、
「inputやbrタグにも閉じタグが必要」とか、そんなところでしょうか
ほぼ機械的に変換できますし、ChromeのReact拡張を入れておけば、
consoleで丁寧にwarningが出るので、順番に潰していけばOKです
erbでなにか埋め込んでいる箇所は、react_renderの引数にその値を埋め込んで、
jsx側のpropsで受け取るようにしておきます
# LatestInfoはReactのクラスでChangelogはRubyのクラス # prerender: true によりサーバサイドでレンダリングされる <%= react_component('LatestInfo', {info: Changelog.latest.as_json}, prerender: true) %>
jsxに移す際に「クラス名」をつけることになるわけですが、
名前をつけるというのはそれが何者であるのかを明示する、ということであり、
これでViewの構造をもう一度見直すことができたのはいい感じでした
ただし、これはあくまで「補助輪」です
この段階ではreact_componentを使いまくりますが、
その後はむしろ、react_componentを使わない=pureなJSに修正していくわけです
2. 動的フォームでstateを使用
これで全体をいったんjsxに書き換えたのですが、
問題になるのが動的に構造を作っている箇所です
ccptsの場合、検索フォームがその一つで、
大項目を選ぶと、小項目がそれに合わせて作られる、という仕組みになってます
(アビリティやスキル検索のところなど)
しかし、jQueryでイベントを発火させても、
書き換えたDOM構造をまたReactが上書きしてしまうのですΣ(゚Д゚)ガーン *5
そこで、この検索フォームをReact化の第一歩として、
少しずつロジックを実装していくことに
最初はスキルとかアビリティとかの単位で分離し、
その中でstateを管理する・・・という形でいったん実装したのですが、
それで困るケースが二つ出てきまして
一つが「条件のリセット」です
それぞれの項目を初期状態にするわけですが、
値を各component単位で管理しているので、
どうやってresetしてもらうか、という問題が
もう一つが「初期クエリがある場合」です
ccptsのデータベースモードは、検索結果を共有する仕組みがあり、
表示時点で指定された検索クエリをformに反映させないといけません
リセットは「空にする」ことで対応できますが、
初期状態は不定なので、単純には行きません(´-ω-)
そこで、Reactの設計らしく、親である検索フォームで全stateを管理し、
子である各フォームにpropとして伝える、という仕組みを採用しました
このとき、各フォームごとにハンドラを親で定義して、
子に引き渡す・・・というのが面倒だったので、
親でstream(Bacon.Bus)を用意しておき、子は変更された値をpushする仕組みにしました
// from children this.notifier = new Bacon.Bus() this.notifier.onValue((s) => { // 子から"{job: 'F'}"みたいにpushしてもらう this.setState(s) })
ここまで書いて気づいたのですが、この「stateの塊」って、
実はほぼ「検索クエリ」なんですよね
FRP化した際、検索ボタンを起点にして、クエリを構成し、
APIに投げて結果からviewを生成・・・というstreamが定義されてました
ということは、検索フォームのstateを検索のstreamに乗せてやれば、
そのまま既存のstreamから検索が動くってことなのです
「クエリの構成」という部分が、フォームのparseではなく、stateで終わるってだけで
逆に、外部からのクエリを検索フォームのstateに反映させれば、
フォームの状態を自由に変えることも可能です
つまり、「検索フォーム」というcomponentを、
「クエリを受け取る口」と「クエリを吐き出す口」を持った、
独立した部品としてみなすことが可能です(`・ω・´)
flux実装であるreduxは、アプリ全体で一つのstateを管理する仕組みのはずですが、
FRPから入った私にとっては、「通知用streamが存在する独立した部品」の方が、
概念としてしっくりきたわけです
RxJSとReactを組み合わせたフレームワークに、
Cycle.jsというのがありますが、それに近い形です
ここまで徹底してはいませんが、考え方は近いです
「何かあったらstreamに流すのでよろしく(`・ω・´)ノ」というのが、
まさにObservableな設計であり、個々の関係を切り離す鍵になってます
3. react-bootstrapの部品を使う
これで完全に「つかんだ」ので、
他の動的な部分ももりもりと進めていこうと思ったのですが、
次に引っかかったのがmodalの扱いです
元々jQuery経由でbootstrapのJSが制御してましたが、
modalの中に状態がある、というケースがありまして、
検索フォーム同様、そこがReactだとうまくいかないわけです(´-ω-)
そこで、このタイミングで「react-bootstrap」を投入することに
リファレンスを見てもらえばわかりますが、ModalやPaginationなど、
動的な処理をしてくれるcomponentがたくさんありまして、
これでだいぶ構造が整理されました(`・ω・´)
Paginationなんか自前で書いてましたが、
この部品を使えば値を渡すだけですからね・・・
以前はここまで整備されていなかったと思います
一方、無理に全ての部品を置き換える必要はなく、
ほとんどはdiv+classでけりがつきますし、
部品にpropが足りないので、あえてdiv+classのままにしたところもあります
4. データベースモードがReact化
あとは、何か通知が必要ならstreamを定義し、
streamを整理していくうちに統合化され・・・と、
開発を進めていったところ、データベースモードがほぼReact化されました
データベースモード : Get our light! - チェンクロ パーティーシミュレーター
そもそも「データベースモード」は、「PT編集モード」の検索部分を切り出し、
見せ方だけ変えたものなので、これで作られた部品は、
ほぼそのままPT編集モードに使いまわせます
(まさに「コンポーネント指向」かと)
この時点で、「いつでもリリース可能」という状態を捨てて、
データベースモードの部品から共通部分を取り出しつつ、
PT編集モードをトップダウンで作っていくモードに
5. jQueryとの連携
Reactの話を見ていると、「jQueryを捨てる」的な話がよく出てきますが、
ccptsに関していえばそれは不可能です
なぜなら、PT編集モードはドラッグアンドドロップが必須で、
それらを簡単に使える仕組みがReactにはまだないからです(´・ω・`)
あと、アニメーションに関する仕組みも貧弱すぎます
一応、react-bootstrapにはFadeの部品がありましたが、
いまいち使い方がわからないし、jQueryと同じ感覚でもないです
そこで、どうやってReactのVirtual DOMと、
jQueryのDOMを連携させるか・・・ですが、
refという仕組みでDOMを生で抜き出すことが可能です
<div className={`${a.jobClass} summary-size arcana`} ref={(div) => { // このdivタグにドラッグのハンドラをセット this._div = div this.setDraggable(a.jobCode, this.props.code) }}>
同じように、jQueryのfadeInやhideもセットできるので、
Fadeのcomponentは捨てまして、全部jQueryに切り替えました
ただし、「どのタイミングでアニメーションを適用するか?」は、
Reactのライフサイクルを把握してないとうまくいきません(´・ω・`)
「componentDidMountのとき」だとか、
「setStateが完了したあとのcallback」だとか、
適切な見極めが必要になります
あと、ブラウザや環境依存でうまく動かないケースもあります
Safariだけ挙動がおかしいケースがあったりで、だいぶ悩みました*6
それでも、Reactの仕組みで自前のアニメーションを適用するよりはるかに楽です
Reactの仕組みは難しすぎ、私には無理です(´-ω-)
6. ES6の困った点
jsxのコードを全部ES6で書いたので、
結果的にViewのコードはCoffeeScriptからES6に変わったのですが、
modelや自前のライブラリはCoffeeScriptのままです
一度は全部書き直そうと思ったのですが、
同じロジックのはずなのに動作しないケースがあったりと、
謎が多かったので戻しました
動いている部品を無理に書き換える必要はないわけですし
Viewは作り直しになったから変える動機があったのであって
そもそも、CoffeeScriptと比較して、ES6は以下のような困った点があります
後置ifが使えない
「unlessが書けない」というのも困るのですが、
それ以上に困るのがこれです
普段Rubyを書くときもそうですが、
私はメソッドの頭にガード条件をよく書きます
someMethod = (a) -> return unless a # aが存在しなかったらreturn # aを使う処理
これがES6だとこうなります
someMethod(a) { if (!a) { return // aが存在しなかったらreturn } // aを使う処理 }
一つくらいのガード条件ならいいのですが、
複数並ぶとさすがにうっとうしくなってきますщ(゚Д゚щ)
とはいえ、これだけでES6からCoffeeScriptに戻すってのは、
現在の状況を考えるとあり得ないのですが、最大の問題は次です
クラスのプロパティも定数も定義できない
これに関してはReactの開発側も困っているようですが、
ES6のclassで定義できるのは「メソッド」だけで、
「プロパティ」や「定数」や「privateな変数」が書けないのですΣ(゚Д゚)ガーン
RubyのActiveRecordのように、
Reactでもpropsの型をチェックする仕組みがあるのですが、
当然、classで共通なので、classのプロパティとして定義したいわけです
しかし、ES6ではそのような記述が許されていないため、
classを定義したあと、classに代入するという、
美しくない方法を使うしかありません(´-ω-)
もっとひどいのが定数です
見かけ上、「定数のように見えるもの」は書けます
static get hoge() { return _hoge // これをどこに定義する?? }
「get / set」と使うと、かっこなしでのメソッド呼び出しや代入が可能ですが、
「値を格納する変数」はどこに書けばいいのか・・・というのが問題です
ただの定数ならclassの後で代入でもいいですが、
その値をclass内のメソッドで使いまわしたいとすると、
class定義の外で変数を定義しないといけません
でも、それってグローバル環境を汚染しているわけで、
「何のためのclassなのよ(ノ゚Д゚)ノ彡┻━┻」という気分に
他でもできないならいいのですが、CoffeeScriptはもちろん、
TypeScriptでもできるし、ES7には入ってるわけですからね・・・
結局、この仕様が気に入らず、babelでの変換もいまいちうまくいかなかったので、
今回はmodel等はそのままにしています
7. 最終調整とReact化の弊害
いろいろと壁はありつつも、なんとか大枠での実装が完了し、
Viewの実装はほぼ完了し、関連ライブラリも全てnpmに移行しました
そこで、テスト環境で実機での確認に移ったのですが、
ブラウザ依存の問題やバグは別として、以下のような問題が発覚しました
・precompileがめちゃくちゃ遅い
デプロイ時に「rake assets:precompile」を呼ぶ際、
CoffeeScriptとは比較にならないレベルで時間がかかります(lll゚Д゚)
Reactのjsxはもちろん、ES6も変換しないといけないし、
Viewで使うJSのファイル数が跳ね上がったから仕方ないのですが、
いくら本番VPSのCPUが弱いからといって、数分待たされるのは怖いです
まあ、precompileが完了してしまえば、
実際の呼び出しで(変換により)待たされることはないので、
viewのコードを変更してデプロイした場合に限られるのですが
頻繁にデプロイするとか、規模が膨大の場合は、
ローカルでprecompileしておいて、
出来上がったファイルをgitで管理する・・・なんても必要かもしれません
・JSの読み込みが遅い
precompileを経由し、全て結合したファイルにアクセスするとはいえ、
JSのファイルサイズは大きく、ロードに結構かかります(lll゚Д゚)
一度キャッシュされれば許容範囲ではあるものの、
それでも以前の単純なviewに比べ、待たされる感があります
以前は枠をサーバでレンダリングし、
中身を非同期通信で呼び出す仕組みだったのでまだ良かったのですが、
Reactだとアプリ本体が全体を描画するので、何も表示されない時間があるのです
JSの読み出しと解釈に時間がかかるって問題を、
JSの文脈(jQuery等)で解決ってのは当然できません
なんとかして別の手段でごまかす必要があります
そこで、サーバ側で「loading」って画面を描画しておき、
Reactの描画が完了した時点で消す、という手段を使いました
これだけでも「何かしてるんだな・・・」という感じが出ます(`・ω・´)
これからのお話
そんなこんなで、やっと安定して稼働し始めたccptsのReact版ですが、
最後にやってみた感想とか今後の話などを...φ(・ω・`)
仕事に役立つのか?
少なくとも、今の環境では使わないかな・・・と思ってます
基本、私の仕事はAPIとかサーバサイドの構築であり、
今回の主目的はES6とか非同期処理の記述に慣れるのが目的でした
例えば、システムの管理画面にReactって話は高コストすぎるかと(´-ω-)
もちろん、「未来の最適解」を「プライベートで」模索しておくことに大きな意味があり、
間接的には仕事に反映はされていくはずではあります
少なくとも、仕事でNode.jsを書く際にES6を使おうとは思いました
・・・まあ、CoffeeScriptの方が気楽なんですけどねΣ(・ω・ノ)ノ
reduxをどうするか?
fluxというか、Reactと組み合わせて使うフレームワークとして、
reduxが話題になっており、当初は私の最終目的もそれであり、
だからこそES6を試した・・・という事情はあります
ただですね・・・FRPとの組み合わせで実装してみると、
「componentの入出力だけお見せするので、好きな時に通知をください(`・ω・´)ノ」の方が、
stateを一箇所で集めるよりも、独立性が高まっている感じがして、しっくりくるんですよね
むしろ、検討するならCycle.jsかな・・・とも思いますが、
そこまでくるともはやReactですらないので、やりすぎかなと(´-ω-)
react_on_rails
完成させてから気づいたのですが、こういう仕組みがあるようです
クライアント部分を独立した形で開発し、
コンパイルしたjsをassetsに配置するという一連の流れを、
うまいことフレームワークとして自動化してくれるというものっぽいです
(最近よく議論されている方式のフレームワーク化ですね)
おそらく便利だと思いますが、あまり情報が出てないのと、
あまりにフルスタックすぎて、知らない人が一から触ると混乱しそうです
慣れた人がショートカットするにはよさげ
そんなこんなで、自分のメモという意味もこめてだいぶ長々と書いてきましたが、
お仕事でがっつりRailsのお仕事に戻ったのもあり、
しばらくはccptsのview周りのいじることはないかな・・・と
(もちろん、機能追加は別として、フレームワーク的な意味で)
次はAPIをRails5に書き換える予定ですが、
RC1はまだですかね・・・(´・ω・`)
*1: 私自身がJSに慣れてない、react-bootstrapが今ほど整備されていない・・・等
*2: これを書いた時点では扱う予定でしたが、現在のお仕事だと使わない予定ですΣ(゚Д゚)ガーン まあ、また別なところで使うとは思いますが・・・
*3: より厳密に言うと、専用のdata属性付きタグが出力され、react_ujs.jsがそれをフックしてreactのcomponentに置き換えるという仕組みなので、静的なHTMLが直に出力されるわけではありません・・・が、jQueryでイベントハンドラをセットする側からすると、ほぼ同じに見える=既存の書き換え不要、というだけです 詳しくはreact-railsのREADME参照
*5: jQueryが「本物のDOM」をいじる -> Reactの仮想DOMはそのままなので、reactが差分を検知 -> 本物のDOMが上書きされる・・・という感じだと思いますが、あってますかね?
*6: jQueryのshowのあと、テーブルが再描画されないという問題だったので、streamに「再描画」メッセージを流して、同じstateで再描画させる手段で解決しました(`・ω・´)
第7の恋愛SLG(「プログラミングで彼女をつくる」を解いた件)
ふと、セブンスドラゴン3が終盤で止まっているな・・・と思い出しましたが、
とにかく今回の「POH7」は恋愛SLG仕立てだそうでΣ(・ω・ノ)ノ
まあ、要するに問題を解くとアイテムがGETできて、着せ替えも可能ってだけなのですが、
やっぱり見せ方は大事ですからね・・・(´-ω-)
そんなわけで、今回は問題が多そうだったので、
計画的にクリアしようと初日からざっと問題を確認したところ・・・
・・・1日で終わりましたΣ(゚Д゚)ガーン
(上の画像には追加問題2問を含む)
最下段の3問が高難易度で、後は(プロのプログラマなら)即座に書き下せる難易度ですので、
その3問の話だけ考えたことを書いていきます
一応、全てのコードはこちらに(´・ω・)っ
https://gist.github.com/parrot-studio/96c758f393806a7df6ac
ついでに、過去の問題を解いた記事はこちら
メガネ
すぐに問題は理解できると思います
ただ、どう「効率よく計算するか?」が難しいのです(´-ω-)
単純に考えると、それぞれの盤面を配列(できれば1次元の)に保持し、
座標を動かしながら、値が一致するかを「各座標で」「順番に」比較していけばいいのですが、
これだと比較にO(m^2)かかるので、おそらく時間切れでpassできませんΣ(゚Д゚)ガーン
これと類似の「2次元の盤面をparseする問題」がPOH2で出ており、
これを解くのに1週間以上かかったわけですが・・・
・・・もちろん、今回の問題もこれの応用でいけます
盤面の一行を「0b0010=2」のように捉えれば、
盤面はそれぞれ「n個の数値の配列」と「m個の数値の配列」と考えられます
あとは、座標を動かして比較するのに必要なところを抜き出せばOKです
# 例として、「0 1 1 0」に「0 1 1」が含まれるかを調べる # 3桁分のマスクを作る mask = ('1'*m).to_i(2) # => 0b0111 # 目的の値とand演算する t = 0b0110 & mask # => 0b0110 # 比較 t == 0b0011 # => false # maskを一桁ずらす mask = mask << 1 #=> 0b1110 # 目的の値とand演算する t = 0b0110 & mask # => 0b0110 # ずらした桁を戻す t = t << 1 # => 0b0011 # 比較 t == 0b0011 # => true
実際のコードは、これを座標をずらしながら、
複数行=m個の数値の比較でやっているだけです
https://gist.github.com/parrot-studio/96c758f393806a7df6ac#file-ex1_glasses-rb
この方法だと「たかだかm個の数値同士の比較」に落とし込めるので、
だいぶ高速化できますヽ(`・ω・´)ノ
ただまあ、これは以前1週間以上悩んだからこその解法であり、
やはり積み重ねは大事ですね
サンタ服
3つの問題の中では最も簡単な問題です
文章を仕様に落とし込めれば楽勝かと(`・ω・´)
問題をよく読んでいくと、求めるのは「体積」なのですが、
「水平方向=z軸」には切りません
つまり、「最小の"面積"」を求めて、最後に高さをかければ十分です
ナイフは常に「並行」に入ります
なので、単純に「x軸とy軸それぞれで一番小さい幅」をかけたものが答えです
しかし、渡されるのは「位置」であって「幅」ではありません
しかも、渡される順番も適当です
そこで、いったんx軸とy軸ごとに配列に格納し、
ソートした後、隣同士の座標で幅を計算=引き算して、
「一番小さな値」を取り出せばOKです
ここまで問題をかみ砕いてしまえば、書くのはとても簡単です(`・ω・´) b
https://gist.github.com/parrot-studio/96c758f393806a7df6ac#file-ex2_santa-rb
ポイントは「端っこの値=0とx(or y)」を配列に入れてしまうことです
そうすることで、特別なif文など不要になり、単純なリスト処理でけりがつきます
水着
一応ラスボスじゃないかとは思いますが・・・
これをクリアすると、海の背景も開放されますし
問題の意味はすぐに理解できますが、
ポイントは「莫大な計算量をどうするか?」です
仕様通りに計算すると、Rubyなら一応ベタに計算できますが、
言語によっては桁あふれになりますし、
Rubyだとしてもパフォーマンスが最悪です(´-ω-)
そこで、どう間引くかが問題になるのですが・・・
正直、自分の中でも確信は持てていません
それでも一応1秒程度で実行できたのがこちらです
https://gist.github.com/parrot-studio/96c758f393806a7df6ac#file-ex3_swimwear-rb
基本はただかけ算をしていくだけですが、
「適当な桁数」で数値を補正しています
- 数値を文字列にして桁ごとに分割
- ひっくり返して頭から0を取り除く
- 「適当な桁数」の数字文字列を取り出す
- 再びひっくり返し、結合してから数値化
どうせ最後に下の桁の0は全部取り除かれますし、
欲しいのはその上の9桁なので、途中でそれを計算してしまえば、
ある程度の桁数に抑えられるでしょう・・・と
とはいえ、途中の桁数でぴったり9桁にすると、
次の数値をかけた時に必要な桁のところに誤差が来てしまうので、
最初12で実行し、11->10と下げていって、10では止まったので11・・・という
ただ、いろいろ試行錯誤はしたのですが、どうしても1秒を切れません
おそらく「reduce」の処理をもっと最適化するか、
そもそもやり方を変えるか・・・(´-ω-)
最初は毎回間引いていたので、一定回数ごとに抑えたところ、
8秒だったのが1秒まで削れました
つまり、間引く処理が遅いことになります
でも、あまり間引く回数を減らすと、今度は桁が大きくなりすぎて計算が遅くなります
まあ、いろいろやっても1.11秒と1.13秒ばかりなので、
なんらかの限界の可能性はありますが・・・
Rubyに関していえば、今回の問題はおそらくBignumの範囲で計算されているので、
Fixnumの範囲で押さえられないか・・・とか、
そのあたりでしょうね(´・ω・`)
まとめ
ここ数回の難易度に比べると、元の難易度に近くなったかな・・・という気はします
サンタ服の問題はともかく、他2問はこの手の問題をやったことがないと難しいでしょう(´-ω-)
むしろ、このサイトのメインターゲットである、
「転職を考えているエンジニア」の観点だと、
この3問以外の、残りの問題があっさり解けるか・・・の方が大事です
それぞれベタな書き方は可能ですが、面接で効率の良いコードを書けると、
「お、この人はいいかも」と思わせることができます(`・ω・´)
(というか、面接官にその観点がない場合、その会社は怪しいかと・・・)
https://gist.github.com/parrot-studio/96c758f393806a7df6ac#file-others-rb
1〜数行で書ける問題ばかりですので、いろいろ試してみてはどうでしょう