ユニファ開発者ブログ

ユニファ株式会社システム開発部メンバーによるブログです。

システムも世の理からは逃れられないという話。

みなさん、こんにちは。 エンジニアの田渕です。

梅雨真っ只中の今週、皆様いかがお過ごしでしょうか?
暑かったり寒かったり、じめじめしてたり……気候が安定しないおかげで、私の周囲では体調を崩す人が続出しています。

さて、今回は、エンジニアブログなのにエンジニアではない方にも読んで頂けそうな記事を書いてみようと思います。
というのも、先日、弊社の営業さんから「読んでおくと良いシステム系の本教えてください」とリクエスト頂いたのですが、なかなかご紹介できる本が無くてですね……。 ということで、自分で記事を一つ、かいてみようかなと。

エンジニアが不思議に思うこと。

長いこと、エンジニアをやっていると、エンジニアとそうではない人の間に、結構大きな感覚の違いがあることに気づきます。 その筆頭にあげられるのが、いわゆる、システムのパフォーマンス、限界についての話。 ちょうど先日も、某大手通販の会社さんがセールを始めた途端、サイトに人が殺到して、繋がりにくいという状況が起こりました。 エンジニアなら割とみんな、「あー、あるある。まー仕方ないよねー。」ってなるんですが、エンジニアでない人と話をしていると「対策しておけよ!」と結構、お怒りだったり。

昔から、うっすら私が思っていることは、
「もしかして、エンジニア以外の人には、システムの世界は簡単に限界なくなんでもできる世界に見えてるのかしら?」 ということです。 まるで、世の中の物理的な法則に縛られていない、別次元の世界の話のような。 いやいやそうじゃないんだよ、システムの世界も、現実世界と同じようなルールに縛られてるんだよ、というのが今日のお話です。

夏休みのアミューズメントパークは長蛇の列

もうすぐ夏休みシーズンですね。 夏休みになると、人気のあるアミューズメントパークの入場ゲートは長蛇の列になります。 行ったはいいけど、入場規制や、あまりの列の長さに涙を飲んだ、という過去をお持ちの方もいらっしゃるはず。 あれ、どうして起こるのでしょう? 答えは簡単で、入場しようと訪れている人の人数に対して、開いている窓口が少ないからです。 窓口が10個開いているところに11組のお客様が同時に来たら、1組は並んで順番を待つことになります。

じゃあ100組同時に来たら?200組同時に来たら? 最後に来た人は、自分の前に広がる列を眺めて、遠い目をするに違いありません。 自分が入場できるまでにとても時間がかかることが、目に見えてわかるからです。

実は、Webシステム/サイトの処理が遅くなる/繋がりにくくなる、という事象の背後で起こっていることの多くは、これと同じようなことです。 Webシステムにはあらかじめ、同時に開いていい窓口の数が設定されています。 この窓口の数よりも少ない人が一斉にやってくる分には何の問題もないのですが、この窓口の数よりもたくさんの人が一斉にやってくると、アミューズメントパークの窓口と同じく、処理の順番待ちが発生します。 ここで、現実世界なら「あ〜前に人が並んでるからね〜。」と自分の目で見て状況を把握出来ますが、Webサイトに訪れたお客様には自分の前に並んでいる他のお客様の姿など見えていません。結果、いつまでも処理がされないまま、ただ待たされ「遅い」「繋がらない」と感じることになります。

お客様に早く入場してほしい!

「いやいやいや。待たせるなんてもってのほかだよ!早く入場してもらってよ!顧客満足度ダダ下がりじゃん!」
……おっしゃる通りです。 この場合、施設側がとれる対処法はいくつかあって、

  1. 入場処理の効率化、簡略化をする……1組あたりにかかる入場処理の時間を短くして、入場できるお客様の数を増やす方法です。

  2. 窓口の数を増やす……入場処理のやり方は変えず、単純に窓口の数を増やすことで、入場できるお客様の数を増やす方法です。

システムの性能改善、パフォーマンスチューニングと呼ばれるものも、概ね同じようなことをします。 「なんだ、窓口の数増やせばいいなら、常に多目にしとけばいいじゃん。」と思うかもしれませんが、窓口は多ければ多いほどお金がかかる。それは、現実世界と一緒です。

アミューズメントパークが繁忙期の人の山に即時に対応できるだけの窓口を作らないのと同じように、 コンビニのレジがお昼時に合わせてレジの数を決めないように、 システムも基本は、「繁忙期に合わせて」ではなく、「平常時の来客に対応するに十分」なだけの窓口しか開けません。 そうじゃないと必要以上にお金がかかるからです。

