読者です 読者をやめる 読者になる 読者になる

アプリ用画像アセットを書き出す Sketch Plugin を作ってみた話 - Re.Ra.Ku アドベントカレンダー day 6

Re.Ra.Ku アドベントカレンダー 6日目です。

こんにちは。ヘルステックチームの磯貝です。主に iOS アプリ開発を担当しています。

iOS 担当です……が、今回は、アプリ内で用いる画像アセットの生成に関して書いていこうと思います!

背景

弊チームでは長らく、モバイルアプリケーションのモックづくりや画像アセット作成に Adobe Illustrator を使用してきました。

しかし最近ではチームメンバーが増え、開発スピードも上がってきたため、モックやアセットに対してより多くのメンバーが簡単にアクセスできるよう、Sketch への転換を進めています。このあたりの話については、後日別の記事で書ければと思います。

転換にあたり、ワークフローの中で必要となるいくつかの機能は、Sketch デフォルトでは実現できないことがわかりました。そこで、その中の一つである、モバイルアプリケーションのプラットフォームに合わせて最適な画像アセットを書き出す機能を、プラグインとして実装してみました。

Sketch Plugin 概要

Sketch はプラグインによる機能拡張に対応しています。プラグインの仕様は詳しく公開されており、ある程度 Mac (もしくは iOS) のフレームワークや JavaScript に通じていれば、作成することができます。現にプラグインのほとんどはサードパーティから提供され、公式サイトや GitHub 等で数多く公開されています。

Sketch Plugin は、CocoaScript という、JavaScript に Objective-C 記法での Cocoa フレームワークアクセス機能を付加した言語を用いて作成します。近い将来 Swift でのプラグイン作成も可能になるとのことですが、今のところはまだ対応していません。

Sketch Plugin では CocoaScript を用いることで、Sketch の提供する API だけでなく、Cocoa フレームワークを通して macOS へアクセスすることができます。そのため、ファイルの読み書きをはじめとした多くの機能を、思ったままに実現することができます。

var helloWorld = function() {
  log("hello, world!");

  var osVersion = [[NSProcessInfo processInfo] operatingSystemVersionString];
  log(osVersion);
}

helloWorld();

// => "hello, world!"
// => "Version 10.11.6 (Build 15G1004)"

ちなみに Sketch は、以前は App Store でも公開されていましたが、今は公式サイトからのダウンロードのみとなっています。そのため、App Sandboxing に関しては、この記事では触れません。

Tips

プラグインの実装に入る前に、開発における Tips をいくつか紹介します。

コード片実行環境

Sketch.app には、1ファイルに収まる CocoaScript を Sketch 上で実行するための環境が用意されています。
メニューの、[Plugins] > [Custom Plugin]を選択してください。先程の Hello World コード片は、この画面からも実行できます。

log() メソッドを使えば、実行結果をコンソール上に出力することができます。

Console.app

プラグイン内における log() の結果は、Mac 標準のグローバルなログとして、/var/log/system.log へ出力されます。同時に、Sketch.app 自体が吐き出すログも確認することができるので、エラーメッセージもある程度は読むことができます。

このログ出力を簡単にフィルタして閲覧するために、Mac にデフォルトでインストールされている Console.app を使います。

Console.app を開いたら、メニューの [ファイル] > [新規システムログクエリー] を選択し、新たなフィルタ設定を追加しましょう。参考までに、私が使用している設定を以下に挙げます:

f:id:y1soga1:20161205020423p:plain

CocoaScript のブラケット表記に関して

CocoaScript は、Sketch や Cocoa フレームワークの API を、Objective-C のブラケット表記を用いて実行することができます。しかし、JavaScript のコード内にブラケット表記が紛れているのは、正直気持ち悪いです。JavaScript 向けのコードフォーマッティングも効きません。

実は、ブラケット表記は JavaScript 形式のメソッドに置き換えることができます。外部引数名をアンダースコア _ で繋いだものをメソッド名とし、引数はまとめて与えます。

var month;

// こんなブラケット表記は
month = [[NSCalendar currentCalendar] component:NSCalendarUnitMonth fromDate:[NSDate new]];

log(month) // => "12"

// こう書くこともできます
month = NSCalendar.currentCalendar().component_fromDate(NSCalendarUnitMonth, NSDate.new());

log(month) // => "12"

画像アセット書き出しプラグイン

さて、ここからは実際に画像を書き出すプラグインを作っていきます。

要件

今回の要件は、以下の通りです。

  • アートボード直下に存在するレイヤーを、透過 png 画像として書き出す。
  • iOS 向けの書き出しは、Asset Catalog 形式として書き出す。その際、アートボードごとに名前空間を切る。
  • Android 向けの書き出しは、resources ディレクトリ以下に各解像度の画像を書き出す。
  • 書き出しの設定には、アートボード名やレイヤー名を用いる。
    • アートボード名は、export:format_a:format_b:base_name とする。
      • アートボードに含まれるルートレイヤーが、それぞれ画像アセットとして書き出される。
      • フォーマットは複数指定できる。対応するのは、iphone, ipad, ios (universal), android (/resources), android-mipmap (/mipmap)
      • base_name は、Asset Catalog においてはサブディレクトリ名となり、名前空間が切られる。Android resources においては、ファイル名の prefix となる。
    • レイヤー名の頭に noexport: が含まれる場合、そのレイヤーは書き出さない。

あとは要件に合わせて粛々と書いていくだけなのですが、数点、ポイントとなりそうな部分をピックアップして解説します。

最終的に出来上がったものは、GitHub で公開しています: sketch-mobile-assets-generator

ファイル構造

Sketch Plugin のファイル構造は、以下のようになっています。

GenerateMobileAssets.sketchplugin    // プラグイン Bundle の拡張子は .sketchplugin
  Contents/                       // 必須
    Sketch/                       // 必須
      manifest.json               // 必須
      generate.cocoascript        // manifest.json から、コマンドに応じて実行するスクリプトを指定する
      foo_script.js
      bar_script.any_extension    // 拡張子はなんでも ok
      SomeOptionalDirectory/      // ディレクトリも追加可能
        shared_contents.js

manifest.json

プラグインのメタデータです。

