RailsコードをGoで書き直して、FFIを使ってRailsからGoの関数を実行させて実行時間を5%以下に短縮させた

こんにちは、Webチームのエンジニアの柴山です。

今回は、弊社Webアプリ「mamanoko」の一部に処理高速化のためにGoを導入したので、どのように導入したかをご紹介しようと思います。

Railsで開発しているWebアプリ内にどのようにGoを取り入れたのか、実際のコード例を交えながら解説していきたいと思います。

なぜRails環境にGoを導入したのか

そもそもなぜGoを導入することになったのかと疑問に思われるかと思います。

mamanokoはメディアサービスのため、ライターさんに文章を書いていただいています。複数のライターさんがおり、文章量も膨大になるため、表記揺れや誤字がどうしても出てきてしまいます。

そのため、文章校正が必要であり、以前からその機能を提供していました。

しかしこの文章校正の機能は記事の文章に対して表記揺れなどチェックしたい単語を総なめする処理をしているため、パフォーマンスがかなり悪かったです。記事の文字数にもよりますが、だいたい1分くらい処理に時間を要していました。また負荷も大きいため、実行中にサーバが落ちてサービスダウンするといったことも懸念されていたため、文章校正機能をピークタイムには実行できず、最大同時実行数は4までという制限も設けていました。

しかしメディアという特性上、少しでも早く、1本でも多く記事を公開してユーザに情報を提供することが事業KPI的にも求められるため、文章校正が使えずライターさんの業務効率が悪くなる、執筆・編集スピードが落ちるといったことは望ましくない状況でした。

そこで文章校正機能の高速化を図るための施策として上がったのが、Goの関数をShared Libraryとしてビルドし、FFIを使ってRailsから実行するというものでした。

追記(2018/07/02)

想像以上に多くの方に読んでいただき、コメントまでたくさんいただきました。当記事を読んで下さった方、コメントして下さった方、ありがとうございました。今後の執筆の励みになります。

ここまでの反響を呼ぶとは思っていなかったため、この記事を新しく読む方に誤解を与えてしまわないように追記を残させていただきます。

今回は実装を行う上でマイクロサービス化や別サーバーを立てるといったことも当初検討していましたが、大掛かりな実装になる可能性があり、時間的な制約がある中で可能な限りスモールな実装でパフォーマンス改善を行いたいと当初考えていたので、やってみて効果が期待できそうならそれで進めようと試験的な取り組みから始めたという経緯があります。結果的にプロトタイプ作成の段階で期待以上の効果があったのでそのまま実装を進め、リリースしました。

しかし、皆さんのコメントから筆者もRubyまたはRailsの範囲内で、アルゴリズムの最適化等で高速化を図れることが理想なのではないかと感じました。また、RubyGolangアルゴリズムに対する理解が少なかったのも事実です。

そういう背景もあり、本記事はあくまで一つの事例として捉えていただけると幸いです。

この記事を公開して、幸いにも多く方からとても勉強になるアドバイスをいただくことが出来ました。 皆様から頂いたアドバイスを元に、今後RubyGolangアルゴリズムの理解を深めて実装を見直すというアプローチを取っていければと考えております。

アドバイスをくださった皆さまに重ねて感謝申し上げます。ありがとうございました。

前置きが長くなりましたが、次項から実際にどのように導入したのかについて解説します。

Rails環境でどのようにGoの関数を実行するか

先にも挙げた通り、FFIという機能を使って実現しています。FFIとは、Foreign Function Interfaceの略です。詳しくはWikipediaをご覧ください。超ざっくり簡単にいうと別の言語で実装された関数を実行する機能です。

RubyにはRuby FFIというGemがあるので、そちらを使用しました。このGemを使って、RubyからCの関数を実行することができます。

簡単なサンプルコードで解説します。

Ruby

まずffiをインストールします。

$ gem install ffi

そして以下のようにしてCの関数を実行します。

require 'ffi'

module Sum
  extend FFI::Library
  ffi_lib 'sum.so'
  attach_function :sum, [:int, :int], :int
end

pp Sum.sum(1, 2)

これでsum.sosum関数を実行するようになります。

Go側

package main

import "C"

// export sum
func sum(a int, b int) int {
    return a + b
}

func main() {}

mainには何も書きません。代わりにFFIで実行したい関数の上に// export 関数名とコメントを書きます。

そして、GoのコードをCのShared Libraryとしてビルドするわけですが、Goのコマンドを以下のように実行するだけです。

$ go build -buildmode=c-shared -o sum.so sum.go

実行するとsum.goコンパイルしてsum.soというファイルがビルドされます。

これでRubyからGoの関数を実行することができます。

以下参考記事です。

c7.se

qiita.com

mamanokoでの実装例

ではここからは実際にmamanokoでどういう実装をしたかをご紹介します。

まず機能的な要求からおさらいすると、以下のようになります。

1. 校正対象の文章をDBから抽出
2. チェックする単語とチェックから除外する単語をDBから抽出
3. 除外単語を除き、校正対象の文章にチェック対象の単語が存在するか精査
4. チェックに引っかかった箇所をハイライトして表示

ここで除外する単語が出てくるのは、例えば「時」という表記は「とき」にするといった場合に、「時間」など「時」が入る単語でチェック対象からは除外したい単語が存在するからです。

ここの3番に該当する処理が相当重たくボトルネックになっていたので、ここをGoでリプレースするのが今回の目的になります。

データはGo側でDBに接続して取得してもよかったのですが、ORMがActiveRecordとの二重管理になるのは避けたかったため、ActiveRecordで取得してシリアライズしてGo側に渡すことにしました。 ここで、Go側で受け取る時の型について1番詰まりました。

結論から言うと、Railsからはstringとして渡すのですが、Go側ではstringでは受け取ることができず、*C.char型で受け取る必要がありました。 いまだになぜstringで受け取れないのかわかってないのですが、Cにコンパイルしているからなんでしょうか。

jsonのstringを渡すわけですが、Go側で*C.charからstringにキャストしなければなりません。それには、C.GoString(articleJSON)とします。

さらに今度はstringにキャストしたJSON文字列をstruct構造体にマッピングする必要があるので、json.Unmarshal([]byte(C.GoString(articleJSON)), &article)となります。

Cのchar型からGoのstringにキャストして、さらにバイトコードにキャストして、構造体にマッピングさせる。ややこしいですね。ここ最近暗黙型変換の世界にずっといたので、明示的にキャストさせるのがとても面倒に思いました・・・

そして色々とごにょごにょした結果を最終的にjsonにしてRails側に戻してやるのですが、ここでもまたキャストが必要になります。構造体 -> []byte -> string -> *C.charという流れになります。

コードにするとこうです。

resultJSON, err := json.Marshal(result)
if err != nil {
    log.Fatalf("result json encode error : %s\n", err)
}

return C.CString(string(resultJSON))

これでFFIRails・Go間の値の受け渡しがひとまずできるようになりました。

それではここまでをコードでまとめます。

まずRails側は、lib配下にlib/ffi/textlint.rbとして作りました。

require 'ffi'

class FFI::Textlint
  extend FFI::Library

  ffi_lib 'lib/ffi/bin/textlint.so'

  attach_function :textlint, [:string, :string], :string
end

そしてGo側です。lib配下にsrcbinディレクトリを切り、GOPATHを設定してRailsと同じプロジェクト内で開発しました。ビルドしたバイナリをbin配下に吐き出すようにしています。

構造体はstructsとパッケージを分けていますが、定義についてはここでは割愛させていただきます。

package main

import (
    "C"
    "encoding/json"
    "ffi/app/struct"
    "log"
    "string"
)

var dictionaries []structs.Dictionary

//export textlint
func textlint(articleJSON *C.char, dictionariesJSON *C.char) *C.char {
    article := new(structs.Article)
    articleErr := json.Unmarshal([]byte(C.GoString(articleJSON)), &article)
    if articleErr != nil {
        log.Fatalf("article json parse error : %s\n", articleErr)
    }

    dictionariesErr := json.Unmarshal([]byte(C.GoString(dictionariesJSON)), &dictionaries)
    if dictionariesErr != nil {
        log.Fatalf("dictionaries json parse error : %s\n", dictionariesErr)
    }

    result := new(structs.Result)

    // lint実施...

    resultJSON, err := json.Marshal(result)
    if err != nil {
        log.Fatalf("result json encode error : %s\n", err)
    }

    return C.CString(string(resultJSON))
}

