ホーム ブログ ページ 18

「興味を発信することで知識が集まる」小さな活動から意識を変えるミニカンファレンスの仕組み

山田アイコ、社内のイベントを発信します!

今回は、自社ゲーム開発をしているアピゲー部のグループマネージャー『吉田 啓紀』さんから、4月より開催しているミニカンファレンスについてお話を伺ってみました。

吉田さん、今日は宜しくお願いします!

インタビューを通してミニカンファレンスの活動を周知していただけると伺いました。是非、宜しくお願いします!

仕事中の吉田氏

吉田 啓紀
2012年にWebのエンジニアとして入社
現在は、アピゲー部のグループマネージャーとして新作ゲームのディレクションと、
いくつかの既存ゲーム運営メンバーのマネージメント業務を行っている。
業務の傍ら、2019年4月より『ミニカンファレンス』を発足。

『定期的』にアウトプットする場を作りたい

まず、ミニカンファレンスを始めたきっかけや目的を教えてください。

社員の中でもっとアウトプットする場がほしいという話がもともとあったんですよ。

後は、部署が結構増えてきて部署間での情報共有が難しくなってきたなと、僕自身も感じていました。

アウトプットする場が、社内に少なかったということでしょうか?

少ないというよりは「あったけれど定常化はされていない」状態でした。
今までは突発的なもので『LT会』とか皆で技術書読もうみたいな『読書会』はありましたが、なかなか続けてやる題材がなかったんです。

開催者も「思いついた時にやる」みたいな感じだったのでほんとにまばらでした。

良い会なのにもったいないですね、そこで定期的な会を作りたいと……。

はい、いつ開催されるか分からない会に向けて目標を建てるのって難しいと思うんです。

前に開催したもので『LT』って名前を使っちゃったし、それなら名前を新しくして定期的にやる会を作ろうと思って始めたのが”ミニカンファレス”です。

ハニカミながらインタビューに答える吉田氏

なるほど!
定期的に発表があった方が、みんなが目標建てしやすくなっていいですね。

そうですね。
「今期あったことをミニカンファレンスまでに整理して発表ができれば」という話も最近ぽつぽつと耳にしていて、良い目標作りのきっかけになっているんじゃないかと思っています。

新技術を導入し、他のタイトルにも取り入れる

ミニカンファレンスでの印象深いエピソードはありますか?

直近だと、「発表した内容がもう明日から使えるね」と話がまとまって、プロジェクトにそのまま入れることになりました。

すごく早いですね!その新作ゲームにはどのように取り入れましたか?

発表した人自身がもうゲームに実装しているものだったんですが、「じゃあそれを他のプロジェクトにも入れよう」とすぐに決まりました。

どんなゲームなのでしょうか。良ければ宣伝していきませんか?

実装元は『ゴエティアクロス』というタイトルで、それとは別に新しく開発しているタイトルに取り入れようということになりました。
開発中のゲームについてはまだ詳しくお知らせできませんが、来年にはユーザーの皆様にお届けできるかと思います。

私もゲスト出演している『悪魔少女✕マルチプレイRPG ゴエティアクロス』。
是非、プレイしてみてくださいね!

アウトプットする事で得られる変化

ミニカンファレンスが会社に与えた変化はありましたか?

社員側が『定期的にアウトプットの場がある』ことを認識するようになったことで、目標設定として「次のキーワードを何か自分で研究してみて、それをミニカンファレンスで発表します。」と声が聞こえてきた事に関しては、すごくいい変化に感じました。

以前からあるブログ(DoRubyなど)ではアウトプットを目標として設定できなかったのでしょうか?

設定してる部署もあればしてないところもありました。ブログっていうのは結局「見る人は見るけど、見ない人は見ない」ものなので難しいと思います。

あと実際に人が前に立って発表することと、システムに書いて打ち込んで媒体として出すっていうのは、結構”行為”として違うなっていう風に思っていて、どちらも学んだことをまとめるっていう意味では同じなのかもしれないですけど、やっぱり個人的には前に出て発表するっていうのをもうちょっとやってもらいたいなと。

真剣な表情で語る吉田氏

吉田さんが考える最終的なゴールはどこですか?

社内のカンファレンスにも出てもらって発表して「俺たちすげえじゃん」みたいなとこまでいけるとゴールかなっという風に思っています。

明確で解りやすいゴール設定ですね!
ちなみに初回のミニカンファレンスでの反応はいかがでしたか?

まあ、「良かったね」という感じでした。マイナスな印象はなく、発表することに対して前向きな人が結構多かったです。

何か課題など出てきましたか?

発表する人が固定化されてきちゃうかなってところですね。
もうちょっといろんな人に発表してもらえればいいんですけど……。

特にどんな人に発表してほしいと思っていますか?

社内ではいろんな技術使っていて、実は業界的に見るとすごいことをやってるんだけど、その場に技術を持ったまま留まっちゃってる「もったいない」状態の人がいるんじゃないかと気づきました。

その人達にいきなり社外に向けて発表してくださいっていうのは、ハードルも高いですし、そんな恥ずかしいことできないってなると思うんです。
なので、その人達にはこのミニカンファレンスを最初の踏み台としてやってもらえたらと思ってます。

吉田さん自身は最初のミニカンファレンスでどんな発表をなさいましたか?

自分はエンジニアの発表をしようと思っていたんですけどせっかくなので、誰もあんまり触れていない広告の話をしました。
そこそこ興味持って見てもらえたと思います。

広告回りをやってる人があまりいないっていうところで布教活動にも使いたくて発表しました。

同じものに興味ある人が集まるきっかけにも使えるんですね

そうですね、広告回りでいうと興味はあるけど具体的に何やってるか知らないっていう状態の人が結構多いので、その辺の共有にも使えます。

ミニカンファレンスを実際に行って吉田さんにとって良かったことはなんですか?

皆色々やってくれてるんですけど、なかなかそれを引っ張り出すことができないなと思っていました。

ミニカンファレンスを開催したことで「あ、そういうこと考えてるんだ!とか、そういうことやってたんだ!」っていう普段見ててもわからない所を引き出せたいいきっかけになってくれました。

一つの手段としてそういう場を設けられたのは良かったと思っています。

メンバーの体験を満面の笑みで喜ぶ吉田氏

発信してもらってより理解しあえたんですね

発信することで逆に周りからも興味や知識が集まって仕事に活かせたり、見る側としてもメリットが大きいと思います。

ありがとうございます。
最後に、今後はミニカンファレンスをどの様にしていきたいとお考えでしょうか?

最初にも言いましたが、いろんな人に自分の興味あるものや取り組んでいることをもっと発信して共有してもらいたい。

他の部署と連携してミニカンファレンスの様子をYouTube配信などもしましたけど、そんな風に一緒に協力してやっていけたらよりよくなっていくんじゃないかと思っています。

発表したスライドを前にいい笑顔を見せる吉田氏

社内で定期的な情報共有の場を作り、社員全体の知識レベルを上げる取り組みはとても重要ですね。
もちろん私達CTO室も共に盛り上げますよ!

アピリッツが気になった方は コチラの採用情報 もチェックしてみてくださいね。

どうしようもないけどやれるだけのことはやってどうにかしよう

0

この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。

がんばろう

貴方はプロジェクトにアサインされ、サーバーの負荷試験を依頼された

「頑張るぞ! ガルバンゾ!」

ここにコードがあった。それは見るからに不味そうなスパゲッティであった

「お? おお……」

ここにスケジュールがあった。それはスパゲッティが腐るまでに時間がない事を示していた

「おお……。お、おお……」

それは即ち、根本解決をするだけの時間的猶予がない事を示していた

「おおあああああっ、うっ、うぐぅっ」

貴方の使命は、せめて足掻く事であった

「……」

現状整理から始めよう

 コードがスパゲッティになる理由は幾らかあれど、そんな事を調査していても始まらない。
 やるべき事は罪を償わせる事ではない。罪を代わりに償う事だ。
 まずは一つ一つ現状を整理し、問題解決には何をすべきか考える事である。

 現状を調査した結果、まずはとても良い事が分かった。

テストを書く文化がある

 これが何を意味するかというと、コードの振る舞いがテストによって保証されているという事だった。
 コードが中でどんなに破茶滅茶な事をしていても、テストが通りさえすればそのコードは少なくとも挙動としては正しい。
 即ち、中を書き換えてもテストが通りさえすればその正当性は保証される。最後にQAなどを通す必要はあるが、これから行う改修に対しての強力な道しるべとなる。

使用している監視ツールはデータベースの重みを調査するものと、サーバーそのものの重みを監視する二つを利用していた

 確かにこれで重い部分がある程度は分かる。ある程度ではあるが。
 これではコード全体の重みは一部見逃される恐れがある。レスポンスの遅さとは、データベースのクエリ以上にコードそのものの重みが原因となる場合があるからだ。
 ただ、それは誰かに何かを依頼する事もなく調査、解決出来る。
 そしてその監視ツールがある事自体はとても有用である、というか無ければ負荷試験は出来ない。

最も問題であるコードは1APIの中で何度も同じデータを参照する事があった

 マスタデータやユーザーデータを、そのコードが呼ばれる度に参照、計算して返していた。
 1回の呼び出しでユーザーデータが多数変更され、また都度都度必要となるマスタデータは変わる。それが1APIの間で何度も発生する。
 そしてまた、これを根本的に解決する事ーー例えば1回の呼び出しに纏めるーーといった事などは影響範囲や再度掛かるQAへのコストなども鑑みると時間の都合上不可能。
 出来るのは1回1回で掛かるコストを出来る限り減らす事、それが最善だった。

改善方針をまず固めて共有しよう

 見ての通り改善すべき事はデータベース上の重みとしてもコードとしてもはっきりしている。
 ただ、改善するものの規模が大きいのならば、見切り発車で改善を決めるのではなく、一旦共有して見落としが無いかなどを第三者の目からも含めて判定した方が良い。

「ここの重みを削れれば、取り敢えず重みは60%位になります」
「60%でもまだ重いな……他の部分は?」
「まだ調査していないです。ただ、ここのコードの重みとしては、このAPIの負荷の割合を見てもデータベースの重みではなく別の部分である事ははっきりしているのと、今のツールでは調査しきれない部分もあるので先にやってしまおうと思います」
「……まあ、良いか。で、どうやって削る?」
「基本的に1API間でユーザーデータを使い回す、1スレッドで同じマスタデータを使い回す、という感じですね」
「どうやって使い回す?」
「ユーザーデータに関してはのユーザーのインスタンスが1APIで使い回されているので、そこにインスタンス変数として保持しまおうかと。
 マスタデータもメモ化してしまいます。
 まあ、要するにユーザーデータ、マスタデータ共にスレッドのメモリにキャッシュします」
「……メモリに負荷が掛かるだろうけどそこ辺りは」
「幸いそちらはまだ余裕があります」
「オーケー」

改修したが想定までは減らなかった