状況に応じて窓口の数を増やす方法はないのか?

年賀状の配達のために某所が一時的にアルバイトを増やすように、じゃあその時だけ、一瞬窓口増やす方法ないの? と言われると、実はあります。近年ではそのあたりの技術が飛躍的に発達し、あらかじめ設定しておけば機械が自動的に窓口を増やすことも出来るようになったので、システム利用者が長蛇の列に並ぶ機会は一昔前に比べると随分減っています。ただ、「無限に増やしていいよ」という設定にはできないので、「ここまでなら窓口増やしていいよ」という感じの設定をあらかじめしておくことになります。その数を超えてしまった=当初の予想を超えてしまえば、やっぱり長蛇の列はできます。

そんな訳で、未だにインターネットの世界でも、長蛇の列に遭遇することになります。

さて、今回は、システムの利用者が並ぶ長蛇の列について解説してみました。 形がないからイメージしづらいインターネットの世界ですが、実はあまり、現実の世界と変わりません。 これからは、「読み込み中」のままでなかなかお返事が帰って来ない画面に遭遇したら「ああ、私今、窓口の前で待たされてるのね。」と思ってください。

ではでは。

イメージで覚えるReact + Redux

こんにちは、tanaka です。

Rails5.1で Webpacker が導入されて React や Vue.js などのフロントエンド技術がさらに身近になりました。ヽ( ´¬`)ノ

ということで、早速 Rails5.1 + React + Redux でちょっとしたアプリを作ってみるぜ !! とコーディングを始めたところ、

・・・・

ちょっと昔に ReactNative を勉強したときに使ったはずの Redux をガッツリ忘れているではありませんか …。 (ノ゚ο゚)ノ

そんな忘れん坊な私のためにReact + Reduxを復習し、今度こそ忘れないぞと誓うとともに記事としてまとめました。

React + Reduxの参考記事

React + Reduxについては詳しい記事がたくさんあります。 私も数回読みました。どれも分かりやすくて良い記事だと思います。

が、それでもまだチョット・・という自分のためにTodoアプリを例に出しつつ図を描きながら説明を試みます。 Reduxのスタンダードな解説はこれらの記事に譲ります。

React + Redux のキホン

覚えることは3つだけです。

  • アプリケーションのすべての状態は ストア で一元管理する。
  • ビューは state と action creator を受け取り、stateに従って描画するだけ
  • 描画を変えたいときはビューが action を発動して state を更新する。(その結果、描画が更新される)

f:id:sanshonoki:20170707113102p:plain

個人的に action は発射ボタンとしてイメージすると理解しやすくしっくりきました。 ビューは発射ボタンを受け取って<input /> などの要素のイベントに紐付けておき、マウスクリック等のユーザーイベントが発生したらそのスイッチが押されるイメージです。

Reducerは状態の管理人というイメージを持ちました。複数の管理人がそれぞれアプリケーションの部分状態を管理しているイメージです。

Reduxは下の図のような 一方向のフローとしての図をよく目にしますが↑の図のほうが実体に則している気がして私にはしっくりきました。

https://github.com/facebook/flux/raw/master/examples/flux-concepts/flux-simple-f8-diagram-with-client-action-1300w.png https://github.com/facebook/flux/tree/master/examples/flux-concepts

ビューは2種類

ビューは コンテナ と コンポーネント の2種類があります。

  • コンテナ(器)
    • ストアからコンポーネントで利用する state を受け取る
    • コンポーネントで使用する action creator のリストを受け取る
  • コンポーネント(部品)
    • コンテナから state や action のリストを渡され描画する
    • マウスクリック等のユーザーイベントがあったら対応する action を発動する (dispatch)

f:id:sanshonoki:20170707141542p:plain

つまり、コンテナが React外部とのインターフェースとなります。

この説明ではトップがコンテナになっていますがトップにコンポーネントがあり、その中に複数のコンテナがあるというパターンもあります。その場合は、各コンテナがそれぞれ自分がもつコンポーネントの描画に必要な状態やアクションをストアから取得します。

全体像をコードつきで

f:id:sanshonoki:20170707114002p:plain

connectは引数なしとありで挙動が変わるので実はややこしいのですがその辺は次の記事に詳しく書かれています。

qiita.com

実際のコード

Actions

アクションはアクション種別とアクションに付随するパラメータを store に渡すだけです。

app/javascripts/packs/actions/index.js

let nextTodoId = 1

export const addTodo = text => {
  return {
    type: 'ADD_TODO',
    id: nextTodoId++,
    text
  }
}

export const toggleTodo = id => {
  return {
    type: 'TOGGLE_TODO',
    id
  }
}

export const enableUI = () => {
  return {
    type: 'UI_ENABLE'
  }
}

export const disableUI = () => {
  return {
    type: 'UI_DISABLE'
  }
}

Reducers

Reducer は状態を新しく生成するだけのpureな関数です。引数で与える state が初期状態となります。

ポイントは

  • 元の state を編集せず 新しい state を返すこと

です。元のstateの値を編集してもビューは更新されないので要注意です。

app/javascripts/packs/reducers/todos.js

const todos = (state = [{id:0, text:"first task (click me)", completed:false}], action) => {
  switch (action.type) {
    case 'ADD_TODO':
      return [
        ...state,
        {
          id: action.id,
          text: action.text,
          completed: false
        }
      ]
    case 'TOGGLE_TODO':
      return state.map(todo =>
        (todo.id === action.id) 
          ? Object.assign({}, todo, {completed: !todo.completed})
          : todo
      )
    default:
      return state
  }
}

export default todos

app/javascripts/packs/reducers/settings.js

const settings = (state = { enabled: true }, action) => {
  switch (action.type) {
    case 'UI_ENABLE':
      return { enabled: true }
    case 'UI_DISABLE':
      return { enabled: false }
    default:
      return state
  }
}

export default settings

app/javascripts/packs/reducers/index.js

import { combineReducers } from 'redux'
import todos from './todos'
import settings from './settings'

const todoApp = combineReducers({
  todos,
  settings
})

export default todoApp

combineReducersで todos と settings の2つの partial state をグローバルな state として束ねます。このグローバルな state を store に渡します。

Containers

app/javascripts/packs/containers/App.js

import React, { Component } from 'react'
import { connect } from 'react-redux'

import { toggleTodo, addTodo, enableUI, disableUI } from '../actions'
import TodoList from '../components/TodoList'

const mapStateToProps = state => {
  return {
    todos: state.todos,
    enabled: state.settings.enabled
  }
}

const mapDispatchToProps = dispatch => {
  return {
    onTodoClick: id => {
      dispatch(toggleTodo(id))
    },
    addTodoClick: text => {
      dispatch(addTodo(text))
    },
    toggleUI: checked => {
      checked ? dispatch(enableUI()) : dispatch(disableUI())
    }
  }
}

const App = connect(mapStateToProps, mapDispatchToProps)(TodoList)

export default App

mapStateToProps関数でグローバルなstateから必要な partial state を取り出し、propsオブジェクトにマッピングしてコンポーネント(TodoList)に渡します。 同様に mapDispatchToProps関数で action creators のうち必要な action を props にマッピングしてコンポーネント に渡します。

コンポーネントからは state.settings.enabledthis.props.enabledとしてアクセスできます。 onTodoClick関数は this.props.onTodoClick(id)として呼び出せます。

Components

app/javascripts/packs/components/TodoList.js

import React, { Component } from 'react'
import { render } from 'react-dom'

const Todo = ({onClick, completed, text }) => (
  <li
    onClick={onClick}
    style={{
      textDecoration: completed ? 'line-through': 'none'
    }}
  >
    {text}
  </li>
)

const AddTodo = ({ onClick, enabled }) => {
  let input

  return (
    <div>
      <form
        onSubmit={e => {
          e.preventDefault()
          if (!input.value.trim()) {
            return
          }
          onClick(input.value)
          input.value = ''
        }}
      >
        <input
          ref={node => {
            input = node
          }}
        />
        <button type="submit" disabled={!enabled}>
          Add Todo
        </button>
      </form>
    </div>
  )
}

const TodoList = ({ todos, enabled, onTodoClick, addTodoClick, toggleUI }) => (
  <div>
    <AddTodo onClick={addTodoClick} enabled={enabled} />
    <ul>
      {todos.map(todo => (
        <Todo key={todo.id} {...todo} onClick={() => { if (enabled) { onTodoClick(todo.id) } }} />
      ))}
    </ul>
    <input type="checkbox" checked={enabled} onChange={(e) => toggleUI(e.target.checked)} />UI Enabled
  </div>
)

export default TodoList

コンポーネントは props、ここでは { todos, enabled, onTodoClick, addTodoClick, toggleUI }のオブジェクトをコンテナから受け取って描画とイベントバインディングを行います。 ユーザーがクリックしたらイベントに紐付けられた action が発動します。

今回は1つのファイルにTodoやAddTodoなどのサブコンポーネントを含めていますが通常は別ファイルで定義します。

プロダクトレベルのアプリケーションに向けて

プロダクトレベルのアプリケーションを実装するときはさらに

  • 非同期処理(API叩くときetc..)
  • ルーティングによるページ遷移
  • Global state と Local state の共存

を考えていく必要があります。 私も今まさに勉強中なので詳しくは書けませんが勉強した範囲でポイントとなりそうなことを書いておきます。

非同期処理

非同期処理のポイントは

  • データをAPIから取得するアクション
  • APIから取得したデータを state に反映するアクション

を分けて実装することです。

具体的なコードは以下のような感じになります。(react-thunkを使います)

export const addTodo = (id, title) => {
  return {
    type: 'ADD_TODO',
    id,
    title
  }
}

export const createTodoAsync = title => {
  return (dispatch, getState) => {
    const _todo = { title: title, completed: false }

    fetch('/api/todos', {
         method: 'POST',
         headers: {
           'Accept': 'application/json',
           'Content-Type': 'application/json'
         },
         body: JSON.stringify(_todo) })
    .then(response => {
      return response.json()
    })
    .then(data => {
      dispatch(addTodo(data.id, data.title));
    })
  }
}

ルーティング

複数のページをもち、ページごとにビューを切り替えたいときはルーティングを行います。 ルーティングは react-router-dom というパッケージを利用します。

基本的な使い方は medium.com が参考になります。

import { BrowserRouter as Router, Route } from 'react-router-dom'

document.addEventListener('DOMContentLoaded', () => {
  ReactDOM.render(
    <Provider store={store}>
      <Router>
        <Route exact path="/" component={MainPage} />
        <Route path="/contact" component={ContactPage} />
      </Router>
    </Provider>,
    document.body.appendChild(document.createElement('div')),
  )
})

のようにURLのパスに応じて表示するコンポーネントが選択されるようになります。

また、action(ビュー外)からページ遷移する方法は少しトリッキーになっていて以下が参考になりました。

Local stateとの共存

Redux ではアプリケーションの状態はすべてストアで一元管理することになっています。しかし、そうなると特定のコンポーネント内でしか使わない state もストアで管理することになりストアが肥大化、複雑化しすぎるという問題が出てきます。

そこで Reduxで管理するグローバルな state と各ビュー内のローカルな state を分けて管理するというやり方も考えていく必要があるでしょう。

made.livesense.co.jp

Rails + React + Redux 導入方法

最後に Rails で React を使う方法を紹介します。 RailsにReactを組み込むこと自体はかなり簡単でものの数分で準備できちゃいます。

注意点としては

  • yarnをインストールしておくこと (0.20.1+)
  • Node.jsのバージョンが古いとwebpackerがインストールできないのでバージョンを上げておくこと (6.4.0+)

ぐらいです。

手順は

  1. React込みのアプリを作成する

    $ rails new --webpack=react myapp

  2. 適当なページ作る

    $ rails g controller page hello

  3. layoutファイル修正 (app/views/layouts/application.html.erb)

    <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>

  4. サンプルのjsxを組み込む (app/javascripts/pack/application.js)

    require('./hello_react.jsx') // 追加する

  5. ビルド

    $ bin/webpack

  6. サーバーを起動してブラウザで表示 (/page/hello)

    • Hello Reactが表示されるはず

2-3分でHello Worldまで行き着けます

これだけだとreduxは使えないので 次に reduxで必要なパッケージをインストールします。

$ bin/yarn add redux
$ bin/yarn add react-redux

環境構築は以下のページを参考にしました。

あとは app/javascripts/packs/ 以下に作りたいアプリケーションを実装していくだけです。

Happy coding ! ┗|l`・ω・´l|┛

週次 1 on 1 & 180度評価

 みなさまこんにちは。ユニファCTOの赤沼です。もう6月も終わり、2017年も半年終わってしまいましたね。日本の多くの会社では事業年度は4月からで、まだ第一クォーターが終わったところかと思いますが、ユニファの事業年度は1月1日〜12月31日となっていて、6月末で半期終了ということになります。こういった節目につきものなのが、人事評価と目標設定ですね。ユニファでは基本的に半期ごとに前の半期の振り返りをして、設定してあった目標に対しての達成度を確認するとともに、次の半期の目標を決めていますので、メンバーも今まさに2017年上期の振り返りの自己評価と、下期の目標を設定している最中です。そしてその後で私と各開発部メンバーでの面談を経て最終的に評価や目標を決定するのですが、ここではその「自己評価〜目標設定〜個別面談」のプロセス以外で開発部で行なっている取り組みを少し紹介したいと思います。

週次 1 on 1

 開発部では週次でマネージャー(私もしくはサーバサイドエンジニアのリーダー)と各メンバーでの1 on 1の機会を設けています。一回あたりの時間は15分間を基本としていて、必要であればさらに別途時間を取るようにしています。アジェンダについては基本的にメンバー側主導で、悩み事や相談しておきたいことを話してもらったり、マネージャーに聞いておきたいことを聞いたりするための場としています。こういう場があることで、「わざわざそのためにミーティングを設定するほどではないんだけどマネージャーに聞いておきたいようなこと」が埋もれることは減っているのではないかと思います。マネージャー側としては各メンバーの状況を週次で確認できるので、期初に設定した目標や会社側がメンバーに期待する役割に対してズレが発生していないかを確認できる場にもなります。

 半期ごとの面談では1時間ほどかけて振り返りや目標についての話をするのですが、メンバー数も増えてきて、それぞれの状況も見えづらくなっているので、半年ごとの面談だけではメンバーとの認識のズレに気づくのが遅くなってしまいます。メンバー側としても、毎週時間を確保しておくことで、些細なことでも相談しやすくなるので、コミュニケーションの活性化に役立っているのではないかと思います。

180度評価

 評価のためのプロセスは基本的にメンバー本人と私の二人の視点で行われます。期初の目標設定時にできるだけ客観的に振り返ることができる目標にしてはいますので、数字で明確に判断できるような目標や、やったかやらなかったかなどで判断できるような目標を設定したいところですが、そういう目標ばかりとは限りません。また、目標としてはあげていなかったことや、数字としては見えて来ないところでメンバーやチームにポジティブな影響を与えていても、本人と私だけの視点だとなかなか拾うことができません。私の入社当初はメンバーも少なかったので、一緒に仕事をしていれば自然と把握できていましたが、人数もだいぶ増え、正直なところ日常の業務だけでは各メンバーの状況が把握できなくなってきています。

 そこで今回からの試みとして、本人と私以外の同僚から、その人についての評価を聞く、180度評価を取り入れてみることにしました。開発部の組織構造はほぼフラットで、上司や部下という関係もほとんどなく、また、私としても各メンバー間は対等な意識を持ってもらいたいという思いもあり、「360度評価」から上司に対しての評価の部分を除いた「180度評価」にしています。(ちなみにこの180度評価という言い方は私もつい最近知りました。)各メンバーから、前の半期に主に一緒に仕事をしたメンバーを、ビジネス側のメンバー含めてリストアップしてもらい、それをベースに私の方でヒアリングするメンバーを決め、話を聞かせてもらいます。半期の間の仕事を振り返ってできるだけ具体的に話を聞けると良いですが、これについてはあまりしっかりやろうとし過ぎるとそれぞれのメンバーの工数もかかり負担になりますので、まずは緩く思いつく範囲で話を聞くというところから初めてみようと思っています。

開発メンバーの評価方法については随時改善

 以前エンジニアの評価をテーマにした勉強会に参加したことがあり、数社の事例を聞いたのですが、各社とも評価方法には悩んでいて、色々試して改善してきているということのようでした。ユニファも開発部に限らずメンバーが増えてきて、評価の仕組みを整える必要が出てきました。なかなか最初から最適な評価プロセスを用意するのは、特に開発部メンバーについては難しいですが、少しずつでも改善していきたいと思っていますので、「うちではこんなことやってる」みたいな話があればぜひ教えていただければと思います。

OkHttp3 + Retrofit2 + gson + RxJava + RxLifecycle + retrolambda を使って簡単にAPIを叩く

こんにちは。スマートフォンアプリエンジニアのまさです。
最近Androidの開発をすることになったために、同僚に助けて頂きながら勉強しました。 非同期、安全に、簡単にAPIを叩くために、ライブラリを使って
https://httpbin.org/get
を叩き、jsonデータを取得する方法をご紹介します。

まずは各種ライブラリを使えるようにするために

build.gradle

buildscript {
    dependencies {
        classpath 'me.tatarka:gradle-retrolambda:3.3.1'  // 追加
    }
}

app/build.gradle

apply plugin: 'me.tatarka.retrolambda'  // 追加

android {
  ...
  
   // 追加
  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }
}