func main() {}

これでRails側からGoに好きなデータを渡してあげて、あとはGoの方でゴリゴリ処理を実行することができるようになりました。

FFIを使った時のパラメータの型については、以下の資料が参考になりました。

www.slideshare.net

実際のパフォーマンスへの影響

RailsのコードをGoにリプレイスすることでどれだけパフォーマンスが向上したのか気になるかと思います。

記事の文字数に依存するため一概には言えませんが、だいたい以下の通りだと思います。

Before
45~60sec

After
1~3sec

計測はChromeのDevツールのNetworkでサーバリクエストのレスポンスタイムを指標にしました。

めちゃくちゃ速くなりました。サーバへの負荷も軽減され、ピークタイムには使用できなかったものが24時間いつでも使えるようになりました。

今の所リリースから4ヶ月くらい経ちますが、一度もサービスダウンもエラーも発生していません。滞っていたライターさんたちの校正作業も捗るようになったらしく、めちゃくちゃ感謝されました。やはりエンジニアとしてユーザから感謝される、ユーザに喜んでもらうというのが一番嬉しく、モチベーションになりますね。

Mamanoko Tips

最後に文章校正機能の実装の一部をご紹介しようと思います。

文章に対して単語のチェックをする処理からまず見ていきます。

文章のstringから該当する単語のindexのsliceをとれば良さそうと考えましたが、stringパッケージにそれらしい関数が見当たらなかったので、regexパッケージのFindAllStringIndexを使うことにしました。しかしこいつが曲者で、stringのindexではなく、文字のバイトの開始位置と終了位置のsliceのsliceを返します。つまり返り値は[][]byteとなっています。

本当はregexはパフォーマンス的にあまり使わない方が良いらしいので避けたかったのですが、致し方なくこのようにしました。他に良い方法があればご教示いただきたいです。

コードにするとこうです。dictionary.Wordが校正対象単語で、textが文章です。

dictionaryWord := regexp.MustCompile(regexp.QuoteMeta(dictionary.Word))
highlights := dictionaryWord.FindAllStringIndex(text, -1)

さらに、ここで抽出されたものの中から、除外単語に該当する単語を除かなければなりません。

同じ要領で除外単語のindexをとります。そして、highlightsからindexがかぶるものを除外します。

var highlightBytesList [][]int
for _, highlightBytes := range highlights {
    isHighLight := true
    for _, unhighlightBytes := range unhighlights {
        if unhighlightBytes[0] <= highlightBytes[0] && highlightBytes[1] <= unhighlightBytes[1] {
            isHighLight = false
            break
        }
    }
    if isHighLight {
        highlightBytesList = append(highlightBytesList, highlightBytes)
    }
}

ここではチェック単語より除外単語の方が文字数が必ず長くなるという特性を活かして、除外単語のバイトの範囲内にチェック単語のバイトがあれば除外するようにしました。

これで、文章の中のどこに校正すべき単語が存在するかがわかりました。

最後に、校正単語の箇所をハイライト表示させます。該当単語を<span class="hightlight"></span>で囲みます。

var lintedText []string
for index, textRune := range text {
    isHighlight := false
    for _, highlightBytes := range highlightBytesList {
        if highlightBytes[0] <= index && index < highlightBytes[1] {
            isHighlight = true
            break
        }
    }

    if isHighlight {
        lintedText = append(lintedText, "<span class=\"highlight\">"+string([]rune{textRune})+"</span>")
    } else {
        lintedText = append(lintedText, string([]rune{textRune}))
    }
}

return strings.Join(lintedText[:], "")

textはstringですが、stringに対してfor rangeでループを回すと、1文字ごとのバイトの開始位置とrune文字を取得できます。

これを利用して、先ほど取得した校正対象単語のバイト位置が格納されたsliceを比較してハイライト表記するかどうかを判別します。

rune文字は[]rune{textRune}とすることで元の文字列に変換できます。

そして出来上がった[]stringをstringパッケージのJoinを使って繋げて一つの文字列にしてあげれば完成です。

所感

実は今回Go言語を触るのは初めてで、1週間のインプット期間を設けてから本機能の実装に取り掛かりました。

Go言語は言語仕様がかなりシンプルで、実装していてどの関数を使うべきか迷うことがなかったり、標準パッケージがかなり強力で大体それだけでなんとかなるというのがとても良いと思いました。フォーマッタもあるのでフォーマットを気にすることなくガリガリ実装できるのは良い開発体験につながりました。

ポインタのところが自分には難しく、まだ理解しきれていないですが、それでもすんなり入ることができて、Go言語好きになりました。

しかしRubyRailsなど高級多機能な言語、FWから移ってくると「え、この関数ないんか」といった事態に遭遇して自分で実装することになるといったことが多くて大変そうです・・・

ともあれ今回の目的であった文章校正機能の高速化はGoでリプレースすることで見事達成することができました。しかしまさかここまでパフォーマンス向上するとは自分でも驚きです。

We're Hiring!!

弊社ではWeb / ネイティブエンジニアを募集しております。

ご興味がありましたらお気軽にご連絡ください!

www.wantedly.com

React環境にReduxを導入して秩序をもたらした話

初めまして、エンジニアの柴山です。 2017年11月に入社し、Webチームにアサインされ、子育て情報サービスmamanokoの開発をしています。

今回は、今年の2月にmamanokoにReduxを導入した際の考慮したことや設計などについて書きます。

導入前の状態

Reduxを導入する前は素のReactだけで実装していました。 stateの複数コンポーネントでの参照・更新をなるべく避け、stateを渡すのではなくコンポーネントをモノリシックにしてバケツリレーしないようにさせたりすることでなんとか回避している状態でした。

とは言ってもやはりコンポーネントはどんどん肥大化し、コンポーネントの中に複雑な処理が混ざり込み、とてもリーダブル・メンテナブルとは言い難い状態となっていました。

また、実装者によって実装の仕方も違ってきてしまい、どうすべきといった明確な基準がないためコードレビューも難しくなり、品質の担保が困難になっていました。

そこでReduxを導入して複雑化し保守性が低くなっている問題を解決しようということになりました。

導入手順

  • Reduxの公式リファレンスを読む
  • Reduxの事例を調査し、ベストプラクティスを模索し設計
  • 実装してリリース

Reduxの公式リファレンスを読む

まずはやはりReduxについてちゃんと理解しておく必要があるので、公式のリファレンスを熟読しました。 Reduxの元となっているFluxも簡単にではありますが公式を読みました。 Reduxに限った話ではありませんが、このライブラリがどのような問題を解決するために生まれたのか、何をもたらしてくれるのかをしっかり理解しておくことが大事だと思います。

Fluxは、Reactで実装するにあたって問題となる至る所で参照・更新され、管理が煩雑になるstateを、データフローを単一方向にすることで管理しやすくし、処理の見通しをよくするためにFacebookが提唱したアーキテクチャです。 FluxはFacebookが提唱したアーキテクチャの名称で、facebook/fluxはこのアーキテクチャのリファレンス実装です。こちらはReactと併用することを前提にしています。 Fluxと言う時はアーキテクチャのことを基本的に指すことが多いと思います。

ReduxはFluxをシンプルな関数のみで実装したもので、JSアプリケーションの状態管理のためのライブラリです。なので、React以外のフレームワーク/ライブラリでも、もちろん素のJSとも組み合わせて使えます。

詳しいことに関しては、公式を読むか、既に丁寧に解説してくださっている記事がたくさんあるので、そちらを読んでください。

mae.chab.in

qiita.com

Reduxの事例を調査し、ベストプラクティスを模索し設計

Reduxについて理解ができたところで、実際に他社さんのサービスでどのように使われているのかなどの事例を調査し、どうやって実装するのが一番良いのかを模索しました。 特に気になったのは、ディレクトリ構成Middlewareをどうしているかです。

また、弊社アプリはSPAではなく、サーバサイドで生成したビューの一部をReact on RailsでReact化してSSRしています。そのため、SSRの事例を特に調査しました。

ディレクトリ構成

ディレクトリ構成に関しては、actionreducermiddlewareなどをそれぞれディレクトリを分けるか、ドメインでひとまとめにしてしまうかを考えました。 ちなみにドメインactionreducerをひとまとめにするパターンはDucksといいます。基本的にactionは特定のreducerに紐づくことが多く、その場合actionreducerをそれぞれ別のディレクトリ、別のファイルに分ける必要がないため、このパターンは複雑性を回避するには有効な手段だと思います。

