読者です 読者をやめる 読者になる 読者になる

新規サービスの管理画面を短期間で見栄え良く実装する

こんにちは、クックパッド料理教室の京和です。

管理画面はほとんどのウェブサービスに存在し、ユーザサポートやサービスの状況・KPIなどを確認するために、スタッフが毎日利用するとても重要なものです。にも関わらず、新規サービスでは人員が不足していることから、ついおざなりなデザインや実装になりがちなのではないでしょうか。

今回はクックパッド料理教室で採用している、RailsのMountable EngineとBootstrapのデザインテンプレートを使った、見栄えがよくメンテナンスしやすい管理画面を短期間で実装する方法についてご紹介します。

Mountable Engineとは

Mountable EngineはRailsアプリケーション上で動く、ミニRailsアプリケーションのようなものです。 ミニと書きましたが、Railsアプリケーション(Rails::Application)はRails::Engineクラスを継承しており、Mountable Engineの実態はまさにこのRails::Engineクラスです。
こうした事ができるのはRailsのマイクロカーネルアーキテクチャのおかげです。

Railsの内部構造やその思想・哲学については少々古い資料にはなりますが、@amatsuda が執筆したWeb+DB PRESS Vol.58の特集「詳解Rails 3」で詳しく解説されており、非常にオススメです。

簡単な使い方紹介

Mountable Engineは以下のコマンドで簡単に作ることができます。

rails plugin new asterisk --mountable

実行すると #{Rails.root}/asterisk 以下にファイルが生成されます。 下記のとおり、Railsアプリケーションとよく似た構成になっていますね。

asterisk
├── Gemfile
├── Gemfile.lock
├── MIT-LICENSE
├── README.rdoc
├── Rakefile
├── app
│   ├── assets
│   ├── controllers
│   ├── helpers
│   ├── mailers
│   ├── models
│   └── views
├── bin
├── asterisk.gemspec
├── config
├── db
├── lib
└── spec

ControllerやViewはもちろん、Gemfileやroutes.rbなども用意されています。bin/railsコマンドもあるので、scaffoldなどのGeneratorを利用することも可能です。
こうして生成したMountable Engineは、本体のGemfileとRoutingに定義することでアプリケーションから呼び出す事が可能です。

Gemfile

gem "asterisk", path: "asterisk"

config/routes.rb

mount Asterisk::Engine => '/', constraints { subdomain: 'asterisk' }

ここではconstraintsのsubdomainオプションを使ってドメインを分けています。
Mountable Engineは通常のRailsアプリケーションとほとんど同じ感覚で書くことができます。Engineについての詳しい解説はRailsGuidesをご覧ください。

なにが嬉しいのか

Mountable Engineを使った際の利点は、端的に言うとアプリケーションを分ける場合と分けない場合の「いいとこ取り」ができることです。

管理画面を別アプリケーションとして実装する場合、コードの共通化、特にモデルのメソッドやValidation・Scopeなどの共通部分をどのように管理するかが課題となることが多いです。多くの場合シンボリックリンクやgit submodulesを使うと思いますが、実運用は厳しいのが実情だと思います。一方で、管理画面を同じアプリケーション内に実装した場合、ユーザ用アプリケーションと密結合することになり、セキュリティやメンテナンスコストについて不安を抱えます。

Mountable Engineの場合、名前空間とコードベースを分離しつつ、モデルのコードは共通して呼び出すことができますし、更には管理画面でしか使わないテーブルであれば、Engine側にのみモデルを定義すると言ったことも可能です。別のアプリケーションとして切り出したいといった場合も比較的容易にできるでしょう。

料理教室ではスタッフ向けの管理画面と先生向けの管理画面を別々のMountable Engineとして実装していますが、1つのアプリケーション内にこれらが混在していた場合、メンテナンスするのはかなり難しかったと思います。

Bootstrap Templateを組み込む

Bootstrapのデザインテンプレートは有償・無料含めて様々なものがあります。少しググれば沢山のものが見つかるでしょう。 クックパッドではAce - Responsive Admin Template(有償)やAdminLTE Template(無料)などが使われています。
数年前は有償の方がクオリティが高かった印象がありますが、最近は無償でもハイクオリティなものが増えてきているように感じます。

導入はほとんどの場合、ファイルをapp/assets以下などのAsset Pipelineのパス以下に置くだけでOKです。Mountable Engineの場合はアセットファイルも分離されるので、本体とは異なるデザインテンプレートも気軽に設置することができます。

欠点

Mountable Engineは名前空間が分かれてるとは言え1つのRubyプロセスで動いているため、相互で参照することが可能です。 モデルを共通化できることは便利ですが、一方でコンテキストが異なるアプリケーションが混在している状況でもあるため、さじ加減を間違えると技術的負債になってしまいます。例えば(どんなアプリケーションにも言えることですが)default_scopeは原則使わないほうがよいでしょうし、Concernに分けて実装すると言ったことも必要です。