dependencies {

    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:25.3.1'
    testCompile 'junit:junit:4.12'

    compile 'io.reactivex:rxandroid:1.2.1'
    compile 'io.reactivex:rxjava:1.2.1'
    compile 'com.trello:rxlifecycle:1.0'
    compile 'com.trello:rxlifecycle-components:1.0'

    compile 'com.squareup.okhttp3:okhttp:3.5.0'

    compile 'com.google.code.gson:gson:2.8.0'

    compile 'com.squareup.retrofit2:retrofit:2.1.0'
    compile 'com.squareup.retrofit2:converter-gson:2.0.2'
    compile 'com.squareup.retrofit2:adapter-rxjava:2.0.2'

    compile 'io.reactivex:rxandroid:1.2.1'
    compile 'io.reactivex:rxjava:1.2.1'

    retrolambdaConfig 'net.orfjackal.retrolambda:retrolambda:2.3.0'
}

のように編集して、Sync Now します。 これで、今回使うライブラリを使えるようになります。

それでは適当にActivityを作って実装してみます。

package com.unifa_e.studyrx;

import android.os.Bundle;
import android.util.Log;

import com.google.gson.annotations.SerializedName;
import com.trello.rxlifecycle.components.support.RxAppCompatActivity;

import okhttp3.OkHttpClient;
import retrofit2.Retrofit;
import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory;
import retrofit2.converter.gson.GsonConverterFactory;
import retrofit2.http.GET;
import rx.Observable;
import rx.Observer;
import rx.android.schedulers.AndroidSchedulers;
import rx.schedulers.Schedulers;