しかし、今回は弊社アプリはSPAではなくSSRのまたそのページの一部分でのみReactを使用しているため、既に関心ごとによってディレクトリが分けられている状態でした。そのため、全てをひとまとめにするよりは、どれがactionで、どれがreducerかということが明確に分かった方が良いと判断し、Ducksパターンではなく、ベーシックなディレクトリ構成を取ることにしました。

最終的にこのような構成となりました。

〇〇    // add_todo_formなど部品名が入る
├── components    // 純粋なReactコンポーネント
│   ├── 〇〇.jsx
│   └── △△.jsx
├── containers    // コンポーネントにReduxをつなぐためのクラス
│   ├── 〇〇.jsx    // ファイル名・クラス名はコンポーネントと同じ命名とする
│   └── provider.jsx    // storeとつなぐ一番親となるProviderクラスはprovider.jsxとする
└── stores    // Redux関連のコード
      ├── action_types.js    // Action名の定数群
      ├── actions.js    // Actionの定義
      ├── reducer.js    // Reducerの定義
      ├── middleware.js    // Middlewareの定義
      └── store.js    // Storeの定義

ディレクトリ構成に関してはこちらがとても参考になりました。

speakerdeck.com

Middlewareをどうするか

一番頭を悩ませたのがここです。

Redux自体が副作用を必要とする例えば非同期処理などをする手段を用意はしているものの、ほぼ別のライブラリに頼ることを前提としている感じです。公式ではMiddlewareのところでredux-thunkが登場します。正直Reduxについて理解するだけでお腹いっぱいなのに、また別のMiddlewareのライブラリについて理解しないといけない必要が出てくるのはとても大変だし、それによって初心者の方にとっては参入障壁が高く感じられるポイントではないかと思います。

redux には良い middleware が必須 - Togetterでも議論されていますが、Reduxはシンプルでピュアな反面、実サービスにおいて一番肝心な要素となるMiddlewareが完全に別のライブラリに丸投げされていて、Reduxが果たして最適解なのかという状態を生み出している気がします。ReduxがMiddlewareをもっといい感じに面倒見てくれるようにしてくれればいいのに(いい感じとは?という疑問はありつつ)とつくづく思います・・・おかげでMiddlewareライブラリが乱立していて初学者を混乱させていると思います。

事例を色々見てみると、redux-sagaを導入している方が多かったため、redux-sagaを導入しようかとも考えましたが、ES6のgeneratorを使用して作られており、自分としてはまた理解する概念が増えるのは避けたいと思いました。また、弊社のWebチームのエンジニアは皆Redux初心者でレベルもバラバラだったため、学習コストはなるべく低く抑えたいと思い、redux-sagaの導入は取りやめました。

そこで公式のMiddlewareの章を3、4回くらい熟読し、そもそものMiddlewareに対する理解を深めることにしました。そしてライブラリに頼らず、Middlewareは素の状態で書いたらどうかと思い試してみたところ、思いの外難しくなく、多少記述が多く面倒な程度だったため、これで進めることにしました。

詳しい実装については後述します。

SSR

SSRに関しては、こちらの記事がとても参考になりました。

qiita.com

また、React On Railsの公式ドキュメントを読み、公式のサンプルを参考にして、どのようにRailsからReact + Reduxに繋ぎ込むのかを考えました。

React On RailsがReduxもサポートしてくれているおかげで多少設定のための処理を加えるだけで、Railsで扱うオブジェクトをそのままReduxのStoreに直接渡すことができたのでとても助かりました。

こちらも詳しい実装については後述します。

実装

調査と設計が終わったところで実装に移ります。 ここではReduxの登場人物ごとの基本的な実装方針について紹介します。

※便宜上TODOアプリ風な実装例を紹介します。

Action

まずAction Typeについてはお決まりで例のごとく以下のようにしています。

export const FETCH_TODO_REQUESTED = 'FETCH_TODO_REQUESTED';
export const FETCH_TODO_SUCCESS = 'FETCH_TODO_SUCCESS';
export const FETCH_TODO_FAILURE = 'FETCH_TODO_FAILURE';

そしてActionについては、Flux Standard Action(以下FSA)に則って実装するためにredux-actionsを使用しています。 FSAに則ることで、チームで開発する際に人による実装のズレを最大限無くしています。 FSAでは、Actionは必ず下記のように、typepayloadmeta、そしてerrorの4つのプロパティのみを使用することを要求しています。

{
  type: 'FETCH_TODO_REQUESTED',
  payload: {},
  meta: {},
  error: false
}

redux-actionsを使用することで、下記のように実装することで簡単にFSAに則ったActionを実装できます。

import {createAction}   from 'redux-actions';
import * as actionTypes from './action_types';

export const fetchTodoRequested = createAction(actionTypes.FETCH_TODO_REQUESTED);
export const fetchTodoSuccess = createAction(actionTypes.FETCH_TODO_SUCCESS);
export const fetchTodoFailure = createAction(actionTypes.FETCH_TODO_FAILURE);

これで、アクションを実行した際にredux-actionsが勝手に上述したJSONの形式でActionを生成してくれます。

さらに、このアクションにErrorオブジェクトを引数として渡すことで、redux-actionsが勝手にerrortrueにした状態のActionを生成してくれます。

これによって、例えばaxiosを使用する際に、エラーハンドリングが簡単になります。

axios.get('~~~')
.then(response => store.dispatch(actions.fetchTodoSuccess({todos: response.data})))
.catch(error => store.dispatch(actions.fetchTodoFailure(error)));    // errorはErrorオブジェクトなので、そのままActionに渡すだけ

Reducer

Reducerもredux-actionsを使って実装しています。

import {handleActions} from 'redux-actions';
import * as actionTypes from './action_types';

const initialState = {
  todos: null,
  isLoading: false,
  error: null
};

export default handleActions({
  [actionTypes.FETCH_TODO_REQUESTED]: state => ({
    ...state,
    isLoading: true
  }),
  [actionTypes.FETCH_TODO_SUCCESS]: (state, action) => ({
    ...state,
    todos: action.payload.todos,
    isLoading: false
  }),
  [actionTypes.FETCH_TODO_FAILURE]: state => ({
    ...state,
    isLoading: false,
    error: action.payload.response.data
  })
}, initialState);

switch文で記述するところを、redux-actionsのhandleActionsを使うことでJSONで記述できるようになります。

Middleware

Middlewareは前述の通りで、特にライブラリに頼らず、素の状態で実装しています。

import * as actions     from './actions';
import * as actionTypes from './action_types';

export default store => next => action => {
  switch (action.type) {
    case actionTypes.FETCH_TODO_REQUESTED:
      next(action);

      axios.get('~~~')
      .then(response => store.dispatch(actions.fetchTodoSuccess({todos: response.data})))
      .catch(error => store.dispatch(actions.fetchTodoFailure(error)));

      break;
    case actionTypes.FETCH_TODO_SUCCESS:
      next(action);

      ...

      break;

    ...

    default:
      next(action);

      break;
  }
}

このように、意外と難しくもなく、switch文の記述が多少面倒なくらいなので、素の状態でも十分使えます。

Store

React On RailsでのStoreは通常のReduxと少し実装の仕方が異なります。 React On RailsではStoreをpropsとlocation(option)を引数にとり、storeを返す関数として実装するように要求しています。 React On Railsのドキュメントの「Multiple React Components on a Page with One Store 」の所に書いてあります。 コードは下記の通りです。

import {createStore, combineReducers, compose, applyMiddleware} from 'redux';
import todoReducer from './reducer';
import todoMiddleware from './middleware';

const composeEnhancer = process.env.NODE_ENV === 'production'
  ? compose
  : window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

export default props => {
  const reducers = combineReducers({
    todoReducer
  });
  const enhancer = composeEnhancer(
    applyMiddleware(todoMiddleware)
  );
  return createStore(reducers, props, enhancer);
};
const composeEnhancer = process.env.NODE_ENV === 'production'
  ? compose
  : window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

ここでは本番環境以外ではChromeのDevツールからデバッグが行えるように、Redux DevTools extensionの設定を加えています。

Storeを生成したら、React On Railsへの登録を行います。

import todoStore from '~/store';

