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

開発環境でDockerをちっちゃく導入する - Re.Ra.Ku アドベントカレンダー day 19

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

こんにちは、神場です。

前回まででSwift関連の記事を2つほど書かせていただきましたが、今回はDockerの記事です。

開発環境でのDocker

開発環境でDockerを導入する場合、アプリケーション、DBなどに分けてdocker-composeでコンテナを立てるのが一般的かと思います。

RailsPostgresqldocker-compose.ymlの例

version: '2'
services:
  web:
    build: .
    command: bundle exec rails s -p 3000 -b '0.0.0.0'
    volumes:
      - .:/app
    ports:
      - 3000:3000
    links:
      - db  
  db:
    image: postgres
    restart: always
    environment:
      POSTGRES_PASSWORD: password
      POSTGRES_USER: me
    volumes:
      - ./postgres:/var/lib/postgresql/data
    ports:
      - 5432:5432

このようにアプリケーションのレイヤーも含めてコンテナ化する例が多いと思うのですが、場合によってはアプリケーションレイヤーを除いてコンテナ化するのが良い場合もあるのではないか?というのがこの記事の内容です。

(上の例であれば、postgresのコンテナさえ立てておけばポートフォワードはされているので、アプリケーションのコンテナを立てなくてもホスト側からpsql -U me -h 127.0.0.1で接続出来ます。)

アプリケーションレイヤーを除いてコンテナ化した場合のメリット

思いつくものをいくつか列挙してみます。

ちょっと動作を試したい・見たいときにコンテナにsshで入る必要がない

これは要はアプリケーションレイヤーまでコンテナ化すると、例えばある単体テストを個別に実行したい、コンソール(RubyirbScalasbtなど)を起動したいという時にdocker exec等でコンテナに入っておく必要があるということです。手元でサクッと試したい、という場合にローカルのほうが楽な場合があるという感じでしょうか。

公式のイメージがあるものだけを使えばDockerfileを書く必要がない

MySQLPostgreSQLなど有名どころのものであればすでにイメージがあるので、自前でDockerfileを書く必要はなくなります。ただ、もちろんその代わりアプリケーションを起動するローカルの環境は整える必要があるので、以下の条件

  • アプリケーションを起動するまでの初期設定が複雑である
  • 使っている言語にバージョン管理やパッケージ管理(Rubyならrbenvbundler)のエコシステムが揃っていない

に当てはまるときは、素直にDockerfileを書いたほうが良さそうです。

buildする必要がない

Dockerfileがないのでもちろんbuildもなくなります。

まとめ

上記のことを踏まえてまとめると、

  • アプリケーションの初期設定が容易である
  • 開発の初期段階で、まだDockerfileが頻繁に変更されるフェーズにある
  • プロジェクト全体としてはDockerを使っていないが、個人的には使いたい
  • アプリケーションレイヤーをコンテナ化すると開発環境で気になるほどパフォーマンスが下がる(実際にどのぐらい下がるのか未検証ですが。。)

といったような場合には検討しても良さそうです。限定的ではありますし、環境を完全に揃えたい場合はもちろんDockerfileを書くべきですが、最初の導入としてまずはアプリケーション以外のレイヤーだけをコンテナにするという選択肢もあるのではないかと思います。

BottomNavigationViewのカスタマイズ - Re.Ra.Ku アドベントカレンダー day 18

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

こんにちは。安部です。

BottomNavigationViewのカスタマイズを試してみました。

ちょっと無理やりやってる感じの箇所もあります。

Version

使用したSupportLibraryは25.1.0です。

BottomNavigationViewの基本

基本的な使い方は以前、私の書いたものを参照してください。

BottomNavigationViewを試してみる - Qiita

アイコンとテキストの色を状態によって変更する

未選択の状態、タップされてる状態、選択された状態で色を変更する方法です。

f:id:STAR_ZERO:20161217224859p:plain:w200

res/color/bottom_navigation.xml を下記のように作成します。

<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_checked="true" android:color="#FF0000" />
    <item android:state_pressed="true" android:color="#00FF00" />
    <item android:color="#FFFFFF" />
</selector>

次にそれをレイアウトで指定してあげます。app:itemIconTintapp:itemTextColorです。

<android.support.design.widget.BottomNavigationView
    android:id="@+id/navigation"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@color/colorPrimaryDark"
    app:itemIconTint="@color/bottom_navigation"
    app:itemTextColor="@color/bottom_navigation"
    app:menu="@menu/bottom_navigation" />

常にテキストを表示させる

メニューの数が3つを超えると選択されているメニュー以外のテキストが表示されなくなってしまいます。それを無理やり表示する方法です。

正直よろしくない方法だと思います。現状で頑張った結果です。

