wataメモ

日々のメモをつらつらと書くだけ

Ruby on Railsでサービスを作ってみたpart4

 さて今回も引き続きRuby on Railsでサービスを作ってみたメモを書いていく。前のpartはこちらからどうぞ。(part1part2part3)今回で画面の機能、開発周りとしては最後にしたい。

アンケート結果

画面キャプチャ

f:id:wata_htn:20150930173811p:plain

ヘッダー周り

 上部に回答人数や、質問数を一応表示させている。アンケート情報については単にタイトルしか無いのでそれを出しているだけ。回答が増えていくのを時系列に折れ線グラフ化しても面白かったかも知れない。時間軸を指定して、その時点での投票ランキング等も遡れて見れるというのも良いだろう。

回答結果

 質問のタイプによって右側の結果表示が異なる。単数選択の場合は円グラフ、複数選択の場合は棒グラフ、フリーフォーマットの場合は単なるリスト、日程調整なら横の積み上げ棒グラフ+表とした。

 どうでも良いが、やろうと思えば「ボケて」みたいなことも出来るのではと思った。適当な画像を入れて質問にボケを入れる。選択しは☆の数とする感じだ。ということでボケての殿堂入りからネタを拝借して作ってみた。

f:id:wata_htn:20150930181653p:plain

 うーん。やっぱり画像の下にボケが無いとわかりづらくてダメだった。

レスポンシブ

 もちろんスマフォでも見やすくするために画面サイズによって横並びではなく縦にスタックされるようにしている。

f:id:wata_htn:20150930175627p:plain

実装について

 そんなに真新しいことはなく、今まで紹介してきた感じで作成している。画像はsmall_lightのサイズ指定、テンプレートはenumによるpartial化。書いていて今思ったがここでgonのwatchを使えば開いておくだけでリアルタイムにグラフを更新することが出来そうだ。まあ、そんなに勢い良く更新される想定でもないのであまり意味がある機能かは分からないが。

 Highchartsも普通に使っているだけなので、特に面白いことも無い。強いて言えば回答結果は同じanswersテーブルに格納しているので、データはJSONをカラムに入れる形を取っている。JSONをカラムに入れると、様々なデータを保持することが出来便利なのだが、DBで検索が出来なくなる等の弊害もある。今回は使わなかったがモデルのカラムにJSONが入っているパターンのvalidateを使っても良かったかもしれない。ただ、JSONの中にはシステムで作っているのと、ちょっとデータパターンが色々違うのであまりやるメリットがあるかどうかは疑問。

まとめ

 Railsにはbefore_action、Strong Parameters、ルーティング、ルーティングのパスによる指定等DRYの原則を順守したり、変更時に変更箇所を最小限にすることや、変更するにしても場所をわかりやすく出来るようになっている。もちろんこの当たりは知っていないと出来ないことだったりするので、この辺の情報をきちんと把握することがRails開発での生産性に大きく影響するのだろう。Ruby開発という意味ではもちろん有用なgemを知っていることも大きく影響するのは間違いないが。

次回について

 さて、画面、機能周りが終わったので次は開発環境周りの事を書いていこうと思う。

Ruby on Railsでサービスを作ってみたpart3

 今回で3回目の「Ruby on Railsでサービスを作ってみた」記事投稿。part1part2はこちらからどうぞ。では今回も画面や機能の開発周りの続きを書いていく。詳細なのは書いてもしょうが無いかと思って掻い摘んでいると、逆にブログだけだとわからない感じになっている気がするが気にしない。もう少し理解したい人は是非ソースを読んでフィードバックを貰えると嬉しい。

アンケート編集

画面キャプチャ

f:id:wata_htn:20150930140945p:plain

アンケート情報

 上部にアンケート情報、と言ってもタイトルと非公開、画像ぐらいの設定しか無いが設定出来、「質問追加」ボタンを押すと質問入力フォームを動的に追加して行ける。質問フォームは「回答パターン」によって表示がかわり、単数または複数の場合は回答候補を動的に追加していくことが出来る。
 一応スマフォでも投稿出来るように、簡単なレスポンシブ画面になっている。右上の削除ボタンは画面サイズ的に厳しかったので、狭まると単なる「☓」ボタンに変化させた。

f:id:wata_htn:20150930174316p:plain

非公開アンケート

 非公開の場合はURLをユーザに通知するのと、非公開であることを少しでもわかりやすくするために見た目を変えている。これもGitHubのprivateリポジトリイメージ。

f:id:wata_htn:20150930174524p:plain

アンケート削除

 削除の場合は取り消せない操作なので、ユーザにアピールを強めに行うUIにしている。全画面Dimmerにして、確認事項を注視出来るようにした。

f:id:wata_htn:20150930174740p:plain