また、子(Engine)から親(Application)への参照以外にも、やろうと思えば親から子のモデルを参照することもできてしまいます。当初は管理画面でしか使わなかったためMountable Engine内に実装したモデルが、仕様変更が続いた結果ユーザ向けアプリケーションからもよく参照されるようになっていた、と言った事例がありました。

サービスは日々変化していきますから、コードベースもそれに合わせて適宜リファクタリングをしていくべきでしょう。とはいえ、こうした問題は、通常発生する技術的負債とあまり変わらないと思います。

Appendix:

その他、管理画面で気をつけている事などを書いてきます。

管理画面に名前をつける

料理教室では私たちの管理画面を「Daddy」と呼んでいます。名前の由来はクックパッドの歴史に触れるので詳しくはお話できないのですが、他のサービスの管理画面でも同じように名前を付けている事が多いです。
名前を付けることで、ともすれば無機質な印象のする「管理画面」から、毎日扱う「サービス」となり、より愛情を注ぐことができます。愛着をもてる名前を考えることはとても重要だと思います。

ガイドラインをつくる

クックパッドではドメインの分離や認証方式・監査ログの取得など、管理用アプリケーションを実装する際のガイドラインが定められています。原則として、すべてのサービスの管理画面でこのガイドラインを満たすことが求められており、これらがセキュリティやコンプライアンスを担保しています。そして、それらの実装を支援するため、共通ログ基盤Figlogなどの様々なライブラリが存在しています。

おわりに

Mountable EngineとBootstrap Templateによる管理画面の実装は、私が料理教室事業部に配属された当初、管理画面がとても貧弱だったことからついカッとなってリニューアルしようと考えたことがきっかけでした。調査やBootstrap Templateの選定なども含めて3日程度で実装は完了し、メンバーからはとても喜ばれました。また、幾つかの新規サービスでも同様の構成が採用されることになり、サービス間での管理画面の実装方式が共通化されると言ったメリットも生まれました。
配属直後で高まっている意識と比較的余裕のある状況は、チャレンジのしやすい大きなチャンスと言えるかもしれません。

管理画面は毎朝必ず見るもので、利用する時間は業務の中でも大きな割合を占めます。管理画面を素敵にして毎日の業務を楽しくすることは、毎日の料理を楽しみにし続けていくためにとても重要なことだと考えています。

クックパッド料理教室ではエンジニアを募集しています。 サービスに興味をお持ちの方はぜひこちらからご連絡ください。

/* */ @import "/css/theme/report/report.css"; /* */ /* */ body{ background-image: url('http://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527163350.png'); background-repeat: repeat-x; background-color:transparent; background-attachment: scroll; background-position: left top;} /* */ body{ border-top: 3px solid orange; color: #3c3c3c; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; line-height: 1.8; font-size: 16px; } a { text-decoration: underline; color: #693e1c; } a:hover { color: #80400e; text-decoration: underline; } .entry-title a{ color: rgb(176, 108, 28); cursor: auto; display: inline; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; font-size: 30px; font-weight: bold; height: auto; line-height: 40.5px; text-decoration: underline solid rgb(176, 108, 28); width: auto; line-height: 1.35; } .date a { color: #9b8b6c; font-size: 14px; text-decoration: none; font-weight: normal; } .urllist-title-link { font-size: 14px; } /* Recent Entries */ .recent-entries a{ color: #693e1c; } .recent-entries a:visited { color: #4d2200; text-decoration: none; } .hatena-module-recent-entries li { padding-bottom: 8px; border-bottom-width: 0px; } /*Widget*/ .hatena-module-body li { list-style-type: circle; } .hatena-module-body a{ text-decoration: none; } .hatena-module-body a:hover{ text-decoration: underline; } /* Widget name */ .hatena-module-title, .hatena-module-title a{ color: #b06c1c; margin-top: 20px; margin-bottom: 7px; } /* work frame*/ #container { width: 970px; text-align: center; margin: 0 auto; background: transparent; padding: 0 30px; } #wrapper { float: left; overflow: hidden; width: 660px; } #box2 { width: 240px; float: right; font-size: 14px; word-wrap: break-word; } /*#blog-title-inner{*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-position: left 0px;*/ /*}*/ /*.header-image-only #blog-title-inner {*/ /*background-repeat: no-repeat;*/ /*position: relative;*/ /*height: 200px;*/ /*display: none;*/ /*}*/ /*#blog-title {*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-image: url('http://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527172848.png');*/ /*background-repeat: no-repeat;*/ /*background-position: left 0px;*/ /*}*/