iOSエンジニアのしだです。
今回は直接的なiOS・Androidアプリ開発に関わることではありませんが、 今関わっているプロダクトで音について考える機会があったので共有したいと思います。
続きを読むiOSエンジニアのしだです。
今回は直接的なiOS・Androidアプリ開発に関わることではありませんが、 今関わっているプロダクトで音について考える機会があったので共有したいと思います。
続きを読むみなさん、こんにちは。 エンジニアの田渕です。
梅雨真っ只中の今週、皆様いかがお過ごしでしょうか?
暑かったり寒かったり、じめじめしてたり……気候が安定しないおかげで、私の周囲では体調を崩す人が続出しています。
さて、今回は、エンジニアブログなのにエンジニアではない方にも読んで頂けそうな記事を書いてみようと思います。
というのも、先日、弊社の営業さんから「読んでおくと良いシステム系の本教えてください」とリクエスト頂いたのですが、なかなかご紹介できる本が無くてですね……。
ということで、自分で記事を一つ、かいてみようかなと。
長いこと、エンジニアをやっていると、エンジニアとそうではない人の間に、結構大きな感覚の違いがあることに気づきます。 その筆頭にあげられるのが、いわゆる、システムのパフォーマンス、限界についての話。 ちょうど先日も、某大手通販の会社さんがセールを始めた途端、サイトに人が殺到して、繋がりにくいという状況が起こりました。 エンジニアなら割とみんな、「あー、あるある。まー仕方ないよねー。」ってなるんですが、エンジニアでない人と話をしていると「対策しておけよ!」と結構、お怒りだったり。
昔から、うっすら私が思っていることは、
「もしかして、エンジニア以外の人には、システムの世界は簡単に限界なくなんでもできる世界に見えてるのかしら?」
ということです。
まるで、世の中の物理的な法則に縛られていない、別次元の世界の話のような。
いやいやそうじゃないんだよ、システムの世界も、現実世界と同じようなルールに縛られてるんだよ、というのが今日のお話です。
もうすぐ夏休みシーズンですね。 夏休みになると、人気のあるアミューズメントパークの入場ゲートは長蛇の列になります。 行ったはいいけど、入場規制や、あまりの列の長さに涙を飲んだ、という過去をお持ちの方もいらっしゃるはず。 あれ、どうして起こるのでしょう? 答えは簡単で、入場しようと訪れている人の人数に対して、開いている窓口が少ないからです。 窓口が10個開いているところに11組のお客様が同時に来たら、1組は並んで順番を待つことになります。
じゃあ100組同時に来たら?200組同時に来たら? 最後に来た人は、自分の前に広がる列を眺めて、遠い目をするに違いありません。 自分が入場できるまでにとても時間がかかることが、目に見えてわかるからです。
実は、Webシステム/サイトの処理が遅くなる/繋がりにくくなる、という事象の背後で起こっていることの多くは、これと同じようなことです。 Webシステムにはあらかじめ、同時に開いていい窓口の数が設定されています。 この窓口の数よりも少ない人が一斉にやってくる分には何の問題もないのですが、この窓口の数よりもたくさんの人が一斉にやってくると、アミューズメントパークの窓口と同じく、処理の順番待ちが発生します。 ここで、現実世界なら「あ〜前に人が並んでるからね〜。」と自分の目で見て状況を把握出来ますが、Webサイトに訪れたお客様には自分の前に並んでいる他のお客様の姿など見えていません。結果、いつまでも処理がされないまま、ただ待たされ「遅い」「繋がらない」と感じることになります。
「いやいやいや。待たせるなんてもってのほかだよ!早く入場してもらってよ!顧客満足度ダダ下がりじゃん!」
……おっしゃる通りです。
この場合、施設側がとれる対処法はいくつかあって、
入場処理の効率化、簡略化をする……1組あたりにかかる入場処理の時間を短くして、入場できるお客様の数を増やす方法です。
窓口の数を増やす……入場処理のやり方は変えず、単純に窓口の数を増やすことで、入場できるお客様の数を増やす方法です。
システムの性能改善、パフォーマンスチューニングと呼ばれるものも、概ね同じようなことをします。 「なんだ、窓口の数増やせばいいなら、常に多目にしとけばいいじゃん。」と思うかもしれませんが、窓口は多ければ多いほどお金がかかる。それは、現実世界と一緒です。
アミューズメントパークが繁忙期の人の山に即時に対応できるだけの窓口を作らないのと同じように、 コンビニのレジがお昼時に合わせてレジの数を決めないように、 システムも基本は、「繁忙期に合わせて」ではなく、「平常時の来客に対応するに十分」なだけの窓口しか開けません。 そうじゃないと必要以上にお金がかかるからです。
年賀状の配達のために某所が一時的にアルバイトを増やすように、じゃあその時だけ、一瞬窓口増やす方法ないの? と言われると、実はあります。近年ではそのあたりの技術が飛躍的に発達し、あらかじめ設定しておけば機械が自動的に窓口を増やすことも出来るようになったので、システム利用者が長蛇の列に並ぶ機会は一昔前に比べると随分減っています。ただ、「無限に増やしていいよ」という設定にはできないので、「ここまでなら窓口増やしていいよ」という感じの設定をあらかじめしておくことになります。その数を超えてしまった=当初の予想を超えてしまえば、やっぱり長蛇の列はできます。
そんな訳で、未だにインターネットの世界でも、長蛇の列に遭遇することになります。
さて、今回は、システムの利用者が並ぶ長蛇の列について解説してみました。 形がないからイメージしづらいインターネットの世界ですが、実はあまり、現実の世界と変わりません。 これからは、「読み込み中」のままでなかなかお返事が帰って来ない画面に遭遇したら「ああ、私今、窓口の前で待たされてるのね。」と思ってください。
ではでは。
こんにちは、tanaka です。
Rails5.1で Webpacker が導入されて React や Vue.js などのフロントエンド技術がさらに身近になりました。ヽ( ´¬`)ノ
ということで、早速 Rails5.1 + React + Redux でちょっとしたアプリを作ってみるぜ !! とコーディングを始めたところ、
・・・・
ちょっと昔に ReactNative を勉強したときに使ったはずの Redux をガッツリ忘れているではありませんか …。 (ノ゚ο゚)ノ
そんな忘れん坊な私のためにReact + Reduxを復習し、今度こそ忘れないぞと誓うとともに記事としてまとめました。
React + Reduxについては詳しい記事がたくさんあります。 私も数回読みました。どれも分かりやすくて良い記事だと思います。
が、それでもまだチョット・・という自分のためにTodoアプリを例に出しつつ図を描きながら説明を試みます。 Reduxのスタンダードな解説はこれらの記事に譲ります。
覚えることは3つだけです。
個人的に action は発射ボタンとしてイメージすると理解しやすくしっくりきました。
ビューは発射ボタンを受け取って<input />
などの要素のイベントに紐付けておき、マウスクリック等のユーザーイベントが発生したらそのスイッチが押されるイメージです。
Reducerは状態の管理人というイメージを持ちました。複数の管理人がそれぞれアプリケーションの部分状態を管理しているイメージです。
Reduxは下の図のような 一方向のフローとしての図をよく目にしますが↑の図のほうが実体に則している気がして私にはしっくりきました。
https://github.com/facebook/flux/tree/master/examples/flux-concepts
ビューは コンテナ と コンポーネント の2種類があります。
つまり、コンテナが React外部とのインターフェースとなります。
この説明ではトップがコンテナになっていますがトップにコンポーネントがあり、その中に複数のコンテナがあるというパターンもあります。その場合は、各コンテナがそれぞれ自分がもつコンポーネントの描画に必要な状態やアクションをストアから取得します。
connect
は引数なしとありで挙動が変わるので実はややこしいのですがその辺は次の記事に詳しく書かれています。
アクションはアクション種別とアクションに付随するパラメータを 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' } }
Reducer は状態を新しく生成するだけのpureな関数です。引数で与える 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 に渡します。
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.enabled
は this.props.enabled
としてアクセスできます。
onTodoClick
関数は this.props.onTodoClick(id)
として呼び出せます。
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などのサブコンポーネントを含めていますが通常は別ファイルで定義します。
プロダクトレベルのアプリケーションを実装するときはさらに
を考えていく必要があります。 私も今まさに勉強中なので詳しくは書けませんが勉強した範囲でポイントとなりそうなことを書いておきます。
非同期処理のポイントは
を分けて実装することです。
具体的なコードは以下のような感じになります。(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(ビュー外)からページ遷移する方法は少しトリッキーになっていて以下が参考になりました。
Redux ではアプリケーションの状態はすべてストアで一元管理することになっています。しかし、そうなると特定のコンポーネント内でしか使わない state もストアで管理することになりストアが肥大化、複雑化しすぎるという問題が出てきます。
そこで Reduxで管理するグローバルな state と各ビュー内のローカルな state を分けて管理するというやり方も考えていく必要があるでしょう。
最後に Rails で React を使う方法を紹介します。 RailsにReactを組み込むこと自体はかなり簡単でものの数分で準備できちゃいます。
注意点としては
ぐらいです。
手順は
React込みのアプリを作成する
$ rails new --webpack=react myapp
適当なページ作る
$ rails g controller page hello
layoutファイル修正 (app/views/layouts/application.html.erb)
<%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
サンプルのjsxを組み込む (app/javascripts/pack/application.js)
require('./hello_react.jsx') // 追加する
ビルド
$ bin/webpack
サーバーを起動してブラウザで表示 (/page/hello)
2-3分でHello Worldまで行き着けます
これだけだとreduxは使えないので 次に reduxで必要なパッケージをインストールします。
$ bin/yarn add redux $ bin/yarn add react-redux
環境構築は以下のページを参考にしました。
あとは app/javascripts/packs/ 以下に作りたいアプリケーションを実装していくだけです。
Happy coding ! ┗|l`・ω・´l|┛
みなさまこんにちは。ユニファCTOの赤沼です。もう6月も終わり、2017年も半年終わってしまいましたね。日本の多くの会社では事業年度は4月からで、まだ第一クォーターが終わったところかと思いますが、ユニファの事業年度は1月1日〜12月31日となっていて、6月末で半期終了ということになります。こういった節目につきものなのが、人事評価と目標設定ですね。ユニファでは基本的に半期ごとに前の半期の振り返りをして、設定してあった目標に対しての達成度を確認するとともに、次の半期の目標を決めていますので、メンバーも今まさに2017年上期の振り返りの自己評価と、下期の目標を設定している最中です。そしてその後で私と各開発部メンバーでの面談を経て最終的に評価や目標を決定するのですが、ここではその「自己評価〜目標設定〜個別面談」のプロセス以外で開発部で行なっている取り組みを少し紹介したいと思います。
開発部では週次でマネージャー(私もしくはサーバサイドエンジニアのリーダー)と各メンバーでの1 on 1の機会を設けています。一回あたりの時間は15分間を基本としていて、必要であればさらに別途時間を取るようにしています。アジェンダについては基本的にメンバー側主導で、悩み事や相談しておきたいことを話してもらったり、マネージャーに聞いておきたいことを聞いたりするための場としています。こういう場があることで、「わざわざそのためにミーティングを設定するほどではないんだけどマネージャーに聞いておきたいようなこと」が埋もれることは減っているのではないかと思います。マネージャー側としては各メンバーの状況を週次で確認できるので、期初に設定した目標や会社側がメンバーに期待する役割に対してズレが発生していないかを確認できる場にもなります。
半期ごとの面談では1時間ほどかけて振り返りや目標についての話をするのですが、メンバー数も増えてきて、それぞれの状況も見えづらくなっているので、半年ごとの面談だけではメンバーとの認識のズレに気づくのが遅くなってしまいます。メンバー側としても、毎週時間を確保しておくことで、些細なことでも相談しやすくなるので、コミュニケーションの活性化に役立っているのではないかと思います。
評価のためのプロセスは基本的にメンバー本人と私の二人の視点で行われます。期初の目標設定時にできるだけ客観的に振り返ることができる目標にしてはいますので、数字で明確に判断できるような目標や、やったかやらなかったかなどで判断できるような目標を設定したいところですが、そういう目標ばかりとは限りません。また、目標としてはあげていなかったことや、数字としては見えて来ないところでメンバーやチームにポジティブな影響を与えていても、本人と私だけの視点だとなかなか拾うことができません。私の入社当初はメンバーも少なかったので、一緒に仕事をしていれば自然と把握できていましたが、人数もだいぶ増え、正直なところ日常の業務だけでは各メンバーの状況が把握できなくなってきています。
そこで今回からの試みとして、本人と私以外の同僚から、その人についての評価を聞く、180度評価を取り入れてみることにしました。開発部の組織構造はほぼフラットで、上司や部下という関係もほとんどなく、また、私としても各メンバー間は対等な意識を持ってもらいたいという思いもあり、「360度評価」から上司に対しての評価の部分を除いた「180度評価」にしています。(ちなみにこの180度評価という言い方は私もつい最近知りました。)各メンバーから、前の半期に主に一緒に仕事をしたメンバーを、ビジネス側のメンバー含めてリストアップしてもらい、それをベースに私の方でヒアリングするメンバーを決め、話を聞かせてもらいます。半期の間の仕事を振り返ってできるだけ具体的に話を聞けると良いですが、これについてはあまりしっかりやろうとし過ぎるとそれぞれのメンバーの工数もかかり負担になりますので、まずは緩く思いつく範囲で話を聞くというところから初めてみようと思っています。
以前エンジニアの評価をテーマにした勉強会に参加したことがあり、数社の事例を聞いたのですが、各社とも評価方法には悩んでいて、色々試して改善してきているということのようでした。ユニファも開発部に限らずメンバーが増えてきて、評価の仕組みを整える必要が出てきました。なかなか最初から最適な評価プロセスを用意するのは、特に開発部メンバーについては難しいですが、少しずつでも改善していきたいと思っていますので、「うちではこんなことやってる」みたいな話があればぜひ教えていただければと思います。
こんにちは。スマートフォンアプリエンジニアのまさです。
最近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を叩くObservable
をSubScribe
して実行します。
AppCompatActivity
ではなく、RxAppCompatActivity
を継承することで、
RxLifecycle
を .compose(bindToLifecycle())
を処理チェーンに入れるだけで、利用できます。
いちいちライフサイクルを気にせずにRxを使えるので、すでに存在しないActivityのUIにアクセスするというようなことがなくなって、安全で簡単ですね。
最後に onNext
に渡ってきたResクラスを表示させています。
如何でしたでしょうか。最初はとっつきにくかったですが、Rxを使うと見た目もよくなりますし、非同期処理の敷居が下がってとてもいいと思いました。
こんにちは、チョウです。
最近HTTP APIを利用できるようにクライアントを作ってみました。そこで使ってるHTTP Clientのライブラリはnet/httpです。ほかにいろいろライブラリがありますが、依存を増やしたくないため、見送りました。
HTTP API Clientで対応したいケース
つまり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で別々のケースを対応する想定です。
つぎは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('/')
だけで使えます。
ご参考になれば幸いです。
こんにちは、夏に向けて腹筋強化中のWebエンジニア、本間です。
はじめに、この開発者ブログを開設してから、半年が経過しました🎉
気づけばエントリー数も20を超え、開発者ブログっぽくなってきなーと感じております。 この勢いを続けていけるよう、会社としても個人としてもこれからも頑張っていく所存です。
話は変わりまして、今回のブログでは「Rubyのスレッドで並列化するのに向いている処理」を簡単に調べたので、メモがわりに残しておこうと思います。
調査を実施した理由として、バックグラウンドジョブを動かすGemの1つに sidekiq という有名なGemがあります。 このsidekiqの特徴の1つに「マルチスレッドで動作する」というものがあります。 一方、MRI(CRuby)にはGVL(Global VM Lock)、またはGIL(Global Interpreter Lock)と呼ばれる排他制御の機構があり、「同時に実行できるスレッドは1スレッドのみ」という制限がかかっています。 このロックにより、sidekiqでは複数スレッド立ち上げても処理性能が向上しないのでは、と疑問に思ったことがきっかけです。
ただし、GVLはIO関連の処理が実行されている間は解放されて他スレッドの処理を実行できる、と聞いたこともあります。 どの処理だとマルチスレッドにしてもGVLの影響を受けずに並列化できるのか、調査してみました。
続きを読む