MatrialDesignの仕様を確認すると3つ超えるとテキスト出てないので、別に無理やりやる必要はないのですが、なんか要望とかであがってきそうなパターンだなって思ってやってみました。

f:id:STAR_ZERO:20161217225142p:plain:w200

BottomNavigationView navigationView = (BottomNavigationView) findViewById(R.id.navigation);
BottomNavigationMenuView menuView = (BottomNavigationMenuView) navigationView.getChildAt(0);
for (int i = 0; i < menuView.getChildCount(); i++) {
    BottomNavigationItemView itemView = (BottomNavigationItemView) menuView.getChildAt(i);
    itemView.setShiftingMode(false);
    itemView.setChecked(false); // Viewの状態を反映させるために呼んでいる
}

スクロール時に隠す

RecyclerView等をスクロールしたときにBottomNavigationViewを隠す方法です。Toolbarでよくあるやつですね。

f:id:STAR_ZERO:20161217225309g:plain:w200

CoordinatorLayout.Behaviorを継承した下記のクラスを作成します。スクロールに合わせてViewの位置を変更しています。

public class BottomNavigationBehavior extends CoordinatorLayout.Behavior<BottomNavigationView> {

    private int defaultTop;
    private int defaultBottom;
    private int defaultHeight;

    public BottomNavigationBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean onLayoutChild(CoordinatorLayout parent, BottomNavigationView child, int layoutDirection) {
        defaultTop = child.getTop();
        defaultBottom = child.getBottom();
        defaultHeight = defaultBottom - defaultTop;
        return super.onLayoutChild(parent, child, layoutDirection);
    }

    @Override
    public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, BottomNavigationView child, View directTargetChild, View target, int nestedScrollAxes) {
        return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL;
    }

    @Override
    public void onNestedScroll(CoordinatorLayout coordinatorLayout, BottomNavigationView child, View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
        ViewCompat.offsetTopAndBottom(child, dyConsumed);
        if (dyConsumed > 0 && child.getTop() > defaultBottom) {
            child.setTop(defaultBottom);
        } else if (child.getTop() < defaultTop) {
            child.setTop(defaultTop);
        }
        child.setBottom(child.getTop() + defaultHeight);
    }
}

次にレイアウトファイルです。ちょっと長いですが全部のせておきます。

BottomNavigationViewapp:layout_behaviorに先程のクラスを指定します。これでスクロール時に隠れるようになります。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <android.support.design.widget.CoordinatorLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:fitsSystemWindows="true">

        <android.support.design.widget.AppBarLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:theme="@style/AppTheme.AppBarOverlay">

            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="?attr/actionBarSize"
                android:background="?attr/colorPrimary"
                app:popupTheme="@style/AppTheme.PopupOverlay" />

        </android.support.design.widget.AppBarLayout>

        <android.support.v7.widget.RecyclerView
            android:id="@+id/recycler_view"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:scrollbars="vertical"
            app:layout_behavior="@string/appbar_scrolling_view_behavior" />

        <android.support.design.widget.BottomNavigationView
            android:id="@+id/navigation"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="bottom"
            android:background="@color/colorPrimaryDark"
            app:itemIconTint="@android:color/white"
            app:itemTextColor="@android:color/white"
            app:layout_behavior="com.star_zero.example.bottomnavigation.BottomNavigationBehavior"
            app:menu="@menu/bottom_navigation" />
    </android.support.design.widget.CoordinatorLayout>

</LinearLayout>

まとめ

BottomNavigationViewは制限がけっこうあって、5つより多くのメニューを設定しようとするとExceptionが投げられてクラッシュしたり、アイコンやテキストの大きさを自由に変えたりがかなり厳しいです。

あとはバグがそれなりある気がします。試した感じSnackbarがうまく表示できていない感じでした。Issue Trackerにもあがっています。

現状ではアプリの仕様によっては実現が難しいものが出てくるかもしれませんので慎重に導入を検討したいですね。

Re.Ra.Kuで使われているプログラミング言語とその選択理由 - Re.Ra.Ku アドベントカレンダー day 17

丸山です。

アドベントカレンダーも17日目、だいぶ佳境に入ってきましたね!本日は弊社ではどのようなプログラミング言語が使われていて、そこにはどのような判断が働いているのか、ということについて書いていきたいと思います。

iOSアプリケーションと Androidアプリケーションの開発言語

iOSアプリケーションとAndroidアプリケーションの開発には、Swift(古いものは一部Objective-C)、AndroidアプリケーションはJavaで書かれています。プラットフォーマー(GoogleとかAppleとか)がサポートする環境に素直に乗っている形ですね。