ReactOnRails.registerStore({
  todoStore
});

これでサーバ・フロント両方でStoreを使う準備が整いました。

Rails側から直接Storeに値を渡したい時には下記のようにします。

= redux_store 'todoStore', props: { todoReducer: { todos: @todos } }

※弊社アプリではテンプレートエンジンにslimを使用しています。

redux_storeの第一引数は上記ReactOnRails.registerStoreで指定したものと同じ名前をStringで指定します。なのでここではtodoStoreとなります。

第二引数はprops: {}となっており、Storeに登録したい値を渡します。 todoReducer: { todos: @todos } }としている一番上の階層のキーは、StoreでcombineReducersで指定しているReducerと同じ名前を指定します。ここではtodoReducerとなります。 そうすると初期ロード時のStoreの状態は以下のようになります。

{
  todoReducer: {
    todos: [
      'hoge',
      'fuga'
    ]
  }
}

ReactでStoreを使う際に例のごとく登場するProviderでのStoreの渡し方も少し特殊です。

import {Provider} from 'react-redux';

export default () => {
  const store = ReactOnRails.getStore('todoStore');
  return (
    <Provider store={store}>
      <Component />
    </Provider>
  );
}

ReactOnRails.getStore()でstoreを取得して、それをProvideのpropsに渡してあげます。 こちらでもReactOnRails.registerStoreで指定したものと同じ名前を指定する必要があります。なのでtodoStoreとなります。

これでようやくSSRの準備が完了しました。

Component

ComponentはPresentationalとContainerとで分けています。

Presentational Componentは状態を持たず、propsで受け取ったデータの表示とアクションの実行が役割になります。

const Todos = ({todoApp, actions}) => (
  <ul>
    {todoApp.todos.map(todo => (
      <li key={todo.id}>
        <div>
          {todo.text}
        </div>
        <button onClick={() => actions.deleteTodo({ id: todo.id })}>削除</button>
      </li>
    ))}
  </ul>
)

Container ComponentはReduxと繋いでPresentational Componentに必要なデータを流し込むのが役割となります。実装は下記のようになります。

import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import Todos from '../components/todos';
import * as actions from '../stores/actions';

const mapStateToProps = state => ({
  todoApp: state.todoReducer
});

const mapDispatchToProps = dispatch => ({
  actions: bindActionCreators(actions, dispatch)
});

export default connect(mapStateToProps, mapDispatchToProps)(Todos);

これで実装については簡単にではありますが全て紹介しました。少しでも実装の助けになればと思います。

その他参考資料

全体的に以下の記事を参考にさせていただきました。かなりよくまとめてくださっているので、React + Reduxで開発する時に大いに助けになると思います。

qiita.com

qiita.com

↓こちらは最近公開された記事で、実装時の参考にはしておりませんが、非常に参考になりました。

qiita.com

所感

私も今回初めてReduxを触ったのですが、やはりReactを使用していてある程度規模が大きくなった場合にReduxを使うと責務がいい感じに分けられるので複雑性が解消され、コードがとても読みやすくなり、保守性も高まると感じました。 チームで開発する際の実装の一貫性も保ちやすくなると思います。

ただ、MobXなども出てきており、Reduxよりも筋の良いライブラリが登場する可能性もまだあるのではないかと思っています。 というより、Reduxがもっと強くなってくれればとつくづく思います・・・

まだ実際にアプリケーションにはほんの一部分にしか導入できていないので、なんとも評価しきれない部分もありますが、Redux導入を進めていく中で問題が生じたらその際にまた考えようかと思います。

We're Hiring

弊社ではWeb / ネイティブアプリエンジニアを募集しております。 ご興味がありましたらお気軽にご連絡下さい! エンジニアの方、ぜひ情報交換しましょう!

www.wantedly.com

www.wantedly.com

Upgraded Ruby on Rails from v4.2.4 to v5.1.4

こんにちは、エンジニアの神山です。
今年の1月に弊社アプリケーションで使用しているReactをFiber(v16)にアップグレードしました。
そしたら今度はRuby on Railsのアップグレードもすることになり、先日に無事アップグレードが完了したのでそのことについて書きました。

Railsは弊社の基盤フレームワークなので、慎重に確認と修正をしながら進めていきました。

詳細事項

  • Rubyバージョン: 2.3.3
  • Railsバージョン: 4.2.4 -> (5.0.6) -> 5.1.4
  • 実施期間: 2018年2月

手順

手順は以下の通りです。これらを追って説明していきます。

  • テストのカバレッジを上げる
  • すべてのGemのバージョンアップ
  • Railsのアップデート情報の検索
  • Railsのバージョンを5.0.6に上げる
  • Railsのバージョンを5.1.4に上げる
  • リリース

テストのカバレッジをあげる

これはGemやRailsのアップグレードをする際に、現在のコードがアップグレードしてもバグを起こさないか見つけるために行います。特に後方互換性がないアップグレードの時にとても役に立ちます。
今回のようなメジャーバージョンアップだと非推奨や使用不可になったコードが沢山出てくるのですが、テストの時点でほぼ気づくことが出来ました。

弊社は普段からテストを書くことを習慣にしていて、カバレッジは常に約95%あたりをキープしております。今回のアップグレードでも95%あれば十分であると判断したため、テストは追加で書かずにすみました。

カバレッジの目安はアプリケーションやチームによって変わってくると思うので一概には言えませんが、テストケースが十分にあるということは改めて大切だと思いました。

すべてのGemのバージョンアップ

一般的にGemはRubyRailsのバージョンがあがると追随してアップグレードされます。そのためRails5系の環境に沿ったGemを使用するべく、全てのGemのアップグレードも行います。

ただ注意として、そのGemの開発者がアップグレードに追随していないこともあります。するとRails5系で使いたいのに使えない、Rails4系までしか動かないとかも起こります。

そのときは同じ機能を持つGemで開発が活発なものを探すか、自分でパッチをあてないといけません。ただパッチをあてるとその瞬間から自分たちでそのGemを管理していかないといけなくなるので、注意と覚悟が必要です。
そのため信頼できる別のGemを探すほうがアプリケーションの開発に集中できる状態を保てるので、入れ替えのコストはかかりますが適切な選択だと思います。

ちなみにRefileというGemを使用しているのですが、アップグレードに追随していませんでした。この記事の下に詳しく書いておきます。

Railsのアップグレード情報の検索

アップグレードの情報を得るため、ドキュメントを予め読んでおくのは大切です。何かエラーが出たときに解決がスムーズになります。そのため変更点をまとめておくといいと思います。

私は以下のドキュメントを読みました。

Rails公式のリリースノートとアップグレードガイドの2つは絶対に読んでおくべきだと思います。

Railsのバージョンを5.0.6に上げる

今回のアップグレードはRails5.1系がゴールですが、一度Rails5.0系にアップグレードしてからRails5.1系にアップグレードしました。

理由は以下の通りです。

  • メジャーバージョンのアップグレードは一般的に規模の大きいアップグレードで、仕様が大きく変わる可能性があり、アプリケーションが最新のメジャーバージョンに対応できるか確認するため。
  • Rails4.2系からRails5.1系に一気にあげると変更点が多くコードの変更量も多くなるので、エラーが起きたときに原因がわかりにくくなってしまうため。
  • Rails5.1系へのアップグレードが現実的に難しいとわかっても、Rails5.0系へのアップグレードは出来る可能性があるため。

Railsはマイナーバージョンのアップグレードでも大きくコードが変更されることもあるので、段階的にアップグレードすることにしました。特に4系から5系はConfig周りの変更も多いので、慎重を期したほうが良いと思います。

Railsのバージョンを5.1.4に上げる

Rails5.0.6が正常に動いたので、そこからRails5.1.4へのアップグレードを行いました。ちなみにこのアップグレードの最中にRails5.1.5がリリースされたので、一度Rails5.1.5へのアップグレードを試みました。しかし正常に動作しなかったので最終的にはRails5.1.4になりました。こちらも記事の下に起きたことを書いておきます。

Rails5.0系からRails5.1系への変更もなかなかの量があり大変でした。特にActiveRecordまわりのメソッドが非推奨になったり新しく追加されたりしたので、Modelを念入りに確認しました。

アップグレード時に大変だったこと

RefileというGemがRails5系に対応していない