「まあ、そう簡単に減らせたら苦労しないよなー……」
 確かに、無駄なデータ検索量は減った。幸いマスタをメモリに持たせる施策も、強いメモリ負荷もなく実現出来た。
 しかしながらAPIの重みを見ると、データベース起因ではない不明な重みが大半を占める事となっていた。
 これは即ち、コード自体に問題があるという事だ(掛けている負荷が適切であれば)。

 アプリケーションに対するプロファイリングツールを導入し確認してみると、膨大な配列から必要なデータを参照してくる部分に大きな問題があった。
 既存の部分だけではなく、自身が改善したユーザーデータやマスタデータを使い回す処理でも、その引っ張ってきたデータから目的のデータを探り当てる処理で重みが発生してしまっていた。
「データをDBからメモリ上にデータを乗せるだけではまだ駄目か……」
 こうなると、その検索処理などをどうにかするしかない。
 コードを読み込み、どのようにデータが扱われているかを確認していく。
・ある条件に当てはまるユーザーデータは、特殊な条件の場合にしか参照されない事が分かり、基本的に検索から抜いても良い事が判明した。
・同じマスタの中でも種類が膨大にあり、複雑な検索条件でもメモ化の形を工夫すれば配列検索の回数を減らせる事が分かった(マスタを分けろよという愚痴は心の奥底に仕舞った)。
 そんな事からユーザーデータやマスタデータを分類分けしていくと、何とかなりそうな目星がついてきた。

※ruby_on_railsだとindex_byやgroup_byがとても有用。単純にデータ検索をするにしても配列からfindを仕掛けるよりもハッシュにしてキーが存在するかを判定した方が確実に早いです。
サンプルに以下。

1.

array = (1..1000).to_a
begin
  now = Time.now
  1000.times do |n|
    array.include?(n + 1)
  end
  p Time.now - now
end
=> 0.0051824

2.

hash = (1..1000).each_with_object({}){|n, res| res[n] = true}
begin
  now = Time.now
  1000.times do |n|
    !!hash[n + 1]
  end
  p Time.now - now
end
=> 0.0001501

かなり改善された

「ここまで改善出来ました!」
「これなら耐えうるな、で、コードは?」
「こうなりました!」
「……複雑だな……」
「……そうなんです。まあ、テストコードがあったので振る舞いは保証されています」
「そうだな、そうか(テストコードの網羅率って100%だったかな……)」

 根本原因を解決出来ずに上部だけで対応しようとすると、そうなります。
 できる事はスパゲッティを美味しく作り直す事ではなく、防腐剤を振りかける事、ただそれだけだったのですから。
 しかしながら、そうしなければいけない時もあるのです。きっと。

美味しいスパゲッティの作り方

  1. 厚切りの芽を抜いたニンニクと唐辛子をたっぷりのオリーブオイルで常温から弱火で炒めましょう
  2. ニンニクはきつね色になったら取り出しましょう。唐辛子も焦げる前に取り出しましょう
  3. 好きなキノコを好きなだけ入れて塩を振って炒めましょう
  4. 塩をきつめに入れた熱湯で茹で上げたスパゲッティを和えましょう
  5. 皿に盛り付けてから取っておいたニンニクと唐辛子と、あればパセリを意識高めに散らしましょう
  6. 食べよう

カスタマーサポートの極意 2019 #3 レポート

0

この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。

セミナー概要

・カスタマーサクセスを意識したカスタマーサポートの方法
・顧客から高い満足度を得続けるカスタマーサポートの方法
・カスタマーサポートに適したサービスの選定方法

登壇者

  • 弁護士ドットコム:三浦陽菜様「クラウドサインの新しいカスタマーサポートの方法」
  • ベルフェイス:森本真伍様

クラウドサインの新しいカスタマーサポートの方法

  • クラウドサインとは
    • クラウド型電子契約サービス
  • リレーションの現状
    • チャットサポートの目標
      • 速さ
      • 満足度
    • 体制と測定方法
      • 3人体制
      • チャットの最後の満足度を聞く
    • 結果
      • 初回連絡60秒以内(満足する結果)
      • 満足度90%以上(満足する結果)
  • 満足度とチャーンの結果
    • 満足度が低い顧客が解約しているわけではない
    • 解約している顧客はサポートに問い合わせがない
  • サポートの意味は?
    • 関係を築き続けることが大事
  • 関係を築き続けるには?
    • CX(カスタマーエクスペリエンス)の向上が大事
  • CXとは?
    • 顧客に手間を取らせない(1回の連絡も手間)
  • 手間を取らせない方法
    • 1.疑問が生まれない
    • 2.疑問を自分で解決できる
    • 3.疑問は聞けば分かる
  • 「疑問を自分で解決できる」の現状
    • ヘルプセンターで解決できる
  • 問題点
    • 存在の認知度が低い
    • チャットの方がラク
  • 問題点の解決策
    • elevio
  • elevioについて
    • 開発せずにツールチップ(はてなマーク)を設定可能
    • intercomと連携できる
    • elevioからアドバイスをいただける
  • elevioをを使った今後の予定
    • データ収集
  • まとめ
    • ユーザーに努力をさせないこと
    • 爆速で解決できること

ベルフェイスのカスタマーサポート

  • ベルフェイスとは
    • インサイドセールスサービス
  • カスタマーサポートの極意とは
    • 個に頼る
  • KPIの達成状況
    • チャットの初動時間:60s以内(コツは画面と音)
    • 満足度:95%以上
    • (こちらからのアンケートなどに対しての)回答率:40%
  • KPIと行動指針
    • カスタマーファースト
    • オーナーシップ
    • ハイスピード
    • 改善
    • 細部を大事に
    • メンバー内協力
  • 具体的な対策
    • よかったことの共有
    • 満足度が低い対応への改善検討(メンバー全員で)
    • チャット対応のポイントを共有(docbase)
  • 課題
    • 数値を正しくとる
  • まとめ
    • 行動指針をCS向きにする

Search Engineerring Tech Talk 2019 Spring レポート

0

この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。

Search Engineering Tech Talk 2019 Spring レポート

Search Engineering Tech Talk とは

検索技術勉強会の目的は、「検索」/「検索システム」にまつわる技術や手法に関して共有できる場を提供すること

登壇者

  • ナビタイム 小式澤 篤 :「安心な移動」のためのPOI(Point-of-Interest; 地点)検索
  • 菅谷 信介 :社内ドキュメント検索システム構築のノウハウ
  • @818uuu :料理動画アプリ「クラシル」の検索について

「安心な移動」のためのPOI(Point-of-Interest; 地点)検索

  • POIとは
    • point of interest
    • 移動の目的地となる場所
    • 緯度軽度や名前や住所などの情報
  • POIの特徴
    • 短文か単語が多い
    • 文章が少ない
    • 要素が多い
  • 経路探索におけるPOI検索の立ち位置
    • 目的地検索ができなければ、そのあとのナビゲーションができない
  • 安全な移動のためのPOI検索
    • ほとんどの人が上位5位を選択している
    • キーワードとの一致率が大事
  • 問題点1
    • いろんな県のものが出てくる
      • ホテル椿山での検索でホテル椿山東京以外のホテル
      • ディズニーでの検索で東京ディズニーランド以外のもの
      • スカイツリーも同様
  • 対策1
    • 人気なもの(有名度)が高いものを優先して上位に表示 →大方解決
  • 問題点2
    • チェーン店などでの検索で、人気の店舗が上位されてしまう
    • 「コンビニ」での検索で、「セブンイレブン 新宿駅」が表示されるなど
  • 対策2
    • 距離が近いものも優先して上位に表示 →大方解決
  • まとめ
    • 並び順優先順位
  1. 有名度
  2. 適合率
  3. 距離
  4. 再現率

社内ドキュメント検索システム構築のノウハウ

  • 企業内検索とは
    • 企業内の情報を検索する(ファイルサーバーやウェブウェイトなど)
  • fessとは
    • オープンソースの全文検索システム
    • elasticsearchを使っている
  • 企業内検索を構築する際のよくある課題
    • クロール対象が大規模
    • セキュア
    • 業務システムとの連携
    • ファイルが様々
  • 大規模化について
    • ドキュメント数は数千万くらい
    • クラスタにして分散検索
    • クロールする際の工夫
    • 更新ファイルリストを生成し、更新されたもののみをクロールする(全件だと1日のクロールで終わらないため)
  • セキュアについて
    • 認証状態により検索結果を出し分ける
      • ad連携
      • ログイン
    • クロール時に権限情報を付与する
      • 閲覧することができる権限をクロール時に取得するため
    • シングルサインオン
    • 検索システムにも自動でログインする
      • リバースプロキシがた
      • windows統合認証
      • openid connectなど
  • 業務システムとの連携
    • データはデータベースにあるため、sqlを検索サーバーに入れる
  • ファイルの種類について
    • ツールを使って文字列の抽出
      • ms office po
      • pdfbox
      • tika など

料理動画アプリ「クラシル」の検索について

  • クラシルについて
    • 検索対象:動画
    • ドキュメント?数:約2万5千本
  • 同義語対策について
    • 日々検索キーワードをチェックし手動で登録
      • ある程度効果はある
    • 同義かどうかが難しい場合がある
      • 「インゲン豆」「さやいんげん」など
  • 0件ヒット対策
    • 0件のヒットとなっているキーワードのレシピを日々チェック
    • 新生ワードを早期発見するために日々リサーチ(twitterなど)
  • 運用中気になったこと
    • 食材クエリとメニュークエリで離脱率が異なる
    • androidとiosでクエリが異なる
    • テレビの影響は大きい
    • 検索数が少ないキーワードへの対処優先度
  • KPI
    • 最初は離脱率
    • その次、検索経由のCV
      • ただし検索した後、買い物に出かけるなど、タイムラグがあるため純粋なCVを測ることが難しい(検討中)

Rails & Webpack案件でのgmo paymentのトークン決済

0

この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。

webpackの仕様を理解しないとたまにトラブルが起きるという話

GMOペイメントゲートウェイのトークン決済とは

ECサイトで決済処理を実装するときにGMOペイメントゲートウェイを活用する事になりました。
多様な決済方式に対応していて便利ですね。

GMOペイメントゲートウェイにはトークン決済という方式があります。
クレジットカード情報をJavaScript上でAPIに送信し、トークン化する事でセキュリティ的な観点で安心して決済処理を行う事ができる、というものですね。

実装

トークン化はコントローラを介さずJavaScriptで行います
必要なライブラリはCDNで配布されています。
本番用と開発用があるのでお間違えの無いように

<!-- トークン決済開発用javascriptを読み込み -->
<%= javascript_include_tag 'https://stg.static.mul-pay.jp/ext/js/token.js' %>
※URLが変更されている場合があるので必ず最新のドキュメントを参照してください

Multipaymentが定義されるのでカード情報をsubmitする際に利用します。

Multipayment.getToken({ 
   パラメータ 
   }, 任意のコールバック関数); 

の形式です

公式ドキュメントのコードサンプルは以下の通り
最低限の機能なので、カード情報の入力に不足がある場合はアラートを表示して送信しない等の処理を挟むのをオススメします。

