【Ruby】Array から Hash を作る方法7選(a.k.a. やっぱり Array#zip はかわいいよ)

ラクスルでサーバサイドエンジニアをやっている小林です。

最近の業務では、主に Ruby を書いています。

さて、Ruby の組み込みライブラリにはいろいろな便利メソッドがありますが、
みなさん推しメソッドはありますか?

個人的推しメソッドは Array#zipHash#transform_values です。

Hash#transform_values について少し紹介すると、
もともと Rails4.2 の ActiveSupport で実装され、
Ruby2.4 で組み込みライブラリに移植されました。
移植の際は、名前をどうするかという議論でとても盛り上がったようです。
また、Ruby2.5 からは姉妹メソッドとも言える Hash#transform_keys が実装されました。

Hash#transform_values の話はこの辺にして、今回は Array#zip の推しポイントを紹介するため、
Array から Hash を作る方法について考えてみようと思います。

コーディングをしていると、下記のようなコードを書きたいことはないでしょうか?

  • ActiveRecord で取ってきて、id をkey、インスタンスを value にしたHash を作りたい
    • 例) [AR1, AR2, …] ⇒ { id1: AR1, id2: AR2, … }
  • 文字列のArrayに対し、元の文字列をkey、正規化後の文字列を value にしたHash を作りたい
    • 例) [Str1, Str2, …] ⇒ { Str1: NormalizeStr1, Str2: NormalizeStr2, … }

このようなときの実装方法をいくつかあげ、後半で性能比較をしてみようと思います。

 

以下、ActiveRecord で User 一覧を取得し、id を key、インスタンスを value とするHash を作成する場合を考えます。

空Hashに追加していく

他の言語でも実装できる、一番オーソドックスな方法かと思います。

array = User.all
hash = {}
array.each do |user|
  hash[user.id] = user
end
hash

 

Array#to_h を利用する

Ruby 2.1 から Array#to_h というメソッドが追加になっています。

レシーバを[key, value] のペアの配列として、Hash を返します。

これを利用すると、下記のように書くことができます。

array = User.all
array.map { |user| [user.id, user] }.to_h

 

Array#zip & Array#to_h を利用する

[key, value] のペアを作るのであれば、 Array#zip が便利です。

メソッドチェインですっきりと書けるところが、個人的気に入っています。

これだけでも、Array#zip がかわいいと思えます。

array = User.all
array.map(&:id).zip(array).to_h

 

Array#transpose & Array#to_h を利用する

レシーバを二次元配列として、転置配列を作成する Array#transpose を利用しても、
同じことができます。

array = User.all
keys = array.map(&:id)
[keys, array].transpose.to_h

 

Enumerable#each_with_object / Enumerable#inject を利用する

Array#to_h がない時代は Enumerable#each_with_objectEnumerable#inject を使うことが
多かった気がします。

array = User.all
array.each_with_object({}) do |user, hash|
  hash[user.id] = user
end

 

Enumerable#index_by を利用する(ActiveSupport)

よくあるパターンなので、ActiveSupport に Enumerable#index_by という、
まさになメソッドがあります。

ただ、こちらは Proc の返り値を key とする Hash を返すので、Array の要素を key、
Proc の返り値を value とする Hash を作りたい場合は、Hash#invert で一手間加える必要があります。

array = User.all
array.index_by(&:id)

 

Enumerable#reduce & Hash#merge を利用する

Lisp 的な発想で、 Enumerable#reduce と Hash#merge を使って畳み込みを行うことで 、
Hash を作ることもできます。

ちなみに、Ruby の Enumerable#inject とEnumerable#reduce は違う名前ですが、
同じ挙動をします。

少し話がそれますが、なぜ同じ挙動で名前が違うメソッドがあるのかについては、
るびまに書かれているので、読んでみると面白いかもしれません。

array = User.all
array.map {|user| {user.id => user} }.reduce(&:merge)

 

比較

みなさん、どの方法で実装することが多いでしょうか?

好みやコードの読みやすさなどで意見が分かれそうですが、一指標として、
各実装方法の性能評価をしてみたいと思います。

今回は、Array から Hash に変換する性能のみを評価するため、変換やメソッド呼び出しはせず、
Array から key と value が同じ Hash に変換する場合の性能を比較してみようと思います。

検証コード

ベンチマークの取得には、 Ruby on Rails Guides にも紹介されている
benchmark-ips gem を利用したいと思います。

検証コードは以下のとおりです。

#!/usr/bin/env ruby

require 'active_support/all'
require 'benchmark/ips'

array = (1..10_000).to_a

Benchmark.ips do |r|
  r.config(time: 20)

  r.report "Empty Hash" do
    hash = {}
    array.each do |num|
      hash[num] = num
    end
    hash
  end
  r.report "to_h" do
    array.map { |num| [num, num] }.to_h
  end
  r.report "zip & to_h" do
    array.zip(array).to_h
  end
  r.report "transpose & to_h" do
    [array, array].transpose.to_h
  end
  r.report "each_with_object" do
    array.each_with_object({}) do |num, hash|
      hash[num] = num
    end
  end
  r.report "index_by" do
    array.index_by { |num| num }
  end
  r.report "reduce & merge" do
    array.map { |num| {num => num} }.reduce(&:merge)
  end

  r.compare!
end

実行環境は以下のとおりです。

ruby 2.5.0p0 (2017-12-25 revision 61468) [x86_64-darwin16]
activesupport (5.1.4)
benchmark-ips (2.7.2)

 

結果

Warming up --------------------------------------
          Empty Hash    80.000  i/100ms
                to_h    73.000  i/100ms
          zip & to_h   108.000  i/100ms
    transpose & to_h    98.000  i/100ms
    each_with_object    70.000  i/100ms
            index_by    63.000  i/100ms
      reduce & merge     1.000  i/100ms
Calculating -------------------------------------
          Empty Hash    773.943  (±10.3%) i/s -     15.280k in  20.013161s
                to_h    733.483  (± 8.9%) i/s -     14.600k in  20.090466s
          zip & to_h      1.065k (±10.3%) i/s -     21.060k in  20.027320s
    transpose & to_h      1.005k (± 8.5%) i/s -     19.992k in  20.067946s
    each_with_object    719.383  (± 7.1%) i/s -     14.350k in  20.063602s
            index_by    654.471  (± 8.6%) i/s -     12.978k in  19.999714s
      reduce & merge      0.962  (± 0.0%) i/s -     20.000  in  20.834702s

Comparison:
          zip & to_h:     1065.3 i/s
    transpose & to_h:     1004.8 i/s - same-ish: difference falls within error
          Empty Hash:      773.9 i/s - 1.38x  slower
                to_h:      733.5 i/s - 1.45x  slower
    each_with_object:      719.4 i/s - 1.48x  slower
            index_by:      654.5 i/s - 1.63x  slower
      reduce & merge:        1.0 i/s - 1107.19x  slower

Array#zip 早いですね!!メソッドチェーンですっきりかける上、処理も早いという、
これは推さざるをえない感じがしませんか?

Array#transpose も Array#zip とほぼ同じくらいの性能ですが、
やはり個人的にはメソッドチェーンで書ける Array#zip のほうが好きですね。