ネイティブアプリの開発にはそんなに選択肢があるわけではないのですが、Xamarinに乗ってC#、あるいはReactNativeでJavaScript、Kotlinを利用する、など、プラットフォーマーが公式に用意した環境以外にも選択肢があります。

公式の用意する環境に乗ると、「そもそもJavaの表現力が……」とか「NSFoundationつらくない?」とかいろいろなデメリットがある一方、そうでない環境には「慣れ親しんだ言語で書ける」とか「AndroidとiOSで一部コードを共有できる」とか「高い表現力を持つ言語で書ける」とかいろいろなメリットがあります(CocoaTouchを理解しなくていい?それは幻想です)。

一方で、プラットフォーマーが用意したものではない環境には「MSが飽きたら終わり」「Facebookが飽きたら終わり」みたいな怖さもあります。また、どこかでプラットフォーマーの用意する環境へのブリッジが必要になり、ある意味「一枚余計なレイヤー」が発生することになります。組み合わせる技術要素は多くなれば多くなるほどトラブルの原因は増えます。

そのようなことを考えると、プラットフォーマーが用意したものではない環境に乗ってしっかりと開発するためには、プラットフォーム依存のフレームワーク(CocoaTouchとかAndroidSDKとか)に対する深い理解だけではなく、その環境に対する深い理解も必要となってきます。

なので、技術選定には、このコストを支払うことができるかどうか、というのがかなり重要なポイントになってくると思います。「俺はめちゃめちゃC#が得意なんだ、C#の中身ならなんでも知ってる」という人がすでにメンバーに存在する、とか、「ReactNative?中身全部読んだよ。俺の書くJavaScriptはめちゃめちゃメンテナブルだしな」みたいな人がすでにメンバーに存在する、とか、そういう「強み」がすでにある場合ならば、このコストを支払うのは問題にならないかもしれません。あるいは、サーバーの運用などに関してもC#やnodeJSのプロフェッショナルがいて、なおかつ少ないメンバーですべてのレイヤーを見なければいけないなどの場合、一気通貫で同じ言語で開発したい、という強いモチベーションが生まれたりすることもあるでしょう。そのときには、「このコストを支払ってでも統一言語でやっていく」というのは合理的な選択になりそうです。

弊社の場合、すでにiOSのプロフェッショナルとAndroidのプロフェッショナルがメンバーにいるので、そこであえて他のプラットフォームの乗っかるということにメリットは見いだせませんでした。その為、素直に公式の環境に乗っています。個人的にはXamarin気になってはいます。

APIサーバーの開発言語

HTTPを喋ってネイティブアプリやブラウザ上のSPAアプリケーションと通信するAPIサーバーは、Scalaで書かれています。

ネイティブアプリやブラウザで動くSPAアプリケーションにはどうしても言語選択の自由の幅が少なくなりますが、サーバーサイドは何で書いても自由です(えっServerSideSwift?ちょっと勇者すぎませんかね……)。

Scalaを選択するメリットとしては、高い表現力と安全性が挙げられるでしょう。一方でデメリットとして、「むずかしい」とか「いろいろな書き方ができすぎて宗教戦争が起こりそう」とかいろいろあるのですが、幸い今のところメンバーのバランス感覚(どこまで抽象度を上げて書くのかとか)はメンバーごとに大きく乖離することはなく、「仕事で書くならだいたいこのへんがちょうどいいバランスだよね」というなんとなくの合意が取れているように思えます。

弊社のメンバーの共通の得意言語はRubyなので、Rubyで書くというのもアリっちゃアリなんですが、「技術的に尖ったメンバーを集めたい、テックな企業にしていきたい」という経営側からの要望と、私の趣味と他社での運用実績が一致した結果Scalaでやっていこうという感じになっています。実際現状の人材面を見るに、この選択は悪くなかったなあと思っています。

その他細かい部分

デプロイのスクリプトや雑な集計とかがPerlだったり、DBのマイグレーションのサポートをRubyでやっていたり、サーバー管理のための細かいツールをGolangで書いたりしています。

このあたりは「このライブラリが使いたいからこの言語を使う」という理由で言語を選択したり、「雑な集計なら雑に書き慣れてるPerlでやりたい」っていう思いだったり、「いろんな環境で動かすコマンドになるからバイナリ一枚ぺろっと置けば動いてほしい」という理由があったりします。

まとめ

弊社では様々な要求に合わせて、適材適所で様々な言語を利用してアプリケーションの開発や運用を行っています。

合理的な理由があれば新しい言語もどんどん取り入れていく環境ですので、「オッいいですね、ここはちょっと働いてみたいですね」という感じで興味のある方はぜひご連絡お待ちしております。twitter: @neko_gata_s が一番連絡つきやすいと思います。まずは飯でもいきましょう。

MockWebServerを使ってAndroid通信をテストする - Re.Ra.Ku アドベントカレンダー day 16

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