<script type="text/javascript"> 
  function execPurchase(response) { 
   if (response.resultCode != "000") { 
    window.alert("購入処理中にエラーが発生しました"); 
   } else { 
    // カード情報は念のため値を除去 
    document.getElementById("cardno").value = ""; 
    document.getElementById("expire_year").value = ""; 
    document.getElementById("expire_month").value = ""; 
    document.getElementById("securitycode").value = ""; 
    document.getElementById("tokennumber").value = ""; 
    // 予め購入フォームに用意した token フィールドに、値を設定 
    //発行されたトークンは、有効期限が経過するか、一度 API で利用されると、無効となります。 
    //複数のAPIでトークンを利用される場合は、tokenNumberにてトークンを複数発行してください。

    document.getElementById("token").value = response.tokenObject.token; 
    // スクリプトからフォームを submit 
    document.getElementById("purchaseForm").submit(); 
   } 
  } 

  function doPurchase() { 
   var cardno, expire, securitycode, holdername; 
   var cardno = document.getElementById("cardno").value; 
   var expire = 
   document.getElementById("expire_year").value + document.getElementById("expire_month").value; 
   var securitycode = document.getElementById("securitycode").value; 
   var holdername = document.getElementById("holdername").value; 
   var tokennumber = document.getElementById("tokennumber").value; 
   Multipayment.init("tshop00000001"); 
   Multipayment.getToken({ 
    cardno : cardno, 
    expire : expire, 
    securitycode : securitycode, 
    holdername : holdername, 
    tokennumber : tokennumber 
   }, execPurchase); 
  } 
</script> 

問題点

このサンプルでいうexecPurchaseにあたるコールバック処理は、
グローバルスコープに無いとCDNから取得したライブラリ側で認識されないのですが、

webpackがスコープを管理する都合でサンプルをこのまま記述してもexecPurchaseはundefinedになってしまいました。

解決策

window.execPurchase = execPurchase;

上記をスクリプトに追記し作成したコールバック関数をwindowオブジェクトに持たせることで解決しました。
package.jsonに設定を記載する等の手段がありそうでしたが上手くいかなかったのでこの手法に落ち着きました。

更なるトラブル

Uncaught ReferenceError: r is not defined

おっ解決したと思いきやデプロイしたら動かん…

何故ならwebpackはproduction環境では変数名等を変換して難読化させた状態でビルドするからです。

window.execPurchase = r;

Multipayment.getToken({ 
    cardno : cardno, 
    expire : expire, 
    securitycode : securitycode, 
    holdername : holdername, 
    tokennumber : tokennumber 
   }, r); 

上記のようにコールバック関数がr一文字に変換されていました
window.execPurchase = execPurchase;で宣言してもこれでは動かない…
今回の場合、幸いなことにコールバック関数は文字列を渡しても問題なく処理してくれるので

Multipayment.getToken({ 
    cardno : cardno, 
    expire : expire, 
    securitycode : securitycode, 
    holdername : holdername, 
    tokennumber : tokennumber 
   }, "execPurchase"); 

のようにコールバック処理を指定する引数を文字列にすることで解決できました。
変数名は勝手に変換されますが文字列はそのまま残ります。

普段意識しないですがwebpackはこういう変換処理をしてくれているんですね。
トラブルが起きない分には素晴らしいですがたまにこういう事が起きるという話でした。

Rails & Webpack案件でのgmo paymentのトークン決済

0

この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。

webpackの仕様を理解しないとたまにトラブルが起きるという話

GMOペイメントゲートウェイのトークン決済とは

ECサイトで決済処理を実装するときにGMOペイメントゲートウェイを活用する事になりました。
多様な決済方式に対応していて便利ですね。

GMOペイメントゲートウェイにはトークン決済という方式があります。
クレジットカード情報をJavaScript上でAPIに送信し、トークン化する事でセキュリティ的な観点で安心して決済処理を行う事ができる、というものですね。

実装

トークン化はコントローラを介さずJavaScriptで行います
必要なライブラリはCDNで配布されています。
本番用と開発用があるのでお間違えの無いように

<!-- トークン決済開発用javascriptを読み込み -->
<%= javascript_include_tag 'https://stg.static.mul-pay.jp/ext/js/token.js' %>
※URLが変更されている場合があるので必ず最新のドキュメントを参照してください

Multipaymentが定義されるのでカード情報をsubmitする際に利用します。

Multipayment.getToken({ 
   パラメータ 
   }, 任意のコールバック関数); 

の形式です

公式ドキュメントのコードサンプルは以下の通り
最低限の機能なので、カード情報の入力に不足がある場合はアラートを表示して送信しない等の処理を挟むのをオススメします。

<script type="text/javascript"> 
  function execPurchase(response) { 
   if (response.resultCode != "000") { 
    window.alert("購入処理中にエラーが発生しました"); 
   } else { 
    // カード情報は念のため値を除去 
    document.getElementById("cardno").value = ""; 
    document.getElementById("expire_year").value = ""; 
    document.getElementById("expire_month").value = ""; 
    document.getElementById("securitycode").value = ""; 
    document.getElementById("tokennumber").value = ""; 
    // 予め購入フォームに用意した token フィールドに、値を設定 
    //発行されたトークンは、有効期限が経過するか、一度 API で利用されると、無効となります。 
    //複数のAPIでトークンを利用される場合は、tokenNumberにてトークンを複数発行してください。

    document.getElementById("token").value = response.tokenObject.token; 
    // スクリプトからフォームを submit 
    document.getElementById("purchaseForm").submit(); 
   } 
  } 

  function doPurchase() { 
   var cardno, expire, securitycode, holdername; 
   var cardno = document.getElementById("cardno").value; 
   var expire = 
   document.getElementById("expire_year").value + document.getElementById("expire_month").value; 
   var securitycode = document.getElementById("securitycode").value; 
   var holdername = document.getElementById("holdername").value; 
   var tokennumber = document.getElementById("tokennumber").value; 
   Multipayment.init("tshop00000001"); 
   Multipayment.getToken({ 
    cardno : cardno, 
    expire : expire, 
    securitycode : securitycode, 
    holdername : holdername, 
    tokennumber : tokennumber 
   }, execPurchase); 
  } 
</script> 

問題点

このサンプルでいうexecPurchaseにあたるコールバック処理は、
グローバルスコープに無いとCDNから取得したライブラリ側で認識されないのですが、

webpackがスコープを管理する都合でサンプルをこのまま記述してもexecPurchaseはundefinedになってしまいました。

解決策

window.execPurchase = execPurchase;

上記をスクリプトに追記し作成したコールバック関数をwindowオブジェクトに持たせることで解決しました。
package.jsonに設定を記載する等の手段がありそうでしたが上手くいかなかったのでこの手法に落ち着きました。

更なるトラブル

Uncaught ReferenceError: r is not defined

おっ解決したと思いきやデプロイしたら動かん…

何故ならwebpackはproduction環境では変数名等を変換して難読化させた状態でビルドするからです。

window.execPurchase = r;

Multipayment.getToken({ 
    cardno : cardno, 
    expire : expire, 
    securitycode : securitycode, 
    holdername : holdername, 
    tokennumber : tokennumber 
   }, r); 

上記のようにコールバック関数がr一文字に変換されていました
window.execPurchase = execPurchase;で宣言してもこれでは動かない…
今回の場合、幸いなことにコールバック関数は文字列を渡しても問題なく処理してくれるので

Multipayment.getToken({ 
    cardno : cardno, 
    expire : expire, 
    securitycode : securitycode, 
    holdername : holdername, 
    tokennumber : tokennumber 
   }, "execPurchase"); 

のようにコールバック処理を指定する引数を文字列にすることで解決できました。
変数名は勝手に変換されますが文字列はそのまま残ります。

普段意識しないですがwebpackはこういう変換処理をしてくれているんですね。
トラブルが起きない分には素晴らしいですがたまにこういう事が起きるという話でした。

wheneverで時刻を設定する際システムで設定されているのとは別のタイムゾーンを使用する

0

この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。