{
  "name": "Generate mobile assets",
  "description": "Plugins to generate assets for iOS and Android.",
  "author": "yisogai",
  "version": 0.1,
  "identifier": "jp.co.reraku.GenerateMobileAssets",
  "commands": [                          // プラグインとして実行できる操作を記述する
    {
      "name": "Generate All",            // メニューに表示されるタイトル
      "identifier": "all",               
      "shortcut": "ctrl shift e",
      "script": "generate.cocoascript"   // コマンド選択時に実行されるスクリプトファイル
                                         // デフォルトでは 'onRun()' メソッドが実行される
    },
    {
      "name": "Generate Android",
      "identifier": "android",
      "script": "generate.cocoascript",
      "handler": "android"               // 'onRun()' 以外のメソッドを実行したい場合に指定
    },
    {
      "name": "Generate iOS",
      "identifier": "ios",
      "script": "generate.cocoascript",
      "handler": "ios"
    }
  ],
  "menu": {                              // 実際にプラグインメニューに表示される内容
    "title": "Generate Mobile Assets",   // ルートディレクトリ名
    "items": [
      "all",                             // コマンドの 'identifier' を指定する
      "-",                               // '-' は区切り線となる
      {                                  // ネストすることもできる
        "title": "Platforms",
        "items": [
          "android",
          "ios"
        ]
      }
    ]
  }
}

詳しくはこちら

generate.cocoascript

スクリプトの拡張子は特に決まっていませんが、ここでは公式でも用いられている .cocoascript を使用します。

@import "lib/document_parser.js"
@import "lib/config_loader.js"
@import "lib/exporter.js"

// manifest.json で指定したエントリーポイントとなるメソッドには、context オブジェクトが渡される。
// そこからプラグイン実行対象のファイルへアクセスできる。
var onRun = function(context) {
  _generate(context, ["xcode_asset_catalog", "android_res"])
}

var android = function(context) {
  _generate(context, ["android_res"])
}

var ios = function(context) {
  _generate(context, ["xcode_asset_catalog"])
}

var _generate = function(context, configTypes) {
  var document = context.document
  // Cocoa フレームワークへのアクセス
  var baseDir = document.fileURL().path().split(document.displayName())[0] + "build"
  var exporter = new Exporter(document, baseDir)

  var layerGroups = DocumentParser.parse(document)
  var configs = ConfigLoader.load(configTypes)

  exporter.export(layerGroups, configs)
}