未実装機能

 結構アンケートは作るのが大変なので、テンプレート機能や、編集中にうっかり別画面へ遷移した時のalertを出すことや、現状の状態をlocalStorageに入れておいて、うっかりブラウザを閉じてもlocalStorageに残っている場合は復元させる等も良いだろう。今回は時間の都合上割愛。

実装について

 編集画面の実装を紹介するにはまず、どこがどのタイミングでレンダリングされているかを示す必要がある。アンケートの質問は複数登録可能で、質問に対する回答も複数登録することになる。すべてをサーバサイドでレンダリングしてしまうと、質問を追加する度に画面をリフレッシュしたりしなければならないので、質問以降はクライアントサイド(JavaScript側)でレンダリングを行いたい。

f:id:wata_htn:20150930141224p:plain

 こうすることで、画面のリフレッシュは防げるが、初回表示の時に質問をサーバサイドでレンダリングしてしまうと、今度はテンプレートの共有化問題が発生してしまう。サーバ側のテンプレート(今回で言えばslim)とクライアント側のテンプレート(今回で言えばHandlebars)で同じ質問をレンダリングする為のテンプレートが別れてしまうということだ。JavaScript側でも昨今はサーバサイドレンダリングの技術があるのでそれを使っても良いし、サーバから返すデータをJSONではなくhtmlにして単純にappedすることも出来る。リクエストが毎回飛んでしまうのもアレなので今回は素直にクライアントサイドにすべて寄せることにした。
 アンケート情報は通常のslimでレンダリングを行い、質問の情報はgonを使ってhtml上にJSONレンダリングしている。特に今回はwatch機能は使っていないので無理にgonを使う必要な無かった。

app/views/survey/edit.html.slim

  | $(function() {
      var edit = new SurveyEdit();
      edit.init();
      $(gon.questions).each(function(i, v) { // ここでgonというグローバル変数に入っているquestionsを使ってクライアントサイドでレンダリングを行っている
        edit.add_question(v); // 下で紹介しているadd_questionメソッドに渡している
      });
    });

app/controllers/survey_controller.rb

  def set_questions
    # ここでgonに必要なデータを代入している
    # 色々コントロールしたかったのでas_jsonした物を代入
    gon.questions = @survey.questions.as_json({
      methods: [
        :error_class, :text_error_class, :value_error_class, :value_data,
        :resize_image_url
      ],
    })
  end

 今回はhandlebars_assetsを利用しているので自分でcompileしなくてもいいし、テンプレートも最初から別ファイルで管理出来るようになっている。(前はscriptタグでテンプレートを埋め込んでいた)その場合にJavaScript側から参照するには「HandlebarsTemplates」というグローバル変数から参照するのだが、長いので公式ドキュメントのように「JST」に変更している。

app/assets/javascripts/survey_edit.coffee

  add_question: (v) =>
    $('#add-q-pos').append(JST['survey/question'] v) # ここでHandlebarsテンプレートにデータを渡してレンダリングしている
    $('select').dropdown()
    $('select[name^="surveys[questions_attributes]"]', '#q-' + v.id).trigger('change', [true])

 このテンプレートの別ファイル管理および自動コンパイルは非常に便利だと感じた。このブログを書いている時点では「.slimbars」対応はしていないので通常のhtmlで記述しているがテンプレートがslimで書けるのも魅力的だ。

ディレクトリ構成イメージ

app/assets/javascripts
|--answer.coffee
|--app.coffee
|--application.js
|--question.coffee
|--survey.coffee
|--survey_edit.coffee
|--templates # ここにテンプレートを置く
   |--survey
      |--_multiple.hbs # 「_」から始まるファイルはPartialsとなる
      |--_single.hbs
      |--question.hbs
      |--single.hbs

 画像周りで前回small_lightを使っているという事を書いたが、前回はサーバサイドのレンダリングの時だったので綺麗に実装出来たが、クライアントサイドのレンダリングとなると話が違ってくる。今回は質問の画像がそれにあたるわけだが、画像のサイズはコントローラとかで指定するのではなく、テンプレート側で指定したい。しかしクライアントサイドに来た時には既にJSONになっており、後からURLを変更するのがちょっと面倒なことになってしまう。  そこでJSONとして出力するときにはプレースホルダーを埋め込んだ状態で出力し、Handlebarsでレンダリングする時にHelpersで独自に関数を用意して置換する方法を取った。JSONかでは先程紹介したas_jsonのところで呼び出すメソッド(resize_image_url)を追加している。

app/models/question.rb

  def resize_image_url
    return '' unless self.image.url
    path = self.image.url.gsub Survey::Application.config.image_asset_host, ''
    File.join Survey::Application.config.image_asset_host, "/resize/w/:dw/h/:dh/", path # ここで「:dw」と「:dh」というプレースホルダーの状態を作る
  end

app/assets/javascripts/survey_edit.coffee