他の手法についても見てみると、Enumerable#index_by が思ったより遅いです。
実装を見たところ、空Hashに追加していく実装と同じなので、
yield の呼び出し分オーバーヘッドがかかっている感じでしょうか。

Enumerable#reduce と Hash#merge を利用する方法は、
配列長分 Hash#merge が実行されるため、かなり遅くなっています。

 

ただ、実際のコードでは、key と value が同じということはなく、
key や value に対して何かしらの処理を行うため、Hash の作成コストより、
他の処理のオーバーヘッドが大きくなります。

別途、key を Integer#to_s して Hash を作成する場合のベンチマークも取ってみましたが、
空Hash に追加する方法が一番早く、Enumerable#reduce & Hash#merge を除く
実装方法については、それほど変わらないという結果になりました。

#!/usr/bin/env ruby

require 'active_support/all'
require 'benchmark/ips'

array = (1..10_000).to_a

Benchmark.ips do |r|
  r.config(time: 20)

  r.report "Empty Hash" do
    hash = {}
    array.each do |num|
      hash[num.to_s] = num
    end
    hash
  end
  r.report "to_h" do
    array.map { |num| [num.to_s, num] }.to_h
  end
  r.report "zip & to_h" do
    array.map(&:to_s).zip(array).to_h
  end
  r.report "transpose & to_h" do
    [array.map(&:to_s), array].transpose.to_h
  end
  r.report "each_with_object" do
    array.each_with_object({}) do |num, hash|
      hash[num.to_s] = num
    end
  end
  r.report "index_by" do
    array.index_by(&:to_s)
  end
  r.report "reduce & merge" do
    array.map { |num| {num.to_s => num} }.reduce(&:merge)
  end

  r.compare!
end

=begin
Warming up --------------------------------------
          Empty Hash    17.000  i/100ms
                to_h    15.000  i/100ms
          zip & to_h    19.000  i/100ms
    transpose & to_h    18.000  i/100ms
    each_with_object    19.000  i/100ms
            index_by    18.000  i/100ms
      reduce & merge     1.000  i/100ms
Calculating -------------------------------------
          Empty Hash    192.959  (± 8.8%) i/s -      3.825k in  20.060582s
                to_h    178.314  (± 9.0%) i/s -      3.525k in  20.023982s
          zip & to_h    181.005  (±10.5%) i/s -      3.572k in  20.065550s
    transpose & to_h    176.782  (± 9.1%) i/s -      3.510k in  20.039329s
    each_with_object    192.932  (± 5.2%) i/s -      3.857k in  20.054975s
            index_by    182.739  (± 4.4%) i/s -      3.654k in  20.036235s
      reduce & merge      0.905  (± 0.0%) i/s -     19.000  in  21.030102s

Comparison:
          Empty Hash:      193.0 i/s
    each_with_object:      192.9 i/s - same-ish: difference falls within error
            index_by:      182.7 i/s - same-ish: difference falls within error
          zip & to_h:      181.0 i/s - same-ish: difference falls within error
                to_h:      178.3 i/s - same-ish: difference falls within error
    transpose & to_h:      176.8 i/s - same-ish: difference falls within error
      reduce & merge:        0.9 i/s - 213.18x  slower
=end

ちなみに、空Hash に追加するという Enumerable#index_by の実装も、
リファクタリングされて現在の形になっています。

まとめ

コーディングの際よくあるパターンとして、Array から Hash を作成する実装方法を7つあげ、
性能比較をしてみました。

個人的推しメソッドである Array#zip のかわいさが少しは伝わったでしょうか?

 

iframe で複数の管理画面を1つの統一されたサイトに見せるパターンのまとめ

こんにちは。エンジニアの二串です。普段はサーバサイドの開発をしていますが、今日は最近仕事で取り組んだフロントよりの話になっています。

iframe を使って複数の管理画面サイトをあたかも1つの統一されたサイトとして見せるパターンをまとめます。

iframe を使って別サイトをはめ込むことは簡単ですが、実際複数ページあるサイトをはめ込もうとすると、ただ iframe を使うだけでは力不足です。例えばiframe内でのページ遷移、高さ調整、パーマリンク等で問題を感じます。

今回紹介する tips のいくつかはインターネット上で見つけられますが、今回の複数管理画面を1つにマージするという視点でまとまった記事は見つけられなかったので、まとめたら便利なのではとおもい記事にまとめました。

コードスニペットを以下で掲載していますが、より詳しくはデモアプリも併せて見ていただければとおもいます。

背景

移行イメージ

ラクスルに存在する最もレガシーなシステムは admin と呼ばれるいわゆる管理画面です。PHPで作られています。そのレガシー管理画面を素早く新システム(Ops Rails製)へ移行する必要がありました。

当時の状況としては

  • 新管理画面はまだ機能が少ないが、今後の新規開発は新管理画面でしたい
  • 旧管理画面には多くの機能がまだあり、理想的には新管理画面に移植したいものの、工数的にさくっと移行できない
  • オペレーションの中心は旧管理画面から新管理画面に移行してしまいたい

というような状況でしたので、iframe を使って新管理画面の1機能として、旧管理画面の各ページを埋め込んでしまって、ユーザー目線での移行については少ない工数で完了させることにしました。

iframe埋め込みによる問題点

単に iframe の src 属性に URL を指定すると、以下の点が課題になります。

  • 旧管理画面のページ数は多いので、ページごとに iframe を設置するのは大変
  • iframe の高さ動的に変えれない(初期ページや子側でページ遷移した場合)。結果的にページが切れる
  • ブラウザアドレスバー問題
    • 子側でページ遷移を何回かした後に F5 を押すと元に戻る。またURL を他の人にシェアできない。
  • 子側ページ内のリンクが 旧管理画面URL 問題
    • リンク先 URL を Slack 等でシェアできない(できるけど iframe が外れる)
    • 別タブで開くと iframe が外れる
  • そもそも、デザインの統一性がない(子側のヘッダやサイドバーメニューが邪魔)

さらに補足すると、今回の2画面はドメインが異なるため、iframeの親・子間で window をまたいで javascript や css で操作しようにもクロスドメイン(CORS)のため DOM 操作できません。

仕組み全体像

以上の問題を踏まえ、以下の指針を立てました

ページ表示の流れ

  • 新管理画面側(Rails)では `GET /proxy?path=path/to/page` というエンドポイントを作り <iframe src=”https://旧管理画面/path/to/page”> をレンダリングする。つまりどんなページもはめ込むことができる。
  • 親・子それぞれに今回のための javascript  を設置し(parent.js, child.js)、window間通信は window.postMessage() API を使う。これによりクロスドメインであってもwindow間で通信可能
  • この通信路を使って子の page height などを 親に伝える。高さ以外の情報もあるので、メッセージタイプ(type)、メッセージデータ(data)を含めた object をデータペイロードに入れて送ることにする。
  • iframe内のリンクの諸々の問題は、a タグの href の値を力技で新管理画面のproxy URL に書き換える。
  • サポートブラウザは Google Chrome のみとする。社内利用のツールなので工数削減も踏まえ割り切れる点と、そもそも社内で Google Chrome が標準で使われているので。