public class MainActivity extends RxAppCompatActivity {

    private static final String TAG = MainActivity.class.getSimpleName();

    private static final String accessServer = "https://httpbin.org/";
    private OkHttpClient mClient = new OkHttpClient();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        access();
    }

    private void access() {
        requestGet()
                .compose(bindToLifecycle())
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Observer<Res>() {
                    @Override
                    public void onCompleted() {
                        Log.d(TAG, "onCompleted");
                    }

                    @Override
                    public void onError(Throwable throwable) {
                        Log.e(TAG, "Error : " + throwable.toString());
                    }

                    @Override
                    public void onNext(Res res) {
                        Log.d(TAG, res.toString());
                    }
                });
    }

    private Observable<Res> requestGet() {
        Retrofit retrofit =
                new Retrofit.Builder()
                        .baseUrl(accessServer)
                        .addConverterFactory(GsonConverterFactory.create())
                        .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                        .client(mClient)
                        .build();

        return retrofit.create(SampleApi.class).get();
    }
}

class Res {
    String url;
    String origin;
    Headers headers;

    @Override
    public String toString() {
        return "Res{" +
                "url='" + url + '\'' +
                ", origin='" + origin + '\'' +
                ", headers=" + headers +
                '}';
    }
}

class Headers {
    @SerializedName("Host")
    String host;
    @SerializedName("Connection")
    String connection;