Refileを使っているアプリケーションは注意が必要です。Refileのリリースブランチは2年前のものであるため、Rails4系の仕様になっております。 ただmasterブランチはRails5系の仕様になっているので、そちらを使用すれば問題ありません。

しかしRefileに関連するGemでrefile-s3refile-mini_magickを使用しているのですが、その2つで問題が起きました。
これらはRefileのアドオンのようなもので、内部でRefileを使用しております。しかし使用しているRefileはリリースブランチのバージョン(つまりRails4系)であったため、Rails5系の環境では動きませんでした。

そこで「同じ機能を持つGemで開発が活発なものに変える」か「そのGemのレポジトリをフォークしてパッチをあてて使う」の2つを考えました。

Refileはファイルアップローダーであり、同じ機能を持つGemは有名所だと以下の3つが見つかりました。

上2つは有名なGemですがShrineは初めてしりました。Refileのコアな開発者が作成したGemで、開発も盛んで現在のトレンドのようです。(細かな違いなどは比較記事がネット上にあるのでそちらを参考にしてください。)

ただ今回は「そのGemのレポジトリをフォークしてパッチをあてて使う」にしました。理由はRails5.2系からActiveStorageというファイルアップローダーの機能が搭載されるためです。
そのためRails5.2系にアップデートする段階で再度判断することにし、今回はパッチで対応しました。またあてるパッチの量も少なく済みそうであったことも理由の1つです。

ちなみにあてたパッチはGemfileのrefileの箇所です。

- gem "refile", github: "refile/refile"
+ gem 'refile', git: 'https://github.com/refile/refile', branch: 'master'

ここで注意しなくてはいけないのがRails5系との互換性です。当然ながらGemfileの中身を変更しても、Gemのコード自体が対応していないと使用することはできません。その為パッチをあてるときは、Gemの中身を一通り読んでおくべきだと思います。

Rails5.1.5が正常に動作しない

まだ原因の根源まではわかっていませんが、ActiveRecordで作るクエリの条件式に関連先のテーブルのものを使用するとエラーが起きました。

Hoge.includes(:fuga).where(fugas: { bool: true })

キャッシュの変更に悩まされる

Rails5系からActiveRecord::Relationにも.cache_keyというメソッドが実装されました。しかしActiveRecordで作るクエリの条件式に関連先のものを使用するとエラーが起きました。