Handlebars.registerHelper 'resize_image_url', (url, opt) ->
  return '' if url == null
  url.replace(':dw', opt.hash.dw).replace(':dh', opt.hash.dh) // ここでプレースホルダーを置換している

 Handlebarsのテンプレート上の記述は以下のようになる。上のソースでregisterHelperしたresize_image_urlの第2引数のoptに渡すために「dw="400" dh="400"」という記述を追加している。

app/assets/javascripts/templates/survey/question.hbs

<img src="{{#resize_image_url resize_image_url dw="400" dh="400"}}{{/resize_image_url}}" class="ui image fluid">

 これでテンプレート側で画像サイズの指定が出来るようになった。
 次は画像の削除だが、新しい画像と差し替えたい場合は気にせず新規の画像をアップロードして貰えばいいが、削除だけしたい場合はCarrierWaveの仕様に乗っ取る必要がある。CarrierWaveは画像を削除させたい場合は「remove_#{column_name}」というパラメータを送ることでよしなに対応してくれる。この「column_name」はモデルのカラムでCarrierWaveがmountするカラムとなる。CarrierWaveの設定周りはpart1のCarrierWaveの設定を参照して欲しい。前回は紹介しなかったが、ファイル名を保存するカラムに設定で生成したUploaderをmountする必要がある。

app/models/surveys.rb

mount_uploader :image, SurveyUploader

 後はhtml側でhidden値を追加し、削除ボタンが押されたらhidden値を書き換えるようにしている。

f:id:wata_htn:20150930170711p:plain

app/views/survey/edit.html.slim

.blurring.dimmable.image.survey-image
  .ui.dimmer
    .content
      .center
        / 削除用dimer+button
        .ui.inverted.button.del-i
          | 削除
  .image
    = image_tag_resize @survey.image, dw: 400, dh: 400, class: 'ui fluid image'
/ hidden値をデフォルトfalseで入れておく
input type="hidden" name="surveys[remove_image]" value="false"

app/assets/javascripts/survey_edit.coffee

# hoverでdimmer+ボタンを表示させるように設定
$('.survey-image').dimmer {on: 'hover'}

# 削除ボタンを押されたらアニメーションしてhidden値書き換え
$(document).on 'click', '.del-i', ->
  $(@).parents('.survey-image').transition('jiggle').transition('scale')
    .parent().find('input[name="surveys[remove_image]"]').val true

 今度はレコードの更新処理について。親子関係(今回で言うとアンケートと質問の関係)になっていて、子供が複数の場合、追加する分にはパラメータを渡せばいいし、更新の場合はidを渡せばハンドリングは出来る。ただ、削除の場合は普通にやってしまうとパラメータが渡ってこないので通常だと面倒な処理だがRailsactiverecord)なら簡単に出来る。

 まずは親のモデルに子供とまとめての更新、削除を許すように設定する。

app/models/surveys.rb

accepts_nested_attributes_for :questions, :allow_destroy => true

 こうしておけばパラメータ名として「surveys[questions_attributes][][text]」みたいに渡せば後はよしなにやってくれる。わかりづらいのはリレーション名は「questions」としているがその場合の一括更新パラメータ名は「questions_attributes」となることだ。決まった数のレコードまでしか登録出来ないUIならまだしも、動的に上限なしの場合はどうしても動的にフォームを作成する必要があり、その場合はJavaScript側のテンプレートでパラメータ名を指定しなければならない。この時にはこのルールを知らないと途方にくれてしまう。
 削除の場合はパラメータを渡さないのではなく「_destroy」パラメータを渡すことで削除が行われる。その為、既にDBに存在していた質問の削除のボタンを押すと、画面上から消すがhiddenでパラメータは渡すようにしている。もちろん、今回の登録で質問を追加して、DB登録する前に削除する場合はDOM毎削除している。途中の削除やらを気にしたくないので、インデックスを指定していない都合上、パラメータのインデックスズレを起こさないように基本的にパラメータは削除だろうが更新だろうが、ワンセット送った方が良いだろう。

app/assets/javascripts/survey_edit.coffee

$(document).on 'click', '.del-q', ->
  id = $(@).data 'id'
  if id # DBに存在している場合は「_destroy」hidden値を書き換えてfadeOutさせる
    $("#q-#{id}").find('input[name="surveys[questions_attributes][][_destroy]"]').val true
    $("#q-#{id}").fadeOut 200
  else # まだ登録されていない場合はDOM毎削除
    $(@).parents('.question').fadeOut 200, ->
      $(@).remove()

 ここまでお膳立てが整えばコントローラ側は非常にシンプルに出来る。

app/controllers/survey_controller.rb

@survey.update! survey_params

 survey_paramsはStrong Parametersの結果を返すメソッドだ。子供のパラメータの指定も一挙にやっている。

app/controllers/survey_controller.rb

def survey_params
  params.require(:surveys).permit(:name, :is_result_secret, :is_secret, :image, :remove_image,
    questions_attributes: [
      :id, :text, :type, :value, :no, :_destroy, :image, :image_cache, :remove_image
    ])