    @Override
    public String toString() {
        return "Headers{" +
                "host='" + host + '\'' +
                ", connection='" + connection + '\'' +
                '}';
    }
}

interface SampleApi {
    @GET("/get")
    Observable<Res> get();
}

※今回は備忘録的にご紹介のため、1ファイル内にシンプルに実装しており、アクセッサやファイルを分けるなどの処理を行なっておりません。

これで実行いただくと、https://httpbin.org/get APIをrequestし、responseのjsonデータをパースしてResクラスへデータを入れ込み、ログを表示します。

簡単な説明

まずは responseに帰ってくるjsonをクラスへデータを入れ込むためのクラスである、 Resクラスと、Headersクラスを作成します。
@SerializedName をつけることで、サーバサイドとメンバ変数名が違う場合には、紐付けを行うことができます。
※今回はサンプルなので、すべてのプロパティを実装していません。

getResメソッドの.addConverterFactory(GsonConverterFactory.create()) をつけることでJSONパーサを行ないます。
Rxをつかい、Observableを返すよう実装しています。

onCreateからAPIを叩くObservableSubScribeして実行します。

AppCompatActivity ではなく、RxAppCompatActivity を継承することで、 RxLifecycle.compose(bindToLifecycle()) を処理チェーンに入れるだけで、利用できます。
いちいちライフサイクルを気にせずにRxを使えるので、すでに存在しないActivityのUIにアクセスするというようなことがなくなって、安全で簡単ですね。