Hoge.includes(:fuga).where(fugas: { bool: true}.cache_key
/ => ActiveRecord::StatementInvalid: PG::UndefinedTable

またView上で使用する際も同様のエラーになります。

- cache hoges do …
/ => ActiveRecord::StatementInvalid: PG::UndefinedTable

ちなみにActiveRecord::Relations#cache_keyは、他にも注意点があります。下のドキュメントがよくまとめられているので、一読しておくことをおすすめします。

techracho.bpsinc.jp

所感

今回のアップグレードを通して以下のことを学びました。

  • フレームワークの更新を常に把握する。
  • アップグレードはマイナーバージョンの安定版が出たら即行う。
  • テストは常に書く。

上の項目は普段から習慣付けるといいと思います。そうでないとアップグレードしようとしたときにつらい思いをすることになります。
場合によっては気づいたら最新のバージョンとコードが乖離しすぎてて、アップグレードは現実的に不可能なんてことにもなり兼ねません。

雑感ではありますが、アップグレードはなかなか出来ることでもないのでとてもいい経験になりました。

We’re Hiring

弊社ではWeb / ネイティブアプリエンジニアを募集しております。
ご興味がありましたらお気軽にご連絡下さいませ!
エンジニアの方、ぜひ情報交換しましょう!

www.wantedly.com

www.wantedly.com

CSSについて本気出して考えてみた 〜CSS × Atomic Design〜

こんにちは、WEBエンジニアの神山です。

最近シェルをZshからFishに変えました。Fishについて書きたいのですが本題とそれるので一言でまとめます。
「うお!」

さて今回はAtomic Designという考え方を知りまして、それを踏まえてCSSに対する考えを書きました。
今まではBEMやSMACSSを使っていたのですが、そこに感じている悩みやAtomic DesignをもとにしたCSS設計の利点などを自分なりにまとめてみました。

正直CSSについて詳しい人ではないので正確な考察が出来ているかはわかりません。。
もし違っていたり、より良い考え方などありましたらコメントを頂けると非常に助かります。。

そもそもAtomic Designとは

Atomic Designとは、ボタンやフォームなど小さいパーツを組み合わせて、一枚のページを作っていく考え方です。
たとえば検索ページを作る際に、まず入力フォームを作り、その横に「検索」ボタンを作り、フォームの上に「〜〜を検索する」という見出しを作り、検索ページにヘッダーやフッターを配置し、、、みたいに考えていきます。

Atomic Designは、原子や分子など化学から発想を得ております。化学を勉強したことがある人は理解しやすいかもしれません。 まずボタンや入力フォームという原子があり、それらが組み合わさって検索フォームという分子になり、そういう分子がたくさん集まってページという物質が出来上がるというイメージです。

こちらがAtomic Designの考案者が解説したサイトになります。英語ですがわかりやすい図を用いて解説してあり、直感的に理解しやすいです。

bradfrost.com

Atomic Designは具体的に以下の手順でデザインを考えていきます。

  1. Atoms(原子)
    原子は、ボタンや入力フォームにあたります。
  2. Molecules(分子)
    分子は、検索フォームにあたります。
  3. Organisms(有機体)
    有機体は、ヘッダーやメインコンテンツ、サイドバー、フッターにあたります。
  4. Templates
    テンプレートはページのワイヤーフレームにあたります。
  5. Pages
    ページはワイヤーフレームにコンテンツを入れて、文字通りページとして完成したものにあたります。

こちらにそれぞれの具体例がたくさん載っているので、導入の際に参考にしてみてください。

http://demo.patternlab.io/demo.patternlab.io

CSS設計のポイント

CSS設計のポイントは以下の4つで考えております。
当たり前のことなのですが、これらをきちんと守らないと管理しづらいCSSになってしまいます。

  • 予測しやすい
  • 再利用しやすい
  • 保守しやすい
  • 拡張しやすい

これらは下のサイトを参考にさせて頂きました。

article.enja.io

現状のCSS

さて、上にあげたCSSの設計のポイントを実用的にするために、様々な設計手法が存在します。

有名なものだとSMACSSなどOOCSSをもとにしたものが広く使用されていると思います。またBEMのように少し違う側面での設計手法も存在しており、他の設計手法と組み合わせて使うことも多いと思います。

私はBEMとSMACSSをかけ合わせて使っております。どちらも好きな設計手法です。

BEMは命名をBlock、Element、Modifierで分けるというルールがシンプルでわかりやすく、このルールをきちんと守っていれば、管理しやすいCSSができあがると思います。

<div class=”article>
  <div class=”article__heading”>hoge</div>
  <img class=”article__image” />
</div><div class=”article>
  <div class=”article__heading--small”>fuga</div>
  <img class=”article__image--small” />
</div>
.article {
  &__heading {
    ...

    &--small {
      ...
    }
  }
  
  &__image {
    ...

    &--small {
      ...
    }
  }
}

またSMACSSはCSSをカテゴライズして管理する手法です。

  • Layout
    ヘッダー、メインコンテンツ、サイドバーやフッターの配置を整える
  • Module
    リストやボタンなど再利用そうなもののデザインを定義する
  • State
    クリックされたものなど状態により変化したもののデザインを定義する
  • Theme
    ページ単位のデザインを整える
  • Base
    CSSの基幹部分やタグのデフォルト値などを決める

また私はボタンやテキストのデザインを決めるとき、マルチクラスを使用しております。

/modules/button.scss

.button {
  display: inline-block;
  padding: 12px 20px;
  border-radius: 2px;
  background-color: transparent;
  color: #fff;
  font-size: 80%;
  text-align: center;
  text-decoration: none;
  white-space: nowrap;
  vertical-align: middle;

  &-default {
    border: 1px solid whiten($base-color, 90%);
    background-color: #fff;
    color: whiten($base-color, 20%);
  }

  &-small {
    padding: 8px 12px;
    font-size: 70%;
  }

  &-full {
    width: 100%;
  }
}

これをHTMLでは以下のように使っております。

<div class=”article>
  <div class=”button button-default button-small”>small</div>
  <div class=”button button-default button-full”>full</div>
  ...
</div>

これでボタンのデフォルトのデザインを統一させることができ、いろいろなパターンのボタンを用意することが容易になります。

現状のCSSの悩み

たとえば警告用のボタンを作りたいときに、2通りの方法が考えられます。

一つは、ボタンクラスに要素を増やす方法。
警告用のボタンであれば、button button-warningというCSSを用意します。

もう一つは、Moduleの一部として考える方法。
ログインボタンのデザインであれば、login_form-buttonというCSSを用意します。

どちらも正解ですしチームで方針をしっかり決めれば良いことだと思います。
しかし2通りの方法があると、CSSを設計する上で管理しづらさ、わかりづらさが出てしまうと考えております。
そしてこれを強く問題視している理由は、デザインを考える視点が真逆だからです。

ボタンやテキストなどのパーツを組み合わせて使うという考え方は、小さいものから考えて大きいものを作っていくという考え方です。 ボタンやテキストを予め定めておき、何かのModuleを作るときはそれらを組み合わせてModule全体をデザインしていくイメージです。

一方、フォームの中のボタンやテキストのデザインを変えるという考え方は、大きいものから考えて小さいものを作っていくという考え方です。 何かのModuleを作るときはModule全体のデザインを考えて、それを細かく落とし込んだときにボタンやテキストが作られるイメージです。

多くの設計手法は大きいものから考えていくやり方です。しかしボタンやテキストなどのデザインを統一したいとなると、小さいものから考えていくやり方も必要になります。
つまり両方の考え方が混在してしまいます。

Atomic DesignをもとにCSSを設計

これを解決するために、Atomic Designの考えをもとにCSSを設計すべきではと考えました。上で紹介したように、Atomic Designは全てのデザインを小さいものから作っていくという考え方をしているからです。

考え方を統一すれば、ボタンの変更ならボタンを管理しているCSSを変えれば良いと明確になります。ボタンやテキストのデザインがぶれることもなくなります。 さらに何かのModuleを作るときもデザインの雰囲気が似てきて、組み合わせても違和感のないものが作られるようになると考えております。

またAtomic Designはコンポーネントを組み合わせていくというシンプルな考え方なので、CSSもシンプルになります。

まずAtomsというカテゴリーを作り、そこでボタンやテキストなどのCSSを管理します。
次ぎにMoleculesというカテゴリーを作り、ボタンやテキストなどを組み合わせて作ったコンポーネントを管理します。ここでのCSSはボタンやテキストの間隔を調整するものになります。ボタンを変更したいときはAtomsを変更するようにします。

というようにOrganismsやTemplatesというカテゴリーも作っていきます。PagesはCSSとは関係ないので必要なさそうです。

またSMACSSと比較すると、視点こそは違うものの区分けは似ているので、SMACSSからの移行も難しくはなさそうに思えます。
LayoutはOrganisms、ThemeはTemplatesに相当すると思います。Baseは相当するものがありませんが、Baseというカテゴリーは作ったほうがいいと思います。

結果的にこのようなディレクトリ構成になると思います。

stylesheets/
  |- atoms/
    |- button.scss
    |- heading.scss
    |- text.scss
  |- molecules/
    |- form.scss
    |- navigation.scss
  |- organisms/
    |- header.scss
    |- sidemenu.scss
  |- templates
    |- template.scss
  |- base/
    |- base.scss
    |- mixin.scss
  |- application.scss

APBCSS

Atomic Designを用いたCSSの設計手法にAPBCSSというのがありました。Atomic Designを用いたCSSを考える上でとても参考になりました。

ただディレクトリ構成をAtomic Designのように/atoms/moleculesとしたかったので、導入は見送りました。 ディレクトリ構成はわかりやすさが肝心だと考えております。

まとめ

私としては、Atomic Designを使用するとCSSにおいて、管理が的確になると思います。いわば関心の分離です。

ただこれが全のアプリケーションにおいて最高のCSSの設計になるとは思っておりません。アプリケーションによっては使いづらいこともあると思いますし、SMACSSやBEMでも十分に使いやすいCSSは組めると思います。あくまで一例として捉えていただければと考えております。

また一番大事なのは、アプリケーション全体でデザインが統一されるようにすることであり、それを管理しやすくするためにCSSの設計を考えるということなのかなと思います。

とりとめのない結論になってしまいましたが、CSSが幸せになればいいなと願うばかりです。

We’re Hiring

弊社ではWeb / ネイティブアプリエンジニアを募集しております。
ご興味がありましたらお気軽にご連絡下さいませ!
エンジニアの方、ぜひ情報交換しましょう!

www.wantedly.com

www.wantedly.com

Hatena Engineer Seminar #9 @ Tokyoに参加しました!

f:id:cluex-developers:20171031080636p:plain

こんにちは。iOSエンジニアの高島(Takasy (@takattata) | Twitter)です。

今回参加した勉強会ではブログ枠ではなかったものの、すごく面白かったので記事を書かせて頂こうと思います。 hatena.connpass.com

会場はサポーターズさん提供でした。
存じ上げなかったのですが、こちら、イベントスペースを無料貸出されているとのことで、覚えておこうと思いました。

間違えていたらそっと教えてくださると嬉しいです!

GUIアプリケーションの構造と設計

Hiroki Kato (@cockscomb) | Twitterさん

speakerdeck.com

togetter.com

peaks.cc こちらの本の6, 7章を書かれている方でした。宣伝されていたので紹介しておきますね。
ちなみに次の発表者のいけしょーさんは4章を書かれています。
ベータ版が既に配信されていて、入手はしたものの未だ読んでいないので、早く読もうと思います。

それはさておき、発表の内容としては、GUIアプリケーションを作る際に必要になる3つのパターンについての話でした。
GUIアプリケーションの特性として、

  • モデルの状態をもとにUIを常に更新し続ける
  • 画面間でのデータのやりとりが必要
  • メインスレッドを止めてはいけない

があり、それを解決する為に

  • Observerパターン
  • オブジェクトの生成・管理
  • 非同期処理の抽象化

の3つのパターンをうまく使おうという内容です。
このパターンを1つずつ、Android, iOS, Webだと何に当たるかを並べて紹介され、実際にどの様なものなのかをKotlinで説明して下さる、という流れです。
どのアーキテクチャを使ってもどれにもこれらが入ってくると思うし、うまく使えば整理することができるのではないか、と締めくくられていました。

この3つのパターン、ここをまず抑えたら初級レベルから抜け出せそうだな、と感じました。
学ぶべき要点が分かって非常にありがたかったです。
また、Android開発を未だしたことがないので、そういうのがあるのかーという程度にしか分からならかった点もあったので、しっかりスライドで学ばせて頂こうと思います。

そういえば、話の中でCoreDataがすごく好きと仰っていたので、その理由を詳しく懇親会でお聞きできれば良かったなぁ、と今書いていて思いました。

Swift4を振り返りSwift5の夢を見る

いけしょー (@ikesyo) | Twitterさん

speakerdeck.com

togetter.com

すごく面白かったです!
が!理解とメモが追いつかなかった...!是非スライドをご覧ください。
どの発表もそうでしたが、この発表のスライドでちゃんと復習したいと思います。

流れとしては

  • Swift 4.0
  • Swift 4.1
  • Swift 5.0

の順に、特筆すべき新機能や特徴、それが入るとなぜ良いのかなどの説明をしてくださっていました。 1つずつ挙げるとかなりの長さになってしまいそうなので割愛します。

Swift5でメインになってくるABI Stabilityですが、これが入るのは知っていたものの、入ると何が嬉しいのかは分かっておらず、懇親会で質問させていただきました。
(※内容に誤りがある場合、自分の記憶違い+うろ覚え故ですので、コメント頂けると幸いです!)

そもそも現状Swiftでアプリを作るとSwiftの標準ライブラリなどがアプリに含まれる為、Objective-Cでアプリを作る時より大きくなっている。  
iOSにSwiftの標準ライブラリなどを入れられると良いけれど、ABI Stabilityが実装されていない状態でそれをしようとすると、例えばiOSに古いバージョンのものが入っていて、アプリは最新のSwiftのバージョンのものをコンパイルしてiOSで起動させようとすると起動しない、ということが起こる。  

以上を踏まえて、Swift 4 ABI安定化への道を読むと多少理解出来るかと思います。

何度か紹介されていたSwift Tweets - connpassはちゃんと後追いしようと思います。
次回はこちらです。
swift-tweets.connpass.com

あと、プロポーザルを見たことはほぼないので、確認してみようと思いました。
プロポーザルの番号もあげてくださっているので、そこから見てみようと思います。
チームの人とこんなプロポーザルあったね、という話が出来る環境、すごく面白そうですよね。

Kotlinの言語機能をフル活用したAndroidアプリの開発

たくじ (@takuji31) | Twitterさん

speakerdeck.com

togetter.com

可愛いうさぎの写真(飼っていらっしゃるそう)と、
「Kotlin、まさかNull安全だけで満足していませんか?」 という煽り気味なセリフ(ご本人談)から始まりました。

はてなでの新技術導入の流れからKotlinを導入する話、Kotlinの言語機能でこれが良い、という流れでした。
Kotlinを導入する時はGoogleI/Oの発表直後から1日半ほどで決定したそうです。

紹介されていたのは、

  • data class
  • 拡張関数 / 拡張プロパティ
  • Delegated property

などです。

特に拡張関数ではKotlin化の手順も紹介されていて、JavaからKotlinに一部的にでも書き換えている場合には非常に参考になりそうでした。

また、質疑応答では、
Q:「拡張関数は便利ですが、使うルールはどう決まっていますか?」
A: 「厳格には決まっていません。チーム的にも結構、使って良いよね、という感じです。レビューでダメだったら指摘します。
Androidフレームワークを拡張する時とか、引数4つのメソッドだけど使うのは1つだけの時とかに1つだけ設定する拡張関数作るとかで使ってます。
長くなりそうなコード且つよく使う時に使います。」
Q: 「どのファイルに何を書くとかは?」
A: 「ViewならViewの拡張関数をまとめているファイルがあったりするので、そんな感じにまとめています。」
とのことでした。

はてなブックマークアプリにおけるチーム開発について

ペロペロ (@experopero) | Twitterさん

speakerdeck.com

togetter.com

東京にディレクター、デザイナー、プランナーがいて、京都にエンジニアがいるというチーム構成になっている為、互いの認識のずれを防ぐ工夫をしていらっしゃいました。
リリースに至るまでの流れと工夫している取り組みの紹介という内容でした。

ビジョンの共有をしっかりとしているとのことで、エンジニアだとしても技術だけでなく、プロジェクトにも主体的にならないといけない、という主旨の話をされていました。
本当にその通りだと思います。
そもそもビジョンと合致しているならば、エンジニアだからこそ湧く発想などをどんどんコミットしていきたいと思いそうですが、意識していないとその感覚も薄れていくので、こういう共有は定期的にしっかりとするべきだと改めて感じました。

特に面白かったのは、内側タイムと、見積もり時に日数をピザで例えるところですね。
インターン生用のタスクを準備する時の話も興味深かったですね。
長くなりそうなので割愛しますが、スライドに分かりやすくまとめられていましたので、是非ご覧ください。

おわりに

総じて内容が濃く、非常に面白かったです!
懇親会も非常に盛り上がっていたように感じます。
iOSだけでなく、Androidの発表もある勉強会に参加する度にAndroidを少しでも分かっていたら、もっと楽しめるんだろうなぁ...!と若干悔しくもあるので、そろそろAndroidも触ってみようかな、と思いました。
勉強会、色々な刺激を受けて良いですね。
今後も積極的にHatena Engineer Seminarの参加狙っていこうと思いました。
はてなさん、ありがとうございました!

We're hiring!!

弊社ではWeb / ネイティブアプリエンジニアを募集しております。
ご興味がありましたらお気軽にご連絡下さいませ!
エンジニアの方、ぜひ情報交換しましょう!

www.wantedly.com

www.wantedly.com

potatotips#44に参加しました!(iOS編)

今月勉強会に色々参加させて頂いている高島(Takasy (@takattata) | Twitter)です。
前回Money Forwardさんで開催された

potatotips.connpass.com

に参加した際にすごく良かったので、今回エウレカさんで開催された

potatotips.connpass.com

もブログ枠で参加してきました!

f:id:cluex-developers:20171026080141p:plain

会場はお洒落で、Wi-Fiだけでなく電源も提供されていて、勉強会では珍しいなと思いました。

今回は弊社のエンジニアの井戸田くんがAndroidブログ枠で参加していたので、iOSブログ枠だった自分はiOSの発表だけに記載を留めさせて頂きますね。 Androidは乞うご期待でお願いします。

ありがたいことにtogetterにまとめてくださっている方がいらっしゃったので貼っておきます。 togetter.com

間違っている箇所等ありましたら、気軽にコメントにお願いします!

アプリ内でWebAPIを抽象化するためのフレームワークAbstractionKitの紹介

speakerdeck.com

ひらり (@hiragram) | Twitter さん

VASILYさんのテックブログにも書いていらっしゃるとのことで、併せて読むとなお良しです。 tech.vasily.jp

紹介されているAbstractionKitはこちら github.com

APIリクエスト周りで困ったことがまだ特になく、発表を聞いている時は話の内容がよく理解できませんでした。

  • CleanArchitectureでレイヤー分かれているからAPIリクエストが点在することはない
  • 基本的にデータは画面間を跨がない

あたりのおかげでしょうか。
同じように分からなかった方は、ブログで流れを追ってからスライドを見ると良いと思います。

  1. EndpointDefinitionというプロトコルを使用して、エンドポイントを定義する。(=エンドポイントのモデル化)
  2. 通信ライブラリと1の橋渡しとなるGenericRequest構造体を作る
  3. 抽象レイヤーとして機能する部分を作る(2を使って取得した結果からどうするかを記述する)
  4. 他画面へのデータ反映の為にブロードキャスト用のストリームを準備して、受け取りたい時に購読する

これは後ほどソースを読み込ませて頂こうと思いました。勉強になります。
スライドのまとめページの「AbstractionKit + APIKit + RxSwiftの説明時間あったらします(多分ない)」は是非ともして頂きたいなぁ。

Showcase Library について

(まだ資料があげられていなかったので、資料が上がり次第更新させて頂きます。) github.com

「UIViewが5つ横に並んでいて、これを無限スクロールでカルーセル表示したい」
を簡単に作る為に作ったライブラリの紹介でした。
横だけでなく、縦や円状の動きなどにも対応していました。
X/Y軸に-1~1で範囲を持ち、動く方向を指定できるとのことです。
図や動画など、視覚的に理解を促進するスライドだったので、資料上がるのが楽しみです。
これも後ほどソースを読み込ませて頂きたいと思いました。

Introduction Differ

speakerdeck.com

corin (@corin8823) | Twitter さん

ちょうど一週間前にもCA.swift #4 - connpassで発表されていたのに、今回も発表されていました。すごい。
Fluxでの設計についてはその時の資料を参照されると良いと思います。こちらの発表も面白かったです。
speakerdeck.com

UITableViewのリストから1行削除する時、reloadDataを走らせてテーブルごとリロードするとアニメーションしないし、1行の更新なのに全て更新されるのが微妙、という点から、
github.com こちらの紹介をされていました。

デモはこちら github.com

Fluxでアプリ作ってみたいと思うので、その際に参考にさせて頂こうと思いました。

WKWebViewのキャッシュについて調べた

www.slideshare.net

hotpepsi (@hotpepsi) | Twitter さん

競技プログラミングが趣味な強者です。
iOS10でWKWebViewのキャッシュが原因でユーザーのアプリの使用量がすごいことになったところから調査が始まったそうです。
調査する時の勉強にもなりそうだと思いました。
WKWebViewがオープンソースだということも知りませんでした。いずれ読んでみたいです。
各OSでのWKWebViewの調査結果が載っていて、iOS10で肥大化したのはバグではないかとのことでした。
バグレポートに書く + WWDCで開発者に伝え、iOS11ではキャッシュがアプリの使用サイズには含まれなくなったそうです。
「同じAPIでも、内部実装や挙動が変わることがある」というのを忘れないようにしようと思いました。

入力を型で表現する

speakerdeck.com

Motoki Narita (@motokiee) | Twitter さん

KAURUの製品情報の取得がサクサクですごく触ってみたくなるデモから始まりました。
この出品画面がそこそこ複雑で、今後入力経路が増えることも考えて、複数の入力を1つの型で表現するようにされた話でした。
データの有無で判別するとかでなく、入力の型自体で分けるのは分かりやすくて良いなと思いました。
まとめにも書かれていらっしゃいましたが、「データ自体の処理と画面の処理を分離しやすい」のは良いですよね。
ソースをたくさん貼ってくださっていて、具体的な実装方法が分かるので、スライド必見です。

carthage verify

speakerdeck.com

r_plus (@r_plus) | Twitter さん

CocoaPodsしか使ったことがないのでピンと来なかったのですが、Carthageではライブラリに追随していない時にはビルドエラーになるという仕組みが標準ではないそうです。
その問題にどうアプローチしていったかの話でした。
結論としてはCarthage/workflowsにあるcarthage-verifyスクリプトを使うとCocoaPodsと同じことが実現できるそうです。
Carthageを使う時の為に覚えておこうと思います。

余談ですが、コントリビュータのいけしょー (@ikesyo) | Twitterさんが素敵なツイートをしていらっしゃっいました。

AR FACE DETECTION

speakerdeck.com

Kaoru (@TachibanaKaoru) | Twitter さん

ARKitの紹介や現在iPhoneXのみ利用可能な顔認証の話でした。
ARの敷居は低く、どちらも実装が非常に簡単とのことです。
Xcode9のテンプレートプロジェクトから立ち上げて動くので、たしかにすぐ出来そうだな、と思いつつ、SceneKitやSpriteKitを触ったことがないので、そこのインプットも考えると優先順位が後回しになっていく現状がありますね...。ですが、やはりARKitは触っていきたいな、と改めて思わされる内容でした。
どういう仕組みで動いているのかが分かる発表だったので、是非スライドをご覧ください。
紹介されていた不思議の国のアリスARは触ってみようと思います。(かなりウサギが怖いですが。)

iOSのCorner Rouding Strategy

speakerdeck.com

Shunki Tan🤔 (@shunkitan) | Twitter さん

紹介されていたドキュメントはこちら Texture | Corner Rounding

スクラムマスター。懇親会でチーム開発についての話聞いてみようかと思って完全に忘れました...。
CALayerのcornerRaduisは毎フレーム、レンダリングが発生しているとのことで、極力使うのは避けようという話でした。
Cornerの種類を挙げ、ハイパフォーマンス順に実現の仕方を紹介してくださいました。
紹介されていたドキュメントのフローがすごく分かりやすかったです。
アプリのパフォーマンスを上げないと辛くなった時に手をつけようと思います。

終わりに

以上、iOSの発表だけですが、1つ1つの内容が濃かったなと感じます。
ブログを書くと内容を振り返らざるを得ないから良いですね!
参加した勉強会は今後なるべく記事にしていこうと思いますので、今後ともよろしくお願いします。

ちなみに、KAURUチームが書いた薄い本の仁義なきじゃんけん大会では敢え無く負けました。

We're hiring!!

弊社ではWeb / ネイティブアプリエンジニアを募集しております。
ご興味がありましたらお気軽にご連絡下さいませ!
エンジニアの方、ぜひ情報交換しましょう!

www.wantedly.com

www.wantedly.com

iOS Tips集 2

こんにちは。エンジニアの井戸田です。
Cluexでは7月に「ままのて」という、 妊娠、育児に関する医師監修のQ&Aやメッセージをお届けするアプリをローンチしました。

前回iOS Tips集 第1弾を書きました。第1弾はこちらの記事になります。 cluex-developers.hateblo.jp

今回はその第2弾になります。Nativeチームから集めた知識を文章化しました。

NavigationBarを透過、borderを消す方法

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        // 処理
        self.navigationController?.navigationBar.setBackgroundImage(UIImage(), for: .default)
        self.navigationController?.navigationBar.shadowImage = UIImage()
    }
}