以下、長くなりますが、1つずつ見ていきます。

GET /proxy?path= エンドポイント

# app/controllers/proxies_controller.rb
class ProxiesController < ApplicationController
  def show
    path = params[:path] || ''
    @src = 'https://旧管理画面FQDN/' + path
  end
end

# app/views/proxies/show.html.erb
<iframe id="proxy-iframe" src="<%= @src %>" height="1024" style="overflow: hidden; border: none;" scrolling="no" frameborder="no"></iframe>

# config/routes.rb
get '/proxy', to: 'proxies#show'
  • GETパラメタの path から旧管理画面の URL を構築し iframe の src 属性へセット
  • height=”1024″ は初期値として。後述の方法で別途ページ毎に動的に変える。
  • その他、iframeのボーダー枠線。スクロールバーを消す。これはiframeっぽさを無くすための工夫
  • id属性 `proxy-iframe` は後述の javascript の処理で利用

これで旧管理画面の任意のページを新管理画面で扱えるようになりました。

なお、旧管理画面側が X-Frame-Options を返却していたので iframe 内で表示させるため nginx で消しこみました。

親・子window間通信

新・旧管理画面それぞれに以下の javascript を置きます。今回のケースでは、子から親に一方通行でメッセージを伝え親側でアクションさせればよかったので、子と親の通信はいわゆるクライアントサーバモデルになっています。

子側

const postMessage = (type, data) => {
  const rawData = JSON.stringify({ type: type, data: data })
  window.parent.postMessage(rawData, '*')
}

// iframe内でレンダリングされた場合にのみ発火させる
if (window !== window.parent) {
  window.addEventListener('load', () => {
    // 子の高さを親に伝え、親の iframe の heigit を変更
    postMessage('PageHeight', {height: document.body.scrollHeight})
  }, false)
}
  • iframe 内でレンダリングされた場合にのみ実行している
  • on load 時に高さを送る。メッセージには type と data がありJSON シリアライズして送る。

iframe 内でレンダリングされた場合にのみこの仕組が発動するようにすることで、移行をやりやすくしました。旧管理画面直アクセスでは発動しないので、旧管理画面直アクセスへのパスを残しつつ、緩やかに移行を進めれます。

親側

const messageHandlers = {
  PageHeight: (data) => {
    // iframe の height を調整
    let elm = window.document.getElementById('proxy-iframe')
    elm.style.height = `${data.height}px`
  }
}

window.addEventListener('message', (event) => {
  console.log('[ops] message received', event)

  // On development environment webpack-dev-server sends message, so skip message from it.
  if (/^https?:\/\/(localhost|127.0.0.1)/.test(event.origin)) return

  // Verify received message from for security manner of postMessage() API,
  const originRe = /旧管理画面URL/
  if (event.origin.search(originRe) === -1) {
    console.log('Blocked the message from ' + event.origin)
    return
  }

  const data = JSON.parse(event.data)
  if (data.type === null) throw new Error('[ops] Message received, but no type specified')
  if (!messageHandlers.hasOwnProperty(data.type)) {
    throw new Error(`[ops] Message received, but type "${data.type}" is invalid`)
  }
  messageHandlers[data.type](data.data)
})

細かいエラーハンドリングも載せましたが、中心は以下のとおりです。

  • window.postMessage() で送信されたデータは ‘message’ イベントで受け取れる
  • type に対応するメッセージハンドラ関数へディスパッチする
  • PageHeight ハンドラでは iframe の height 属性を変更

iframe高さの動的調整

1つ前のセクションのとおりです。

ブラウザアドレスバー問題

iframe内でページ遷移を繰り返してもブラウザアドレスバーのURLは最初のURLのまま問題。実際のカスタマーサポートなどの現場では、「あ、この注文、サポートが必要そうだから Slack で注文詳細のURLをシェアしておいて…」 といったことありますので、iframe内で遷移したあとの URL も気軽に貼り付けれる必要がありました。

そこで、子の a タグのクリックイベントを prevent default  で止めて、PageMove メッセージを送り、親側windowでページ遷移させてしまうことにしました。詳しくはサンプルアプリのコードを参照ください。

なお、a タグ以外に POST 等の後のサーバサイドでのリダイレクト処理される場合にも同様の課題はあります。管理画面の特性を検討した結果、リダイレクトについては特に何も対策せずとも運用上なんとかなると判断してます。

子側ページ内のリンクが 旧管理画面URL 問題

リンク先をシェアされたり、別タブで開こうとすると iframe が外れてしまう。

これに対しては、子側の DOMContentLoaded で hrefの属性値を 新管理画面の proxy URL に挿げ替えてしまうことにしました。