lib/*

cocoascript ファイルは、相互にインポートすることができます。ここでは、プラグインの機能を実装しています。
ほとんどは JavaScript のコードであり、特殊な操作は行っていません。一部、Sketch API や Cocoa フレームワークを操作している箇所のみを抜粋します。

// exporter.js

...

Exporter.prototype._exportAsAndroidRes = function(baseName, layers, config) {
  var that = this
  var root = this.baseDirectory + "/" + config.id

  layers.forEach(function(layer){
    config.densities.forEach(function(density){
      // レイヤーのエクスポートには、Sketch API である MSExportRequest を使用する
      var request = MSExportRequest.exportRequestsFromExportableLayer(layer)[0]
      // request に scale を設定することにより、書き出す画像の解像度を変更できる
      request.scale = density.scale
      var filename = baseName + "_" + layer.name() + ".png"
      var file = root + "/" + density.folder + "/" + filename
      // ブラケット表記をアンダースコアによる JavaScript 風表記へ変換して記述
      that.document.saveArtboardOrSlice_toFile(request, file)
    })
  })
}

...
// exporter.js

...

Exporter.prototype._writeObjectToJsonFile = function(object, path, filename) {
  var json = JSON.stringify(object, undefined, 2)
  var string = NSString.stringWithFormat(@"%@", json)
  var file = path + "/" + filename

  // Cocoa フレームワークへのアクセス
  var manager = NSFileManager.defaultManager()
  
  // こちらも、ブラケット表記をアンダースコアによる表記へ変換している。
  // また、null と nil は同等のものとして扱われる。
  manager.createDirectoryAtPath_withIntermediateDirectories_attributes_error(path, true, null, null)

  string.writeToFile_atomically_encoding_error(file, true, NSUTF8StringEncoding, null)
}

...

まとめ

いかがでしたでしょうか。かなり駆け足になってしまいましたが、自分が Sketch Plugin を作るにあたり、引っ掛かりを感じた箇所については一通り共有できたかと思います。

用途が限定される CocoaScript ですが、みてみると分かる通り、案外簡単に書くことができます。
普段づかいのエディタを秘伝のスクリプトでモリモリ拡張しているそこのあなた! エディタ以外の環境も、ぜひ使いやすいように改良してみてください。

とはいったものの、だんだん iOS が恋しくなってきたので、次回は iOS や Swift に関しての話題を書ければと思います。それでは!


今回作成したプラグイン: sketch-mobile-assets-generator

いってきたぜ #builderscon tokyo 2016 - Re.Ra.Ku アドベントカレンダー day 5

丸山です。この記事はRe.Ra.Kuアドベントカレンダー5日目の記事です。前の記事はアンドロイドアプリの設計手法についての記事でした。明日はおそらくiOSネタになると思います。お楽しみに!

さて、12/3(土)にbuilderscon tokyo 2016というカンファレンスが開催されました。

buildersconは「知らなかった、を聞く」というキャッチフレーズ(?)を掲げた、多様な方面のエンジニアリングに関わる発表が聞ける、「エンジニアのためのお祭り」です。わたしは今回は聴衆として、また、発表者として参加してまいりました。

参加まで

ビルダーズコンの構想を聞いて、「これはスタッフとしてお手伝いしたい!」と思って、最初の方のスタッフミーティングにはちょこちょこ顔を出していたのですが、業務の都合や家庭の事情でなかなかコミットすることができず、途中から存在感が消えてしまっておりました。残念。来年はお役に立ちたい!

とはいえ、絶対に発表はしたいなあと思っており、トークには応募しました。そしたら、あれよあれよと言う間にどんどん面白そうなトークが応募され、「あー、これはトーク通らないかもなあ」と思って半分くらいは発表を諦めていました。

チケット発売のタイミングでは、「チケットを購入し聴衆として参加しよう」!と思っていたのですが、蓋を開けてみればなんとチケットは3時間でソールドアウト!あらためてビルダーズコンの注目度の高さにおどろき、「ああ、もうだめだ、これでトークが採用されなかったらわたしは記念すべき第一回ビルダーズコンになんの関わりもなく終わっていくのか……ああ……」と思ってかなり苦しい気持ちになったことを覚えています。

祈るような気持ちでトーク採択を待ち続けていましたが、なんとかトークを採択していただき(「片手間JavaScripter」にも知ってほしい、Vue.jsで実現するMVVMパターン、Fluxアーキテクチャとの距離)、発表者としてなんとか参加できることになり、意気揚々と参加してきました。忙しい中発表の練習に付き合ってくれた同僚に感謝の意をここで伝えておきたいです。

当日

とにかく最高だった!とくに面白いと思ったトークは以下の通りです。

OSS は Windows で動いてこそ楽しい

ミーハーなので、「うおおおおお生mattnさんや!!」みたいな感情が湧いてきたところから始まったのですが、mattnさん自身からも発表からも「ハックする気持ち」が溢れ出てて、とにかくかっこよかった。質疑応答の最中に、「若いプログラマに勢いだけは負けたくない」とおっしゃっていましたが、家族と子供がいて普段の業務も忙しい中でそのような気持ちを持ち続けることがどれだけ難しいことか!そして、mattnさんほどの方もそのような思いに突き動かされていると言うこと。そのひとことがかなり印象に残っています。

動け!Golang 〜圧倒的IoTツール開発へようこそ〜

Akerunのkazphさん。社名はAkerunじゃなかったはずだけど、Akerunのひと。わたしはソフトウェア技術者なので、普段の仕事の中工場の話に触れることはほとんどというか一切ないのだけれど、web系に馴染みの深い技術で(というかそもそもkazphさんの出自自体がweb系技術者である)リアルの業務を自動化、改善していく内容は「うおお、これはエンジニアリングだ!うおお!」という感じで相当面白かった。

製品にファームを焼くときに、商品個体のidをどうやって書き込むのかと言うとバイナリを書き換えてるんだよ〜ってのは「まあそうか、そらそうだ」という感じだったのですが、そこはPerlやRubyで行なっていると聞いて、「おおお、LLと低レイヤーの組み合わせ!萌える!」となりました。拙作のwaveずたずた(Rubyでwavファイルをずたずたにするやつ)を思い出した。

発表のあと個人的に「golangでBLE扱うっていってましたけど、ドライバ周りとかどうなってるんです?」と質問させていただいたのですが、「そこは抽象化してくれてるライブラリがあって、それ使えば同一ソースコードでMacでもRasPiでもWinでも動くんすよ」という感じの回答で、なるほど便利という感じでした。

Open Beer Serverの理論とその実装

今回の漫談枠。かと思いきや、実装の話は普通に興味深かった。わたしは電気系かなり弱いので知らないことたくさん知れてよかった。電磁でもってバルブを開閉するソノレイドバルブというやつは頭いいなーって思った。リレーのこと知ったときにも「あー頭いいなー」と思ったが、同質の「はー頭いいなー」であった。だんだん感想が雑になってきた。

一から始めるJavaScriptユニットテスト

はてなの id:shiba_yu36 さんによる発表。JavaScriptのテスト周りはツールがいろいろあって、わたしも最初混乱したんだけど、この発表ではそれらがかなりわかりやすくまとめられていてよかった。karmaをCIに載せる話が一番参考になった。弊社はUIテスト諦めちゃってるので参考にしていきたい。

お昼をご一緒させてもらって、懇親会でもお話しさせてもらって、JSプロジェクトのアーキテクチャの話とかチーム開発の難しさとかいろいろお話しできてうれしかったです。

自分の発表について

ツイッターを追ったかぎりではそれなりに評判がよくて一安心しています。30minにかなりの内容を詰め込んでしまったため駆け足になってしまった。

今週の土曜に新潟県長岡市で行われるNDSという勉強会の50回目(!)で再演を行うので、是非みなさん足を運んでください。東京駅から1h30min程度で着きます。

内容やスライドの公開はないのか、という声がちらほら聞こえてきますが(ありがたいことです)、再演が終わったあと、またこのブログで実況中継シリーズ形式で発表内容を公開させていただきますので、今しばらくお待ちください。

最後に、ほんとに最高のカンファレンスでした。スタッフよみなさんありがとうございました!来年もトーク応募するぞ!!!

Androidアプリの設計の話 - Re.Ra.Ku アドベントカレンダー day 4

Re.Ra.Ku アドベントカレンダー 4日目です。

こんにちは、安部です。Androidアプリを担当しています。初投稿です。

今回はおそらくAndoidアプリ開発者は常に頭を悩ませる問題である、設計の話です。私もずっと試行錯誤を繰り返し、現在進行系で悩んでる問題でもあります。少し見えてきた部分があるので現在の状況を共有したいと思います。

抱えてる問題

Androidアプリ開発していくうえで、よくぶつかる壁です。

ライフサイクル

Androidのライフサイクルは複雑でViewの状態をどのように管理すればよいのか難しいです。今はActivityだけではなくFragmentもあるので、更に複雑にしていると思います。

また、画面の回転などの対応を考えたりすると更に大変です。

非同期処理

通信は非同期にしなければいけません。そのためUI側もそれを意識しなければいけなくなり、Viewの状態管理も複雑になっていきます。

テスト

ContextなどAndroid固有のものを必要とするクラスが自然と増えてしまうので、すぐにテストがしにくくなってしまいます。

最近はこのあたりはDagger2でDIすることでだいぶ改善されつつありますが、まだまだコストが高い印象です。チーム等で検証してバランスを取ってやっていくのが良いかなと。

DDD

DDDをやらなきゃと思って、どこに何を書けば良いのかわからない状況に陥りやすいです。いざやってみると頭を抱えることが多くなります。

現在の状況

現在はMVVM+DDDみたいな形でやっています。

レイヤー分離

現在のレイヤーの分け方は次のようになっています。

  • Presentation
    • Viewに対しての操作や状態管理、イベントハンドリングを行います。
  • Application
    • Domainの直接のクライントになります。ここはPresentationから呼び出され、Domainへ処理を移譲するものになります。
  • Domain
    • モデルやビジネスロジックやリポジトリとAPIアクセスのインターフェースなどがあります。
  • Infrastructure
    • 詳細な実装を記述します。DBアクセスやAPIアクセスなどの詳細な実装になります。

依存関係は Presentation → Application → Domain になります。InfrastructureについてはDIを使って依存を解決するので、他のレイヤーに直接でてくることはありません。

packageも同じような分け方になります。

Presentationレイヤー

ここではUIに関するものを処理します。

現在はDataBindingを使った、MVVMっぽい感じにしています。まだまだ改善の余地があると思っていますが。

MVPでもMVVMでもやることは変わりません。UIなどの表示とイベントに関する責務を負います。それ以外のことはしないようにします。逆に他のレイヤーにこれらの関心ごとが漏れないようにします。

ここではビジネスロジックを処理しませんが、おそらく一番コード量が多いレイヤーになるかと思います。特にViewの状態を管理したりするのはライフサイクルなども絡んできてかなり複雑になると思います。

私の場合ですが、ここから直接Domainレイヤーの処理を呼ばずに、後述のApplicationレイヤーを経由するようにしています。

このあたりのDataBindingとMVVMについては来年のDroidKaigiで話す予定です。

Applicationレイヤー

ここはDomainの直接のクライアントになります。Presentationレイヤーから呼び出されDomainレイヤーへ処理を委譲します。Domainレイヤーに受け渡すためのデータの加工を少しやったりもします。

Domainレイヤー

ここが一番どうしたら良いのかが分からなくなることが多いと思いますが、最近思うのはAndroidアプリではDomainレイヤーはビジネスロジックがそこまで多くない気がします。

アプリの性質にもよりますが、だいたいAPI経由で処理をサーバーに移譲している思うので、ほぼサーバー側にビジネスロジックが実装されていることになります。そのためDomainレイヤーにはAPIに対するインターフェースとモデルが多い感じなっています。

ただ、しっかりとモデルを設計する必要はあるので、そこまで単純ではないと思います。

ここではAPIやDBの実装の詳細は記述しません。例えば、API通信の処理についてはインターフェースのみを定義して、実装はInfrastructureレイヤーに記述するようにします。

リポジトリの場合は次のような感じのみです。

public interface UserRepository {
    User getUser();
}

APIのインターフェースは私はGateWayと命名しています。ここでは戻り値をRxJavaのObservableにすることで非同期を扱いやすくしています。

public interface UserGateWay {
    Observable<User> getUser();
}

また、ドメインモデルにも実装の詳細が入り込まないようにします。例えば、Realmを使ってる場合でRealmObjectを継承したりはしないようにします。DBテーブル情報はInfrastructureレイヤーのほうで定義してあげて、それからドメインモデルに変換してあげるようにします。

ここでは基本的にAndroidのフレームワークに依存するようなものが無い状態(import android.*がない)が理想だと思います。しかし、一部Parcelなどは例外にしています。最初はPresentationレイヤーでDTOみたいなのを作って対応していたのですが、ごちゃごちゃしてきてしまったので、Parcelは例外的にOKにしました。

Infrastructureレイヤー

Domainレイヤーに定義されたインターフェースを実際に実装します。

ここはPresentationレイヤーの次にコード量が多くなる気がします。

APIやDBから取得したレスポンスをドメインモデルへの変換もここで行います。

public class RealmUserRepository implements UserRepository {

    public User getUser() {
       // DBから取得して、ドメインモデルのUserへ変換
    }
}
public class APIUserGateWay implements UserGateWay {

    public Observable<User> getUser() {
       // APIから取得して、ドメインモデルのUserへ変換
    }
}

DI

Infrastructureレイヤーにおいたものは基本的に他のレイヤーで直接依存するようがないことが重要です。そのためDIで依存を解決してあげます。

変更に強くなったり、テストしやすくなったり、BuildVariantごとに差し替えたりできるようになります。可能であれば初めから導入しておくことをおすすめします。あとから導入すると結構大変なので。

すべては書きませんが、Dagger2の場合は次のような感じのモジュールを使う感じなります。

@Module
public class DomainModule {
    @Provides
    @Singleton
    UserRepository provideUserRepository(Context context) {
        return new RealmUserRepository(context);
    }
}

これをApplicationレイヤーでInjectしてあげます。

public class UserService {

    private UserRepository repository;

    @Inject
    UserService(UserRepository repository) {
        repository = repository;
    }

    public User getUser() {
        return repository.getUser();
    }
}

このようにすればInfrastructureレイヤーの詳細な実装が他のレイヤーに漏れることはありません。

まとめ

現在の設計の状況をざっくり書いてみました。本当はサンプルを用意できればよかったのですが…

実際に試さないとわからない部分も多くあるので、最初は多少オーバーキル的な箇所があっても良いと思います。ただあまり時間をかけすぎるのも駄目なのでバランスを取りながらやっていくと良いと思います。

設計に正解はないと思っています。今の設計もまだまだ改善すべきところはあります。このあたりは随時改善していきたいです。

プログラミングしながらできる、健康への近道講座:1回目 - Re.Ra.Ku アドベントカレンダー day 3

こんにちは!
Re.Ra.Kuの泉原です。

アドベントカレンダー3日目は、非エンジニアの泉原が担当します。

本日はbuilderscon開催日ですね。今回のbuildersconにも、弊社CTOの丸山が登壇しています。
皆様丸山の登壇内容のチェックもぜひお願いいたします。
トーク内容は近日中に当ブログでお知らせする予定なので、会場にいない皆様はブログをご確認くださいませ。

非エンジニアの私は、Re.Ra.Kuを代表してエンジニアさんに向けた健康情報をお伝えしていこうと思います。
1回目は、「プログラミング中の姿勢の保ち方」について、まとめます。

悪い姿勢とは

まず、以下の画像をご欄下さい。 f:id:nozomiii:20161202120856p:plain

恐らく誰から見ても悪い姿勢だと思いますが、 誰でも経験のある姿勢なんじゃないかな?と思います。

この姿勢が「悪い」ところは大きく分けて3つです。

  • 腰が倒れていて、体が前のめりになっている
  • 腕に引っ張られて、肩甲骨も前のめりになっている
  • 肩甲骨が上がっているので、首もすぼまっている

悪い姿勢が体に「悪い」理由

悪い姿勢になっていると、体の部位が正しい位置からずれるため、
いらない力が入って筋肉が硬くなり、コリにつながります。
また、筋肉が硬くなると、血の巡りが悪くなるため、
硬くなっている筋肉だけでなく、脳や内臓など、
全身の血の巡りも悪くなり、動きが悪くなります。

単純に見た目が悪いだけでなく、
影響している筋肉も、影響していない体の部位にも負担を大きくかけてしまうので、
姿勢が悪いことは体全体の負担になってしまいます。

綺麗な姿勢の作り方

悪い姿勢を正しい姿勢に戻すにはどうしたら良いか?
それは、骨盤を意識することです。

  • 坐骨を椅子に立てるように座る
  • 骨盤の真上に頭がくるように意識する

たった2つを意識するだけで、

f:id:nozomiii:20161202120926j:plain

こんなに綺麗な姿勢になります。

この姿勢になると、以下の箇所も正しい位置に戻って疲れのたまりにくい身体に近づきます。

  • 前のめりになっていた体重が正しくなり、身体の負担が減る
  • 肩甲骨が自然と寄るので胸が開き、呼吸も大きくなる
  • 胸が開くので腕の疲れも取れやすくなる

また、プログラミングの合間に手のひらを上に向けると さらに肩甲骨が寄って、上半身の疲れが取れていきます。 f:id:nozomiii:20161202120929j:plain

最後に

なんだそんなことか、当たり前のことか、という話だと思いますが、
この当たり前なことをちょっとした意識するかどうかでも
体は大きく変化することもあります。

より快適にプログラミングに注力できるよう 今までよりも少しだけ意識を身体に向けてもらえると良いなと思います。

なお、悪い姿勢が癖になっている身体は、筋肉のコリや体の癖も強くなっています。
そういった方はRe.Ra.Kuの店舗でコリや癖をほぐし、綺麗な姿勢を保ちやすくすることをお勧めします。

reraku.jp

次回以降は、プログラミングの合間にできる簡単なストレッチ方法をお伝えします。
今後とも宜しくお願いいたします!

リラクのサーバサイド事情 with Scala - Re.Ra.Ku アドベントカレンダー day 2

Re.Ra.Ku アドベントカレンダー 2日目です。

こんにちは。ヘルステックチームの近藤です。開発では主にサーバサイドを担当しています。

ところでみなさん、リラクのサーバサイドに使われているプログラミング言語をご存知でしょうか?ご存知の方は通ですね。ご存知でない方はこれを機に興味を持っていただけると幸いです。リラクではScalaを採用しています。

ですので、サーバサイド担当の私の話はほとんどがScalaの話です。しょうがないですよね。そもそも私Scala大好きですし。

ということで、リラクのサーバサイド事情 with Scalaと称して、Re.Ra.Ku アドベントカレンダー 2日目もといRe.Ra.Ku Scalaアドベントカレンダー 1日目です。と言いつつ、サーバサイドの一般的な話が多めなので、Scala成分は少し薄めです。

ちょとした方針

サーバサイド事情とか言っていますが、特に変わったことはしていません。クライアントからのリクエストはHTTPで受け付けていますし、データベースにはRDBを利用しています。よくあるものです。

ただ弊社では以下の方針を執っています。

JSONしか喋らない

サーバサイドと言うと範囲が広いのですが、実際はほぼアプリ(クライアント)のAPIサーバーしか書いていません(あとは非同期処理用のワーカーとか)。ですので、HTTPのレスポンスボディはJSONです。

ただし、アプリはスマートフォンアプリに限らずウェブアプリも指しています。ウェブアプリは静的ファイルによるSPAになっていて、そのファイルをScalaから出力することはありません。

よって本当にJSONしか喋りません。

操作指向のURL

JSONしか喋らないとは言え、プロトコルはHTTPですので、当然リクエストメソッドやURLという概念があります。

APIサーバーを書くときにRESTを採用することって結構あると思います。http://example.com/users/1 というURLで「ユーザID: 1のユーザー」というリソースを表し、そのURLへのGETで情報の取得、PUTで更新、DELETEで削除といったやつですね。

しかし弊社ではRESTのようなリソース指向のURLにせず、操作指向のURLにしています。

操作指向のURLとは何か。「ユーザID: 1のユーザー」の情報を取得する場合は GET http://example.com/getUser?id=1 とし、更新する場合は POST http://example.com/updateUser とするものです。

なぜ操作指向のURLを採用したか、ですが:

  • リソース指向のURLでは表現に無理が生じる(ときがある)*1
  • クライアントが求めるものは画面に表示するための情報(レポート)であり、リソース指向だとひとつのAPIで得られる情報に過不足が生じる

あたりが理由です。もちろん広く公開するAPIならば誰が利用するか分からないのでリソース指向になるでしょう。しかし、クライアントが求めている情報が明確なのであれば、それ専用のAPIを作ってしまった方が得策です。そうなってくると操作指向のURLの方が都合が良くなってきます。

アーキテクチャ

アーキテクチャは一応レイヤードアーキテクチャを採用しています。何故「一応」なのかと言うと、使っている言葉はレイヤードアーキテクチャのものなのですが、依存関係逆転の原則などを用いて依存方向を厳密にしているので、実態はオニオンアーキテクチャやクリーンアーキテクチャの様相を呈しているためです。が、一応レイヤードアーキテクチャと言っておきます。

ドメイン層はDDDによってモデリングしています。

アプリケーション層はドメイン層のモデルを活用してクライアントが直接利用するアプリケーションサービス(ユースケース)を実現しています。

プレゼンテーション層はHTTPリクエストを解釈し、アプリケーションサービスが求める形に変換、アプリケーションサービスから得られた結果をHTTPレスポンスに変換しています。

そしてインフラストラクチャ層はドメイン層のインタフェースを実装しています(RDBへの永続化、外部ウェブサービスとのHTTP通信など)。

レイヤードアーキテクチャを採用した理由ですが、少なくともトランザクションスクリプトやアクティブレコードパターンでは自身の首を締めることになるだろう、という考えがあったからで、あまり積極的な理由はありません。実際今のアーキテクチャで不自然なところもあったりするので、それは今後の開発で次第に変えていくと思います。

プロジェクトの分割

ビルドツールにはsbtを採用しています。ですので、ディレクトリ構造もsbtに準拠しています。

ただし、よほど単純なものでない限りは必ずマルチプロジェクトにします。以下は実際に使っている project/build.scala を簡略化したものです。

// project/build.scala

import sbt._
import sbt.Keys._
import org.scalatra.sbt._
import org.scalatra.sbt.PluginKeys._
import com.earldouglas.xwp.JettyPlugin

object ServerBuild extends Build {
  val serverSettings = Defaults.coreDefaultSettings ++ Seq(
    organization := "jp.co.reraku",
    version := "0.0.1",
    scalaVersion := "2.11.8"
  )

  // ルートプロジェクト: 他のすべてのプロジェクトをまとめているプロジェクト
  lazy val server = Project(
    id = "server",
    base = file("."),
    settings = serverSettings
  ) aggregate(domain, jdbcImpl, appApi, adminApi)

  // アプリ用APIプロジェクト: スマートフォンアプリに提供するAPI用プロジェクト(要件によって名前が変わる)
  lazy val appApi = Project(
    id = "app_api",
    base = file("app_api"),
    settings = serverSettings ++ ScalatraPlugin.scalatraSettings ++ Seq(
      parallelExecution in Test := false,
      libraryDependencies ++= Seq(
        // Scalatraなどに依存
      )
    )
  ) dependsOn(domain, jdbcImpl) enablePlugins JettyPlugin

  // 管理画面用APIプロジェクト: 管理画面ウェブアプリに提供するAPIプロジェクト(要件によって名前が変わる)
  lazy val adminApi = Project(
    id = "admin_api",
    base = file("admin_api"),
    settings = serverSettings ++ ScalatraPlugin.scalatraSettings ++ Seq(
      parallelExecution in Test := false,
      libraryDependencies ++= Seq(
        // Scalatraなどに依存
      )
    )
  ) dependsOn(domain, jdbcImpl) enablePlugins JettyPlugin

  // JDBC実装プロジェクト: ドメイン層のインタフェースをJDBCで実装したクラス群がまとまったプロジェクト
  lazy val jdbcImpl = Project(
    id = "jdbc_impl",
    base = file("jdbc_impl"),
    settings = serverSettings ++ Seq(
      parallelExecution in Test := false,
      libraryDependencies ++= Seq(
        // ScalikeJDBCなどに依存
      )
    )
  ) dependsOn domain

  // ドメインプロジェクト: ドメインモデルを表しているプロジェクト
  lazy val domain = Project(
    id = "domain",
    base = file("domain"),
    settings = serverSettings ++ Seq(
      libraryDependencies ++= Seq(
        // Joda-Timeなどの基本的な値を表現するライブラリのみに依存(フレームワークなどの詳細に依存しない)
      )
    )
  )
}

実際は serverSettings にscalac用のオプションを設定したり、sbt-scalariformを設定していたりします。もちろんプロジェクトの数も要件に合わせて増減します。

ですがコアはさきほどの project/build.scala のようになっており、ドメイン層をひとつのプロジェクトとして分離しているところが肝です。

サーバサイドにおいて、管理画面から登録したデータはアプリ側から参照され、アプリ側から記録したデータは管理画面で閲覧されるように、アプリ側APIと管理画面側APIの核となるビジネス要件、つまりドメインモデルは同一でないと都合が悪いです。そのため各APIごとにGitリポジトリを分けるといったことはしていません。以前1度だけそのように開発しましたが、各Gitリポジトリのドメインモデルがほとんど似たコードになってしまいました。

ではひとつのGitリポジトリかつひとつのプロジェクトで開発をするのか。いや、コードの規模が膨らむにつれて大変なことになるのが目に見えます。ですので、ひとつのGitリポジトリで、複数のプロジェクトに分ける。そして各ユースケース群(例えばアプリ側APIと管理画面側API)から共通して参照され、操作を行うドメインモデルはそれひとつでプロジェクトとして切り離す、という風にしました。

ちなみにドメインモデル以外にも共通で参照されるものがあります。それはドメインモデルの実装です。jdbcImplプロジェクトがそれに該当します。共通で参照されるものであればdomainプロジェクトに含めてしまっても、と考えるかもしれませんが、domainプロジェクトはドメインモデルの集合で、何を用いて永続化するのか、という詳細を知るべきではありません。よって各実装もそれぞれひとつのプロジェクトに切り離しています。jdbcImplはその例のひとつですね。

このように分けると、各プロジェクトがどのライブラリに依存するべきなのかはっきりしてきます。domainプロジェクトはさきほども述べた通り、詳細を知るべきではありません。よってdomainプロジェクトの依存ライブラリにScalatraやScalikeJDBCが含まれていたら危険信号というわけです。プロジェクトを分割することによって、こういった効果も生まれます。

アプリケーション層

それでは各層がどのように実装されているかコードで示していきます。ちなみにドメイン層はDDDの話が絡み、複雑になってくるので、今回触れずに次回触れてみようと思います。よってまずはアプリケーション層から。

package application.user

import domain.user._
import infrastructure.jdbc.user._

import scalikejdbc._

class UpdateUserNameCommand(val userId: UserId, val name: String) {
  // 引数のバリデーションを行う
}

object UpdateUserNameApplicationService {
  val userRepo: UserRepository[DBSession] = new JDBCUserRepository()

  def apply(command: UpdateUserNameCommand): User =
    DB localTx { implicit session =>
      val user = userRepo.find(command.uesrId)
      val nameUpdatedUser = user.updateName(command.name)
      userRepo.store(nameUpdatedUser)
      nameUpdatedUser
    }
}

アプリケーションサービスはドメインモデルを操作し、クライアントのユースケースを実現するためにあります。APIで言うと各APIひとつひとつがアプリケーションンサービスに該当します。上記は「ユーザーの名前を更新する」というユースケースを実現しています。また用いる実装の選択やトランザクションの制御もアプリケーション層が担います。よってdomainとinfrastructure.jdbc、そして実装の詳細(scalikejdbcが該当)をインポートしています。

ちょっと特殊なところはUpdateUserNameCommandというクラスでしょうか。これはプレゼンテーション層の処理を簡易にするためにあります。

プレゼンテーション層

ということでプレゼンテーション層です。各クライアントからの入力をアプリケーションサービスの入力に整え、アプリケーションサービスからの出力を各クライアントの出力に整える役割があります。

今回の話で言うと、APIはHTTPにて受け付けるので、入力および出力はすべてHTTPになります。弊社ではウェブアプリケーションフレームワークとしてScalatraを採用しています。よってHTTPの解釈に関してはScalatraに委ねており、入力および出力はすべてScalatraのオブジェクトをどうこうする形になります。

以下がScalatraからの入力をアプリケーションサービスの入力に変換する例です。

package presentation.user

import application.user._
import domain.user._

import org.scalatra._
import org.json4s._

object UpdateUserNameInbound {
  case class Body(userId: String, name: String)
  
  private implicit val jsonFormats: Formats = DefaultFormats
  
  def apply(context: ScalatraContextWrapper[ScalatraBase with JsonSupport[_]]): UpdateUserNameCommand = {
    val body = context.parsedBody.extract[Body]
    
    UpdateUserNameCommand(
      userId = UserId(body.userId),
      name   = body.name
    )
  }
}

ScalatraContextWrapperに関しては詳しい説明を割愛します。ざっくり言うと、Scalatraからの入力を1枚クラスで包んだものです。

大切なことはそのScalatraからの入力を解釈し、アプリケーションサービスが求める入力に変換する点です。これがもし別のウェブアプリケーションフレームワークを使うことになったらそれ用の変換を行うオブジェクトを追加してあげれば良い、ということになります。

次にアプリケーションサービスからの出力をScalatraの出力に変換する例です。Scalatraの出力、まあHTTPの出力になるわけですが、ActionResultというおあつらえ向きのものがあるので、それを出力とします。

package presentation.user

import application.user._
import domain.user._

import org.scalatra._

object UpdateUserNameOutbound {
  case class Body(userId: String, name: String)
  
  def apply(command: UpdateUserNameCommand, user: User): ActionResult =
    Ok {
      Body(
        userId = user.id.toString,
        name   = user.name
      )
    }
}

本来はアプリケーションサービス内で発生した例外のハンドリングも行うのですが、それについては省略しています。

入力と同様、大切なことはアプリケーションサービスからの出力をScalatraが求める出力に変換する点です。その点のみに終始した十分にシンプルなコードだと思います。

アプリケーション層と同様に、インポートしているパッケージに注目すると、application(と一部ドメインモデルのクラスを利用するためにdomain)とその詳細(org.scalatraやorg.json4s)の2種類だけです。決してinfrastructure.jdbcなどには依存しません。

これで:

  1. Scalatraからの入力をUpdateUserNameCommandに変換
  2. UpdateUserNameCommandをアプリケーションサービスが受け取り、Userを返す
  3. UserをScalatraへの出力に変換

という流れが出来ました。ですので:

package presentation

import presentation.user._
import application.user._

import org.scalatra._
import org.scalatra.json._

class UserServlet extends ScalatraServlet with JacksonJsonSupport {
  private implicit val formats: Formats = DefaultFormats
  
  before {
    contentType = formats("json")
  }
  
  post("/UpdateUserName") {
    val command = UpdateUserNameInbound(ScalatraContextWrapper(this))
    val user = UpdateUserNameApplicationService(command)
    UpdateUserNameOutbound(command, user)
  }
}

のようにScalatraのルールに則って書いてあげると動作します。

ちなみに実際のコードではこれと同じことを実現するために様々なインタフェースを定義したり、もっと簡便に書けるようになっています。

インフラストラクチャ層

インフラストラクチャ層はドメイン層のあらゆる詳細を実装する役割を担います。以下はdomain.user.UserRepository[X]のScalikeJDBC実装例です(UserTableやUserRecordは実際のテーブルと密接に結び付いたクラス)。

package infrastructure.jdbc.user

import domain.user._

class JDBCUserRepositroy extends UserRepository[DBSession] {
  def find(id: UserId)(implicit session: DBSession): User = {
    val u = UserTable.u
    
    val user = withSQL {
      select.from(UserTable as u).where.eq(u.id, id.value)
    }.map(UserRecord(u)).single.apply
    
    user getOrElse { throw new EntityNotFoundException(s"$userId is not found") }
  }
  
  def store(user: User)(implicit session: DBSession): Unit =
    if (exists(user.id)) {
      UserTable.update(user.id, user.name)
    }
    else {
      UserTable.insert(user.id, user.name)
    }

 def exists(id: UserId)(implicit session: DBSession): Boolean =
    try {
      find(id)
      true
    }
    catch {
      case _: EntityNotFoundException =>
        false
    }
}

うーん普通のコードですね。まあそれだけ驚きが少ないということで。

最後に

サーバサイドの事情としてAPIの細かい方針やらプロジェクトの概観をだらだらと述べてみました。実際のコードはもっと複雑ですし、核となるドメイン層のコードはまったく掲載していません。いやー実際のコードおもしろいんですけどね。でもまるまる掲載するとあれもこれも紹介したくなってしまい、2日目にしてやたらと長大な記事が上がってしまいます。うーん断念です。

ですので、次はドメイン層がどうなっているか紹介していければなと思います。それでは。

*1:私は常々ログイン/ログアウトを http://example.com/session へのPOST/DELETEで表現することに疑問がありました

非golangプログラマのためのghq入門 - Re.Ra.Ku アドベントカレンダー day 1

丸山です。今年もアドベントカレンダーの季節がやってきましたね。個人的にはScalaのアドベントカレンダーと、PostgreSQLのアドベントカレンダーを楽しみにしています。

弊社も技術ブログを持っているのだから、せっかくならばアドベントカレンダーをやろうじゃないか、というわけで、これから25日まで、毎日様々な記事が書かれていきます。

ScalaやAndroid Java,SwiftやJavaScriptなど、様々なプログラミング系の話題のほかにも、弊社らしくプログラマ向けのストレッチの話など健康にまつわる話題も飛び出す予定なので、ぜひ興味のある話題のときは読んでやってください。

さて、一発目のネタはghqです。

ghqは非golangプログラマ向けにも便利なリポジトリ管理ツール

ghqは、id:motemen さんが開発した、リモートリポジトリを便利にfetchしてきたり管理するためのコマンドラインツールです。ghqのrootディレクトリをGOPATHと同じにしてくと、ghqを利用してcloneしてきたリポジトリはgolangのディレクトリ命名規則に沿った場所に展開されます。そのため、golangを利用した開発を行っているプログラマにとってとても便利なツールです。

そのような背景から、「ghqってgolang向けのツールでしょ」というような「誤解」をたまに目にすることがありますが、ghqは非golang向けのプログラマにも十分に有用なツールです。

ghqのインストール、設定

homebrewでインストールできので、それでインストールしておくと楽です。

$ brew tap motemen/ghq
$ brew install ghq

次に、ghqのrootディレクトリを設定しておきましょう。ghqで管理するリポジトリはここで設定したディレクトリ以下に展開されることになります。わたしはGOPATHが~/devなので、それに合わせて~/dev/srcghq.rootに設定していますが、golang使ってないひとはどこでもかまわないと思います。

$ git config --global ghq.root ~/dev/src

ghqでリポジトリを取得する

$ ghq get <リモートリポジトリのlocation> 

とすることで、そのリポジトリがghq.root以下に展開されます。

たとえば、

$ ghq get git@github.com:rails/rails.git

とすれば、~/dev/github.com/rails/railsにrailsのリポジトリが展開されることになります。

ちなみに、getしてくるリポジトリのlocationはかなり柔軟に解決してくれて、railsの例でいえば

$ ghq get rails/rails

というように指定するだけでも同じく~/dev/github.com/rails/railsに展開してくれます。

ghq管理してるリポジトリに一発で飛ぶ

さて、これだけなら「べつにgit cloneでよくね?深いディレクトリ掘られると逆に面倒じゃね?」という感じになるのですが、ghqはpecoと組み合わせたときにこそ真価を発揮します。

pecoのインストールがなされていない場合、インストールしておきましょう。

$ brew install peco

さて、ここからpecoとghqを組み合わせて便利にする時間です。zshをお使いの場合は、以下のような記述を.zshrcなどにしてみてください。

function peco-ghq-cd () {
    local selected_dir=$(ghq list | peco --query "$LBUFFER")
    if [ -n "$selected_dir" ]; then
        selected_dir="`ghq root`/$selected_dir"
        BUFFER="cd ${selected_dir}"
    fi
    zle clear-screen
}
zle -N peco-ghq-cd
bindkey '^f' peco-ghq-cd

この状態でターミナルを立ち上げなおし、Ctrl+fを押すと、ghq get で取得したリポジトリが一覧でできます。飛びたいリポジトリを選択すると、ターミナル上のバッファにcd <選択したディレクトリのパス>が表示されているはずです。これで、リポジトリ間を移動するときにいちいち ls して cd してみたいなことをせずにすむようになりました!

こうやって記事で書かれてしまうと「なんだかあんまり便利に見えないなあ」なんて思うかもしれませんが、一度ghqのある生活に慣れてしまうともうghqのない生活に戻りたくないくらいに便利なので、ぜひみなさん試してみてください。

参考にしたURL

React + ReduxではないSPAフロントエンド事情

丸山です。

弊社も最近はiOSアプリやAndroidアプリだけではなく、いわゆるSPAなWebApplicationを開発しています。

SPAを採用した背景

その背景には

  • サーバーサイドで複雑なビジネスロジックを実装することになるので、サーバーサイドはScalaを利用したい
  • ScalaでHTMLを吐いてもいいのだけれど、そういうのは動的型付け言語のほうがやっぱり楽な気がする
  • だったらScalaでJSONを喋るAPIだけ作ろう
  • そのAPIと喋ってwebのユーザーインターフェイスを実現する部分はRoRにするか?
  • それともいっそSPAにしてブラウザから直接APIとコミュニケーションすればよくないか?

と、いろいろ考えたところで、まずは社内向けのツールで技術検証も兼ねてSPAでScalaなAPIとコミュニケーションするのを試してみることにしました。

まだまだ開発中なのですが、意外とうまくいっているな、というのが現在の感想です。

採用したフレームワーク

最近SPAといえばだいたいReact + Reduxというのがトレンドであるように思います。しかし弊社ではVue.jsとvue-routerを利用しています。

これには、まだまだ開発メンバーの少ない弊社では、iOSアプリケーションやAndroidアプリケーションを開発しているメンバーがSPAアプリも開発するというようなことが多発することが予測できることが影響しています。

弊社ではiOSアプリの設計についてはだいぶ方向性が見えてきていて、その内容はこの前のヤパチーでも発表させていただきました。

techblog.reraku.co.jp

上述の発表内容を見ていただければわかると思いますが、弊社ではかなり古典的なMVWな設計でアプリケーションを実装しています。

さて、上述の通り、弊社には「JavaScriptバリバリ専任マン」のような開発者はおらず、iOSアプリなどを書いている開発メンバーがJavaScriptも書くことになります。そのようなビジネス上の制約があるとき、「なるべくiOSやAndroidで使っていた考え方と同じような考え方でSPAも開発することができる」というのは無視できないメリットです。そのため、「なるべく普通のMVW」が実現できて、なおかつModelの設計については何も強制してこないフレームワークが必要になったわけです。そのようなことを考えた結果白羽の矢が立ったのがVue.jsでした。今の所この選択は間違えていなかったな、と思っています。

Vue.jsを利用したSPAフロントエンド開発事情

使っている技術スタックとしては、JSまわりにVue.js、vue-router、Babel、CSSはsass、これらを webpack でビルドして使っています。

タスクランナー的なものは採用しておらず、npm scriptを利用しています。package.jsonの一部を抜粋するとこんな感じ。

  "scripts": {
    "build": "npm run build_js && npm run build_html",
    "build_js": "webpack --progress",
    "build_html": "cp src/index.html build/index.html",

    "watch": "npm run build_html && webpack -d --watch --progress",

    "clean": "npm run clean_js && npm run clean_html",
    "clean_js": "rm -rf build/js/*",
    "clean_html": "rm -rf build/index.html",

    "test": "mocha-webpack --colors --webpack-config webpack.config-test.js \"test/**/*.js\""
  },