こんにちは、安部です。

MockWebServerを使うと通信部分のモックが簡単にできるので、使い方を紹介したいと思います。

okhttp/mockwebserver at master · square/okhttp

(試してませんが、Android以外のJavaプロジェクトでも使えると思います。)

セットアップ

build.gradleに追加します。今回サンプルで使うOkHttpとRetrofitも追加してます。

testCompile 'com.squareup.okhttp3:mockwebserver:3.4.1'

compile 'com.squareup.okhttp3:okhttp:3.4.1'
compile 'com.squareup.retrofit2:retrofit:2.1.0'
compile 'com.squareup.retrofit2:adapter-rxjava:2.1.0'
compile 'com.squareup.retrofit2:converter-gson:2.1.0'

OkHttp

OkHttpを使った通信のテストのサンプルです。

テスト対象コード

URLをもらって通信して結果を返却するだけのコードです。実践的ではないですが、使い方をわかりやすくするために単純にしています。

モックのURLを渡せるようにエンドポイントのURLはコンストラクタで設定しています。

public class Connection {

    private String baseURL;

    public Connection(String baseURL) {
        this.baseURL = baseURL;
    }

    public String run() throws IOException {
        OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder().url(baseURL + "test").build();

        Response response = client.newCall(request).execute();
        return response.body().string();
    }
}

テストコード

簡単なテストコードです。

基本的な使い方はレスポンスを設定して、URLを取得して、そこにアクセスする感じです。

public class ConnectionTest {

    private MockWebServer server;

    @After
    public void tearDown() throws Exception {
        if (server != null) {
            // モックサーバーを停止
            server.shutdown();
        }
    }

    @Test
    public void run() throws Exception {
        server = new MockWebServer();
        // レスポンスで返したいものを設定
        MockResponse response = new MockResponse()
                // header
                .addHeader("Content-Type", "text/plain")
                // status code
                .setResponseCode(200)
                // body
                .setBody("レスポンス");

        server.enqueue(response);

        // モックサーバーのURL設定と取得
        HttpUrl url = server.url("/");

        // 実行
        Connection connection = new Connection(url.toString());
        String result = connection.run();

        // レスポンスを確認
        assertThat(result, is("レスポンス"));

        // リクエストを確認
        RecordedRequest request = server.takeRequest();
        assertThat(request.getPath(), is("/test"));
    }
}

Retrofit + RxJava

RetrofitとRxJavaを使ったサンプルです。

テスト対象コード

想定として、GitHubのユーザー情報を取得する感じのものです。

RxJavaのObservableを返す形になっています。使う側は取得したObservableをsubscribeする感じになるかと思います。

public interface GitHubService {

    @GET("users/{user}")
    Observable<User> getUser(@Path("user") String user);

}
public class GitHubAPI {

    private String baseURL;

    public GitHubAPI(String baseURL) {
        this.baseURL = baseURL;
    }

    public Observable<User> getUser(String user) {
        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl(baseURL)
                // Gson
                .addConverterFactory(GsonConverterFactory.create())
                // RxJava Adapter
                .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                .build();

        GitHubService service = retrofit.create(GitHubService.class);

        return service.getUser(user);
    }
}
public class User {

    public String id;

    public String name;
}

テストコード

MockWebServerを作るあたりは先程の例と変わりませんが、Observableをテストする方法として、TestSubscriberを使っています。TestSubscriberを使うとだいぶテストが書きやすくなると思います。

public class GitHubAPITest {

    private MockWebServer server;

    @After
    public void tearDown() throws Exception {
        if (server != null) {
            // モックサーバーを停止
            server.shutdown();
        }
    }

    @Test
    public void getUser() throws Exception {
        server = new MockWebServer();

        MockResponse response = new MockResponse()
                .addHeader("Content-Type", "application/json")
                .setResponseCode(200)
                .setBody("{\"id\":123456,\"name\":\"Kenji Abe\"}");

        server.enqueue(response);

        HttpUrl url = server.url("/");
        GitHubAPI api = new GitHubAPI(url.toString());

        // Test用のSubscriber
        TestSubscriber<User> testSubscriber = TestSubscriber.create();

        api.getUser("STAR-ZERO").subscribe(testSubscriber);

        // 終了するまで待つ
        testSubscriber.awaitTerminalEvent();
        // 完了を確認
        testSubscriber.assertCompleted();

        // onNextにあたるイベントからデータ取得
        User user = testSubscriber.getOnNextEvents().get(0);

        assertThat(user.id, is(123456));
        assertThat(user.name, is("Kenji Abe"));

        // リクエストを確認
        RecordedRequest request = server.takeRequest();
        assertThat(request.getPath(), is("/users/STAR-ZERO"));
    }

}