StatusBarの文字色を白にする

storyboard、コードで変更することは可能ですが、1つ1つ全てのViewControllerに対してやらなくてはいけません。
以下の方法では一回の設定で全てのViewControllerのStatusBarの文字を白にすることができます。

TARGETSのinfoに追加する。

key: View controller-based status bar appearance
Value: No

key: Status bar style
Value: UIStatusBarLightContent

画像に合わせてアスペクト比を変更

表示した画像のアスペクト比が異なる場合、StroyboardのAspect Rationは使えません。
ソースコード上で画像のアスペクト比を変更させることができます。

  1. NSLayoutConstraint というクラスを使用して制約を設定する。
imageView.image = image

let constraint = NSLayoutConstraint(item: imageView,
                                    attribute: .height,
                                    relatedBy: .equal,
                                    toItem: imageView,
                                    attribute: .width,
                                    multiplier: (response.value?.size.height)! / (response.value?.size.width)!,
                                    constant: 0)

NSLayoutConstraint.activate([constraint])
  1. storyboardの方でUIImageViewの上下左右の制約をかける。ここではアスペクト比やwidth, heightの制約はかけません。
  2. tableView:estimatedHeightForRowAtIndexPath メソッドで UITalbeViewAutomaticDimension を返す。
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: indexPath) -> CGFloat {
    return UITableViewAutomaticDimension
}

