似ているけどちょっと違うものたちをモデリングする技術
似ているけどちょっと違うものたちをモデリングする技術
2019/11/3 YAPC::Nagoya::Tiny 2019
株式会社はてな hitode909
自己紹介
株式会社はてな アプリケーションエンジニア hitode909
以前はブログチーム
2017年からマンガチームで今に至る
京都から来ました
会場にはてな社員x4、アルバイトx1が京都から来ている
PerlとTypeScriptが好き
2018年 PerlでISUCONに出る→決勝出場
2019年 Node.JSで出る→予選敗退
最近はブラウザでVJ活動している
ブラウザでVJした - hitode909の日記
(ここまで1分)
発表の前に宣伝
VSCodeでPerlを書いてるみなさまに朗報、今朝formatOnTypeできるようになりました
いますぐ "editor.formatOnType": true して perltidy-moreをインストールしましょう
Support formatOnType by hitode909 · Pull Request #8 · kak-tus/perltidy-more
perltidy-more - Visual Studio Marketplace
https://gyazo.com/341ff5730e743b7891dae1be8aeb9bcc
似ているけどちょっと違うものたちをモデリングする技術
第一部 似ているけどちょっと違うものたちの紹介
第二部 似ているけどちょっと違うものたちに立ち向かうための設計
第三部 似ているけどちょっと違うものたちにどう向き合っていくか
第四部 現状困っていることを紹介して会場の皆様からアドバイスをいただく
第一部 似ているけどちょっと違うものたちの紹介
はてなのマンガチーム、GigaViewerについて
チームのミッション「紙の雑誌に代わるWEBマンガサイトを用意することで漫画文化を守り育てていくこと」
2017年に1サイト目をリリース、現在、7社、9サイトにビューワを提供している
https://hatenacorp.jp/press/release/search?q=マンガビューワ
似ているけどちょっと違うものたちの紹介
サイトをざっと並べて差分を見比べる
見せ方
毎日更新 https://shonenjumpplus.com/
週毎の更新 https://kuragebunch.com/
作品を主体としたデザイン https://comicborder.com/
五十音順 https://comic-days.com/
ビューワー
無料で読めるエピソード https://comic-zenon.com/episode/10834108156688950516
オフラインで読む(β) https://tonarinoyj.jp/episode/13932016480028985383
単行本 https://comic-days.com/volume/10834108156646044419
雑誌 https://comic-days.com/magazine/10834108156684697726
ユーザー登録、課金
定期購読 https://comic-days.com/premium
レンタル機能 https://shonenjumpplus.com/article/rental
(ここまで3分)
アプリケーション全体の要件
トップページや作品紹介ページなど
サイトごとに異なる
作品が並ぶ、お知らせバナーが表示されるなど、似たような機能は存在する
ビューワページ
全サイト共通だが、機能があったりなかったりする
新機能を特定のサイトに追加し、好評なら横展開するなど
管理画面
使える機能だけの管理機能が並んでいる
マンガの取り込み機能
協力会社さんのAPIと連携してデータを引っ張ってくる場合や、はてなの提供する管理画面を使ってデータを入れてもらう場合がある
サイトによって、エピソードに対応する価格があったり、無料配信期間だけを取り込んだり
現状の構成や、検討したけど採用しなかった設計の紹介
現状の構成
Web アプリケーションをマルチテナント型 SaaS ソリューションに変換する の③
https://gyazo.com/f33c52a1a2deac6df652eb6b4807b861
共通のコードベース
デプロイしているコードは共通で、リクエスト時のHostヘッダに応じてリクエスト先のサイトを切り替える
アプリケーションはPerl5Perlで実装している
一度のデプロイで全サイトにデプロイする
逆に、不具合が起きると全サイトに影響が及ぶ
共通のデータベース(MySQL)
1テーブル内にいろんなサイトの情報を保存している
media_id(サイトごとに割り当てているid)をつかって区別する
共通のRedis
キーの一部にmedia_idを入れて区別
思い思いのキーを発行するのではなく、アプリケーション内すべてのキーの生成を1クラスに集約して、重複が起きないようにしている
(ここまで5分)
検討したけど採用しなかった設計
ビューワ vs 各サイト のマイクロサービス
Core=共通のビューワ, Media = トップページや連載一覧ページなど各サイトごとにちがう
なぜ採用しなかったか
ビューワページはすべて共通、ではなく、ヘッダ・フッタはサイトごとに出し分けたい、といった要件
1サイト1アプリケーションでどんどん増えていくことへの懸念
サイトごとのデータベースの分割
データベースのマイグレーションのタイミングとアプリケーションのマイグレーションのタイミングを合わせる運用の難易度
かわりに、1アプリケーション内でメディアごとの差異をうまく分けるような作戦を採用した
リクエストのHostヘッダをもとに対象メディアを決定
対象メディアのMediaインスタンスを作る
Mediaインスタンスからmedia.idを得てデータの区別に使う
(ここまで7分)
(水を飲みましょう)
第二部 似ているけどちょっと違うものたちに立ち向かうための設計
コードベースが線形に増えないための工夫
値置き場を定義ファイルに追い出す
ネームスペース
フィーチャートグル
コードベースが線形に増えないような工夫をする
1メディア→10メディアと増えるときにコードベースが10倍の規模で増えてはいけない
だんだん収束し、メディア間の差異だけを実装すればよい形になっているべき
列挙を避け、ルールベースで共通化する
1サイト→2サイト目への展開のときには、べたっと書いてしまいがち
code:_.pm
config production => {
'root-uri.jump_plus' => 'https://shonenjumpplus.com',
};
config staging => {
'root-uri.jump_plus' => 'https://giga-jump-plus.example.com',
};
config local => {
'root-uri.jump_plus' => 'http://giga-jump-plus.localhost:3000',
};
2サイト目
この調子で、9サイト、6環境まで増殖し、新サイトを立ち上げるときの最初の開発が設定を真似して書き続けることになっていた
code:_.pm
config production => {
'root-uri.jump_plus' => 'https://shonenjumpplus.com',
'root-uri.tonarino_yj' => 'https://tonarinoyj.jp/',
};
config staging => {
'root-uri.jump_plus' => 'https://giga-jump-plus.example.com',
'root-uri.tonarino_yj' => 'https://giga-tonarino-yj.example.com',
};
config local => {
'root-uri.jump_plus' => 'http://giga-jump-plus.localhost:3000',
'root-uri.tonarino_yj' => 'http://giga-tonarino-yj.localhost:3000',
};
ルーティング時に、全メディアのリストを使ってルールベースで展開するようにした
productionでのURLはコードに書き、それ以外の環境は環境はmedia.nameを使って機械的に生成する
そのかわり、全メディア分のテストをループで回して壊れていないことを確認
code:_.pm
config staging => {
'root-uri.pattern' => 'https://giga-%s.example.com',
};
config local => {
'root-uri.pattern' => 'http://giga-%s.localhost:3000',
};
(ここまで8分)
値置き場を定義ファイルに追い出す
OGPを生成するクラス、Twitter用のmetaタグを出力するクラス
似たような文字列の生成を手で実装するとたいへん
sub page_title { "$page_name - $site_name" } みたいな実装を持つクラスがメディア数分用意されていた
新メディアを立ち上げるときにはOGPクラス、Metaタグクラス、Twitterクラスなど数クラスと、そのテストをコピペベースで実装
各種文字列を書いたYAMLファイルを読み込んで生成するかたちにリファクタリング中
YAMLに対してJSON SchemaでバリデーションできるVSCode拡張を利用
https://marketplace.visualstudio.com/items?itemName=redhat.vscode-yaml
サイトごとの差異を前提としたネームスペース
共通のものか、個別のサイトのものなのか
特定のメディアでしか使わないコードは、メディア名をネームスペースに入れる
特定のメディアで求められる売上レポート Giga::Batch::Media::Comicdays::ContentSalesReport
メディア名以下は共通の都合にとらわれず自由に実装できる
Controllerにもサイトごとなのか共通部分なのかを明記する
全メディア共通のビューワページ Giga::Web::Core::Viewer
サイトごとに異なるトップページ Giga::Web::Media::Comicdays::Top
(ここまで10分)
(ここで折り返しなので水を飲む)
機能に着目したネームスペース
すべてのサイトに共通の機能と、サイトごとにあったりなかったりする機能がある
サイト(Media)が複数の機能(Feature)を持つという形でモデリングすることにした
Core
作家、作品、エピソード、など、マンガビューワなら必ずあるような概念
Feature
更新のあった作品、ユーザーアカウント、課金機能、など、メディアによってあったりなかったりする概念
Giga::Feature::以下にさまざまな機能を置いていく
現在30機能、全体の20%のクラスがFeature/以下に納まっている
課金機能ならGiga::Repository::Product ではなく Giga::Feature::Payment::Product::Repository
https://gyazo.com/04f054cb699b8d4629eb5f85c4af5611
has_feature
5サイト目くらいでの議論
どのメディアが何の機能を使っているかを調べるのが難しく、Featureへのメソッド呼び出しがあるかを追うしかなかった
有効な機能のリスト(feature_names)をMediaインスタンスの情報として持たせることにした
https://gyazo.com/e32ce12c511ecbf9bf4e779399b6288a
$media->has_feature(機能名)という形で、サイトに対して機能が有効かを問い合わせできる
if ($media->has_feature('UserAccount') { このメディアにはユーザーアカウントの機能がある }
if ($media->has_feature('Shop') { このメディアでは読み物を購入可能 }
has_featureをありとあらゆるレイヤで利用しているので紹介
初級〜上級
code:_.plantuml
@startuml
class Media {
id
name
feature_names
+ has_feature(feature_name)
}
@enduml
初級編: has_featureを画面の出し分けに利用する
media->has_featureメソッドを使ってViewの要素を出し分ける
ビューワーページのような全メディア共通の画面で有用
ビューワのこの部分にボタンを出すかどうか、など
中級編: has_featureをルーティングに利用する
このようなcontrollerが増えてきたことに気づく
code:_.pm
sub user_account_detail {
return $c->error(400) unless $c->media->has_feature('UserAccount');
...;
}
ルーターから得たルーティング先のメソッドを呼び出す前に、has_featureをチェックし、未対応なメディアならエラーのレスポンスを返す
code:_.pm
GET '/user_account/:user_account_id' => 'Giga::Admin::UserAccount#user_account_detail', { has_feature => 'UserAccount };
上級編: has_featureをメソッド呼び出し時の権限チェックに利用する
どのメディアがどのFeatureのメソッドを呼び出しているかを制御できなくなってきたので、このような仕組みを導入した
「このクラスはこの機能を持ったメディアのみ呼び出して良い」という宣言をする仕組み
ユーザーログインの存在しないはずのメディアに紐づくユーザーのデータを勝手に作ってしまうことがないように
code:Giga::Feature::UserAccount::Service.pm
package Giga::Feature::UserAccount::Service;
use Giga::HasFeature q(UserAccount);
sub get_signup_credential_data {
args my $class => 'ClassName',
my $media => 'Giga::Media',
my $secret_token => 'Str',
;
...
}
メソッド呼び出し時に、UserAccount featureを持ったmediaが渡ってこなければ実行時に例外を出す
導入にあたっては1日2機能ずつくらい手分けして対応
実装にあたっては、以下の3モジュールを利用している
機能実装側
code:_.pm
# on_scope_end を使って
use B::Hooks::EndOfScope;
# 公開中の関数全てに対して
use Module::Functions ();
# Class::Method::Modifiers::install_modifier を使って引数のhas_featureをチェックする
use Class::Method::Modifiers ();
便利グッズ(今回最大のPerlコードのコーナー)
code:Giga::HasFeature.pm
sub import {
my ($class, @feature_names) = @_;
my $pkg = caller;
on_scope_end {
my $all_functions = [ grep { /\Aa-z/ } Module::Functions::get_public_functions($pkg) ];
Class::Method::Modifiers::install_modifier($pkg, 'before', $all_functions, sub {
my ($self, %args) = shift;
my @missing = grep { !$media->has_feature($_) } @feature_names;
Carp::confess "Feature " . join(', ', @missing) . " is not allowed for @{ $media->name }" if @missing;
});
}
これによって、渡すメディアによって、実行時に以下のメソッド呼び出しが成功したり、例外が発生したりする
code:_.pl
my $data = Giga::Feature::UserAccount::Service->get_signup_credential_data(media => $media, token => ...);
難しいところかつ全員が使うことになるので、モブプロで完成させて、コードレビュー無しでCIが通ったらマージした
(ここまで13分)
GigaViewerにおけるFeatureとFeature Togglesとの関係性
Featureという名前で、オンオフがある、ということでFeature Togglesとの関係を考えておく
Feature Toggles (aka Feature Flags)
特定のメディアにだけプレミアムな機能を提供するという意味ではPermissioning Togglesと近い
アプリケーション中すべての箇所でFeature Togglesを活用するアーキテクチャだと言える
Feature Toggles
Release Toggles
開発中の機能の切り替え
Experiment Toggle
ユーザーごとに別の体験を提供する A/Bテスト
Ops Toggle
パフォーマンス的な懸念があるときに徐々にロールアウトする、すぐに戻すため
Permissioning Toggles
有料会員向けのプレミアム機能
新機能のcanary release
Giga::FeatureをRelease Togglesとして使う
media->wip_featuresで開発中のメディアをモデリングする
リクエストをもとに $media->enable_featureすると、1リクエスト中だけ開発中のFeatureが有効になる
リリース時にはwip_features_namesからfeature_namesに移動すると常時有効になる
開発環境でのテストに便利
https://gyazo.com/411080ade4ba5cf0d664bbe36c31e6bf
code:_.plantuml
@startuml
class Media {
id
name
feature_names
wip_feature_names
+ has_feature(feature_name)
+ enable_feature(feature_name)
}
@enduml
現状分析
Mediaクラスやhas_featureメソッド自体はたいしたことをしていない、素朴な仕組み
一方で、Mediaクラスやhas_featureを使って実現することは、凝ったことや難しいことをしている
メタプログラミングしまくっている
実装は難しく、何が起きてるか一見するとわからない
普段書く機能の実装は簡単(Easy)になる
使い所を見極めて適用し、皆が存在を知っている状態にする
一方で、単なるControllerとかModelでは難しいことはせず、Simpleに書いている
(ここまで15分)
(水を飲みましょう)
第三部 似ているけどちょっと違うものたちにどう向き合っていくか
実装上の方針は紹介したのでここから精神的な話
ドキュメントや、知識伝達について
ドキュメントの用意
年間数サイトのスピードで新サイトをローンチしているので、ドキュメントを用意することが重要
以前ははてなグループやGoogle Docsにまとめていたが、手早く書ける点や、ページ間リンクのはりやすさを重視して、現在はScrapboxを利用している
エンタープライズ版でクライアント情報もセキュアに管理。株式会社はてなのScrapbox利用術 - Scrapbox - Medium
差異のモデリングのためのパターン集をドキュメント化する
「メディア固有の拡張処理」というページに、現在採用している設計上のパターンをまとめている
議論なども含めて書いている。いくつか紹介
具体的な実現方法についてのパターン
https://gyazo.com/20ef008b45ef894e4147fda0b2a6d78e
抽象的なパターン
https://gyazo.com/78fe0971aa1907bce690fbb77ba68261
「パターン、Wiki、XP」や「組織パターン」に影響を受けていて、パターンを集めるのが好き
https://gyazo.com/e66f28a8a020650a86f1e2ce32f6f4ac https://gyazo.com/58abd70ec8399ed453ea126d357d7df4
ストレングスファインダーをやって「収集心」が出た人におすすめ
パターンが集まり、ネットワークができてくると、だんだん気持ちよくなってくる
コンテキストマップを書く会
「コンテキストマップを書く会」を開催して、Google Presentationを使ってソフトウェア設計の図を描いていた
ドメイン駆動設計からきた名前だけど、実態は、現状認識を図示する会
コードベースが急に大きくなった段階で人によってイメージがずれていたのでやってみた
何枚か様子を紹介
式次第
https://gyazo.com/7ec2e484dada0af702e5905dd303f0dd
https://gyazo.com/40bb68932326f9a10c9185ba67199fb0
https://gyazo.com/90314808c154fac70c4c8cb75f98695e
冒頭で紹介したコアとメディアについての図もこの会で描かれたもの
89b7
敷居を下げるためにあえて雑な図を置いてみたりしている
https://gyazo.com/d9073478e5c05361e60bc97149f07201
開発環境にnode_modulesが3つあって混乱してお絵かきしたときの図
https://gyazo.com/7ef48276880fe989c808de68f0451c55
みなでお絵かきすると、コードと、コードを読み解いた結果のメンタルモデルを揃えていける
コードの形に合わせて認識を揃えたり、理想の形に合わせてコードの設計を変えたり
東京・京都の2拠点でお絵かきするために最近Google Jamboardがオフィスに導入された
でかすぎてオフィスの模様替え計画が変更になった
https://gyazo.com/0cce6a31dfbb4f9050424484e697ac3e https://gyazo.com/490321149881ba5fb024fb3cce569cc9
https://gsuite.google.com/intl/ja/products/jamboard/
(ここまでで18分)
第四部 現状困っていることを紹介して会場の皆様からアドバイスをいただく
懇親会で話したい
表示まで考えたデータモデリング
「エピソードには無料なものや、価格と紐付いているものがある」みたいな分析はできている
「今日無料になったエピソードを集めて出す」のような要件は各サイト微妙に違う
データストアやリポジトリの共通化を優先して、controllerから大量にレコードを集めてから絞り込むようなコードを書きがち
データの保存時のことは考えられているが、表示するときに効率の悪いクエリが発行されている
現状のアーキテクチャでどこがボトルネックになり、そのときにどうするかという道筋
急に設計を変えるのは難しい。徐々にやる必要があるが、ボトルネックがどこになるかがまだ見えていない
現状理解のためにコーディング規約を整備したり、それをふまえて、どこの改善に手を付けるべきか、という計画を立てようとしているところ
常に並行に開発ラインが走っているので、チーム感を醸成しにくい
新サイトの開発や特定のサイトの開発をしていると、仕事が細分化していき、全体感が見えなくなる
触るコードが分かれているため、チーム一丸となって何か大きなことをするという意識に向きにくい
ドキュメンテーションやお絵描き会などでカバーしようとしている
(残り30秒くらい)
まとめ
はてなで開発しているギガビューワーにおける事例を紹介しました
似ているけどちょっと違うものたちに対処している事例
似ているけどちょっと違うものたちがある前提での暮らし
振り返ると
アプリケーションの要件に合わせた構造を作る
チーム内で設計に関する認識を揃えたり議論したりすることが重要
さまざまなサイトの差分を吸収するためには、裏側やフレームワーク部分でちょっと複雑なことをすることも許容する
質問コーナー
フィーチャーの設定を間違って出して事故ったことは
いまのところない
フェーチャーというのはロールなのではないか
今は単なるクラスメソッドの呼び出しになっています
フィーチャーの有無の組み合わせが爆発しないか
フィーチャーのセットに名前をつけて管理しないと爆発しそう
難しくないですか
難しくて、必要に応じて変えていっている、モブプロで書いたりしています
採用情報
はてな マンガチーム - Qiita Jobs