まとめ

MockWebServerを使うことで通信部分のテストが可能になります。使い方もそこまで難しくはないと思うのでオススメです。

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

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

あっという間に3回目です。
前回もブックマークや星を下さった方々ありがとうございます。
毎日プログラミングをして社会が便利になる仕組みを作っているエンジニアの皆様が、
より元気でより健康的にプログラミングできるよう、微力ながら協力できればと思っております。
今回も宜しくお願い致します!

3回目の今回は、骨盤周りのストレッチ方法をお伝えします。
今回も、デスクから離れずに出来るストレッチのご紹介です。

骨盤の仕組み

前回同様、ストレッチに入る前にまずは体の仕組みについて簡単に触れます。

1. 体の基盤となる骨盤

私たちの体は、重力やご自身の骨・筋肉・脂肪等の重みを常に支えて生活しています。
建物の基盤が弱いと倒れてしまうのと同じように、
人間の体も基盤がぐらぐらしていると、上に乗っている骨や筋肉の位置がずれてきてしまいます。
人間の体の基盤となるのは、下半身(足、腰)であり、下半身にある筋肉・関節と関連性が高いのが骨盤です。
下半身の筋肉と関連性が強い骨盤の位置を整えると、全身のバランスが整いやすくなります。

骨盤を整えて安定した基盤を作り、肩甲骨で上半身のバランスを綺麗に保つことができると、疲れにくい体を作ることができます。
肩甲骨の仕組みについては前回記載していますので、こちらも確認いただけますと幸いです。
プログラミングしながらできる、健康への近道講座:2回目 - Re.Ra.Ku アドベントカレンダー day 9 - Re.Ra.Ku tech blog

2. 全身の代謝アップが期待できる部位

骨盤周りの筋肉は体を支えるための筋肉なので大きいものが多いのも特徴です。
筋肉の柔軟性が上がると使えていなかった筋肉が正しく働くようになるため、代謝も上がりやすくなります。
体型のお悩みがある方や、冷え症が気になる方にも、骨盤周りのケアはオススメです。

骨盤周りのストレッチ

それではストレッチに入っていきます。
前回同様、1回目の講座で出てきた姿勢の作ってストレッチしてみてください。
プログラミングしながらできる、健康への近道講座:1回目 - Re.Ra.Ku アドベントカレンダー day 3 - Re.Ra.Ku tech blog
綺麗な姿勢を保つだけでも腰・骨盤周りのストレッチと筋トレ代わりになります。
腰の疲れが気になる方は、無理の無い範囲で普段から姿勢を意識するようにしてみてください。

綺麗な姿勢を作ったら、息を吸って、左手で椅子の背もたれを持ち、左膝に右ひじを引っ掛けて、息を吐きながら椅子を回してください。
(回せない椅子の方は腰をひねってください。)
f:id:nozomiii:20161214113440j:plain
気持ち良く伸びたな、と実感できたら息を吸いながらゆっくり体を戻します。
戻した後は反対側も行います。
ひねることで腰の筋肉を伸ばしていきます。

腰を伸ばしたら、今度は股関節をストレッチします。
左足のふくらはぎを右足の太もも〜膝の間に乗せます。
乗せたら、胸を張って、頭を下げないようにして、おへそから体を倒します。 f:id:nozomiii:20161214113352j:plain

このストレッチも、気持ち良く伸びたなと実感できたところで息を吸いながらゆっくり体を戻します。
股関節を伸ばし、リンパが流れやすくなるストレッチです。

体の力が抜けていれば抜けているほど、疲れは溜まりにくくなります。
いつもプログラミングを頑張っている体にお礼を伝える気持ちを持って、
優しく心地の良い負荷でストレッチしてみてください。

最後に

弊社リラクでは、今回お伝えした骨盤周りのストレッチをしっかり行うコースもご用意しています。
肩甲骨ストレッチも骨盤ストレッチも同時に行うコースなので、体のバランスも整いやすくなります。
腰のお疲れが気になる方はぜひチェックしてみてください!
reraku.jp

次回は、目と首のストレッチ方法をお伝えします。
今後とも宜しくお願いいたします!

バリデーションライブラリ clarifyの紹介 - Re.Ra.Ku アドベントカレンダー day 14

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

こんにちは。ヘルステックチームの近藤です。

突然ですがみなさんバリデーションしていますか?病めるときも健やかなるときも、アプリケーションサービスを書くときもドメインモデルを書くときも、バリデーションはなんだかんだでせざるを得ないですよね?

当然私も日々バリデーションしているわけですが、いろいろと思うところがあり、自分でバリデーション用のライブラリを書きました。今回はそれを紹介します。