end

 後は親のリレーションでdependentとかautosaveとかを設定しておく。

app/models/surveys.rb

has_many :questions, -> { order(:no) }, :dependent => :destroy, :autosave => true

次回について

 今回も長くなってきたのでこの辺で。次回は画面の機能、実装周りの最終回としたい。

Ruby on Railsでサービスを作ってみたpart2

 今回は前回の続きということで、画面や機能の開発周りの事を書いていく。画面開発自体は2日ぐらいでがっと作っただけなので、そんなに複雑でもないし画面数が多いわけではない。サイトマップは以下のようにシンプルな構成だ。

f:id:wata_htn:20150929232058p:plain

アンケート一覧

画面キャプチャ

f:id:wata_htn:20150929232255p:plain

ヘッダ周り

 URL的にはTOP画面となり、「survey!」の起点となる画面だ。ヘッダには左上がロゴ的なイメージでTOP画面へのリンクとなっている。右上のOctocatはsurveyのソースが公開されているGitHubリポジトリへのリンクとなっている。 「お申し込みはこちら」ボタンは現状では飾りだ。連打したければいくらでも連打出来る。作り的にはBacklogの様にサブドメイン毎にアンケートが別れるようになっている。例えば「aaa.survey.example.com」と「bbb.survey.example.com」は別々の一覧となり、互いのアンケートを見ることが出来ない様になっている。イメージとしてはIP制限や何かしらの認証を掛けて、認証が通った人のみに閲覧できるようにすれば良いなと考えた。(現状は実装してないけど)無記名とはいえステートレスなので同じユーザを特定しなければならない。その為にはCookie(本当は年単位のやつを発行したいが今はしていない)にuuidを保持させそれを持って同一ユーザとして特定する予定。

新規作成ボタン

 「新規作成」ボタンを押すとモーダルでアンケート新規作成を行う。いわゆる「登録画面」を設けなかったのは「新規登録」はAPIで行い、その後は必ず「編集画面」へ遷移させることで、登録と編集の画面を一元化させる目的がある。もちろん「partial」でフォームの内容を共有したりすることで一元化させることも可能だが、今回はこの形で進めることにした。

新規登録モーダル

f:id:wata_htn:20150929232934p:plain

 「survey!」は無記名を基本としているので、結果画面は途中だろうがなんだろうが誰でも見えてしまう。「最終的な結果が出るまでは途中経過も見せてくない」という場合野為に「非公開」という機能を用意した。これはチェックを付けて新規作成すると、ハッシュ値が生成され、それを知らないとアクセス出来ないようにしている。なので新規登録した人はこのURLを覚えておく必要がある。

実装について

 モーダル表示自体はSemantic UIの機能を使って表示しているだけ。「OK」ボタンを押すとAjaxでアンケート登録APIを叩き、OKなら編集画面へJavaScript側で遷移させている。かなりパワフルなコードなので参考にはしないほうが良いだろう。

  $('.ui.modal.reg').modal 'setting', {
    onHide: ->
      return unless reg
      h = if hash then "?hash_key=#{hash}" else ''
      location.href = "#{location.href}survey/#{sid}/edit#{h}"
    onApprove: ->
      s = $('#is_result_secret').prop('checked')
      $.post 'survey', {surveys: {name: $('#name').val(), is_result_secret: s, hash_key: ''}}
        .done (r) ->
          return alert 'アンケートのタイトルを入れてください' if r.is_success != 1
          reg = true
          survey = r.results
          sid = survey.id
          hash = r.results.hash_key if survey.is_result_secret
          $('.ui.modal.reg').modal 'hide'
      false
  }

 軽く解説すると、モーダルを表示させ、OKボタンが押された場合はonApproveコールバックが呼ばれるのでそこでAjaxを投げている。そのレスポンスデータが成功だった場合はhideを呼び出し、onHideイベントでlocation.hrefで移動している。ちゃんとエラーをハンドリングし、ユーザにエラーの内容を返すべきだが横着してこの辺はまだ出来ていない。

ページャリンク

 ページャの部分はKaminariのデフォルトの動きを使っているだけなので特に書くことは無い。

一覧

実装について

 Kaminariを使っての実装なので基本は簡単だが、サブドメイン対応を行うためにScopeを1つ作っていてそれを利用して絞り込んでいる。

app/controllers/welcome_controller.rb

@surveys = Surveys.page(params[:page])
  .joins(:group)
  .eager_load(:questions)
  .current_group(request.subdomain) # ここでsubdomainを渡している
  .order(:updated_at => :desc)

app/models/surveys.rb

scope :current_group, ->(subdomain) { joins(:group).where(groups: {name: subdomain}) }

