似ているけどちょっと違うものたちをモデリングする技術 似ているけどちょっと違うものたちをモデリングする技術 2019/11/3 YAPC::Nagoya::Tiny 2019 株式会社はてな アプリケーションエンジニア hitode909 会場にはてな社員x4、アルバイトx1が京都から来ている 2018年 PerlでISUCONに出る→決勝出場 VSCodeでPerlを書いてるみなさまに朗報、今朝formatOnTypeできるようになりました いますぐ "editor.formatOnType": true
して perltidy-moreをインストールしましょう 似ているけどちょっと違うものたちをモデリングする技術 第二部 似ているけどちょっと違うものたちに立ち向かうための設計 第三部 似ているけどちょっと違うものたちにどう向き合っていくか 第四部 現状困っていることを紹介して会場の皆様からアドバイスをいただく はてなのマンガチーム、GigaViewerについて チームのミッション「紙の雑誌に代わるWEBマンガサイトを用意することで漫画文化を守り育てていくこと」 2017年に1サイト目をリリース、現在、7社、9サイトにビューワを提供している 作品が並ぶ、お知らせバナーが表示されるなど、似たような機能は存在する 新機能を特定のサイトに追加し、好評なら横展開するなど 協力会社さんのAPIと連携してデータを引っ張ってくる場合や、はてなの提供する管理画面を使ってデータを入れてもらう場合がある サイトによって、エピソードに対応する価格があったり、無料配信期間だけを取り込んだり 現状の構成や、検討したけど採用しなかった設計の紹介 デプロイしているコードは共通で、リクエスト時のHostヘッダに応じてリクエスト先のサイトを切り替える アプリケーションはPerl5Perlで実装している media_id(サイトごとに割り当てているid)をつかって区別する 思い思いのキーを発行するのではなく、アプリケーション内すべてのキーの生成を1クラスに集約して、重複が起きないようにしている Core=共通のビューワ, Media = トップページや連載一覧ページなど各サイトごとにちがう ビューワページはすべて共通、ではなく、ヘッダ・フッタはサイトごとに出し分けたい、といった要件 1サイト1アプリケーションでどんどん増えていくことへの懸念 データベースのマイグレーションのタイミングとアプリケーションのマイグレーションのタイミングを合わせる運用の難易度 かわりに、1アプリケーション内でメディアごとの差異をうまく分けるような作戦を採用した リクエストのHostヘッダをもとに対象メディアを決定 Mediaインスタンスからmedia.idを得てデータの区別に使う 第二部 似ているけどちょっと違うものたちに立ち向かうための設計 1メディア→10メディアと増えるときにコードベースが10倍の規模で増えてはいけない だんだん収束し、メディア間の差異だけを実装すればよい形になっているべき 1サイト→2サイト目への展開のときには、べたっと書いてしまいがち 'root-uri.jump_plus' => 'https://shonenjumpplus.com',
'root-uri.jump_plus' => 'https://giga-jump-plus.example.com',
'root-uri.jump_plus' => 'http://giga-jump-plus.localhost:3000',
この調子で、9サイト、6環境まで増殖し、新サイトを立ち上げるときの最初の開発が設定を真似して書き続けることになっていた 'root-uri.jump_plus' => 'https://shonenjumpplus.com',
'root-uri.tonarino_yj' => 'https://tonarinoyj.jp/',
'root-uri.jump_plus' => 'https://giga-jump-plus.example.com',
'root-uri.tonarino_yj' => 'https://giga-tonarino-yj.example.com',
'root-uri.jump_plus' => 'http://giga-jump-plus.localhost:3000',
'root-uri.tonarino_yj' => 'http://giga-tonarino-yj.localhost:3000',
ルーティング時に、全メディアのリストを使ってルールベースで展開するようにした productionでのURLはコードに書き、それ以外の環境は環境は media.name
を使って機械的に生成する そのかわり、全メディア分のテストをループで回して壊れていないことを確認 'root-uri.pattern' => 'https://giga-%s.example.com',
'root-uri.pattern' => 'http://giga-%s.localhost:3000',
OGPを生成するクラス、Twitter用のmetaタグを出力するクラス sub page_title { "$page_name - $site_name" }
みたいな実装を持つクラスがメディア数分用意されていた 新メディアを立ち上げるときにはOGPクラス、Metaタグクラス、Twitterクラスなど数クラスと、そのテストをコピペベースで実装 各種文字列を書いたYAMLファイルを読み込んで生成するかたちにリファクタリング中 YAMLに対してJSON SchemaでバリデーションできるVSCode拡張を利用 特定のメディアでしか使わないコードは、メディア名をネームスペースに入れる 特定のメディアで求められる売上レポート Giga::Batch::Media::Comicdays::ContentSalesReport
メディア名以下は共通の都合にとらわれず自由に実装できる Controllerにもサイトごとなのか共通部分なのかを明記する 全メディア共通のビューワページ Giga::Web::Core::Viewer
サイトごとに異なるトップページ Giga::Web::Media::Comicdays::Top
すべてのサイトに共通の機能と、サイトごとにあったりなかったりする機能がある サイト(Media)が複数の機能(Feature)を持つという形でモデリングすることにした 作家、作品、エピソード、など、マンガビューワなら必ずあるような概念 更新のあった作品、ユーザーアカウント、課金機能、など、メディアによってあったりなかったりする概念 Giga::Feature::以下にさまざまな機能を置いていく 現在30機能、全体の20%のクラスがFeature/以下に納まっている 課金機能なら Giga::Repository::Product
ではなく Giga::Feature::Payment::Product::Repository
どのメディアが何の機能を使っているかを調べるのが難しく、Featureへのメソッド呼び出しがあるかを追うしかなかった 有効な機能のリスト(feature_names)をMediaインスタンスの情報として持たせることにした $media->has_feature(機能名)
という形で、サイトに対して機能が有効かを問い合わせできる if ($media->has_feature('UserAccount') { このメディアにはユーザーアカウントの機能がある }
if ($media->has_feature('Shop') { このメディアでは読み物を購入可能 }
has_featureをありとあらゆるレイヤで利用しているので紹介 + has_feature(feature_name)
初級編: has_featureを画面の出し分けに利用する media->has_featureメソッドを使ってViewの要素を出し分ける ビューワーページのような全メディア共通の画面で有用 中級編: has_featureをルーティングに利用する このようなcontrollerが増えてきたことに気づく sub user_account_detail {
return $c->error(400) unless $c->media->has_feature('UserAccount');
ルーターから得たルーティング先のメソッドを呼び出す前に、has_featureをチェックし、未対応なメディアならエラーのレスポンスを返す GET '/user_account/:user_account_id' => 'Giga::Admin::UserAccount#user_account_detail', { has_feature => ['UserAccount] };
上級編: has_featureをメソッド呼び出し時の権限チェックに利用する どのメディアがどのFeatureのメソッドを呼び出しているかを制御できなくなってきたので、このような仕組みを導入した 「このクラスはこの機能を持ったメディアのみ呼び出して良い」という宣言をする仕組み ユーザーログインの存在しないはずのメディアに紐づくユーザーのデータを勝手に作ってしまうことがないように 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モジュールを利用している use B::Hooks::EndOfScope;
use Module::Functions ();
use Class::Method::Modifiers ();
my ($class, @feature_names) = @_;
my $all_functions = [ grep { /\A[a-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;
これによって、渡すメディアによって、実行時に以下のメソッド呼び出しが成功したり、例外が発生したりする my $data = Giga::Feature::UserAccount::Service->get_signup_credential_data(media => $media, token => ...);
難しいところかつ全員が使うことになるので、モブプロで完成させて、コードレビュー無しでCIが通ったらマージした GigaViewerにおけるFeatureとFeature Togglesとの関係性 Featureという名前で、オンオフがある、ということでFeature Togglesとの関係を考えておく 特定のメディアにだけプレミアムな機能を提供するという意味ではPermissioning Togglesと近い アプリケーション中すべての箇所でFeature Togglesを活用するアーキテクチャだと言える パフォーマンス的な懸念があるときに徐々にロールアウトする、すぐに戻すため Giga::FeatureをRelease Togglesとして使う media->wip_features
で開発中のメディアをモデリングする リクエストをもとに $media->enable_feature
すると、1リクエスト中だけ開発中のFeatureが有効になる リリース時にはwip_features_namesからfeature_namesに移動すると常時有効になる + has_feature(feature_name)
+ enable_feature(feature_name)
Mediaクラスやhas_featureメソッド自体はたいしたことをしていない、素朴な仕組み 一方で、Mediaクラスやhas_featureを使って実現することは、凝ったことや難しいことをしている 使い所を見極めて適用し、皆が存在を知っている状態にする 一方で、単なるControllerとかModelでは難しいことはせず、Simpleに書いている 第三部 似ているけどちょっと違うものたちにどう向き合っていくか 年間数サイトのスピードで新サイトをローンチしているので、ドキュメントを用意することが重要 以前ははてなグループやGoogle Docsにまとめていたが、手早く書ける点や、ページ間リンクのはりやすさを重視して、現在はScrapboxを利用している 差異のモデリングのためのパターン集をドキュメント化する 「メディア固有の拡張処理」というページに、現在採用している設計上のパターンをまとめている 「パターン、Wiki、XP」や「組織パターン」に影響を受けていて、パターンを集めるのが好き ストレングスファインダーをやって「収集心」が出た人におすすめ パターンが集まり、ネットワークができてくると、だんだん気持ちよくなってくる 「コンテキストマップを書く会」を開催して、Google Presentationを使ってソフトウェア設計の図を描いていた ドメイン駆動設計からきた名前だけど、実態は、現状認識を図示する会 コードベースが急に大きくなった段階で人によってイメージがずれていたのでやってみた 冒頭で紹介したコアとメディアについての図もこの会で描かれたもの 敷居を下げるためにあえて雑な図を置いてみたりしている 開発環境にnode_modulesが3つあって混乱してお絵かきしたときの図 みなでお絵かきすると、コードと、コードを読み解いた結果のメンタルモデルを揃えていける コードの形に合わせて認識を揃えたり、理想の形に合わせてコードの設計を変えたり 東京・京都の2拠点でお絵かきするために最近Google Jamboardがオフィスに導入された >

第四部 現状困っていることを紹介して会場の皆様からアドバイスをいただく 「エピソードには無料なものや、価格と紐付いているものがある」みたいな分析はできている 「今日無料になったエピソードを集めて出す」のような要件は各サイト微妙に違う データストアやリポジトリの共通化を優先して、controllerから大量にレコードを集めてから絞り込むようなコードを書きがち データの保存時のことは考えられているが、表示するときに効率の悪いクエリが発行されている 現状のアーキテクチャでどこがボトルネックになり、そのときにどうするかという道筋 急に設計を変えるのは難しい。徐々にやる必要があるが、ボトルネックがどこになるかがまだ見えていない 現状理解のためにコーディング規約を整備したり、それをふまえて、どこの改善に手を付けるべきか、という計画を立てようとしているところ 常に並行に開発ラインが走っているので、チーム感を醸成しにくい 新サイトの開発や特定のサイトの開発をしていると、仕事が細分化していき、全体感が見えなくなる 触るコードが分かれているため、チーム一丸となって何か大きなことをするという意識に向きにくい ドキュメンテーションやお絵描き会などでカバーしようとしている はてなで開発しているギガビューワーにおける事例を紹介しました 似ているけどちょっと違うものたちに対処している事例 似ているけどちょっと違うものたちがある前提での暮らし チーム内で設計に関する認識を揃えたり議論したりすることが重要 さまざまなサイトの差分を吸収するためには、裏側やフレームワーク部分でちょっと複雑なことをすることも許容する フィーチャーのセットに名前をつけて管理しないと爆発しそう 難しくて、必要に応じて変えていっている、モブプロで書いたりしています