バリデーションライブラリ clarifyのコンセプト

そのバリデーションライブラリはclarifyという名前で、とあるコンセプトに沿って作られています。それは「値を与えたらバリデーション結果を返す」というものです。

まあ当たり前のことなんですが、じゃあ反対にやりたくなかったのは:

  • とあるtraitを継承し
  • DSLにてバリデーションの内容を定義したり
  • validateメソッドでそのオブジェクトが妥当かどうかを確かめる

みたいなやつです。具体的な例で言うとActiveModel::Validationsのようなものです。

何故それをやりたくなかったかと言うと、8日目のリラクのサーバサイド事情 with Scala / ドメイン編でも触れた「ドメインにフレームワークを持ち込みたくない」という事情に起因します。ドメインにおける表現の自由度をバリデーションによって奪いたくありません。

また、バリデーションは「外側の値(ユーザが入力したもの、APIにて受け取ったものなど)が内側のルールに即しているか」を確かめるものです。故にどこにでも現れる処理だと思います。アプリケーションサービスではクライアントから与えられた値が妥当かどうか、ドメインモデルではドメインに沿っているかどうか……。冒頭で言った「アプリケーションサービスを書くときもドメインモデルを書くときも」というのはただノリで言ったわけではなく、本当にそうだと思っています。

そういうことなので、取り回しが利きやすい「値を与えたらバリデーション結果を返す」という単なる「関数」である形を目指しています。

インストール

じゃあ実際にどう使うのかを紹介します。とは言えこのライブラリ、とりあえず私自身が使えるようにしたかったものなので、ドキュメントは雑ですし、テストもなく、配布もただ.jarファイルを置いただけという代物です。なのでまあこういうものがあるんだーーーぐらいのものです。

とりあえず.jarは下記の場所にあります。

https://takkkun.github.io/clarify/org/usagram/clarify_2.11/0.0.4/clarify_2.11-0.0.4.jar

sbtを使用している場合はlibraryDependenciesに下記を加えてあげればOKです。

"org.usagram" %% "clarify" % "0.0.4" from "https://takkkun.github.io/clarify/org/usagram/clarify_2.11/0.0.4/clarify_2.11-0.0.4.jar"

Validator

clarifyは「値を与えたらバリデーション結果を返す」に終始していると言いましたが、その最小単位はValidatorと呼ばれるものです。

NotBlank Validatorは文字列が空じゃないかどうか、GreaterThanOrEqualTo Validatorは数値がとある数値以上かどうかを確かめるものです。

import org.usagram.clarify.validator._

val nameValidator: Validator[String] = NotBlank

println(nameValidator.validate("Kondo")) // => no error
println(nameValidator.validate("")) // => CannotBeBlank error

val ageValidator: Validator[Int] = GreaterThanOrEqualTo(0) // GreaterThanOrEqualTo.zero とも書ける

println(ageValidator.validate(17)) // => no error
println(ageValidator.validate(-1)) // ShouldBeGreaterThan(0) error

コレクションのすべての要素が条件を満たしているかを確かめるEach Validatorなんてのもあります。

val tagsValidator: Validator[Set[String]] = Each(NotBlank)

println(tagsValidator.validate(Set("programmer", "scala"))) // => no error
println(tagsValidator.validate(Set("programmer", ""))) // => ErrorAt(1, CannotBeBlank) error

これらはすべてValidator[-V]トレイトのサブクラスで、 validate(value: V): Iterable[Error] (Errorはclarifyで定義しているtrait)を実装しているだけです。もちろんオリジナルのValidatorを作ることも出来ます。

import org.usagram.clarify.validator.Validator
import org.usagram.clarify.error.Error
import org.usagram.clarify.Tags

class StartWith(prefix: String) extends Validator[String] {
  def validate(value: String) =
    failIf(!value.startsWith(prefix)) { // 第1引数の条件を満たしたら第2引数のエラーを返すfailIfというヘルパー
      NotStartWith(prefix)
    }
}

// Validatorそれぞれに専用のエラーを設ける場合はErrorを継承し、
// そのエラーのメッセージを返す message(tags: Tags): String メソッドを実装する必要がある。
//
// Tagsはバリデーションした値に付与されたメタデータ。
// 値に名前を割り振るためのlabelなどがある。

case class NotStartWith(prefix: String) extends Error {
  def message(tags: Tags) = {
    val label = tags.label getOrElse "(no label)"
    s"$label is not start with $prefix"
  }
}

どうでしょう?十分にシンプルかなーと思います。

Validator同士を合成して大きなValidatorを作る

Validatorたったひとつでは現実世界の要望を満たせないことが多いと思います。文字列においても大体「空ではなく、かつ20文字以内」みたいなのがほとんどじゃないでしょうか?