タスクランナーを利用しないという選択にも「専任のJavaScriptマンがいない」ということが影響していて、「なるべく覚えなければいけないことを減らそう」という判断の結果このような運用になっています。「ビルドツールがCLIインターフェースを持ってるならそれをshellから叩いてしまえばいいではないか」というのはまあ自然な発想という気はします。

テスト事情

PDSを意識してアプリケーションの動きをModelレイヤーにどんどん寄せて書いているので、UIテストは今の所行っていません。nodeを利用してApplicationServiceのテスト(あとDomainModelのテストも)をすればそれでだいたい事足りています。

というのも、ヤパチーで発表した連打マシンと同じような感じの設計で、UI上の状態もほとんどAppliactionServiceが保持するModelたちが持っていて、その状態を書き換えたいときはApplicationServiceのメソッドをdispatchする、そしてその結果状態が書き換わったらObserver経由でUIに反映する、という一方向のデータフローを守っているので、ApplicationServiceへのテストがE2Eテストに近い役割を担っていてくれている感じです。

実際Vue.jsに依存するコードはプロジェクトのうちごく一部だけとなっているため、今の所このやり方でも大きな問題は出ていません。リファクタも快適に行えています。

ただ、どこかでUIテストのノウハウとReduxのノウハウもきちんと得ておきたいところではあります。このあたりは今後の課題ですね。

まとめ

弊社で最近行っているSPA開発事情について、「こんな感じだよ」というのを書いてみました。

最後に「また宣伝かよ!」という感じでアレなんですが、株式会社リラクでは「一晩でReduxに書き換えてやるぜ!むしろ俺がReduxのメリットの伝道師になってやる!」という気概のある開発者や、あるいは「これはこれで合理的な選択!一緒に開発していきたい!」という気持ちを持ってくれる開発者、あるいは「まだ自信はないけど、設計についての話ができる仲間たちといろいろ学びつつやっていきたい!」と思ってくれるような開発者を募集しています。マジで。ほんとに無限にやることがあるんです。助けてくれ!少しでも興味があるひとは是非 @ が誰からでもDMを受け付けるようになっているので気軽にコンタクトを取ってください。ほんとに待ってます。