最後に onNextに渡ってきたResクラスを表示させています。

如何でしたでしょうか。最初はとっつきにくかったですが、Rxを使うと見た目もよくなりますし、非同期処理の敷居が下がってとてもいいと思いました。

net/httpでHTTP API Clientを作ってみました

こんにちは、チョウです。

最近HTTP APIを利用できるようにクライアントを作ってみました。そこで使ってるHTTP Clientのライブラリはnet/httpです。ほかにいろいろライブラリがありますが、依存を増やしたくないため、見送りました。

HTTP API Clientで対応したいケース

  • GET with query
  • POST application/json
  • PUT/PATCH application/json
  • DELETE
  • timeout

つまりRESTful HTTP APIでよく見られるケースです。最後のtimeoutはリクエストの内容ではなくて、クライアントは一定の時間内で返せる機能です。

net/httpのドキュメントを見ると、すぐできそうなのはGETのケースです。

uri = URI('http://example.com/index.html')
params = { :limit => 10, :page => 3 }
uri.query = URI.encode_www_form(params)

res = Net::HTTP.get_response(uri)
puts res.body if res.is_a?(Net::HTTPSuccess)

そしてPOSTの方はformが対応してるっぽいです。

uri = URI('http://www.example.com/search.cgi')
res = Net::HTTP.post_form(uri, 'q' => 'ruby', 'max' => '50')
puts res.body