Railsアプリケーションのバッチ処理を [whenever](https://github.com/javan/whenever) でスケジュール設定する際、デプロイ先のタイムゾーンの設定がUTCなんだけど、スケジュール設定は開発者にわかりやすく日本時間としたいケースがあったので、その対応方法について。

問題と背景

  • デプロイ先のサーバのタイムゾーンの設定は UTC となっている(本当はJSTにしておいて欲しかったが、今更サーバの設定を変更したくない)
  • ただし whenever のスケジュール設定(config/schedule.rb)は日本時間で記述したい
  • whenever の schedule.rb ではタイムゾーンを指定することができず、システムで設定されているタイムゾーンを使用する。

対応方法

  • schedule.rb で時刻設定時にタイムゾーンを変換する関数を用意。その関数使って任意のタイムゾーンからシステムのタイムゾーンに変更する
# config/schedule.rb

# Time クラスの拡張を利用するため ActiveSupport を require する
require 'active_support/core_ext/time'

# 時刻の文字列を日本時間で解釈して、システムのタイムゾーンに変換
def jst(time)
  Time.zone = 'Asia/Tokyo'
  Time.zone.parse(time).localtime($system_utc_offset)
end

# 日本時間の 午前 2:00 => UTC の 17:00 にバッチをスケジュール
every 1.day, at: jst('2:00 am') do
  runner 'HogeFugaBatch.execute'
end

参考

データをIDで水平分割する時の方式を選ぼう

0

この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。

舎利子色不異空空不異色色即是空空即是色受想行識亦復如是 舎利子是諸法空相不生不滅不垢不浄不増不減

そもそも水平分割って何さ

同じ情報を複数のテーブルやデータベースに分けて分割する事です。
ユーザー情報が一つのデータベースに入っていたとしたら、

DB1
|ID|name|profile|
|1|イチ|弟達の世話で大変ですが愛おしいです|
|2|ニ|教授は美味しかったです|
|3|サン|お兄ちゃんに良く噛まれます|
DB1
|ID|name|profile|
|1|イチ|弟達の世話で大変ですが愛おしいです|
DB2
|ID|name|profile|
|2|ニ|教授は美味しかったです|
DB3
|ID|name|profile|
|3|サン|お兄ちゃんに良く噛まれます|

こんな風に複数のテーブルやデータベースに情報を分けて保存する事です。

どうして水平分割なんてやるのさ

データベースのレコード検索はインデックスを張る事で高速化が出来ますが、それでも数が莫大になってくると遅くなってきます。
それならば、保存する場所を複数に分ける事で1データベースのレコード数を絞り、その中で検索を掛ける事によって検索が遅くなる事を防ごうという訳です。

どうやって水平分割をするのさ

用途によって必ずしもそうではありませんが、基本的にテーブルの主キーであり、一意の数値であり、連番となるIDを元に保存/検索などをする対象先を判別するようにします。
ここでは、そのIDを元に水平分割をする場合の主に使われる2つの手法を紹介します。

範囲分割

IDを範囲分けして、対象先を判別します。
IDが1から100まではDB1に、
IDが101から200まではDB2に、
IDが201から300はDB3に、
そして301から400まではまたDB1に…という風に分割します。

DB1
|ID|name|profile|
|1|イチ|弟達の世話で大変ですが愛おしいです|
|2|ニ|教授は美味しかったです|
|3|サン|お兄ちゃんに良く噛まれます|
...
|100|ごますり|操られていただけなので勘違いしないで頂きたい|
|301|マンモス|何か話題になりました|
...

DB2
|ID|name|profile|
|101|目覚まし時計|友よ|
|102|サノスおばさん|燃え尽きたぜ…真っ白にな…|
|103|応援部隊その1|ソイヤッ!|
...

DB3
|ID|name|profile|
|201|王様|目覚まし時計が強過ぎて我が家を失いました|
|202|女王様|得意技は磔です|
|203|緊急脱出|ホールインワンしました|
...

剰余分割

IDをDB数で割った余りで対象先を判別します。DBが3つならば、
余りが0ならばDB1に、
余りが1ならばDB2に、
余りが2ならばDB3に、
分割します。

DB1
|ID|name|profile|
|1|イチ|弟達の世話で大変ですが愛おしいです|
...
|102|サノスおばさん|燃え尽きたぜ…真っ白にな…|
...
|201|王様|目覚まし時計が強過ぎて我が家を失いました|
...
DB2
|ID|name|profile|
|2|ニ|教授は美味しかったです|
...
|100|ごますり|操られていただけなので勘違いしないで頂きたい|
...
|103|応援部隊その1|ソイヤッ!|
...
|202|女王様|得意技は磔です|
...
|301|マンモス|何か話題になりました|
...
DB3
|ID|name|profile|
|3|サン|お兄ちゃんに良く噛まれます|
...
|101|目覚まし時計|友よ|
...
|203|緊急脱出|ホールインワンしました|
...

どっちが良いのさ

範囲分割と剰余分割、それぞれに利点欠点はあります。


範囲分割

利点:
対象先のデータベースを増やすといった時に設定の追加が簡単。
欠点:
設定を書くのが多少面倒。想定されるID数までの範囲を分割して、そしてそれを設定として全て書かなければいけない。
また、データの追加が大量に走った場合に処理が1つのデータベースに集中する。
ソーシャルゲームだと、ユーザー登録が沢山走った場合などには1データベースの性能がボトルネックとなる場合がある。


剰余分割

利点:
設定が簡単。IDを割った値で分割するだけ。
範囲分割での欠点のような、データの追加が大量に走る場合でも処理が複数のデータベースに分散されて一つに集中しない。
欠点:
対象先のデータベースを増やしづらい。また、増やしても設定が複雑になってしまう。
このIDまでは3で割った余りで対象先を判別し、このIDからは4で割った余りで分割する…とか、そんな事をしなくてはいけない。


まあ、一長一短ですね。
ただ、汎用性があるのは範囲分割の方だと思います。剰余分割の 対象先のデータベースを増やしづらい。また、増やしても設定が複雑になってしまう。 という事柄がかなり大きいので。他の手法であるハッシュ分割なども似たような形です。
そんな訳で、それぞれの利点欠点を理解した上で、プロジェクトに合った分割方法を選びましょう。

Rails Console から Sidekiq のジョブを操作する

0

この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。

Sidekiq の Web コンソールが重いので Rails console からサクッとジョブのリトライとかしたい場合のメモ

キューの一覧を取得

 Sidekiq::Queue.all

キューに登録されているジョブの一覧を取得

 q = Sidekiq::Queue.new('キュー名')
 q.entries

ジョブが失敗した場合はリトライのキューに移る

リトライの一覧の確認〜ジョブの強制リトライ

 rs = Sidekiq::RetrySet.new
 rs.size
 rs.entries
 rs.entries[0].retry

リトライ待ちのジョブの削除

 rs.entries[0].delete

Dead (リトライ期限が過ぎたジョブ)

ds = Sidekiq::DeadSet.new
ds.size
ds.clear

docker-compose で開発者毎に異なる設定を使いたい場合 | その他 | DoRuby

0

この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。

開発環境として Docker を使っていると、開発者の環境固有の問題に対応するためにその環境固有の設定を追加したい場合がある。そのような場合のTips

背景

  • ローカル開発環境を Docker で構築して開発者間の環境の差異をできるだけ少なくしている
  • ただし、ホスト環境が Windows, Mac, Linux と様々で、個人毎に設定を微妙に変えたいケースがある

追記(2019/6/27)

どうも docker-compose はデフォルトで docker-compose.override.yml を読み込むようなので、以下のように環境変数の設定は不要だった。

対応方法

  • 環境変数 COMPOSE_FILE を使用するexport COMPOSE_FILE=docker-compose.yml:docker-compose.override.yml のように設定すると、 docker-compose.ymldocker-compose.override.yml 両方の設定がマージされる
  • いちいち export ... を打つのが面倒な場合、 direnv を導入すると良い

ローカル環境で’LOADING Redis is loading the dataset in memory’が頻発する

0

この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。

表題の件についてのメモ

背景

  • Rails アプリケーションのローカル開発環境を Docker で構築
  • その環境には Sidekiq の worker が稼働する、 worker コンテナ、Sidekiqがバックエンドとして使用している Redis が稼働している redis コンテナが存在する

現象

docker-compose によるコンテナの立ち上げ時、以下のエラーが発生して worker コンテナが落ちる

worker           | LOADING Redis is loading the dataset in memory
worker           | /app/vendor/bundle/ruby/2.5.0/gems/redis-4.0.3/lib/redis/client.rb:124:in `call'
worker           | /app/vendor/bundle/ruby/2.5.0/gems/redis-4.0.3/lib/redis/client.rb:107:in `block in connect'
(略)
worker           | /app/vendor/bundle/ruby/2.5.0/gems/redis-4.0.3/lib/redis.rb:278:in `info'
worker           | /app/vendor/bundle/ruby/2.5.0/gems/sidekiq-5.2.3/lib/sidekiq.rb:113:in `block in redis_info'
worker           | /app/vendor/bundle/ruby/2.5.0/gems/sidekiq-5.2.3/lib/sidekiq.rb:95:in `block in redis'

原因

redisコンテナ起動時に --appendonly yes が指定されていた。

LOADING Redis is loading the dataset in memory というエラーは、 AOF(Append-only File)からデータをメモリ上に展開している間に更新(SET)などのコマンドを実行しようとした際に発生するらしい。

RDB/AOFファイルをローディングしている間、SETコマンドのようなものを実行すれば “(error)LOADING Redis is loading the dataset in memory”このようなエラーを出すが、infoコマンドは実行されます。 したがって、infoコマンドでローディング中なのか確認することができます。
REDIS INFO [section] コマンド

対処その1

Append-only File は、プロセスの予期しない終了等からデータを保護するための機構であるが、ローカル開発環境においてはそのような考慮は不要(消えても構わない)ので、 --amendonly yes オプションを取り除く。

   redis:
     image: redis:latest
     ports:
       - 6379:6379
     volumes:
       - redis:/data
-    command: redis-server --appendonly yes
+    command: redis-server

対処その2

あるいは、ローカルで保持しているRedis内の情報が消えても構わないのであれば一旦 redis コンテナに割り当てている Volume を削除してあげるという方法もある。

ただし、一時的なものなので AOF が肥大化するにつれていずれ問題が再発する。(と思われる)

ボリュームを削除する手順は以下(コンテナを停止している状態で実施する)

<ボリュームの一覧確認>
docker volume ls

<ボリューム削除>
docker volume rm xxxxxx_redis

CloudFront + API Gateway + Lambdaの環境で生きた無属性のペアとそのこと

0

この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。

API Gateway のエラー時のレスポンスのメッセージは必ずしもその原因と直結するような内容となっていないことが多々あり、原因の特定に時間がかかりがちなのでメモっておく。

※随時追加予定

※「レスポンス」の内容は、curl -vでのリクエストに対するレスポンスを多少加工して掲載している。

401 ‘Unauthorized’

レスポンス

< HTTP/2 401
< date: Wed, 22 May 2019 08:43:33 GMT
< content-type: application/json
< content-length: 26
< x-amzn-requestid: xxxxxxxxxxxxxxxx
< x-amzn-errortype: UnauthorizedException
< x-amz-apigw-id: xxxxxxxxxxxxxxxxxxx

{"message":"Unauthorized"}

原因

  • カスタムオーソライザに必要な情報(特定のリクエストヘッダ等)がリクエストに含まれていない
    • 例えば Cookiesession_idを使ってAPIへのアクセスを認可するオーソライザを設定している場合、Cookieヘッダがリクエストに含まれていない場合に前述のリクエストが返ってくる

403 ‘Missing Authentication Token’

レスポンス

< HTTP/2 403
< date: Wed, 22 May 2019 08:31:54 GMT
< content-type: application/json
< content-length: 42
< x-amzn-requestid: xxxxxxxxxxxxxxxxx
< x-amzn-errortype: MissingAuthenticationTokenException
< x-amz-apigw-id: xxxxxxxxxxxxxx

{"message":"Missing Authentication Token"}

原因

  • API Gateway のエンドポイントのパスが間違っている
    • 例えば正しいエンドポイントのURLが以下であった場合
      • https://xxxxx.execute-api.ap-northeast-1.amazonaws.com/stage/api/v1/{+proxy}
    • 以下のように間違っている(/apiが抜けている)と前述のようなレスポンスが返ってくる
      • https://xxxxx.execute-api.ap-northeast-1.amazonaws.com/stage/v1/xxxxxx

403 Forbidden

(思い出したら書く)

403 ‘User is not authorized to access this resource with an explicit deny’

レスポンス

< HTTP/1.1 403 Forbidden
< Date: Wed, 22 May 2019 08:56:27 GMT
< Content-Type: application/json
< Content-Length: 82
< Connection: keep-alive
< x-amzn-RequestId: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
< x-amzn-ErrorType: AccessDeniedException
< x-amz-apigw-id: xxxxxxxxxxx

{"Message":"User is not authorized to access this resource with an explicit deny"}

原因

  • カスタムオーソライザから DENY のポリシー文書が返却された場合に返ってくるレスポンス
    • カスタムオーソライザの呼び出し自体には成功しているので、大体のケースにおいては正常動作と言える(正常でない場合はカスタムオーソライザをデバッグすることになる)

503 (HTML形式で) ‘ERROR: The request could not be satisfied’

レスポンス

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<HTML><HEAD><META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=iso-8859-1">
<TITLE>ERROR: The request could not be satisfied</TITLE>
</HEAD><BODY>
<H1>502 ERROR</H1>
<H2>The request could not be satisfied.</H2>
<HR noshade size="1px">
CloudFront attempted to establish a connection with the origin, but either the attempt failed or the origin closed the connection.
We can't connect to the server for this app or website at this time. There might be too much traffic or a configuration error. Try again later, or contact the app or website owner.
<BR clear="all">
If you provide content to customers through CloudFront, you can find steps to troubleshoot and help prevent this error by reviewing the CloudFront documentation.
<BR clear="all">
<HR noshade size="1px">
<PRE>
Generated by cloudfront (CloudFront)
Request ID: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
</PRE>
<ADDRESS>
</ADDRESS>
</BODY></HTML>

原因

  • オリジン(API Gateway)に対して http でアクセスしている
    • CloudFront のオリジンの設定(Origin Protocol Policy)が HTTP Only のようになっていないか確認する
    • API Gateway は HTTPS しかサポートしていないので、 HTTPS Only とするべき

nginx の proxy_pass の転送先が API Gateway の場合に `SSL_do_handshake() failed` と言われた

0

この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。

ローカル環境のnginxからAPI Gateway で用意したAPIのエンドポイントに proxy_pass 設定したときにハマった件

背景

  • ローカル環境に nginx を立てて、その nginx で特定の path (例えば /api/v1/xxx) に対して proxy_pass を設定して、API Gateway で作成したエンドポイントに対してアクセスを転送したい

現象

  • 該当 path にアクセスすると nginx から 502 Bad Gateway のエラーが返ってくる
  • nginx のエラーログを見ると SSL_do_handshake() failed というエラーが発生していた

原因

proxy_pass の対象となるエンドポイントが SNI を使用している場合発生するっぽい(API Gateway がデフォルトで用意するエンドポイントは SNI 使ってる)

参考: Nginx reverse proxy error:14077438:SSL SSL_do_handshake() failed – Stack Overflow

対処

以下のように proxy_ssl_server_name on の設定を追加してあげたらOK

location /api/ {
  proxy_pass https://xxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/Dev/api/;
  proxy_ssl_server_name on;
}

ボックスガチャの実装をしよう(ソーシャルゲームにおけるサーバーサイド) 7

0

この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。

どうして世の中は世知辛いのだろう。

前回のあらすじ

前回
 ボックスガチャの実装が完了しました。
 しかし、そこで完了にするのではなくコードレビューや仕様の確認をして、この先に改修が入ったりしても、誰かにクソコードと喚かれたり、見直して自分でクソコード……と呟いてしまうような、また仕様書に無い仕様が実際には入っていたり、逆に実は仕様が欠けていたり……みたいな悲劇が起こらないようにしましょう。

仕様確認をしよう

くらいあんとえんじにあ「クライアントもアセット組み込んで実装終わったから、仕様確認するぞー」

仕様書を見直すと、

・ボックスガチャは通常ガチャと同様、1連と10連で引ける
・ボックスガチャで引けるものは通常ガチャと同等
・10連を選択した時にボックスの中身が9個以下だった場合、消費される対象はその中身分のみとなり、全てを自動で排出する
・リセットする度に上限までボックスの中身は変わる
・リセットは自由に可能。そのステージのボックスガチャを1回も引かなくともリセットが出来る
+ 排出されるキャラクターの中で最高レアリティのキャラはボックスの中身を1/4以上引いていない時しか出ない
+ ボックスを引き切ると別のマスタで定義してあるおまけが貰える

さーばーえんじにあ「(追加された仕様に関しても実装したし)ここあたりは問題ないですよね。マスタ定義書とかも更新してあるのは見ましたし」

※開発途中に仕様が追加変更される事は多分良くある

くらいあんとえんじにあ「あ、いや、 ボックスを引き切ると別のマスタで定義してあるおまけが貰える ってUI上の表示は無くて良いんですっけ。こっちとしては特に大して聞いてないんですけど」
ぷらんな「現状大した物出さないし、最後まで引ききって遊んでくれてありがとう的な、そんな小さなものだから取り敢えず今回は無しね」
くらいあんとえんじにあ「りょーかい。一応追記しておいてくれると助かります」
ぷらんな「わかっタスマニアデビル」
でざいなー「UIの遷移図、仮画像のままなので本画像に差し替えておいてくださインパラ」
ぷらんな「りょーか犬」
さーばーえんじにあ「サーバー側としては他に特にないです猫」
くらいあんとえんじにあ「……クライアントとしても同じです」
でざいなー「……」
ぷらんな「……」
さーばーえんじにあ「……」
くらいあんとえんじにあ「……やらないよ?」

コードレビューをしよう

さーばーえんじにあ「実装の規模でかいんで部屋取ってコードレビューの時間取ろうと思うんですけど、良いですか?」
れびゅわー「この時間に取っておいて。1時間あれば足りる?」
さーばーえんじにあ「まあ、基本的に新規実装なので、既存コードの改修でごっちゃごっちゃそこまでやってないの考えるとその位で多分大丈夫です」

さーばーえんじにあ「実装は大まかに言うと、既存コードをごねごねせずに、引用して来る感じで行いました。
 で、ボックスガチャの実装で普通のガチャと共通の部分は継承してそのまんま使って、この部分とこの部分だけ違うので、こんな感じでオーバーライドして……みたいな感じです」
れびゅあー「設計はまあ事前に聞いてたし特に問題ないと思う」
さーばーえんじにあ「わかりましたー」
れびゅあー「で、一つ一つ改修したコード見ていこうか」

れびゅあー「データ更新の部分さ、この作りだと10連だと10回update叩かないといけないけどさ、一括で何とか出来なかった?」
さーばーえんじにあ「DBのクラスタ設定とか考慮してやると結構複雑になりそうだったので、一旦やってないです。後、ちゃんとindexとか張ってありますし、不要なデータも基本的に即削除してるので、そんなに問題ないかと」
れびゅあー「分かった。まあ、最大10回なら問題ないかな。パーティションは切ってあって、ガチャ期間が過ぎてN日経ったら削除ね……。そこあたりは何か根拠でも?」
さーばーえんじにあ「ガチャで取ってあるログと同じに一旦してあります」
れびゅあー「そうね。多分無いと思うけど、同じボックスガチャを前に引いていた人はその状態からで復刻してやるとか、そんな事がないかとか、ちゃんとプランナーとかと合意取っといて」
さーばーえんじにあ「分かりましたー」

れびゅあー「じゃあ、言ったところ確認とったりとかしたら本ブランチにマージするね」
さーばーえんじにあ「分かりましたー」

タスク完了

上記が終わり次第、 ボックスガチャの実装 というタスクはやっと、しかしながら 基本的にのみ 完了となります。
QAチェックなどで、コードレビューやテストコードもすり抜けて不具合が発覚する可能性なども多々ありますので、その都度修正などをしていきましょう。


……。
…………。
…………こんな風に実装出来たら良かったなぁ……。

ボックスガチャの実装をしよう(ソーシャルゲームにおけるサーバーサイド) 6

0

この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。

ロースザブトンもやしのナムルミスジテールスープサーロインカルビ石焼ビビンバタンサガリ米ハチノスチヂミシマチョウたまごスープハツサンチュニンニクのホイル焼きレバートントロ焼き野菜ミドガルズオルム

前回までのあらすじ

前回

 ボックスガチャのサーバーサイドのコードの実装が完了しました。
 また、テストコードの実装及びに、新規マスタのインポート処理も完了し、残るはクライアントとの繋ぎ込みのみです。

クライアントとの繋ぎこみ

くらいあんとえんじにあ「追加されるAPIのパスとかはもう共有されてるけど、まだパラメータとレスポンスの形ちゃんと貰ってないよ」
さーばーえんじにあ「あ、忘れてました……。纏めて返します」

ボックスガチャを引く POST: gachas/draw
parameter
{
  gacha_code: integer, #対象ガチャコード
  draw_count: integer  #引く回数
}

response
{
  characters: [ #引いたキャラクター達の情報
    {
      character_id: integer, #キャラクターID
      character_code: integer, #キャラクターコード
      level: integer, #レベル
      .....
    }
  ]
}

パラメータ、レスポンスは既存から変更なし
現在のボックスの中身を確認 GET: gachas/current_box_contents
parameter (クエリパラメータ)
{
  gacha_code: integer # 対象のガチャコード
}

response
{
  stage: integer, # 現在のボックスガチャのステージ
  total_count: integer, # 現在のボックスガチャの中身の総数
  remain_count: integer, # 現在のボックスガチャの残りの総数
  contents: [ # ボックスの中身
    {
      character_code: integer, # キャラクターコード
      total_count: integer, # 中身の総数
      remain_count: integer # 残りの総数
    }
  ]
}
ボックスガチャのリセット POST gachas/reset_box
......
各ステージでのボックスの中身の確認 GET: gachas/all_box_contents
......

さーばーえんじにあ「取り敢えずこんな感じですけど問題ありますか?」
くらいあんとえんじにあ「うーん、まあ、現状のUIで表示するものだと、これとこれとこれが足りないから追加して」
さーばーえんじにあ「了解です、改修終わったらAPI定義書に纏めておきますね」


※ APIのパラメータなどに限らず、仕様書とは別に定義書があると何かを調べる時にとても役立ちます。改修の度にきちんと更新されている事を前提として。


1時間後

さーばーえんじにあ「で、ソースと定義書の更新終わったからまたテストして……開発環境に反映!」
くらいあんとえんじにあ「不具合あったら報告するね」
さーばーえんじにあ「入念にテストしたから基本的に大丈夫だと思うけど、まあ、完璧な自信は無いので」

数時間後

くらいあんとえんじにあ「ボックスガチャだったら、引いた後の中身のレスポンスも同時に返してくれない? 続けて引く時にさ、ガチャ結果からクライアントが判断してボックスの中身を減算するとかするより、サーバーからその時点の結果一緒に貰っちゃった方が良いと思うんだ」
さーばーえんじにあ「ボックスガチャの時だけそういうレスポンスを返すって感じですか?」
くらいあんとえんじにあ「そういう事」
さーばーえんじにあ「ボックスガチャでない時は空配列を返す形で良いですか?」
くらいあんとえんじにあ「おk」
さーばーえんじにあ「分かりました(バグは起きてないっぽいかな。良かった)」
くらいあんとえんじにあ「あ、Internal Server Error」
さーばーえんじにあ「ふぇっ!?」

その後、クライアントとの擦り合わせやバグの解消をして

くらいあんとえんじにあ「一通り見ましたが、これで基本的に問題ないです」
さーばーえんじにあ「確認ありがとうございますー」

実装は完了しましたがまだ終わりではありません

さーばーえんじにあ「じゃあ、後はコードレビューとか仕様の認識合わせとかして終わりか。レビューする前に自分のコード一回見直そう。
 ……変数名ちょっと分かりづらいやここ。直しておこう。あ、後、ここのレスポンスの追加、定義書に書いてないや。こうして……」

※ コードレビューは大事。動くだけの汚いコードは後々の全てに悪影響を与えます。ただ、そのレビューをする前にちゃんと自分のコードは見直して、気付いた点は直しておきましょう。そしてその修正でエラーが発生しないようにしましょう。
※ また、実装が完了した後での仕様の認識合わせも重要です。口答だけで追加された仕様などが抜け落ちている可能性などをしっかりと排除しておきましょう。

次(最後)

ボックスガチャの実装をしよう(ソーシャルゲームにおけるサーバーサイド) 4

0

この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。

ど゛う゛じ゛で゛

前回までのあらすじ

前回

ボックスガチャを作る事になりました。
改修、追加すべきAPIは以下で、

・ボックスガチャを引く POST: gachas/draw
・現在のボックスの中身を確認 GET: gachas/current_box_contents
・各ステージでのボックスの中身の確認 GET: gachas/all_box_contents
・ボックスガチャのリセット POST gachas/reset_box

それらの処理の為に、GachaBoxContentManagerという、ユーザーのボックスガチャの状態を一括管理するクラスを作成しました。

そうすると、

・現在のボックスの中身を確認 GET: gachas/current_box_contents
・ボックスガチャのリセット POST gachas/reset_box

この二つのAPIの実装は大体終わったも同然であり、また、

・各ステージでのボックスの中身の確認 GET: gachas/all_box_contents

このAPIの実装もマスタデータを返すだけなのですぐに終わりました。
残りは、

・ボックスガチャを引く POST: gachas/draw

のみです。

ボックスガチャを引く処理を作る

通常ガチャを引くコードは大まかに以下のように分けられていました。

APIが叩かれる
 受け取ったパラメータを元にガチャのプロセスを初期化
 ガチャのプロセスを実行する
  アサーション
  ガチャを指定回数だけ引く
  ガチャの結果からユーザーデータを更新
  ログの出力
 実行後、レスポンスを返す

さーばーえんじにあ「このコードでの一番の問題は、1回ガチャを引く毎に中身のコンテンツが変わる事を考慮していないという事だ。
 ……どうしようかなこれ。
 ボックスガチャを引く為に大きく改修すべき部分は、
 ・ガチャを指定回数だけ引く
 ・ガチャの結果からユーザーデータを更新
 基本的にこの三つか。それ以外の部分は大きく変えたりは多分しなくて良い。
 A. ガチャのプロセスを継承してその部分だけを変えるか
 B. ガチャのプロセスそのものを拡張するか
 どっちが良いかな。
 うーん、例えば、他にも色んなガチャを実装するとしたらどうなるかな……。
 Aだと、それぞれのガチャの特性に合わせた部分だけを変更した新しいプロセスを作る必要が出てくる。でも、他の部分は基本のガチャに準じている訳だからまあ、見やすいかな。
 Bだと、一つのガチャのプロセスの中に色んなガチャに応じた分岐がどんどん増えていく事になる。完ッ璧に見辛いな。更に、改修を加える度に他のガチャに影響が出る可能性も否定出来ない。
 Aだな、うん。そうしよう」
 そうして、以下のように実装が進みました。

APIが叩かれる
 (受け取ったパラメータを元にガチャのプロセスを初期化 <= ガチャ種が増えた事によって削除)
 受け取ったパラメータと、そのガチャの種類に応じたプロセスの初期化を行う <= new!
 プロセスを実行する
 実行後レスポンスを返す
通常ガチャプロセス
 初期化
 アサーション
 ガチャを指定回数だけ引く
 ガチャの結果からユーザーデータを更新
 ログの出力

ボックスガチャプロセス <= new!
 初期化 <= GachaBoxContentManagerの初期化もする
 アサーション
 ガチャの指定回数だけ
  ガチャを引く
  ボックスの中身を減算する
 ガチャの結果からユーザーデータを更新 <= GachaBoxContentManagerからボックスの中身も更新する
 ログの出力

さーばーえんじにあ「こんな感じか。
 既存の通常ガチャへの影響はほぼ無し。じゃ、APIも作り終えたから、後はテストコード書いて……マスタのインポート処理とかをマスタが来次第作成、クライアントとの繋ぎこみをその後、か。
 あれ、まだまだやる事結構あるんか。
 ちょっと面倒だな……」

ボックスガチャの実装をしよう(ソーシャルゲームにおけるサーバーサイド) 2

0

この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。

この予算内で引けなかったら諦める

前回までのあらすじ

前回

ボックスガチャを作る事になりました。

必要な機能:
・ボックスガチャを引く
・現在のボックスの中身を確認
・各ステージでのボックスの中身の確認
・ボックスガチャのリセット
必要な追加データ:
・ユーザーのボックスの現在状態
・ステージ毎のボックスの中身を定義するマスタ
マスタデータ
 ┣キャラクターマスタ CharacterMaster
 ┃┣キャラスキルマスタ CharacterSkillMaster
 ┃┣キャラ成長マスタ CharacterExpMaster
 ┃┃┗キャラパラメータマスタ CharacterParameterMaster
 ┃...
  ...
 ┣ガチャの親マスタ GachaMaster
 ┃┣ガチャの中身マスタ GachaContentMaster
 ┃┗ボックスガチャの中身マスタ <= new!
 ...


ユーザーデータ
 ┣所持キャラクター群 Characters
 ...
 ┣現在のボックスガチャの状態データ群 <= new!
 ...

仕様書が来ました

読み込むと実装に足りるだけの詳細がありました。

・ボックスガチャは通常ガチャと同様、1連と10連で引ける
・ボックスガチャで引けるものは通常ガチャと同等
・10連を選択した時にボックスの中身が9個以下だった場合、消費される対象はその中身分のみとなり、全てを自動で排出する
・リセットする度に上限までボックスの中身は変わる
・リセットは自由に可能。そのステージのボックスガチャを1回も引かなくともリセットが出来る

さーばーえんじにあ「設計に関しても承認貰ったから実装していこう」

まずはテーブルを作りましょう

さーばーえんじにあ「新規テーブルは
・ボックスガチャの中身マスタ
・現在のボックスガチャの状態データ群
か。
名前はどうしようかな」

 ┣キャラクターマスタ CharacterMaster
 ┃┣キャラスキルマスタ CharacterSkillMaster
 ┃┣キャラ成長マスタ CharacterExpMaster
 ┃┃┗キャラパラメータマスタ CharacterParameterMaster

 ┣ガチャの親マスタ GachaMaster
 ┃┣ガチャの中身マスタ GachaContentMaster
 ┃┗ボックスガチャの中身マスタ

 ┣所持キャラクター群 Character
 ...
 ┣現在のボックスガチャの状態データ群

さーばーえんじにあ「ガチャのマスタは先頭にGachaってついていた方がエディタとかで開いた時、ガチャ関連の定義ファイルが一箇所に集中していて分かりやすいよなあ。
 だと、ボックスガチャの中身マスタはGachaBoxContentMasterとかになるかな?
 で、現在のボックスガチャの状態テータ群もそれに倣う形でGachaBoxContentとかになるかなあ。
 ボックスなのにGachaBoxって逆転させるの気持ち悪いけど……でも、データの並び順序考えるとこの方が良いかな」
さーばーえんじにあ「そんなこんなで、マスタ名、GachaBoxContentMasterにしました。通常ガチャとは別にステージ毎の中身の情報をここにお願いします。
 カラムは、通常のガチャの中身を示すGachaContentMasterの一部を変更、追加する形で、
 確率がweight: integerで定義されているのに対して、こちらが示すものは中身の数量なので、quantity: integerにします。
 後は、どのステージかを示すstage: integerをカラムとして追加してあります。
 問題ないですか?」
ぷらんな「ok」

API名を先に定義しておきましょう

既存のAPIとして以下が既に定義されていました。

・ガチャトップ画面 GET: gachas
・ガチャを引く POST: gachas/draw
・ガチャの確率を確認する GET: gachas/rate_list

さーばーえんじにあ「ガチャを引くAPIはこのままでいっか。引くものは確率確認は、ボックスガチャで各ステージでのボックスの中身の確認、としても良い気がする。
 ……でも、レスポンスの形もUIも変わるよなぁ……分けた方が良いかな?」
くらいあんとえんじにあ「あ、分けて欲しいな」
さーばーえんじにあ「分かりましたー」
さーばーえんじにあ「じゃあ、追加する機能に対してのAPIはこんな感じになるかな」

・ボックスガチャを引く POST: gachas/draw
・現在のボックスの中身を確認 GET: gachas/current_box_contents
・各ステージでのボックスの中身の確認 GET: gachas/all_box_contents
・ボックスガチャのリセット POST gachas/reset_box

さーばーえんじにあ「じゃあ、実装かな」

ボックスガチャの実装をしよう(ソーシャルゲームにおけるサーバーサイド) 1

0

この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。

この無料分で引けなかったら諦める

ガチャから派生させてボックスガチャを作る事になりました

ぷらんなー「このゲーム、普通のガチャはあるけどさ、ボックスガチャやりたいから作って」
さーばーえんじにあ「ボックスガチャか」
ぷらんなー「必要な機能は大まかに言うと、
・ボックスガチャを引く
・現在のボックスの中身を確認
・各ステージでのボックスの中身の確認
・ボックスガチャのリセット
ね。リセット回数はガチャを定義する親マスタで制限かけるから。
 細々とした仕様は後で仕様書が完成したら共有するけど、取り敢えず確定事項です」
さーばーえんじにあ「りょーかい」

ボックスガチャってなぁに?

 例えるならば、ガチャポンです。
 中身が有限なガチャです。
 排出率が1%のレアキャラが出るガチャだったら、通常は100回引いても大体63%の確率でしか引けませんが、ボックスガチャならば中身が有限なので、中身を全て引けば100%出す事が出来ます。
 やったね!!

ガチャから派生させてボックスガチャを実装するにはどういうAPIとどういう追加データが必要だろう

さーばーえんじにあ「必要なAPIとしては、まあ、必要な機能の通りで、
・ボックスガチャを引く
・現在のボックスの中身を確認
・各ステージでのボックスの中身の確認
・ボックスガチャのリセット
で大丈夫だよな」
さーばーえんじにあ「で、サーバー側で持つべきデータとしてはまず、
・ユーザーのボックスの現在状態
が必要だよな。で、えっと? 機能に 各ステージでのボックスの中身の確認 なんて項目があるって事は、リセットする度にボックスの中身変わるのかな?」
ぷらんな「そうだよ。仕様書もうそろそろ出来るから、ちょっと待ってて」
さーばーえんじにあ「あ、はい。なら、
・ステージ毎のボックスの中身を定義するマスタ
を別に作った方がいいか。通常のガチャの中身を定義するマスタとは段々かけ離れてくるし。
……その位か」

必要な機能:
・ボックスガチャを引く
・現在のボックスの中身を確認
・各ステージでのボックスの中身の確認
・ボックスガチャのリセット
必要な追加データ:
・ユーザーのボックスの現在状態
・ステージ毎のボックスの中身を定義するマスタ

さーばーえんじにあ「で、ユーザーのボックスの現在状態をどうやって保存するかちょっと悩むな。二つ案あるんだけど」

案1:
中身のコンテンツ一種類毎にレコードを作る
利点: 
・構造が簡単。ユーザー動向の調査の為の検索や緊急対応の為の一括更新などがし易い。
欠点:
・ユーザー一人辺りのデータが多くなる。総レコード数は簡単に考えて、(ユーザー数×ボックスガチャの数×ボックスガチャの平均コンテンツ種別量)となる
・10連でガチャったりする場合、SQLの更新回数が多い(O(n))
案2:
ボックスガチャ一つに対し、データをJSONなどで一括管理。
利点:
・ユーザー一人辺りのデータが少ない。総レコード数は簡単に考えて(ユーザー数×ボックスガチャの数)となる。
・10連だろうと1連だろうとSQLの更新回数は1回(O(1))
欠点:
・ユーザー動向の為の調査の為などの検索、緊急対応の為の一括更新がし辛い。JSONを生で保存するならlike検索などは辛うじて出来るが、圧縮してバイナリなどにした場合、全く出来なくなる
・管理、更新の為に毎回JSONなどを展開、圧縮などする必要がある為、やや面倒

さーばーえんじにあ「……1だな! 2の欠点が、結構でかいからなぁ。ユーザー動向の調査とかはともかく、何か緊急対応しなきゃいけなくなった時、一括更新とか出来なくてユーザー一人一人に対して処理掛ける必要が出てきて、時間が酷く掛かったりとか、そんないやーな予感がする。
 データベース容量は結構余裕あるし、古いボックスガチャのデータとか、引き切ったボックスのデータとかはパーティション区切れば一括削除出来るし、ユーザー単位でそんな数百件もデータ保持しないだろ!
 それに、更新の為の計算量はO(n)だけど、最大10連とかだったら、1回での更新は最大10回だし、ちゃんとインデックス張れば大丈夫でしょ!
 ……まあ、設計書とかちゃんと作るとして」

テーブル概要書

マスタデータ
 ┣キャラクターマスタ CharacterMaster
 ┃┣キャラスキルマスタ CharacterSkillMaster
 ┃┣キャラ成長マスタ CharacterExpMaster
 ┃┃┗キャラパラメータマスタ CharacterParameterMaster
 ┃...
  ...
 ┣ガチャの親マスタ GachaMaster
 ┃┣ガチャの中身マスタ GachaContentMaster
 ┃┗ボックスガチャの中身マスタ <= new!
 ...


ユーザーデータ
 ┣所持キャラクター群 Characters
 ...
 ┣現在のボックスガチャの状態データ群 <= new!
 ...

ボックスガチャの実装をしよう(ソーシャルゲームにおけるサーバーサイド) 3

0

この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。

ここまで来て引き下がる訳にはいかない!

前回までのあらすじ

前回
ボックスガチャを作る事になりました。
改修、追加するべきAPIは以下に纏められました。

・ボックスガチャを引く POST: gachas/draw
・現在のボックスの中身を確認 GET: gachas/current_box_contents
・各ステージでのボックスの中身の確認 GET: gachas/all_box_contents
・ボックスガチャのリセット POST gachas/reset_box

実装する前に

さーばーえんじにあ「これ……全てのAPIに対して、ボックスの中身を確認変更する操作が必要になるな。
 で、ボックスの中身を確認やら変更やらするのは、レコードの仕組み上、複数のレコードを常に触る事になる。
 ちょっと、一つ一つ実装する前にボックスの中身に対する操作をクラスで一括で行う事にして、そこから触った方が良いな……」

 必要な操作を纏めたところ、以下のようになりました。

・ボックスの中身を初期化する
・ボックスの中身を確認する
・ボックスの中身を引く
・ボックスの中身をリセットする(次のステージに進ませる)
+各種デバッグ機能

さーばーえんじにあ「これらをクラスにして、名前、どうしよっかな。ボックスの中身に対する処理を全てやるんだから、GachaBoxContentManagerとかでいっか。で、そこから全てのボックスに対する操作を一括して行う、と」

クラス GachaBoxContentManager
 コンストラクタ : ユーザーとガチャマスタを受け取り、ユーザーに対するボックスが無かったら初期状態で作る(ガチャマスタがボックスガチャ対応でなかったらエラー)
 確認メソッド : ボックスの中身を用途に合った形に整形して渡す
 引くメソッド : ボックスの中身を更新する
 リセットメソッド : ボックスを次のステージへと移行させる
 デバッグ機能 : 取り敢えず強制初期化とかはデバッグで必要になりそうだけど保留

さーばーえんじにあ「じゃあ、今度こそ実装に入るか……って、ボックスの中身を確認するAPIって大体実装終わったも同然じゃんこれ」

ボックスの中身を確認するAPI gachas/current_box_contents
  # GachaBoxContentManagerを対象ユーザーでインスタンス生成
  # インスタンスメソッドから中身を受け取る
  # 整形して返す

さーばーえんじにあ「リセットも」

ボックスのリセットAPI gachas/reset_box
  GachaBoxContentManagerを対象ユーザーでインスタンス生成
  インスタンスメソッドからリセットを実行
  リセット出来たらレスポンスを、既にリセット回数の上限に達しているなどでリセット出来なかったらエラーを返す

さーばーえんじにあ「レスポンスとかはまあクライアント次第だけど、各ステージでのボックスの中身の確認も後はマスタのデータそのまま返すだけだし……

各ステージでのボックスの中身の確認API gachas/all_box_contents
 対象のガチャのマスタデータ取得
 整形して返す

 あら、後はボックスガチャそのものの実装だけみたいなもんじゃん。
 あれま」

味噌カツで例えるソーシャルゲームのサーバーサイドにおける新規実装の流れ 4

0

この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。

そしてソースカツ丼となった

前回までのあらすじ

前回

以下のゲームで、

牧場経営ソーシャルゲーム:
ユーザーは牧場を経営する。また、牧場などで育てたものを調理し、食べる事が出来る。
ユーザーにはステータスが存在し、様々な行動によってステータスが変動する。ある一定のステータスを伸ばすと出来る事が増える。
ステータスは割合で決定されており、どれかのステータスを上げると他のステータスが落ちる。
一度得た能力は消えない。

味噌カツを作る事が決定しました。
提示された仕様書を読み込み、それに対して曖昧な点や改善すべき点を会話して解消、サーバーエンジニアがすべき事が明確になった後に仮実装、暫定での実機確認が完了。その後に仮実装で適当に作っていた部分をちゃんと作り込み、テストコードも作り、実装を一旦完了させました。

コーディング完了後にすべき事

 コーディング完了してそれで全て終わりにしてしまうと、後々負の遺産が溜まっていく事になります。
 そうしてしまうと、そのコードを他者が弄る事になった時に、「ク・ソ・コォォォォドゥォォォォォォォォオオオオオオ」とかその人に叫ばれたりとか、ずーっと誰にも気づかれないまま潜んでいたバグが唐突に本番環境で発生したりとか、そんな事が起きてしまう可能性がとても高くなっていきます。
※クソコードによる影響はどこかしこに記事やブログとして載っているのでここでは書きません。ただ、クソコードはクソだからクソコードと呼ばれるのです。クソコードはクソだからクソコードと呼ばれるのです。

 そんな事を極力防ぐ為に、例えば、以下のような事をしましょう。

実装完了後の仕様書の読み合わせ

 味噌カツ作りを実装がサーバー、クライアント、デザイナー、プランナー全ての担当が完了し、繋ぎ込みも一旦終わった後に、仕様に対して漏れや差異がないかなどを再確認しましょう。


くらいあんとえんじにあ「追加するって言ってたこの仕様、仕様書から漏れてるよ」
ぷらんなー「えっ」
さーばーえんじにあ「あ、後から追加されたこの仕様に対して実装するの忘れてた……」
くらいあんとえんじにあ「えっ、あっ、ここデフォルトデータしか入ってないじゃん……」
でざいなー「UI出来たので本画像渡します、仕様書のここの仮画像、本画像に差し替えておいて下さい」
ぷらんなー「りょーかい」
さーばーえんじにあ「……実装してから気づいたけど、仕様書通りだと、これこれこういう場合にこんな挙動になるけど、これで良いの?」
ぷらんなー「あー、これ、駄目だな、仕様から直します」
くらいあんとえんじにあ「影響はサーバーだけ?」
さーばーえんじにあ「そですね」


 要するに、実装の上で認識のズレが生じていないかの確認が出来ます。
 その認識のズレは基本的にQAチェックなどを行えばある程度はバグとして弾く事が出来ますが、QAもバグを100%弾ける訳ではありません。
 そして、こういう事をしないでQAチェックまでそのまま通って本番に反映されてしまうと、


ぷらんなー「何か変な挙動するんだけど」
さーばーえんじにあ「えっ……あー、これは、仕様です」
ぷらんなー「バグっぽいんだけど」
さーばーえんじにあ「仕様書通りの仕様です」


 みたいな事が起きたりします。確認を何度も挟んだとしても起きる可能性はありますが、チェックを複数段階で行なう事でその可能性を減らす事は出来ます。

影響範囲を纏める

 基本的に新規実装となるとQAチェックもその新規実装に対して全面的にチェックする、という簡単な構造になるのですが、既存コードへの修正はテストコードも改修して通ったとしても、ある程度はQAチェックを挟む必要があります。
 また、新規実装の為に既存コードにも手を加えた、という事に関しては仕様書への記載としてはSV側の改修点として見えない時もあります。
 なので既存コードに対して修正を入れたならば、それもきちんと纏め、QAチェックを依頼しましょう。

デバッグツールを作る

 QAが確認する際に、既存のデバッグツールでは時間が掛かる、などと言った苦情が来る可能性があります。
 余裕があったら必要そうなものを先んじて作っておいても良いでしょう(こういう事こまめにやっておくと好感度UPするんじゃないかな……)。
さーばーえんじにあ「特定の味噌カツのレシピを作れる素材を一括付与するとか、作っといたら便利かな?」

コードレビューを行なう

 第三者を招いてコードレビューを行いましょう(今更ですが、実装が複雑だったり、実装者が初心者だったりするのならば実装する前に設計レビューも設けた方がいいです、書くの忘れた……)。

 コードレビューの際には、コードを直接見せるのとは別途、追加したデータ構造などを記載したエクセルなども用意しておくと分かりやすくなります。


さーばーえんじにあ「今回は味噌カツ作りの実装を行いました。
 まずは、新規追加したモデルの構造から話します。
 マスタに関してですが、味噌カツ作りの為に新規マスタを二つ追加してあります。
 まず一つ目は、味噌カツの親マスタで……、
 次にユーザーデータは、ログが一つです、このログはユーザーの味噌カツ作成履歴を保存、更新しており……、
 ……インデックスは、ユーザーIDとレシピコードでユニークなのでそれでユニークインデックスを張ってあります……。
 ……です」
れびゅあー「マスタインポート時のチェックはどの位やってる?」
さーばーえんじにあ「指定された素材の存在確認くらいですね」
れびゅあー「explain取った?」
さーばーえんじにあ「マスタはメモ化してあるのでクエリは初回しか行われませんし、インデックス張ってある親コード単位とかでしか取ってきてないのでしてないです……。ユーザーデータもそんな感じですね……、問題無いとは思うのですが、余裕があったらやっておきます」
れびゅあー「問題ないと思うが、やっといてね」


さーばーえんじにあ「次に今回の基幹となる、指定された素材群からレシピを確定するというロジックの説明をします。
ここのロジックですが、これこれこういう風に作られるレシピを確定しています」
れびゅあー「そこ、計算量がO(n2 )で、マスタの数が大幅に増えたら計算量ヤバくならない? 改善出来ないかなぁ……」
さーばーえんじにあ「nはマスタの数ですよね、マスタそんなに今は数多くないので現状は大丈夫だと思いますが……でも改善出来そうなのでやっておきますね」
れびゅあー「わかった、その時はまたレビュー頼む」
さーばーえんじにあ「わかりましたー」
れびゅあー「あ、後、もうちょっとコメントあった方が良いと思う。コード自体に問題なくても、仕様がちょっと複雑だから」
さーばーえんじにあ「はーい」


 自分自身の観点からだと、クソコードもクソコードだと気づけない場合が良くあります。クソコードでなくとも、ぱっと見では分からない複雑さになっていたりとか。
 文章とかでもそうですが、一旦時間を置いてまた読み直してみると、前には気付けなかった欠点などにも気付けたりします。

コーディング後のチェックが完了したら

 そうして、自分のコードに対する影響や、後々に対するコスト、QA確認の為の事柄などを鑑みた上で他者からのお墨付きを貰う事で、最終的に味噌カツ作成のサーバー実装が完了となります。

 上記以外にも、バグや後々のリスクなどを防ぐ為に出来る事は色々とあると思いますが、時間も有限ですし、出来る事柄をやったとして、それら全てをやって全てに効果があるかもやや不明でしょう。
 確認行為などを行うにせよ、それをマニュアル的に行うのではなく、どういう目的の為に行うのか、それを念頭に入れて行いましょう。

より良い実装の為に(まとめ)

 サーバーに限らず、クライアントも同じ事だと思いますが、
 実装の最終目標は、仕様に沿った動くコードを作りあげ、追加/新規機能を実現する事です。しかしながら実装=コーディングではありません。
 ただ動くコードを作り上げるのではなく、後々のコストが肥大化しない為に、シンプル且つ柔軟で拡張性のあるコードを作り上げる事。認識の齟齬から後々の不具合を生み出さない事。既存部分への影響を鑑み、それに対する問題がないかの確認。そんな将来に向けた配慮が必須となります。

 上記の実装に対する例は自分が実装をしている時はこんな感じで進めているというようなものですが、これでも不完全な部分はあるでしょうし、改善出来る部分はしていきたいと思っています。
 
さーばーえんじにあ「腹減ったし、今日はソースカツ丼でも食べるか」
ぷらんなー「は?」
さーばーえんじにあ「昨日味噌カツ食べたので」
ぷらんなー「よーしよしよしよしよし」

多分終わり

味噌カツで例えるソーシャルゲームのサーバーサイドにおける新規実装の流れ 3

0

この記事はアピリッツの技術ブログ「DoRuby」から移行した記事です。情報が古い可能性がありますのでご注意ください。

もはや味噌カツではない

前回までのあらすじ

前回

以下のゲームで、

牧場経営ソーシャルゲーム:
ユーザーは牧場を経営する。また、牧場などで育てたものを調理し、食べる事が出来る。
ユーザーにはステータスが存在し、様々な行動によってステータスが変動する。ある一定のステータスを伸ばすと出来る事が増える。
ステータスは割合で決定されており、どれかのステータスを上げると他のステータスが落ちる。
一度得た能力は消えない。

味噌カツを作る事が決定しました。
提示された仕様書を読み込み、それに対して曖昧な点や改善すべき点を会話して解消した後に、サーバーエンジニアがすべき事は以下のように明確になりました。

・味噌カツを消費する事でユーザーのステータスのナゴヤ成分が上昇する、は既存のコードで対応可能か確認する
 =>変わらず
・味噌カツを作るAPIは既存の調理APIとは別として新規に作成する
 =>既存の調理APIとはかけ離れた部分が多くなった為
・味噌カツトップ画面の為のAPIを作成する
 =>クライアント側で持っていない情報を表示する必要が確定した為
 返すものは
  ・味噌カツを作成する為に必要な素材種別群

※実装する事が複雑でなくとも設計書として纏めて共有しておくとベターです。

 仮実装までの想定日数は2日と定めました。

仮実装

 実装に対して何から始めていくのかはどのような開発手法を取っているかによって違ってくるかと思いますが、ゲーム開発においての最優先事項は実機で確認出来る状態を作る、という事だと思いますので、それに沿って仮実装を行います。

さーばーえんじにあ「まあ、軽いやつからやっていくか」

味噌カツを消費する事でユーザーのステータスのナゴヤ成分が上昇する、は既存のコードで対応可能か確認する

さーばーえんじにあ「料理を消費する事でユーザーのステータスが上昇する仕組みはもうあって、料理には出来の良さの値も保存されていて、それに依存してステータスの上昇度合いが変動する仕組みもある。
 だから、それが味噌カツだろうと味噌カツじゃなかろうと、特に何もする必要はやっぱり無いな」

味噌カツトップ画面の為のAPIを作成する

さーばーえんじにあ「やるべき事は、あれ……これ、調理トップ画面もいじらないといけないか。
 味噌カツ以外にもそういう風に調理できるものが増えた場合、調理トップ画面からの遷移先が複数になるかもしれない。その複数の遷移先を判別出来る情報を調理トップ画面でも渡さなきゃいけないか。
 クライアントエンジニアさん、そんな感じで良いですか?」
くらいあんとえんじにあ「りょーかい。今回は取り敢えず遷移先1個だけだけど、レスポンスの形は、今回は配列じゃなくて良いかな?
 遷移先が複数になったらUIもそういう風に対応しなきゃいけないだろうからさ」
さーばーえんじにあ「分かりましたー」

さーばーえんじにあ「じゃ、味噌カツトップ画面では、クライアントから受け取った値を参照して、対応するマスタの情報を返す。
 レスポンスの形はクライアントエンジニアと相談する必要あるな」
くらいあんとえんじにあ「素材の種類が最大6つって決まっているなら、配列じゃなくて、それぞれ別のキーとして返して欲しい」
さーばーえんじにあ「りょーかい。現状だとそれだけだか。すぐ終わる」

味噌カツ作成のAPIを作成する

さーばーえんじにあ「えーっと、普通の調理APIをコピって、違う部分を変えていけばいいな。
 パラメータは味噌カツの素材群と味噌カツを作るという宣言が必要。
 通常の調理の時の素材存在確認とかのアサーションに加えて、本当にその素材で味噌カツが作れるかを確認。
 マスタを参照してどの味噌カツが作られるかを決定。ユーザーのステータスに加えて、そのマスタのデータを基にして味噌カツの出来の良さを決定。
 …………。
 まあ、取り敢えずそこあたりは仮実装だから適当にやっておこう。また後で必要なアサーションをリスト化したりしよう。
 レスポンスは作られた味噌カツでいいのかな?」
くらいあんとえんじにあ「そーだな、調理完了演出はそんなに普通の調理と変わらないから、多分それで事足りる」
さーばーえんじにあ「おっけ。後は必要なマスタのテーブルを作る処理とか、CSVからインポートする処理とか入れて……。
 明日には仮実装反映出来そうです」
くらいあんとえんじにあ「わかりました」
さーばーえんじにあ「APIのURLと必要なパラメータ、返されるレスポンスはこっちに書いておきました。後、調理APIでの追加パラメータはこんな感じです。これでいいですか?」
くらいあんとえんじにあ「……多分」
さーばーえんじにあ「追加で欲しいものあったら言ってください」
さーばーえんじにあ「で、後はマスタ待ちか。
 適当に作ってあるところしっかりと作ったり、テストコード書いたり、影響範囲纏めたり、そういう事しておこう」

仮マスタが来ました

ぷらんなー「マスタ、仮で出来たよ」
さーばーえんじにあ「入れてみますー。あ、ミスった、直して……ん? ちょっといいですか?」
ぷらんなー「ん?」
さーばーえんじにあ「何でも良い部分の素材アイテムコードって、0を入れるんですっけ?」
ぷらんなー「あ、それじゃまずい?」
さーばーえんじにあ「まー、どっちでも良いです。えーっと、既存コードに合わせるとして、他の部分どうだったっけ……基本的に0だけどnullの部分もある……あー、うー、えー……。0の方が多いか……。こっちが対応します」
※既存のクソコードは影響範囲が大きくなるのならばそっとしておいた方が良いかもしれません
ぷらんなー「わかった」
さーばーえんじにあ「後は問題なさそうですね」
ぷらんなー「りょうかい」
さーばーえんじにあ「APIの確認もして……あ、getだとパラメータは全部文字列か……直して……出来た、反映出来る」

仮実装反映後〜補強して本実装

実機確認

さーばーえんじにあ「開発環境に反映しました」
くらいあんとえんじにあ「りょーかい、確認しておく」
くらいあんとえんじにあ「なんかInternal Server Error起きた」
さーばーえんじにあ「えっ、確認します」
さーばーえんじにあ「あー……さっきの確認の時、この条件の時の事漏れてたな……」
さーばーえんじにあ「修正反映しました」
※仮実装でもこういう事を頻発させるとエンジニアとしての信頼度が落ちていきます
くらいあんとえんじにあ「りょーかい……大丈夫、取り敢えず後は問題無さそう」
さーばーえんじにあ「りょーかいです」
くらいあんとえんじにあ「こっちも後で確認出来る状態のもの上げておくから、上げたらそっちでも確認してみてね」
さーばーえんじにあ「わかりましたー」

仮実装から本実装へ

さーばーえんじにあ「さて、アサーションとかちょっと適当にやってた部分ちゃんとやるか。
 今回でやるべきなのは……普通の調理と比較した場合……まあ、この位だな。
 コード修正、書き加えて、その分のテストコード書いて。
 味噌カツ作りが失敗する。
  条件1:味噌カツなのに豚肉ではなく牛肉を選んだ
  条件2:味噌カツなのに味噌が無い
  条件3:味噌カツを作る為のお金が無い
  条件4:味噌カツを作る為に選んだ素材が既に存在しない
  条件5:味噌が賞味期限切れ
  ……
 味噌カツ作りが成功する。
  判定1:味噌カツを作ったので豚肉、味噌、パン粉等が消費される
  判定2:味噌カツを作ったので味噌カツが増えている
  判定3:味噌カツを作ったのでお金が減っている
  判定4:この材料で味噌カツを作ると名古屋コーチンという名の味噌カツが作られ、また、出来栄えは乱数を鑑みた上でこの範囲内に絶対に収まり、その作成情報がログとして保存される
  判定5:この材料で味噌カツを作るとマスタ内のどのレコードの条件も満たせないので、平凡な味噌カツが出来る
  判定6:味噌カツを合計1000個作るとそのアチーブメント”最早名古屋”が達成される
  判定7:神聖なる新生味噌カツ新星を作るとアチーブメント”新しい名古屋”が達成される
  ……
 ……え、単純に箇条書きしたら20件ほど必要なテストが出てきたんだけど、これ全部テストコード書くの?」
※そうです。書かないと改修が必要になった時の既存コードの保証が出来ません。
さーばーえんじにあ「ダミーユーザー作ってダミーマスタ作ってダミーユーザーデータ作って……今日の昼はコ●ダで味噌カツサンド食べよう……。
 マスタの準備さえ出来れば後は似たようなコードコピーして判定部分だけ変えていけばいいはずだから……」

数時間後

さーばーえんじにあ「あー、終わった。まあ、ちょっとバグも直せたし、これで一旦大丈夫か……じゃあ後は、影響範囲纏めて、QAに提出する確認すべき範囲も纏めて、コードレビューして、終わり、か。
 ちょっと遅くなったけどコ●ダ行こう」
くらいあんとえんじにあ「ちょっと良い?」
さーばーえんじにあ「えっ」

次で多分最後

最近人気な記事