サムネイル

 一覧部分に関しては、一番左にアンケートに画像が登録されていればそのサムネイルが表示される。後のpartで紹介する予定だが、画像の表示にはsmall_lightを使っている。画像自体はCarrierWave + fogにS3にアップロードさせて、nginxに組み込んだsmall_lightがリバースプロキシの形でS3にアクセスし、内部的に要求されたサイズの画像にリサイズして返却している。もちろんその結果はCloudFront経由で配信している。今回は最終的には幅と高さを自由に指定出来る形にしているが、本来は指定出来るサイズは制限させるべきだろう。

実装について

 画像のURLについてはCarrierWaveのデフォルト実装のpublic_urlの結果では、small_lightが求める形には出来ない。そこでinitializerでHelperに「image_tag_resize」を追加し、パワフルに解決している。

config/initializers/image_tag_helper.rb

module ActionView
  module Helpers
    module AssetTagHelper
      def image_tag_resize src, opt = {}
        path = src.url.gsub Survey::Application.config.image_asset_host, ''
        opt[:src] = File.join Survey::Application.config.image_asset_host, "/resize/w/#{opt[:dw]}/h/#{opt[:dh]}/", path
        tag("img", opt.except(:dw, :dh))
      end
    end
  end
end

 テンプレート側で表示すべきサイズを渡したいのでoptionとして「dw」と「dh」というのを追加した。大元のtagに渡す時には不要なので使った後は削除はしている。テンプレートでの使い方のイメージとしては以下になる。dwが幅で、dhが高さだ。

= image_tag_resize s.image, dw: 50, dh: 50, class: 'ui image'

アンケート回答リンク

 次がアンケートのタイトルに回答画面へのリンクが貼られている。「非公開」なアンケートは背景が黄色く(GitHubのprivateリポジトリイメージ)なり、タイトルの横に鍵マークが付く。「非公開」アンケートでも回答自体は誰でも、いつでも行うことが出来る。

作成、更新

 あまり出してもそこまで意味が無い情報だが、通常の日時と「time_ago_in_words」を使いたい為だけに表示。軽くレスポンシブ対応もしているので、スマフォのポートレートモドで見るとサムネイルや作成、更新カラムが消えるようにした。

f:id:wata_htn:20150929234250p:plain

編集、結果リンク

 それぞれ「編集画面」、「結果画面」へ遷移するリンクとなっている。「非公開」なアンケートはdisabledになっており押すことは出来ない。もちろん直リンクしてもハッシュ値を知らないと404を返すようになっている。

アンケート回答

画面キャプチャ(質問画像無し)

f:id:wata_htn:20150929234918p:plain

画面キャプチャ(質問画像有り)

f:id:wata_htn:20150929235024p:plain

ヘッダー周り

 アンケートのタイトルが表示されている。その下のStepはアンケートの質問数と現在自分がどこまで答えているかを表している。答え終わった質問については緑のチェックマークに変わり、それはリンクとなっていて、いつでもその質問に戻ることも出来る。

アンケート画像

 各質問毎に画像を登録することが出来、登録されている場合は表示される。ここも同じくsmall_lightとCloudFrontでの配信となっている。

回答

 複数選択か単数選択、フリーテキスト入力、日付選択(調整さんの様な機能)がある。

フリーフォーマット

f:id:wata_htn:20150929235844p:plain

日付選択

f:id:wata_htn:20150930000131p:plain

実装について

 まずはルーティングについてだが、今回はresourcesのネストをすることで指定を行っている。アンケート(survey)には複数の質問(question)があり、それには回答(answer)がある形になっている。具体的なルーティングは以下のようになっている。

config/routes.rb

resources :survey do
  resources :question, only: :show do
    resources :answer, only: [:create, :update]
  end
end

 上記により生成されるルーティングは以下となる。

                      Prefix Verb   URI Pattern                                                   Controller#Action
survey_question_answer_index POST   /survey/:survey_id/question/:question_id/answer(.:format)     answer#create
      survey_question_answer PATCH  /survey/:survey_id/question/:question_id/answer/:id(.:format) answer#update
                             PUT    /survey/:survey_id/question/:question_id/answer/:id(.:format) answer#update
             survey_question GET    /survey/:survey_id/question/:id(.:format)                     question#show

 テンプレートについては質問の基本的な部分は同じだが、回答の部分はタイプによって違う。テンプレートの中で分岐は入れず、activerecordenumを使ってテンプレートファイルを出し分けている。

= render partial: "question/#{@question.type}"

 @question.typeが「single」「multiple」「free」「date」となるのでそれぞれのpartialようのテンプレートを準備した。例えば単一選択の場合は「app/views/question/_single.html.slim」となる。
 この様に実装することによりif分岐を無くし、テンプレートファイルを分割管理出来ることで、1つのでかいテンプレートにならず、開発保守しやすくしている。ファイル名は「partial」の場合は「_」から始める。