application/jsonの場合は基本リクエストのbodyに内容を書き込むので… やはりnet/httpのソースコードを読むしかないのか。 いろいろ調べたところ、このコードでいけそうです。

uri = URI('http://www.example.com/search.cgi')
request = Net::HTTP::Post.new(uri.path, {'Content-Type' => 'application/json'})
request.body = {foo: 1, bar: 'a'}.to_json
response = Net::HTTP.start(uri.host, uri.port) do |http|
  http.request(request)
end

PUTとPATCHは同じ方法で出来ます。DELETEのリクエストにはbodyがないため、GETと似たようなやり方です。

最後のtimeoutはNet::HTTP.startのblockで、httpで設定を変えるらしいです。でもopen_timeoutとread_timeout2つのtimeout設定に分けてます。open_timeoutはsocket接続する時使われるもので、read_timeoutはデータを送ったあとサーバーのレスポンスを待つtimeoutです。

response = Net::HTTP.start(uri.host, uri.port) do |http|
  http.open_timeout = 1
  http.read_timeout = 2
  http.request(request)
end

これでHTTP APIのクライアントに必要なものが揃いましたので、クライアントを作ってみます。

実際のコードではnamespaceがありますが、ここでは省略しました。

まずRequestです。

class Request
  attr_accessor :method # Symbol
  attr_accessor :path # String e.g /bar
  attr_accessor :headers # Hash<String, String>
  attr_accessor :query_params # Hash<String, String/Array<String>>
  attr_accessor :form_params # Hash<String, String/Array<String>>
  attr_accessor :body # String

  def initialize(hash = {})
    @method = hash[:method]
    @path = hash[:path]
    @headers = hash[:headers] || {}
    @query_params = hash[:query_params] || {}
    @form_params = hash[:form_params] || {}
    @body = hash[:body]
  end

  # @return [String]
  def content_type
    @headers['Content-Type']
  end

  # @return [Boolean]
  def has_query_param?
    @query_params.any?
  end

  # @return [Boolean]
  def has_form_param?
    @form_params.any?
  end
end

ここで、query_params, form_paramsとbodyで別々のケースを対応する想定です。

  • query_params => GET
  • form_params => POST form
  • body => POST/PUT/PATCH json

つぎはClientです。

class Client
  def initialize(client_config)
    @client_config = client_config
  end

  def execute(request)
    net_http_request = build_net_http_request(request)
    net_http_response = ::Net::HTTP.start(
        @client_config.host, @client_config.port,
        use_ssl: (@client_config.schema == 'https')) do |http|
      http.open_timeout = @client_config.open_timeout
      http.read_timeout = @client_config.read_timeout
      http.request(net_http_request)
    end
    Response.new(net_http_response)
  end

  private

  def build_net_http_request(request)
    klass = determine_net_http_request_class(request.method)
    r = klass.new(build_path_with_query(request), @client_config.default_headers.merge(request.headers))
    if r.request_body_permitted?
      if request.content_type == 'application/x-www-form-urlencoded' || request.content_type == 'multipart/form-data'
        r.set_form(request.form_params, request.content_type)
      else
        r.body = request.body
      end
    end
    r
  end

  def build_path_with_query(request)
    if request.has_query_param?
      query_params = request.query_params.reject {|k, v| v.nil?}
      "#{request.path}?#{::URI.encode_www_form(query_params)}"
    else
      request.path
    end
  end

  def determine_net_http_request_class(method)
    case method
      when :get
        ::Net::HTTP::Get
      when :post
        ::Net::HTTP::Post
      when :put
        ::Net::HTTP::Put
      when :patch
        ::Net::HTTP::Patch
      when :delete
        ::Net::HTTP::Delete
      else
        raise ::ArgumentError, "unsupported http method #{method}"
    end
  end
