ガチャを1から作り直した話 ─規模の拡大につれて開発速度を落とさないための取り組みについて─

34e9b225b73cd31e1cf54ade49015d24?s=47 Genta Kamitani
August 05, 2020
3.2k

ガチャを1から作り直した話 ─規模の拡大につれて開発速度を落とさないための取り組みについて─

34e9b225b73cd31e1cf54ade49015d24?s=128

Genta Kamitani

August 05, 2020
Tweet

Transcript

  1. ガチャを1から作り直した話
 開発本部 CTO室 SREグループ 神谷 元太
 BIT VALLEY 2020
 プレイベント#2


    ─規模の拡大につれて開発速度を落とさないための取り組みについて─

  2. 自己紹介
 • 神谷 元太
 • 開発本部 CTO室 SREグループ
 • モンスターストライク(以下モンスト)担当


    ◦ サーバーサイドの開発 ◦ 負荷軽減 ◦ 可視化 ◦ 自動化 ◦ etc • 趣味: コーヒーを淹れること
 ◦ 最近エスプレッソマシンを購入
  3. 目次
 • 技術的負債の解消について
 • ガチャの再設計について
 • 設計方針
 ◦ 既存のコードの問題点 ◦

    解決策 • 設計
 • 移行
 • 結果とまとめ

  4. 技術的負債の解消


  5. なぜ負債を解消するのか
 • 解消しないと長期的に見て破綻するから
 ◦ 潜在的なバグのリスク ◦ 開発速度の低下 ◦ セキュリティリスク ◦

    サポート切れ ◦ etc. • 踏み倒せなくなったから
 ◦ 何年持つかわからない場合、クローズまで耐えて「踏み倒す」選択も ▪ 流行るかわからない状態と流行った後では取るべき戦略が異なる ◦ モンストは今年で7周年 ◦ 10周年を無事に迎えることを見据えて動かなければならない
  6. 負債の種類
 • ニーズの変化による負債
 
 • 古いソフトウェアを使い続けることによる負債


  7. ニーズの変化による負債
 原因: 変化するニーズにコードが追随できていないため
 特定の機能の使われ方が時間とともに変化し、コードがその変化に追従しきれなくな る
 
 結果
 • 新機能の実装はできるものの、必要以上に複雑になる
 •

    実装自体が困難になってくる場合も
 • スケーラビリティ、開発速度、バグ発生率等に影響

  8. 古いソフトウェアを使い続けることによる負債
 原因: ソフトウェアはいつかサポートされなくなるため
 サービスを継続する以上、どこかで追従しなければいけなくなる
 追従しないことは「借金」に相当し、放置するほど「利子」が嵩んで追従が困難になる
 
 結果
 • セキュリティリスク
 •

    バグが修正されないリスク
 • 他の新しいソフトウェアに対応できなくなるリスク

  9. 直近の取り組み
 • OS, ミドルウェア等のバージョン追従
 • コア機能の再設計
 ◦ ガチャ ◦ アイテム周り

    ◦ クエスト周り • ウェブアプリケーションフレームワーク移行

  10. 直近の取り組み
 • OS, ミドルウェア等のバージョン追従
 • コア機能の再設計
 ◦ ガチャ ◦ アイテム周り

    ◦ クエスト周り • ウェブアプリケーションフレームワーク移行

  11. ガチャの再設計


  12. なぜガチャ?


  13. モンストにおけるガチャの立ち位置
 モンストにおけるガチャ
 • 売り上げの要
 • モンストをプレイする上での重要な体験の一つ
 
 
 万が一ここが壊れた場合
 •

    ユーザーに金銭的な損失
 • ユーザーからの信頼を大きく失う
 ◦ モンストだけでなく、ミクシィ全体も ◦ コラボ先やソシャゲ業界全体にも影響
  14. ガチャの現状
 度重なる改修により複雑化
 バグると大事なので、大胆な変更ができない
 ◦ 「そのときの変更量が最小になるように」の積み重ねでカオス化 ◦ 大胆なリファクタリングも行われない 
 結果
 ◦

    コードパス上は存在するが、意図して作られたのかよくわからない機能 ◦ あらゆる場所にガチャの種類に関するif文が散らばる ▪ ある種類のガチャの挙動を追うのに、ガチャ周りのコードを全部読む必要がある ◦ ある種類のガチャの挙動の変更が、他の種類のガチャにも影響しうる
  15. モンストのガチャについて
 本来はシンプルなしくみ
 重み付きのランダム抽選
 
 
 ではなぜ複雑化したのか?
 
 
 ガチャの種類が増えすぎたから
 def

    gacha items = [ {value: "はずれ", weight: 10}, {value: "あたり", weight: 1}, ] weight_sum = items.sum do |x| x[:weight] end r = Random.rand(weight_sum) items.each do |item| r -= item[:weight] return item[:value] if r < 0 end end
  16. モンストのガチャの種類
 再設計を始めた時点で12種類の抽選ロジックが存在
 ◦ 普通のガチャ ◦ ホシ玉 (レアなキャラだけが出る) ◦ 初ゲ確定ガチャ(持ってないキャラだけが出る) ◦

    アゲインガチャ(10連 × N回引けて、Nに応じて確定枠が出現) ◦ etc... この時点まで明確な設計方針や、責務の分割などは無かった
 
 
 カオスな状態に

  17. ガチャのフローチャート(っぽいもの)
 = ガチャの種類に  よる分岐

  18. ガチャの種類の増加
 前述したとおり、大胆な変更が行えなかった
 
 結果的に以下のような問題が発生
 ◦ 似たような機能が多いのに、微妙に抽象化しきれてない ◦ 至るところにif文が乱立 ◦ どこまでが仕様でどこまでが偶然生まれた産物なのかよくわからない

    
 
 このままでは破綻する!!

  19. 設計方針


  20. 既存のコードの問題
 • 排出候補がどうやって決まるのかわからない
 • 既存のコードの変更がどこに影響するかわからない
 • インターフェースが統一されてない


  21. 排出候補がどうやって決まるのかわからない
 • 特定のガチャの抽選ロジックだけ知りたい場合でも、
 抽選に関するコードをすべて読まないといけない
 • 至るところにif文が乱立
 ◦ 分岐の粒度もバラバラ • 抽選ロジックを決める部分と抽選ロジック本体が一つに


    ◦ DBレコードやリクエストパラメーターと抽選ロジックが密結合 ◦ 抽選ロジック自体を簡潔に表現できない
  22. 既存のコードの変更がどこに影響するかわからない
 • 12種類の抽選ロジックの全てが同じクラス、同じメソッドに
 • あるif文の中身を変更すると、何種類かの抽選ロジックに影響する
 ◦ 他の抽選ロジックへの影響を抑えるには注意が必要 ◦ そもそもどのガチャで使ってるif文なのかよくわからない •

    コードパス上は存在するが仕様としては存在しない抽選ロジックが誕生
 ◦ 後から見て、その抽選ロジックを保守すべきかどうかわからない
  23. インターフェースが統一されてない
 • 似たような機能が多いが、微妙に抽象化しきれていない
 • 抽選ロジックを単体でテストするのが困難
 ◦ 明確なインターフェースがないため ◦ 抽選ロジックを動かすにはガチャのDBレコードを作るところから始まる

  24. 解決策を練るにあたっての方針
 「よいコード」のための考え方や方法論は、ある程度整備されている
 
 「hogehogeアーキテクチャ」みたいな話は出てこない
 今回はビジネスロジック「しか」出てこず、ロジックのスコープも狭いため
 
 根本にある原則に忠実に
 原則に忠実にしようと設計したわけではなく、問題を解決しようとしたら
 勝手に原則に忠実になった


  25. 原理・原則
 今回紹介する原理・原則
 ◦ Program Intently and Expressively ◦ Open-Closed Principle

    ◦ 関心の分離 ◦ ポリシーと実装の分離 ◦ インターフェースと実装の分離 参考文献: 『プリンシプル オブ プログラミング 3年目までに身につけたい 一生役立つ 101の原理原則』
 ◦ よく使われる原則や考え方がコンパクトにまとまっている ◦ 読みながら方針を立てたわけではないが、説明するのに便利
  26. Program Intently and Expressively (PIE)
 意図を表現できるようなコードにする
 • どう動くか
 • 何をするか


    • 何をしないか
 
 具体的に何をすべきか
 • 適切な変数名
 • 関心事に応じた責務の分離
 • シンプルなインターフェース
 参考:『プリンシプルオブプログラミング』 p.43
  27. PIE: ガチャの場合
 ガチャにおける主要な関心事を表現できるインターフェース
 • 何個の出玉が排出される?
 • K回目の排出候補はどのように決まる?
 
 解決できる問題
 •

    特定のガチャの抽選ロジックだけ知りたい場合でも、
 抽選に関するコードをすべて読まないといけない

  28. Open-Closed Principle (OCP)
 コードは「拡張に対して閉じ」、「修正に対して開いて」いなければならない
 
 拡張に対して閉じている状態:
 コードに新たなふるまいを追加することができる
 修正に対して開いている状態:
 コードを変更しても、他の部分に影響を与えない
 


    同じようなもののバリエーションがたくさんある場合、特に重要
 参考:『プリンシプルオブプログラミング』 p.53
  29. OCP: ガチャの場合
 抽選ロジックにおけるOCP:
 新たな抽選ロジックの作成
 抽選ロジックに対応したクラスを追加する
 既存の抽選ロジックの変更
 抽選ロジックに対応したクラスだけに修正を加える
 
 解決できる問題
 •

    至るところにif文が乱立
 • あるif文の中身を変更すると、何種類かの抽選ロジックに影響する
 • コードパス上は存在するが仕様としては存在しない抽選ロジックが誕生

  30. 関心の分離
 別な関心事( = 機能 or 目的)に関係するコードは、別な場所に置く
 • 別な機能は別な場所に
 • ビジネスロジックとDB操作は別な場所に


    • ビジネスロジックとリクエストパラメーターの処理は別な場所に
 
 分離することによるメリット
 • やりたいことに対応するコードが見つけやすくなる
 • 変更が関係ない部分に及びにくくなる
 • テストもしやすい
 参考:『プリンシプルオブプログラミング』 p.89
  31. 関心の分離: ガチャの場合
 ガチャの場合の関心事
 • 抽選ロジック
 • 抽選ロジックを決める部分
 • 排出候補をどうやって取得するか
 •

    出玉をどう表示するか
 • etc...
 
 解決できる問題
 • 抽選ロジックを決める部分と抽選ロジック本体が一つに
 

  32. ポリシーと実装の分離
 コアになるロジックと、ソフトウェアの前提を分離
 
 分離されていない例:
 • adminユーザーは、全ての情報を閲覧できる
 分離されている例:
 • ユーザーは、情報に関するアクセス権を持っている
 •

    ユーザーは、アクセス権に応じた情報のみ参照できる
 • あるユーザーをadminに指定すると、そのユーザーには全ての情報の閲覧権 限が付加される
 参考:『プリンシプルオブプログラミング』 p.93
  33. ポリシーと実装の分離: ガチャの場合
 ガチャの場合のポリシー
 • あるガチャは、「持っていないキャラだけが排出されるガチャ」である
 • あるガチャは、「レアなキャラだけが排出されるガチャ」である
 ガチャの場合の実装
 • 「持っていないガチャだけが排出されるガチャ」の抽選ロジックは、


    具体的に……である
 • 「レアなキャラだけが排出されるガチャ」の抽選ロジックは、
 具体的に……である
 
 解決できる問題
 • 抽選ロジックを決める部分と抽選ロジック本体が一つに

  34. インターフェースと実装の分離
 「どのように使われるか」と「具体的な実装」を分離する
 
 これによるメリット:
 • モジュール等を使う側が、実装の詳細を知る必要がなくなる
 • 実装の都合に応じて、モジュール等を使う側がコードを変更しなくて
 よくなる
 参考:『プリンシプルオブプログラミング』

    p.93
  35. インターフェースと実装の分離: ガチャの場合
 ガチャの場合のインターフェース
 • 抽選ロジック
 
 ガチャの場合の実装
 • 個々のガチャの種類ごとの抽選ロジックの実装
 


    解決できる問題
 • 12種類の抽選ロジックの全てが同じクラス、同じメソッドに
 • 似たような機能が多いが、微妙に抽象化しきれていない
 • 抽選ロジックを単体でテストするのが困難

  36. 重要視しなかった原則
 DRY (Don’t Repeat Yourself)
 同じコードを何度も書いてはいけない
 
 重要視しなかった理由:
 • OCPや関心の分離等と相反する部分は、前者を優先


    • 「見た目が同じ」からといって「変更の理由が同じ」とは限らない
 
 再設計の結果、コードは冗長になり、行数は増えた
 ※ インターフェースの統一により逆にDRYになった部分もある
 参考:『プリンシプルオブプログラミング』 p.34
  37. 設計


  38. 新抽選ロジック
 大まかに次のように分離された
 • 排出候補を決めるステートマシン
 • 表示上の排出順を決める部分
 • 抽選ロジックのFactory
 • 抽選全体を行うFacade


    
 モンスト固有の知識になってしまうため、詳細は省略

  39. 構成図(before)
 = ガチャの種類に  よる分岐

  40. 構成図(after)
 Class Module (Interface) 依存 実装 (呼び出し元) 抽選FacadeのFactory 抽選ステートマシン 排出結果の表示方法

    抽選Facade 個々の 抽選ステートマシン 個々の排出結果の 表示方法 具体的な 抽選Facade
  41. 移行


  42. 移行方針を立てるにあたって
 前述の通り「バグると大事なので、大胆な変更ができない」
 とはいえ、今後のために大胆な変更を行わないといけない
 
 
 
 変更の影響をできる限り小さくする必要がある


  43. 移行方針
 小さな変更をインクリメンタルに行う
 ◦ 小さい変更なら、コーナーケースの見落としの可能性は低い ◦ バグった場合の影響も小さく、影響範囲の予想も容易 常に動き続ける状態を保つ
 ◦ 先にインターフェースだけ定義 ▪

    中身は既存の実装の単なるラッパー ▪ この時点で新インターフェース経由でしかガチャは実行されないようにする ◦ 一つ一つ中身のロジックを移行 ▪ ガチャの種類ごとの仕様をテストに落とし込む ▪ インターフェースはそのままに、中身のロジックを書き換える ▪ テストが常に通る状態のまま、新しい実装に移行 仕様なのか曖昧な部分
 ◦ 都度企画サイドと確認を取りつつ、仕様も整理
  44. 結果


  45. 結果
 リリース後から現在まで、このリファクタリングによるバグは出ていない
 
 抽選ロジックの追加や変更が格段にシンプルに
 ◦ 抽選ロジックの追加 → 新たな抽選ロジックに対応するクラスを作成 ◦ 抽選ロジックの変更

    → 既存の抽選ロジックに対応するクラスの修正 
 その後も、抽選ロジックは数ヶ月に一つのペースで増え続けている
 ◦ 再設計を行っていなければ、さらに変更の困難なコードになっていた可能性が 高い
  46. まとめ
 • モンストの重要な機能の一つであるガチャを再設計した
 • 地味ではあるけど重要な考え方
 ◦ 問題に対する深い理解 ◦ 根底にある原理原則に充実に •

    いかにして変化を恐れない状態を作るか
 ◦ 気持ちだけではだめ ◦ リスクを最小化しつつ変化を続ける • 解消しないといけない負債はまだまだある
 ◦ 今適切だと思っているものも、将来的に負債になる可能性はある ◦ その時点での最適な手段を適用し続ける戦い
  47. Further Reading
 • 『プリンシプル オブ プログラミング 3年目までに身につけたい 一生役立つ101の 原理原則』上田勲 著,

    秀和システム (2016) • 『リーダブルコード: より良いコードを書くためのシンプルで実践的なテクニック』 ダ スティン・ボズウェル、 トレバー・フーシェ 著, 角 征典 訳, オライリー・ジャパン (2012) • 『Software Architecture is Overrated, Clear and Simple Design is Underrated』 https://blog.pragmaticengineer.com/software-architecture-is-overrated/amp/
  48. None