戻る、次へ、完了ボタン

 2問目以降だと「戻る」ボタンが表示され、1問前に戻ることが出来る、次の問がある場合は「次へ」ボタン、今回の質問が最後の場合は「完了」ボタンが表示される。

実装について

 大した話ではないが、1つ前が存在し、そのidが必要な場合はテンプレート側には実装せずにmodel側にロジックを持たせる。いわゆる「Fat Model」にならないように気をつけなければならないが、テンプレート側に持たせるよりmodelに持たせたほうがオブジェクト指向的には正しいし、テンプレート側がシンプルになる。

- if @question.prev
  = link_to survey_question_path(@survey, @question.prev), :class => 'ui small button' do

 リンクを張る時もベタでURLを書かずにパス(この例だとsurvey_question_path)を利用して記述する。

アンケート完了

 単なる環境画面でほぼ静的画面。

画面キャプチャ

f:id:wata_htn:20150930000306p:plain

実装について

 基本的にはPRGパターン実装をしているのでリダイレクトで完了画面は表示している。

# 次がある場合は次の画面へリダイレクト
return redirect_to survey_question_path(s, q.next) if q.next

# 最後の場合はアンケート回答完了ステータスにし、完了画面へリダイレクト
c.done!
redirect_to finish_survey_path

次回について

 長くなってきたので今回はこの辺で。次回も引き続き残りの画面の作り等を書いていく。

Ruby on Railsでサービスを作ってみたpart1

 Ruby on Railsでサービスを作ってみようと考えやってみた。いくつかのpartに分けて投稿して行こうと思う。まずは今回作ったサービスはアンケート作成と集計サービスだ。ソースはGithub/wata-gh/surveyに公開してある。画面イメージとしては以下。

画面イメージ

アンケート一覧

f:id:wata_htn:20150929174552p:plain

アンケート回答

f:id:wata_htn:20150929174557p:plain

アンケート編集

f:id:wata_htn:20150929174602p:plain

アンケート結果

f:id:wata_htn:20150929174607p:plain

技術要素(アプリ)

 簡単にまとめると以下。

技術要素 名前 備考
言語 Ruby バージョンは2.2.3
F/W Ruby on Rails バージョンは4.2.4
API Jbuilder
テンプレートエンジン slim+slim-rails 記述量が少ないの良い
altJS CoffeeScript+coffee-rails
JSテンプレートエンジン Handlebars+handlebars_assets Reactは選択せず
JSパッケージマネージャ bower+bower-rails
CSSフレームワーク Semantic UI Bootstrapより見た目が好きなため
グラフライブラリ Highcharts
DBマイグレーション Ridgepole 通常のmigrationが面倒なので
ページャ Kaminari+Kaminari Themes
Railsオブジェクト→JS gon Handlebarsとの連携のため
画像アップロード CarrierWave+fog S3にアップロード
テスト RSpec

開発環境

 開発環境および本番環境構築は「Infrastructure as Code」ということでプロビジョニングソースも同じくGithub/wata-gh/survey-provに公開してある。

技術要素 名前 備考
VM VirtualBox MacBookProには直接環境を作らずVMで開発パターン
VM環境構築ツール Vagrant
プロビジョニング Itamae Chefほど多機能は必要ない、簡単
コンテナ Docker+Docker Compose 開発環境での画像リサイズ、配信で利用

開発環境構築

 簡単に手順やらはまったことを書く。

Railsプロジェクト作成

 DBにはmysqlを利用し、bundleはvendor/bundle下に入れたいのでスキップさせる。

rails new survey -d mysql -T --skip-bundle
cd survey

 ただ、mysql2がRails4.2.4と相性が良くないのでmysql2を0.3.20にダウングレードするためにGemfileを編集。

Gemfile

# downgrade to 0.3.20. see https://github.com/brianmario/mysql2/issues/675
gem 'mysql2', '~> 0.3.20'

slimの設定

 Gemfileに追記。

Gemfile

gem 'slim-rails'

bowerの設定

 Gemfileに追記。

Gemfile

gem 'bower-rails'
# package.json作成
npm init
# bowerを追加
npm install bower --save-dev

# bower-rails初期化
./bin/rails g bower_rails:initialize

 Bowerfileにライブラリを追記。

Bowerfile

asset 'jquery'
asset 'moment'
asset 'handlebars'
asset 'highcharts'

 ライブラリの取得。

./bin/rake bower:install

Semantic UIの設定

 Gemfileに追記。

Gemfile

gem 'less-rails-semantic_ui'
gem 'autoprefixer-rails'
gem 'less-rails'
gem 'therubyracer' # to use less
# Semantic UI取得
./bin/rails generate semantic_ui:install

 Asset Pipeline経由で使うためにapp/assets/javascripts/application.jsapp/assets/stylesheets/application.cssに記述。

app/assets/javascripts/application.js

//= require semantic_ui/semantic_ui

app/assets/stylesheets/application.css

 *= require semantic_ui/semantic_ui