end

POSTのform、特にファイルが入ってるケースは直接bodyを入れることができないので、set_formを通してboundaryなどを付け加えてもらう必要があります。

最後はResponseとClientConfigです。

class Response
  # @param [Net::HTTPResponse] net_http_response
  def initialize(net_http_response)
    @net_http_response = net_http_response
  end

  # @return [Fixnum]
  def status_code
    Integer(@net_http_response.code)
  end

  # @return [Hash<String,String>]
  def headers
    headers = {}
    @net_http_response.each_header do |k, vs|
      headers[k] = vs
    end
    headers
  end

  def content_type
    @net_http_response.content_type
  end

  # @return [String]
  def body_as_string
    @net_http_response.body
  end
end

特に注意すべきなのは、net/httpのresponseで取ったstatus_codeのタイプは数値ではなく、文字列です。アプリケーションのコードは基本数値を想定してるので、予め数値に変換するほうをおすすめします。あとなぜかResponseから直接全部のHEADERを取れなくて、コピーの方法でなんとかなりました。

class ClientConfig
  attr_reader :schema # String
  attr_accessor :host # String e.g www.example.com
  attr_accessor :port # Fixnum e.g 80, 443

  attr_accessor :open_timeout # Fixnum
  attr_accessor :read_timeout # Fixnum
  attr_accessor :logger # Logger

  attr_accessor :default_headers # Hash

  def initialize
    @schema = 'http'
    @host = nil
    @port = 80

    @open_timeout = 0
    @read_timeout = 0
    @logger = ::Logger.new(STDOUT)

    @default_headers = {'User-Agent' => "MyApiClient 0.0.1"}
  end

  # @param [String] schema e.g http, https
  def schema=(schema)
    @schema = schema.downcase
  end
end

ClientConfigはとくに複雑ではありません。

実際の使い方

# どっかてclientを初期化
client_config = ClientConfig.new
client_config.host = 'www.example.com'
client = Client.new(client_config)

request = Request.new(method: :get, path: '/')
response = client.execute(request)
puts response.body_as_string

もっと使いやすいようにmoduleを用意しました。

module HttpClientSupport
  attr_writer :http_client

  def execute_http_request(hash)
    request = Request.new(hash)
    @http_client.execute(request)
  end

  def http_get(path, query_params = {})
    request = Request.new
    request.method = :get
    request.path = path
    request.query_params = query_params
    @http_client.execute(request)
  end

  def http_post(path, form_params)
    request = Request.new
    request.method = :post
    request.path = path
    request.form_params = form_params
    request.headers = {'Content-Type' => 'application/x-www-form-urlencoded'}
    @http_client.execute(request)
  end

  def http_post_json(path, json)
    request = Request.new
    request.method = :post
    request.path = path
    request.body = json
    request.headers = {'Content-Type' => 'application/json'}
    @http_client.execute(request)
  end

  def http_put_json(path, json)
    request = Request.new
    request.method = :put
    request.path = path
    request.body = json
    request.headers = {'Content-Type' => 'application/json'}
    @http_client.execute(request)
  end
end

これでincludeしたクラスで

response = http_get('/')

だけで使えます。

ご参考になれば幸いです。

Rubyのスレッドで並列化するのに向いている処理を調べてみる

こんにちは、夏に向けて腹筋強化中のWebエンジニア、本間です。

はじめに、この開発者ブログを開設してから、半年が経過しました🎉

気づけばエントリー数も20を超え、開発者ブログっぽくなってきなーと感じております。 この勢いを続けていけるよう、会社としても個人としてもこれからも頑張っていく所存です。

話は変わりまして、今回のブログでは「Rubyのスレッドで並列化するのに向いている処理」を簡単に調べたので、メモがわりに残しておこうと思います。

調査を実施した理由として、バックグラウンドジョブを動かすGemの1つに sidekiq という有名なGemがあります。 このsidekiqの特徴の1つに「マルチスレッドで動作する」というものがあります。 一方、MRI(CRuby)にはGVL(Global VM Lock)、またはGIL(Global Interpreter Lock)と呼ばれる排他制御の機構があり、「同時に実行できるスレッドは1スレッドのみ」という制限がかかっています。 このロックにより、sidekiqでは複数スレッド立ち上げても処理性能が向上しないのでは、と疑問に思ったことがきっかけです。

ただし、GVLはIO関連の処理が実行されている間は解放されて他スレッドの処理を実行できる、と聞いたこともあります。 どの処理だとマルチスレッドにしてもGVLの影響を受けずに並列化できるのか、調査してみました。

続きを読む