というわけで、関数合成のようにValidator同士を合成することでその要望に応えています。

val nameValidator: Validator[String] = NotBlank && AtMostCharacters(20) // AtMost(20).characters とも書ける

println(nameValidator.validate("")) // => CannotBeBlank error
println(nameValidator.validate("very very very long name")) // => TooLongString(20) error

val tagsValidator: Validator[Set[String]] = Each(NotBlank) && AtLeast(1) // AtLeast.one とも書ける

println(tagsValidator.validate(Set("programmer", ""))) // => ErrorAt(1, CannotBeBlank) error
println(tagsValidator.validate(Set.empty)) // => RequireAtLeast(1) error

&& というメソッドがそれです。論理演算のそれをイメージしてもらえればと思います。一応「または」を意味する /\ というメソッドもあるのですが、個人的にあまり使う機会がないため動作をやや決め切れてなかったりします*1

ComplexValidatorN

「値を与えたらバリデーション結果を返す」についてはValidatorで十分実現できています。ですが、とある値の集合にたいしてバリデーションを行い、すべてのバリデーションを実行後、ひとつでもエラーとなった場合はバリデーション失敗にしたい、というのはよくあります。

Validatorだけでそれを行うことは不可能ではありませんが、ひとつひとつValidatorに値を与え、最後にまとめるのを何度も書くのはしんどいものがあります。

それを解決するのがComplexValidatorNというものです。Nには1から22までの数字が入ります。Validator同士を <-> というメソッドで繋ぐことでComplexValidatorNを作ることが出来ます。下記のケースではValidatorを3つ繋いでいるので、ComplexValidator3が作られます。

class Person(val name: String, val age: Int, val tags: Set[String])

val personValidator = nameValidator <-> ageValidator <-> tagsValidator

// valid case
{
  val name = "Kondo"
  val age = 17
  val tags = Set("programmer", "scala")

  val result = personValidator(
    Indefinite(name) label "name",
    Indefinite(age) label "age",
    Indefinite(tags) label "tags"
  )

  println(result.isValid) // => true
  
  // result.isValid が true の場合、 result.resolve[A] に
  // 与えた関数 (String, Int, Set[String]) => A が呼ばれ、
  // その実行結果を result.resolve[A] の結果として返す。
  val person = result.resolve { (n, a, t) => new Person(n, a, t) }
}

// invalid case
{
  val name = ""
  val age = 17
  val tags = Set("programmer", "")
  
  val result = personValidator(
    Indefinite(name) label "name",
    Indefinite(age) label "age",
    Indefinite(tags) label "tags"
  )
  
  println(result.isValid) // => false
  println(result.invalidValues) // => Invalid("", Tags("name"), CannotBeBlank), Invalid(Set("programmer", ""), Tags("tags"), ErrorAt(1, CannotBeBlank))
  
  // result.isValid が false の場合、 result.resolve[A] は
  // InvalidValueException 例外を発生させる。
  val person = result.resolve { (n, a, t) => new Person(n, a, t) }
}

ここで出てきたIndefiniteですが、これは値の状態を表しています。ComplexValidatorNにおいて値は必ず以下の4種類の状態を取る必要があります。

  • Indefinite: 値がまだバリデーションされていない状態
  • Valid: 値がバリデーションされ、有効だった状態
  • Invalid: 値がバリデーションされ、無効だった状態
  • Unknown: ComplexValidatorNによってバリデーションが実行されたが、バリデーションの結果が不明な状態

なぜComplexValidatorNにおいて値を直接与えず、状態を持たせる必要があるか。それはバリデーションを強制するためにあります。さきほどのComplexValidatorNによるバリデーション例を、Personのコンパニオンオブジェクトにて行うよう書き換えてみます。

class Person private (val name: String, val age: Int, val tags: Set[String])

object Person {
  val personValidator = nameValidator <-> ageValidator <-> tagsValidator
  
  def apply(name: Indefinite[String], age: Indefinite[Int], tags: Indefinite[Set[String]]): Person =
    personValidator(
      name label "name",
      age label "age",
      tags label "tags"
    ).resolve { (n, a, t) => new Person(n, a, t) }
}

val validPerson = Person(
  Indefinite("Kondo"),
  Indefinite(17),
  Indefinite(Set("programmer", "scala"))
)

val invalidPerson = Person(
  Indefinite(""),
  Indefinite(17),
  Indefinite(Set("programmer", ""))
)

ミソはPerson objectのapplyメソッドにおいてIndefiniteを指定している点です。こう指定されていてはapplyメソッドで受け取った値をそのままnew Personとすることが出来ません。Indefinite[String]とStringは当然異なる型ですからね。Indefinite[String]からStringを抜き出すにはComplexValidatorNによるバリデーションを通過させる必要があります。と、こんな感じでバリデーションを強制しています*2