データベース作成

bundle exec rake db:create

Ridgepoleの設定

Gemfile

gem 'ridgepole'

 適当にdb/Schemafileに記述してapply。Schemafileはなんとなく直下ではなくdb配下に置いてみた。

# 開発DB反映
bundle exec ridgepole -c config/database.yml --apply -f db/Schemafile
# テストDB反映
bundle exec ridgepole -c config/database.yml --apply -f db/Schemafile --env test

RSpecの設定

 Gemfileの「group :development, :test」のところに追記

gem 'rspec-rails'

assetsのログ抑制

 assetsのログが出すぎて肝心なログが見えないので「quiet_assets」で抑制。

Gemfile

gem 'quiet_assets'

web_consoleのホワイトリスト追加

 VM上で開発するのでlocalhostからではなくなるので、ホストOSのIPを追加。

config/environments/development.rb

config.web_console.whitelisted_ips = '192.168.33.1'

Kaminariの設定

 言わずと知れたページャのKaminariをSemantic UIテイストで使う。

Gemfile

gem 'kaminari'

 Semantic UI版のslimテンプレートを使用。

./bin/rails g kaminari:views semantic_ui -e slim

CarrierWaveの設定

Gemfile

gem 'carrierwave'
# S3アップロード用
gem 'fog'

 Uploaderを作成。今回は仕様は何も変えていないが、返ることも想定してアンケートと質問それぞれのUploaderを作成。そしてなんとなく直接継承するのではなく、親クラスにまとめておいた。

# アンケート用
./bin/rails g uploader Survey
# 質問用
./bin/rails g uploader Question

unicornの設定

Gemfile

gem 'unicorn'

 config/unicorn.rbを作成。USR2シグナルで再起動が上手く行かなかったので、この部分で対応。
 ※参考URL

unicorn再起動用のrakeタスクをlib/tasks/unicorn.rakeに作成。

./bin/rails g task unicorn

 どの環境で動かすかはRAILS_ENVに依存するようにした。なので「.bashrc」から読み込む様にしている「.bashrc_local」に以下を追加。やはりTwelve-Factor Appの通り環境変数を上手く使うべきだと思う。

export RAILS_ENV=development

開発開始

 大体こんな感じの下準備を行い、後はガリガリ開発をしていく。次回は画面の作り等を書いていこうと思う。

[24時間365日] サーバ/インフラを支える技術読了

 [24時間365日] サーバ/インフラを支える技術を読んだ。

www.amazon.co.jp

 ちょっと古い本ではあるが、オンプレでもインフラ構築することもあると思い勉強の為に手を出した。「Webサーバの冗長化」や「リバースプロキシの導入」、「キャッシュサーバの導入」等半分ぐらいはある程度知っている内容ではあったが、残りの半分は全く知らないことだったので非常に面白かった。知らない部分は実際やってみないとよくわからないとは思ったが、こういう感じなのかというのは理解出来た。
 最近はAWSでインフラ環境を構築するようになって、冗長化やバランシング、レプリケーション等はあまり知らなくても簡単に使える状況だった。そこを支えている技術や考え方を知る上でもお勧めの書籍と言える。ただ、インフラ周りの知見が無いと、殆ど理解できないと思うので入門としては適さないとは感じた。

パーフェクト Ruby on Rails読了

 良書と言われている「パーフェクト Ruby on Rails」を読んだ。結論としてはRailsで開発をしようと考えている人にはお勧めだ。

www.amazon.co.jp

全体として

 全体としてはRailsの概要が記載されていた。詳説というわけではないのでリファレンスとして使えるものではないが、調べる取っ掛かりになる。範囲もRailsの使い方だけではなく、デプロイや監視、Railsの拡張まで入っているので、他のF/Wや言語で開発経験があればすんなり読め、Railsでの開発イメージが湧くだろう。逆に経験が少ないと、「なんだかよくわからない」ということになりそうではある。「ファットモデル」と言われても、モデルにロジックを書くということの意義がわかっていない人や、実際「ファット」になってしまって悩んでみないとピント来ないのではないだろうか。

他言語での開発をされている人へ

 世の中のF/WやRailsの思想や考え方を活用していることが多い。なので本流であるRailsを学んでおくことは現在使っているF/Wの理解度をあげることにも繋がる。そして今回の本にも書いてあったが、「開発のベストプラクティス」が集まった物がRailsなので、単純な知識としても知っておくといいだろう。もちろん完璧というわけではなく、あくまで一つの方向性としてのベストプラクティスである。

paizaのランク上げ

とりあえずpaizaのランクをAランクまで(最初はSランクがあるのを知らなかったので上限だと思っていた)あげようと思い、昨日、一昨日とチャレンジ。ハッカソンはやったことあっても、スキルチェックは1度もやったことなかったのでとりあえずDランクからトライした所感。

paiza.jp