ref.) http://crossbridge-lab.hatenablog.com/entry/2016/02/29/125917

NavigationBarに複数のボタン配置した時の隙間を無くす

図1のようにナビゲーションバーに複数ボタンを入れる場合、以下のようにするとボタンの間に隙間が開いてしまう。

func setupNavigationItem() {
    let starIcon = UIImage.fontAwesomeIcon(name: .starO, textColor: .darkGray, size: CGSize(width: 36, height: 36))

    let icons: [UIBarButtonItem] = [
        UIBarButtonItem(image: starIcon, style: .plain, target: self, action: nil),
        UIBarButtonItem(barButtonSystemItem: .action, target: self, action: nil)
    ]

    navigationItem.rightBarButtonItems = icons
}

f:id:cluex-developers:20171011111208p:plain
図1

図2のように隙間をなくす場合、一度UIButtonに入れてあげてからsizeToFit関数を呼んでUIBarButtonItemにしてあげることで隙間がなくなる。

func setupNavigationItem() {
  let starIcon = UIImage.fontAwesomeIcon(name: .starO, textColor: .darkGray, size: CGSize(width: 36, height: 36))

 let icons: [UIBarButtonItem] = [
      translateBarButtonItem(icon: starIcon),
      UIBarButtonItem(barButtonSystemItem: .action, target: self, action: nil)
  ]

 navigationItem.rightBarButtonItems = icons
}

private func translateBarButtonItem(icon: UIImage) -> UIBarButtonItem {
    let button = UIButton()
    button.setImage(icon, for: .normal)
    button.sizeToFit()
    return UIBarButtonItem(customView: button)
}

f:id:cluex-developers:20171011111216p:plain
図2

UIStackViewに背景色を付ける

UIStackViewにはUIViewが内包されておりません。
UIStackView自体に背景色を付けるためにはUIStackView内にViewをaddSubviewをする必要があります。

まずaddSubviewするUIViewのConstraintsを制御するExtensionを切ります。

extension UIView {
    func pin(to view: UIView) {
        NSLayoutConstraint.activate([
          leadingAnchor.constraint(equalTo: view.leadingAnchor),
          trailingAnchor.constraint(equalTo: view.trailingAnchor),
          topAnchor.constraint(equalTo: view.topAnchor),
          bottomAnchor.constraint(equalTo: view.bottomAnchor)
          ])
      }
}

上記のExtensionは該当するUIViewを引数のUIViewに対して、スペース無く一面に広げる制約を付与します。
下記はUIViewControllerの一例です。
UIStackViewにUIViewを挿入し、最後にpinでUIStackViewのサイズに広げています。

class ViewController: UIViewController {
    @IBOutlet weak var stackView: UIStackView!

    private lazy var backgroundView: UIView = {
        let view = UIView()
        view.backgroundColor = .red
        return view
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        pinBackground(backgroundView, to: self.view)
    }

    private func pinBackground(_ view: UIView, to stackView: UIStackView) {
        view.translatesAutoresizingMaskIntoConstraints = false
        stackView.insertSubview(view, at: 0)
        view.pin(to: stackView)
    }
}

ref.) https://useyourloaf.com/blog/stack-view-background-color/

We're hiring!!

弊社ではWeb / ネイティブアプリエンジニアを募集しております。
ご興味がありましたらお気軽にご連絡下さいませ!
エンジニアの方、ぜひ情報交換しましょう!

www.wantedly.com

www.wantedly.com