Indefinite, Valid, Invalidの3つは想像が付きやすいと思うのですが、Unknownはなんでしょうか。これはComplexValidatorNを作るためのもうひとつのメソッド --> と関係があります。

値の集合をバリデーションするときにどうしても発生し得るのが、「値Bは値A以上であること」などのような、他の値に依存するケースです。まあ普通にValidatorで値Aを参照してあげれば良いのですが、バリデーション前の値はIndefiniteで包まれている以上、直接参照できません。そこで使うのが --> というメソッドです。

val aValidator: Validator[Int] = GreaterThanOrEqualTo(0)

val bValidator: Int => ComplexValidator1[Int] = { a =>
  GreaterThanOrEqualTo(a)
}

val validator = a --> b

// a: valid, b: invalid
{
  val result = validator(
    Indefinite(1) label "a",
    Indefinite(0) label "b"
  )
  
  println(result.isValid) // => false
  println(result.values) // => Valid(1, Tags("a")), Invalid(0, Tags("b"), ShouldBeGreaterThanOrEqualTo(1))
}

// a: invalid
{
  val result = validator(
    Indefinite(-1) label "a",
    Indefinite(2) label "b"
  )
  
  println(result.isValid) // => false
  println(result.values) // => Invalid(-1, Tags("a"), ShouldBeGreaterThanOrEqualTo(0)), Unknown(2, Tags("b"))
}

--> メソッドは左辺のComplexValidatorN (Validator)によってバリデーションされた結果がValidの場合、その値を右辺の関数( Int => ComplexValidator1[Int] )に与えます。そしてその関数から得られたComplexValidatorNを用いてさらにバリデーションを行う。といった具合に動作します。

もし左辺の結果がInvalidの場合はどうなるか。左辺がInvalidだとすると、当然Invalidな値からComplexValidatorNが作られることになるのですが、そんなComplexValidatorNでバリデーションしても意味がありません。ですので、右辺の結果はバリデーションの結果が不明ということでUnknownとなります。

まとめ

ということでclarifyでどんなことが出来るかを紹介しました。サンプル付きで長々と書いていますが、まとめると「値を与えたらバリデーション結果を返す」ということです。ので、比較的導入しやすいとは思います。まあ第三者が導入するためにはドキュメントを整え(この記事がドキュメントで良いのでは?)、なによりテストを書かなければいけませんが……。

ともかく、私は実際に使っているのですが、使い勝手はなかなか良く、自分で言うのもなんですが良いものが出来たと思っています。その上で「ビルトインのValidatorはさすがにもっとあっていいわ……」とか「Indefiniteで包むのさすがにめんどうだなー。ここに限ってはimplicit conversionがあってもいいな」とか考えています。

余談ですが2016年7月2日にあったYAP(achimon)C::Asia Hachioji 2016 mid in Shinagawaにて下記の発表をしました。

この発表で言っている内容自体はclarifyと直接関係がないのですが、この発表で言っていることをやるために軽量なバリデーションライブラリが欲しかったという経緯があったりします。興味がありましたら是非ご覧くださいませ。それでは。

*1:一応説明すると / は左辺および右辺のValidator双方とも条件を満たさない場合、右辺のエラーのみを全体のエラーとして採用する(左辺のエラーは結果に含めない)、というものです。 \ は左辺を採用するものです。ただ今思えば素直に両方ともエラーを返しても良い気がしますし、 And(leftError, rightError) という形で包んであげても良かったかな……

*2:まあapplyメソッドにIndefiniteを指定しない場合はどうしようもないのですが……

実況中継シリーズ Vue.jsで実現するMVVMパターン Fluxアーキテクチャとの距離 - Re.Ra.Ku アドベントカレンダー day 13

前説

丸山です。Re.Ra.Ku. アドベントカレンダー13日目の記事です。前日はiOSアプリのUIをコードで書いてみる話でした。明日はおそらくScalaの話になると思います。

さて、以前も話題にしましたが、builderscon2016が先日開催されました。チケットは3hでSOLD OUT。プラチナチケットと化した参加権ですが、発表する側ならば実質無料で参加し放題!これはいっそ申し訳ないレベルでは!?

というわけで、せっかく発表したのでその内容をなるべく多くの手段で共有したい。そう思い、今回も実況中継シリーズを弊社テックブログで行います。実況中継シリーズというのは、プレゼンをブログで再現するアレです。なお、実際のプレゼンは動画になってYoutubeにアップロードされております。builderscon公式サイトのセッション詳細ページからもご覧いただけますので、よろしければそちらも合わせてご覧ください。なお、先日行われたNDS#50でも再演を行いました。

続きを読む