ランクごとのレベルについて

 Dランク、Cランクについては小手調べ程度なので問題を理解する時間の方が長いくらいだった。Bランクについてはうっかりすると考慮漏れが出る可能性もある感じではあった。
 そしてAランク。このレベルに来ると、設計無しに単純に処理を書き並べてもコード量やバグが多くなってしまうのではと感じた。仕様から設計(コードのイメージ)を見通す力が無いとクリアするのは時間が掛るだろう。制限時間としては6時間あるようなのでベタな方法でも試行錯誤すれば100点ではなくクリアは出来るのかもしれない。

特徴について

 paizaは問題の前提で異常系は気にしなくても良い。 その分問題に集中出来るので、まさにデータやアルゴリズムを扱う能力を試すことが出来る。これは「脳内実行環境」*1の練度やキャパシティを測ることだと考えている。ただ、言語を扱うスキルを測るにはちょっと弱い気はしている。もちろん言語をきちんと把握していれば簡潔に記述することができるが、把握して無くてもクリア出来る、出来ないにはあまり影響無いという意味だ。「脳内実行環境」が出来ている人には、言語仕様をちらっと見ればどの言語でも基本的にはクリア出来ると思う。ただ、コードを見れるのであれば、その人の言語の習熟度もある程度見ることは出来るのは間違いない。
 業務プログラミングではそれ以外にも違ったスキルも求められることになる。特に異常系に関しては極力異常系を気にしなくて書ける様に集中管理等の設計は行うが、どこかでは担保したり、異常系発生時にわかりやすくログを出すなり、回復処理を行わなければならない。その為その辺の知識があるかどうかは測ることが出来ない。この品質に大きく関わる部分を設計出来る人財は業務のプログラミングでは非常に重要で、その価値はトータルで見ると天と地ほどの差になる。
 この業務プログラミングと競技プログラミングの違いを @chokudai さんは以下のようにツイートしている。

見通す力(設計力)について

 あまり意識して来なかったが、確かにこの限られた時間の中で「見通す力」はなかなか身につかない物なのかもしれないと感じた。常にトライアンドエラー、仕様から直感でコードを書き始めていることを続けていると訓練されない能力だ。クラス分け、メソッド分け、データ構造の組み合わせから最適な方法かつ大量データの処理を前提に性能を意識することになる。性能を意識する必要が出てくると有効なアルゴリズムを知っていると良いが、自分は競技プログラマではないので殆ど勉強したことはない。ただpaizaの問題については、アルゴリズムを知らなくても単純な枝刈りとメモ化、全件探索をしないように気をつけるだけで十分だった。(ナップサック問題ぐらいは勉強したが)

クリアの為に意識したこと

 美しいアルゴリズムを知らない場合に意識することとしては、データをごちょごちょ弄らないこと。(例えばArrayの要素を入れたりsliceして途中から抜いたり・・・)これをやりだすと、限られた時間の中ではデータ操作のバグ潰しに時間を取られかねない。データを綺麗に保つより、新しいメモリ空間に中間データを作ったりしてメモリ的には富豪プログラミングを行う。もちろん最初にお題のデータ件数想定を確認はするが、万が一これがダメな場合はその時考えようというスタンスだ。まあ、幸いpaizaの問題では1度としてメモリが足りなくなる様なことにはなっていないが。
 設計については、オブジェクト指向カプセル化)で行い、処理を書く部分は関数型で書く。経験上これが一番バグらずに書ける。このバランスの取りどころは言語化が難しいが、感覚的には 関数型で綺麗に書くために オブジェクト指向で補間する感じだ。

Aランクのクリア時間について

 問題にもよるだろうが、概ね30分前後ぐらいだった。きちんと測っていないが感覚的には以下。

  1. 5分から10分ぐらいで問題の理解および設計
  2. 10分から15分程度で実装
  3. 残り5分から10分ぐらいでテスト。
  4. 数分で最後にソースレビュー

 2.の実装はサンプルのデータを動かしながら動作確認。3.については思いつく例外パターンを想定したデータを作成してテストをしていく。最後に無駄な処理が無いか等の軽いリファクタリングを行う。 100点を目指しているので、あまり大掛かりなリファクタリングはせずブラッシュアップ程度。

最後に

 時間内に書ききらないといけないということで、気持ち的にはどうしても焦っていて問題をよく読まない場合があった。「出力を0埋めの2桁の整数に」を見逃していて100点が取れないことがあった。最初は何がバグっているのかわからず、何度もコードを見なおしてしまった。まったくわからなかったので、再度問題を読んだところ気がつくという体たらく。(自信があったのにテストケースが落ちて「まさか!」と思い、動揺していたのは秘密)
 非常にプログラミング力を向上させる、もしくは新しい言語を勉強する時の腕試しとしては有益なサイトなので、興味があれば是非利用してみては。

*1:頭の中でコードの実行、データの中身をイメージする