const fakeAnchors = function () {
  document.querySelectorAll('a').forEach(function (elm) {
    // getProxyUrlFromAdminUrl() は https://旧管理画面FQDN/foo/bar を
   // https://新管理画面FQDN/proxy?path=foo/bar に変換
    elm.href = getProxyUrlFromAdminUrl(elm.href)
  }
}

// iframe内でレンダリングされた場合にのみ発火させる
if (window !== window.parent) {
  // ...
  window.addEventListener('DOMContentLoaded', function () {
    // ...
    fakeAnchors()
  }, false)
}

なお、javascript を使わず、旧管理画面の view の各aタグを手で全て書き換える方が素直ですが、そうしなかった理由は、数が多いのと、暫くの間旧管理画面にダイレクトにアクセスできるパスは残しておきたかったためです。管理画面だから許される力技…です。

ページ内リンク問題

<a href=”#foo”>foo</a> のようなリンクです。iframe のオプションで scrolling=”no” している、また href をすげ替えた都合でスクロールしてくれないので、このようなリンクがクリックされた場合は、子から親に要素のポジションを計算して親側でスクロールさせるようにしました。詳細はサンプルコードを…といいたいところですがこの部分サンプル書けてないです :pray:

なお、production のコードではこの部分も window.postMessage() で 親にスクロール位置を通知させました。

ヘッダ・サイドバー消し

新管理画面の吸収されてしまった旧管理画面。もう旧管理画面のヘッダやサイドバーメニューは不要なので消します。旧管理画面側で DOMContentLoaded のタイミングで 追加の css ファイルを入れて、消し込むことにしました。

子側

const applyOpsCss = function () {
  const style = document.createElement('link')
  style.rel = 'stylesheet'
  style.href = '/css/additional.css'
  document.getElementsByTagName('head')[0].appendChild(style)
}


// iframe経由でのみ発火させる
if (window !== window.parent) {  
  applyOpsCss()
  window.addEventListener('DOMContentLoaded', function () {
    postMessage('PageDOMContentLoaded', {})
  }
}

  • iframe内でレンダリングされた場合に限り additional.css を適用する。additional.css 内では消したい要素に display: none; を適用している
  • DOMが読み込まれたら親に通知(次を参照)

親側

const messageHandlers = {
  // ...
  PageDOMContentLoaded: (_data) => { window.document.getElementById('proxy-iframe').style.visibility = 'visible' },
  // ...
}
  • スタイルを当てるまでの非常に短い間はヘッダ等が見えてしまうので画面がチラつく。これを防ぐために ngCloak 的なアプローチ、すなわち、iframe に `visibility : hidden;` で予め非表示にしておいて、追加のスタイルが適用されたら PageDOMContentLoaded メッセージを送信しiframe のdisplay visibility を `visible` に変更する

CORS でなければ親側だけで iframe 内の load を検知できそうな気はするのですが、CORS だったのでこのようにしました。

メニューのマージ

このタイミングでメニューバーを整理しました。新・旧それぞれのメニューバーにあった項目を整理し、新管理画面のメニューバーにマージしました。格段に見やすくなってこちらは評判です。

まとめと所感

iframe を使って複数の管理画面サイトをあたかも1つの統一されたサイトとして見せるパターンをまとめました。特に CORS 環境おいては window.postMessage() API を利用することで様々な課題を解決することができました。

この方式で本番運用して4ヶ月ほど立ちますが、大きなトラブルなく稼働できています。管理画面という特性上そのあたりは柔軟に対応できているのと、対応ブラウザを Chrome 1本に割り切ったので、導入後のメンテ工数は今のところほぼなく上手く動いてます。

本番の切り替えについては、旧画面と内容は変わらないので、導入調整コストがほとんどかからず、また利用者=オペレーターさん達の心理的負荷や学習コストも低く抑えることができました。これは本アプローチの最大のメリットではないかとおもいます。

ちなみに、このアイデアを思いついた翌週が運良く Hack It Day だったので、1日つかってプロトタイプを作って、どのあたりに技術課題があるか、工数感などつかむことができたのが良かったとおもいます。 * Hack-It Dayは月に一度、ラクスルのエンジニアが自由に開発することができる日)

さて、長くなりましたが最後までお読み頂きありがとうございます!!

ラクスルでは、絶賛エンジニア募集中です。
「仕組みを変えれば世界はもっとよくなる」
世界が変わっていく瞬間を一緒に体験したいエンジニアの方、お待ちしています☆

【Tableau】DesktopとOnlineの複合で簡単・安価にレポーティング

みなさん、いかがお過ごしでしょうか?
ラクスルの安井一浩です。

データアナリストではない私ですが、Tableauをいじるのが好きなので共有させていただきます。
「昨日、売上が下がったんだけどなぜだかよくわからない」
「あの数値を毎日見たいんだけど、あまり時間が無い」
こんなことが日常的に起こると思います。

それを解決するTableau Desktop&Tableau Onlineをご紹介したいと思います。

システム構成

Tableauレポートシステム図
図を解説します。

各ソフトで役割が違います。

Tableau Desktop:主に分析ツール
役割・・・データ解析、グラフ制作、分析用DBの制作
特徴・・・分析がとてもやりやすい。フィルタと数式がとても使いやすく動作も速い。
頻繁にバージョンアップしているので、複数DBをJOINできるなどできることがどんどん増えている。

Tableau Online:クラウドおよびサブスクリプションツール
役割・・・DB更新、定期レポート配信、グラフデータバックアップ
特徴・・・クラウドから、データ更新やメールのサブスクリプションなどを自動でしてくれる。
バックアップデータを保持してくれるのでTableau Desktopでデータを触っている時に
壊れてしまった場合などは非常に助かる。

Tableau Desktopが得意なところとTableau Onlineが得意なところを分けて、2つのソフトでレポーティングを実現するという構成になっています。
データベースエンジニアやデータサイエンティストの力を借りずとも、レポートが作れてしまうのがメリットですね。
通常のTableau Desktopの使い方の延長でできてしまいます。

システム導入後の作業手順

①データベースに接続して、分析用DBを制作します。※今の画面はMySQL
データベースに接続

②作ったデータベースを抽出する。※ここがポイント抽出画面

③グラフを制作する。※ご自由に
グラフ制作

④データベースをTableau Onlineにアップロードして、アップロードしたデータベースに置き換える
データベースをアップロード

⑤Tableau Onlineでデータベース更新の設定をする※昔はアメリカ時間でしたが、今は日本時間になっています。データベースの更新スケジュールを設定する

⑥Tableau Onlineにグラフをアップロード(パブリッシュ)ワークブックのアップロード

⑦Tableau Onlineでビューに移動してから、メールの定期配信(Subscription)の設定をする。

ビューに移動します。Tableau onlineの画面

ビューからサブスクリプションを設定。ビュー単体や、ワークブック全体などを指定できます。サブスクライブというのが、決定ボタンです。サブスクリプション設定

どこにメールが配信されるかというと、マイアカウントで設定したメールアドレスにメールが飛びます。

1アカウント1メールアドレスと言う設定になっていますので、
複数のメールアカウントに送りたい場合はアカウントを増やすか、
メール側で転送設定をすれば、複数の人に送信することも可能ですのでご安心を。
※ただし、Tableau Onlineで細かい数字は見れませんが・・・

最小構成だと、なんと年間162,000円でレポートシステムができてしまいます。
Tableau Desktop 102,000円/年
Tableau Online 60,000円/年
とは言っても、触っているとどんどんアカウントが欲しくなってくるのが常なのでこれで終わることはマレなんですけどね(;´∀`)

細かいレポーティングが簡単に設定できるのでとてもおススメです。
ラクスルはこのような形でデータ分析とレポーティングは一部Tableauで行っています。

以前はエクセルでの分析がメインだったのですが、データ量が増えてくると厳しいですよね。
また、行数制限の問題等もあります。
Tableauなら、エクセルデータも取り込めますし、DBとのJoinもできる優れもの。
みなさんも試してみてください。

おまけ

Tableauの発音方法はイチロー、サブローみたいにタ → ブ ↑ ロ ↑ ー →って読んでいたのですが、Tableauの人と会話した時はマズローみたいに、タ → ブ ↓ ロ ↓ ー →という読み方でした。後者が正しいようです。

ラクスルでは、絶賛一緒に働いてくれる方を募集中です!
新しいことにチャレンジしたい方は是非。

ラクスル採用ページ https://recruit.raksul.com/

そうだ、ラクスルを作り直そう!

10月からラクスルにジョインさせていただいた水島です。

新参者ですが、宜しくお願いいたします。

さて、入社して間もなくCTOの肝いりでスタートした 「Raksul Platform Project」 のプロマネを拝命したため、今日はその全体感の話をしたいと思います。

なにをするのか

「スタートアップあるある」だなんて言わないでください。

ラクスルをフルスクラッチで作り直そうとしています。

でもそれはあくまで手段です。

目的は、

  • 技術負債と思われている部分を根本的に解消して開発しやすい状態にする(エンジニアを幸せに)
  • システムに柔軟性を持たせて経営戦略の選択肢が増えている状態にする(経営を幸せに)

の大きく2つです。

特に前者の「エンジニアを幸せに」という目的に対する経営陣の温度感が不思議と高いのはポジティブに感じています。短期的な投資対効果とかではなく、「ものづくり」を大切にする会社になるんだという意思が強く感じられるので、長い道のりにはなりそうですが、エンジニアとデザイナーがさらに輝けるようにするために是非やりきりたいと思っています。

後者の「経営を幸せに」という目的は、プロマネである私のバランサーとしてのプライドみたいなものです。ただ作り直してキレイになりました、幸せです!だけでは虚しさがあるので、経営の潜在ニーズを今回のプロジェクトで一定先回りして、いつかドヤ顔したいと思っているのです。

つまりは、単純に他言語、他フレームワークで作り直すということではなく今後のアーキテクチャーから見直していくプロジェクトです。

 

今までのいろんなアンチパターンを思い返して方針を考える

ラクスルの現在のシステムの状態を見たり、私が今まで経験してきた様々なサービス、プラットフォーム、受託システムの開発を思い起こすと、やってはいけないアンチパターンの連続が強烈な反省と共に頭の中を駆け巡ります。

それらをベースに今回はどのようなアーキテクチャーで進めていこうとしているかをいくつかご紹介したいと思います。

アンチパターン1: サービスのコア機能が汎用化されていない

サービスはユーザーのニーズや市場環境を見ながら進化していくものなのですが、再利用可能なコア機能が主力サービスの中に閉じ込められていることがあります。

私の経験を思い返しても、

  • 会員体系の認証、認可機能
  • 決済、経理、資金決済法遵守のための仮想通貨管理などの機能
  • データ分析基盤機能
  • 校閲、投稿内容チェックなどの機能
  • 画像管理、変換機能
  • CMSと広告配信管理機能
  • ファイルシステムみたいな機能

などがありました。

つまりは社内外で今後再利用される可能性があるものを主力のサービスとは疎結合に作っておけばよかったと後悔し、後で掘り起こして、汎用サービス化することを繰り返してきました。

社外のクラウドサービスで置き換え可能な機能も増えているので、要件がほぼフィットするならそれらを活用すべきというのは言うまでもなく、実際にFirebaseなどに載せ替えた部分もありましたが、自社の強みや特殊要件が眠っている場合は、早めの段階で社内クラウドサービスのように汎用的な設計にして再利用可能な状態にしておくべきです。

現在のラクスルにおいて、主力サービスの中に閉じ込められている汎用的なコア機能としては、

  • ラクスル社の他事業(ハコベル事業等)で共通に使える汎用機能レイヤー
    • 個人向け、法人向け決済、経理処理などの機能
    • メール送信とテンプレ管理機能
  • 印刷関連事業で共通に使える汎用機能レイヤー
    • 印刷SCMと発注基盤機能
    • スピードチェック入稿を含むDTP機能
    • オンラインデザイン制作サービス機能
  • raksul.com で共通に使える汎用レイヤー
    • ラクスル会員の認証、認可機能
    • ECの基本機能

と分析しています。

ハコベル事業でも物流の輸配送管理システムなどは、物流全般で共通に使える汎用機能レイヤーと言えるかもしれません。

裏を返すとこれらの機能の汎用化ができていないために、既存事業とシナジーのある多角的な取り組みの開発スピードが落ちていたり、顧客体験の低下や、事業の機会損失が出始めているようにも見えます。

今回のプロジェクトでは、これらの機能を各レイヤーにおいて、社内外の他サービスと接続しやすく切り出して作り直し、そしてそのアーキテクチャーを継続していけるような開発体制を作っていくことが重要となります。

raksul.comの印刷サービスや集客支援サービスは、これらの汎用機能の上に成り立つ一つのサービスという位置づけになります。後付にはなりますが、これがRaksul Platform Projectと呼ばれている所以です。

 

アンチパターン2: データベースとアプリケーションの切り方を間違える

よくあるアンチパターンな構成としては、モノリシックなデータベースが中心にあり、ユーザー向け、管理画面やAPIなどの多数のWebアプリが読み書きしている状態です。

立ち上げ期はスモールな構成なのでいいのですが、そのまま多角的に機能追加を行ってしまった結果、データベースとメインのWebアプリ、管理画面などが肥大化していき、辛くなって対処療法的に小さなアプリだけを切り出し始めます。

 

こうなってしまうと、

  • 各アプリケーションで似て非なるモデルが冗長に開発され、コピペしはじめる(DRYでない)
  • 各アプリケーションがそれぞれの事由でスキーマを変更、書き込みをしていき、影響範囲が広くなる
  • 新しいエンジニアやオペレーターがシステムをキャッチアップするのに時間がかかる

といった弊害が出てきます。

開発の現場から悲鳴が上がり始め、内部にインターナルAPIを置き、データベースやビジネス・ルールを一定ラップして凌ぐことになります。ラクスルにも内部にAuth API, Order APIといった比較的新しくて小さな内部APIが存在していて、一時的な対処はしている状態ですが、抜本的な解決には至っていません。

本質的には適切な関心事であるドメイン毎に、データベースを分割して、各ドメインのアプリケーションが、

  • エンドユーザー向けWeb画面
  • 管理者向けWeb画面
  • 他アプリケーション向けAPI

の3つのプレゼンテーション機能を持ち、一つのデータベースと一つのストレージ(AWS S3 のバケット等)の保全に責任を持つ、という構成にすることで、機能の凝集度を高めるべきです。

早期にこういった発想でデータベースとアプリケーションの切り出しを意思決定したかったと後悔することがよくあります。

 

アンチパターン3: マイクロサービス化しすぎて逆に効率が落ちる

上記で説明した、データベースとアプリケーションの切り方についてですが、柔軟性を求めすぎて細かく切りすぎてしまい、ピタゴラスイッチ状態になるとまた問題が出てきます。

  • dockerなどを使っても環境セットアップするのが辛くなる
  • データを非正規化しすぎて不整合が起きやすくなる
  • 障害対応やチューニングの難易度が上がる

ラクスル内部でも、Auth APIがつらい、直接usersテーブル参照したい、joinしたいといった声が聞こえることがありますが、まさにといった弊害かと思います。それは近くにあるべきはずのものが遠くに切り出されてしまっている可能性があります。

一定仕方のない部分はありますが、なんでもかんでもAPIにして分割していくということではなく、逆に統合すべきドメインを見定め、的確にケーキカットしていくことが非常に重要だと思っています。

ラクスルの場合は、

  • 全社共通で使われるであろう機能レイヤーに2アプリケーション
  • ラクスルのコア技術で印刷事業共通に使われるであろう機能レイヤーに3アプリケーション
  • raksul.comのECサイトのレイヤーに2〜3アプリケーション

の合計7〜8アプリケーション構成程度になってくると思っています。
これでも少し多いですが、これ以上切り出すのは多すぎると思っています。

 

アンチパターン4: テストしにくい状態で身動きが取れなくなる

フレームワークや言語のバージョンアップをするというプロジェクトを今まで何度も経験していますが、ここで辛いのが、テストがないアプリケーションのマイグレーションです。

例えば、Rails5が出ている時期に、 テストが不十分なRails3の巨大なモノリシックなアプリがあってなかなかマイグレーションできませんでした。「Rails3 なんですねえ。。。」と採用面接で何度言われたことでしょう。

持続可能な事業を支えるアプリケーション、開発組織であればここは妥協すべきではありません。

ラクスルでも、PHP + テストがないアプリケーションなどが一部あります。かつ、それがとても事業上重要なアプリケーションだったりします。日々の開発では、End2Endテストと人力のQAで品質を担保していますが、もう少し細かいレベルでのテスティングに力を入れなければ長くアプリケーションの品質を保つのは難しいのは言うまでもありません。

思い切ってこれらのアプリケーションをRuby on Railsでテストフレームワークも含めて標準化し、テストを書きながら作り直していく必要があると考えています。

 

まとめ

さて、ご紹介してきたアンチパターンを踏まえた上で、今のところのRaksul Platform Projectの基本的な方針は以下のような形です。

  • レイヤーとドメインを定義し、コア機能を汎用的に切り出して社内外と接続可能な状態にする
  • 1アプリケーションに対して、1データベース、1ストレージ構成のアプリにする
  • アプリケーションを細かく切りすぎないように7〜8アプリケーションぐらいに再分割して切り出していく
  • Ruby on Railsでアプリケーションを標準化してreadable、testableな状態にする

ラクスルの開発現場はこれから大きく変わろうとしています。また、これによりシステム構成と事業・組織構成の間の歪を最小化することができる考えています。

印刷業界をリードできるコア機能の開発が段々できるようになってきた今、今度はそれらをモダンなアーキテクチャーに載せ替えていき、ワクワクするような経営・事業戦略と共にエンジニアもさらに高いレベルに成長できると思います。

ラクスルでは、絶賛エンジニア募集中です。
「仕組みを変えれば世界はもっとよくなる」
世界が変わっていく瞬間を一緒に体験したいエンジニアの方、お待ちしています♡

スピードチェック入稿リリース秘話

ラクスルの数少ない女子エンジニアの加藤です。

今回は、8月末にリリースしたスピードチェック入稿のリリースまでのプロセスを書いてみたいと思います。

スピードチェック入稿とは

そもそも、チラシや名刺などの印刷をインターネットで注文したことのない方もいらっしゃるかもしれません。

ラクスルは、印刷物を扱うECサイトです。通常のECサイトと違って、商品を選んで配送先と決済方法を選んでもらうだけでは注文は完結しません。お客様が印刷するデータを入稿するという印刷ならではのステップがあります。

また、印刷用データはラクスルで印刷に適しているかどうかチェックして、適していない場合、お客様は再度入稿する必要があります。

これまで、ラクスルではデータが印刷に適しているかどうか、オペレーターが一つ一つチェックし、場合によっては微修正をしていました。人手でやっているため、印刷データが確定して実際に印刷工程に進む準備ができるまでに1日以上かかってしまうこともあり、急いで印刷物が欲しいというお客様の要望になかなか答えられていませんでした。

そこで、サイトを訪問している最中にその場でチェック結果がわかるようなシステムを開発しようということになりました。それが「スピードチェック入稿」です。

スピードチェック入稿画面

スピードチェック入稿画面

プロジェクトの始まり

入社してしばらく経ちそろそろ会社に慣れてきたかと思っていた4月末、プロジェクトのミーティングに呼ばれました。データチェックを自動化する理由が共有されて、成功させるぞという強い意志が感じられましたが、参加メンバーのドメイン知識が十分ではない状況で、議論も少し混乱気味。私個人としては不安がありつつも、とにかくやってみようという気持ちでミーティングを終えました。

プロジェクトは、ラクスルで提供しているデータチェック業務理解から始まりました。実際に、オペレーターの方が作業している所をメンバーみんなで見させてもらう時間もとりました。

・どんなデータが入稿されるのか
・どんなデータのどんな点を確認しているのか
・どんな修正を行っているのか
・どんなメールをお客様に送っているのか

最初はこのような基本的なポイントを一つ一つ確認していき、データチェックのドメイン知識の理解を深めていきました。

デザインと設計のプロセス

スピードチェック入稿のデザインチームはデザインスプリントを実施。毎スプリントごとにお客様からのフィードバックを受けてデザインが進みました。

使う人はどんな印象を受けるのか、どんなところで操作に詰まったり、戸惑ったりするのか。私自身はデザインチームではありませんでしたが、週ごとのデザインとフィードバックのシェアや、お客様へのインタビューの同行を通じで、ユーザ理解を深めることもできました。

ビジネスチームによるデータチェック要件の詳細化とデザインと同時並行で、システムの設計も始めました。

システムは、すぐにWebで時間のかかる処理を実装するときのオーソドックスな構成に決定。
・フロント:データチェック用のAPIを呼び出し結果を描画
・データチェック用のAPI: フロントから受け取ったリクエストを受けてジョブをキューイング
・バックエンドジョブ:データチェックやPDF変換を実施

システム構成

システム構成(模式図)

デザインスプリントが進んでいる間に、最小単位で連携して動くものを実装していきました。

初めて一緒に仕事をするメンバーで構成されたチームでしたが、REST APIなど共通した設計スタイルがあると設計検討も早く進むのを改めて感じました。

自走チームの開発スタイル

メンバーが各作業を進める中、試行錯誤したのはコミュニケーションとタスク管理でした。

ビジネスメンバー、デザイナー、エンジニアそれぞれに違うバックグラウンドを持ち、比較的自由に仕事をするメンバーが揃ったため、仕事自体はすごく速く進むのですが、みんなが協力しないと一つ一つの機能が動きません。

どんな風にコミュニケーションするか、どんな風にタスクの状況を共有したり管理したりするか。朝会や毎週の振り返りで、率直に意見やアイデアを言い合って、改善して進めました。

振り返り

毎週末の振り返り

以前のブログ記事では、開発スプリントやペアプロの紹介もありましたが、今回はペアプロは実施せず、また開発スプリントは回しつつも運用は比較的ゆるめ。それぞれのメンバーのやりやすさを優先させつつ、アクションが滞らないようにコミュニケーションをとって乗り切りました。

やっぱり何か起こる結合とテスト

いくつかのコンポーネントが連携するシステムの開発では、結合とテストの段階になって様々な考慮漏れが判明してスケジュールが遅れるというのが開発でよくある光景ではないでしょうか。

今回のプロジェクトでは、かなり前段階で様々なケースのテストデータを準備したりエラーケースの検討をしてきましたし、結合して動かすこともその都度行なっていましたが、それでもやはり本格的な結合やテストの段階で色々な問題が起こりました。

例えば、初めは入稿している原稿の縦横の方向を自動判別しようとしていましたが、チラシでは判定できるけれども、名刺では判定できないケースがあるということが間際になってわかったりしました。

もちろん前倒して考慮できればベストだと思いますが、初めての要素があるプロジェクトではなかなかそうもいかないものです。このプロジェクトでは見つかった時にその都度、チームで学習しながら、一つ一つアクションを決めていきました。

リリース!

風邪でチームメンバーが次々と休んでスケジュールが遅れ気味になったり、間際で解決が危ぶまれる課題が発生したりと、様々なことがありましたが、8月末に当初の予定通りリリース。日程的にタイトな状況になっていても、みんなでランチに行ったりとチームの雰囲気を保ち、コミュニケーションを密に取れるようにしていたのも、逆に予定通りのリリースに不可欠だったかもしれません。

リリース後も、スピードチェック入稿の状況をチームでモニタリングしながらの対応や改善、対象商品の追加が続き、多くのお客様に使われるサービスに育ってきました。フェーズが移り変わるとチームの状況ややることも変わっていきますが、これからもビジネスメンバー・デザイナー・エンジニアの垣根を超えて、お客様の体験がよくなっていくような開発を続けていきたいと思います。

 

ラクスルでは、絶賛エンジニア募集中です。
「仕組みを変えれば世界はもっとよくなる」
世界が変わっていく瞬間を一緒に体験したいエンジニアの方、お待ちしています♡

TDDがうまくいかないときは、BDD形式でバックログを書いてみる

ラクスルに入ってはや2年を迎えたおっさんCTOの泉です。ラクスルに入ってから 6kg 体重が増え、ますますおっさんとしての安定感が増してきました。次の2年で 6kg 減らす予定です。

さて、今回のお題はエンジニアなら誰しも知っている有名な開発プラクティスであるTDDを実践する上でのTIPSです。

TDDはなかなか実践に至らなかったり、実践してみてもなかなかうまく行かず、挫折してきたエンジニアも多いのではないでしょうか。

ラクスルではXPでTDDを実践しはじめてからかれこれ1年近く経ちますが、なぜ今までTDDはうまくいかなかったのか、いままでとは何が決定的に違うのか、というのをそれとなく考えてみたところ、「バックログをBDD形式で書きはじめた」ことがTDDの実践に大きく影響を与えているのでは、と思うようになりました。

テストに入りやすいストーリーの書き方とは?

いざTDDでやってみよう、と思ったときに一番困るのが、「ストーリーに書かれている要件は理解したが、俺は何をテストすれば良いんだっけ?」と、テストケースそのものが想像できずに悩んでしまうことです。

例えば、「ユーザーは、届け先の住所を郵便番号検索で自動入力させることができる」というストーリーに対してテストケースを作れって言われても、一見シンプルそうなのにどんなテストケースを書けばよいのか、わかるようで正直いまいちわからない。

この時にAS/GIVEN/WHEN/THEN というBDD形式でストーリーを書くと、テストに簡単に入ることができます。(BDDはTDDの流儀の一つと考えると、当たり前っちゃ当たり前なんだが)

もともとこの書き方は、Pivotal Labsとの協業で学んだバックログの記述方法なのですが、この形式で定義されていると、圧倒的にテストに繋げやすい。

いままで自分は、BDDの定義は「エンジニアのお仕事」という理解だったのですが、プロダクトマネージャーが行うことで、エンジニアに要件がより正確に伝わり、結果エンジニアの考える工数を大幅に減らせることがわかりました。

書き方は以下の通り

  • AS – 誰が?
  • GIVEN – 前提条件: テストケースに入る前のシステムの状態は?
  • WHEN – どのような操作や入力があるのか?
  • THEN – その操作や入力のあとに期待すべき結果は?

AS

ここではシステムを使うステークホルダーの名称を書きます。「エンドユーザー」「管理者」「未登録のビジター」等の役割でも良いです。より良いのはオペレーターの「佐藤さん」調達管理の「鈴木さん」ヘビーユーザーの「加藤さん」等、チームで定義したペルソナの名前を表記すると、その人に対する価値提供をよりイメージしやすくなるかと思います。

GIVEN(OPTIONAL)

これはテストの前提条件を表します。例えば、

  • ログインしている状態
  • 「仮受付」の注文データが既に登録されており、支払いの入力が済んでいる状態

等、ユーザーの操作が行われる前に、システムがどういう状態にあるのかを明確にします。

そうすることで、エンジニアがテストを書く際、ログイン状態を作っておいたり、それに必要なフィクスチャーを準備することができ、テストの書きやすさにダイレクトに効いてきます。

ちなみにGIVENは OPTIONAL と書きましたが、たまに「ログイン状態に決まってるだろ」みたいに冗長になることが多いので、前提が書かずとも明確な場合は省いても良いと思ってます。

WHEN

ASで指定したユーザーはどのような操作を行うのか?平たく言えば、システムに対するインプットの定義を記述します。例えば、

  • 届け先フォームの「郵便番号」に「1060047」と入力すると
  • 「検索」ボタンを押すと

等。

かならずしも入力の伴わない行動もあります。例えば導線をクリックするとか。その場合は

  • AS ユーザーとして
  • GIVEN ログイン状態でマイページを表示しているとき
  • WHEN グローバルナビゲーションから「お問い合わせ」をクリックすると

といった形で、「結果をトリガーするアクション」を記述すれば良いのだと思います。

あるいは、バッチスクリプトなど、人間が行動を起こさない場合でも、「AS バッチ WHEN 9:15AMにバッチが起動すると」と記述することも可能です。

THEN

最後にその結果、何を期待するのかを記述します。例えば、

  • 「検索」のボタンが押下可能になる
  • 都道府県=「東京都」、市区町村=「港区南麻布」が自動入力される

等。これはテストにおいて、 assertion に表される部分です。

AND(OPTIONAL)

ちなみに、THENで期待することが二つになったり、アトミックな操作が後続する場合は、WHEN/THENを複数定義して、ANDを使って結合したりします。例えば、

AS ユーザーが
WHEN 届け先の郵便番号に106-0047と入力すると
THEN 「検索」ボタンが押下可能になり
AND
WHEN 自動記入のボタンを押下すると
THEN 都道府県=「東京都」、市区町村=「港区南麻布」が自動入力される AND 番地のフィールドにカーソルが移動する

等。ただし、上記のように、詰め込みすぎな案件が出来上がってしまうので、濫用するのはおすすめしません。このような形になるなら、「検索ボタンのステータス変更」「自動記入」と、2つのストーリーに分解するほうがよいかもしれません。

また分解することで「検索ボタンの押下可能制御は、ユーザビリティーの問題なので後回しにすっか」という意思決定もできるようになります。

実践例

実際、我々が運営する物流サービスの「ハコベル」のバックログを上記の書き方にして見ました。Before〜Afterで一例を見てみましょう。

この例は、いわゆるスマホアプリ内でみる「通知設定」的な機能です。

荷主様より新たな配送案件をお預かりする際に、ドライバーが使っているアプリケーションに新規案件が入ったことを知らせるためにプッシュ通知をしているのですが、ドライバーの方が休暇を取られたりする際にも通知が届いてしまい、ノイズが多くなってしまったので、アプリケーション側で通知の設定を行えるようにしたい、という案件です。

これが元々のストーリーです。

題名:ドライバーは、プッシュ通知、メールの新着通知ON/OFF、ONの時間設定をすることができる

詳細:
・設定ページ上でプッシュ通知のON/OFF設定ができる
・設定ページ上でメール通知のON/OFF設定ができる
・ONの場合は00:00~00:00で30分間隔で通知設定を行うことができる
・OFFになっていても、案件は従来どおり開示される
・XXXXXXの場合は、XXXXXXを優先する (企業秘密❤)

これがBDD形式にすることで、このように書き換わりました。

題名:ドライバーは、プッシュ通知、メールの新着通知ON/OFF、ONの時間設定をすることができる

AS ドライバーとして
GIVEN ログインしている AND 案件一覧を表示しているとき
WHEN 設定画面の⚙アイコンをタップすると
THEN 設定内容が表示されデフォルト値が設定されている(ON)
like
| プッシュ通知 | *ON*/OFF |
| 通知時間設定 | ON/*OFF* |
| 通知時間設定 | 00:00 ~ 00:00 |
| メール通知 | *ON*/OFF |

主語はもともとストーリーの題名ではっきりしていたものの、一体どの画面で何を期待しているのか、がエンジニアからみてもかなりクリアになり工数付けがしやすくなります。

因みにこの例では、 like 〜 (和訳:〜の様に)とありますが、このように表をつかったり画像を埋め込んだりして、どのようにそれが見えるのか、といった補足情報を入れる場合もあります。

元々の要件では、ドライバーが通知の設定画面を表示し、変更をDB反映させたり、実際プッシュ通知の制御をすることも同ストーリー内で定義されてました。

このストーリーは、表示・保存、さらに設定を適用した通知の振る舞いを変える、さらに(企業秘密❤)と4ストーリーに分解されました。この分解を行ったあと、元の要件に記述されていた(企業秘密❤)については、まずは、上記3点をリリースしてみて、その後に考えよう、ということになりました。つまり3つさえ終われば、「無駄な通知は届かない」という価値提供ができ、4点目の実装をまたずに先にリリースすることができるのです。

これくらい明確になれば、Request Spec で、設定ページにアクセス→デフォルト値が設定されている、というテストをすんなり実装できそうです。その後、実装に入り、テストをパスさせることだけに集中すれば、最小工数で実装を終わらせることができます。

TDDってなんでやるんだっけ?

TDDの目的には、テストカバレッジが上げることや、リファクタしやすさ、将来の変更に対する保険といった見方もあると思いますが、もっと本質的なメリットとしては「無駄な開発をしない」という点かと思います。

実装をしていると、例えば「あ、ここは直しておきたいな」「こういうUIの気遣いがあるとユーザー喜ぶんじゃないか」など、ついつい「やっておこうかな、やっておきたいな」という衝動が湧いてきます。このような「ムラムラ感」に対して「いや、やめておこう。いまは、このテストをPASSさせることだけに集中しないと」と抑制が効いてきます。

ぐっとこらえる!

もちろん、そういったムラムラ感は大事です。でも、事業的には、今開発していることは他にやりたいことを犠牲にして優先順位を上げてやっているわけで、その開発に対する投資(つまり開発の時間的な投資)は最小限、すくなくとも計画通りにしておきたいとも思うでしょう。

同じ価値提供をしているのに、工数が2倍に膨れ上がってしまっては「そんなに時間がかかるのであれば他のことをすればよかった」とその投資の正当性が崩れることもあります。開発をTDDで行うと、テストを通すことを最優先にするため、必要最低限の開発に留めることが可能になるのではないでしょうか。

ここはぐっとこらえながら、そのリファクタリングのアイディアは、次のHack-It Day向けに貯めておきたいところです。(*Hack-It Dayは月に一度、ラクスルのエンジニアが自由に開発することができる日)

結論

さて、最後の方はちょっと脱線しましたが、ここまで開発スコープが明示化されていると、少なくともRequest Specを書くことは圧倒的に容易になりテストドリブンの実践がかなりラクになります。

「TDDが出来ないのは俺(エンジニア)が悪いんじゃない!プロダクトマネージャーが悪かったんだ!」って言いたい訳ではないが、「要件ってどうやってエンジニアに伝えればいいんだろう?」と実際悩むプロダクトマネージャー(PM)の方も居ると思うので、この手法はオススメです。

半面、実際に書いてみると意外と難しい作業です。この粒度で要件を定義するためにはそれなりに深く考える必要があります。主語を特定すること、システムにどのような前提があるのか、何をインプットすると何がアウトプットとして出てくるか。慣れないとなかなかチャレンジングな作業だと思います。

しかしPMが「自分が実現させたいことをもう一度整理してみよう」というきっかけにもなりますし、開発が終わった段階で受入テストをする真面目なPMであればその手順が明確なので、スムーズにテストをすることができるかと思います。

また副次的な利点としては、上の例にあるように自然に「分解」されることかと思います。大雑把な要件からこの書き方に変えると、自然に「1ストーリー1アクター1要件」に絞られてくるので、「あ、これだったらもう一つストーリー切らないと」といった感じに、分解が進みます。

それによって「一旦こっちを優先しよう」「これは後回しでいいや」「お、一旦ここまで終わってれば機能リリースできるじゃん」と、より俊敏に動くことができそうです。この「柔軟性」がオプションとして後々選択肢に加わるのであれば、もう少し頑張って定義する価値もあると思います!

RubyKaigi 2017 in 広島!!!

RubyKaigi 2017 in 広島、昨日から3日間の日程で広島国際会議場で開催されています! 直前に台風の通過で天気が危ぶまれましたが、蓋を開けてみたら3日間快晴! 心地よい秋晴れです。

ラクスルもスポンサーとしてブースを出展中。今年も世界各国から、Ruby開発者やRubyを取り入れている企業の方など国際色豊かな雰囲気でカンファレンスは執り行われています。今年のラクスルブースへの最初のお客様はカナダの開発者の方で、弊社エンジニアの吉岡とともに最初から英語で企業説明したりと、盛り上がってます!

今年もカンファレンス参加者の方からお陰様で「ラクスル使ってます!」「最近入稿の仕組み便利になりましたね!(スピードチェック入稿)」などと嬉しいコメントいただきました。開発者としては身の引き締まる思い・・・。

RubyKaigiはコアな開発者が集まる会議とあって、最前線の情報を収集できる貴重な場とあってエンジニアメンツとしても貴重な時間です。ラクスル、ハコベルではRuby/Railsを用いて各種APIやWebシステムを開発しており、参加したエンジニアはブース応援傍ら気になるトークセッションに聞き入ってます。印刷のラクスルでどういうことにRuby使ってる?… とご興味のある方は是非お声がけください。

P.S. 今年も弊社技術顧問のまつもとゆきひろ先生にお立ち寄りいただきました。お忙しいなかありがとうございます!

脆弱性スキャナ「vuls」を導入 & updateしました

こんにちは。もうすっかり老害になったインフラエンジニアの渡邉です。

ようやく朝方涼しくなってきましたね。犬の散歩が捗ります。

突然ですが、みなさん脆弱性対応ってしてますか?

今に始まったことではないですが、特にインフラエンジニアにとっては脆弱性対応はとても面倒で手間がかかる頭の痛いタスクになっています。

というわけで今回は、弊社のraksul.com/hacobell.com環境に巷で話題の脆弱性スキャナ「vuls」を導入した話をしたいと思います。

続きを読む

ペアデザイン

ラクスルでデザインを担当している中村です。

ラクスルに入社してはや半年が経ちました。
ラクスルでは、ここ最近は主にUCD(ユーザー中心設計)などを取り入れたUX活動やUI設計・プロトタイピングなどをしています。

さて、今日は先日実践した「ペアデザイン」についてお話ししたいと思います。

続きを読む