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

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

前説

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

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

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

続きを読む

iOSアプリのUIをコードで書いてみる話 - Re.Ra.Ku アドベントカレンダー day 12

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

こんにちは、磯貝です。前回は Sketch.app のプラグインについての記事でした。

今回は、iOS アプリの UI をコードで書く際の知見をいくつか紹介します。

UI をコードで書く利点

iOS アプリ開発において UI を構築する手段として一般的なのは、Xcode の Interface Builder を用いたものでしょう。優れた GUI ツールにより、実行時の見た目を即座に確認しながら、豊かな表現を行うことができます。

しかし現在弊社で開発されている iOS アプリは、ほぼ全てにおいて UI をコードにより実装しています。
この方法には、以下のような利点があります:

  • ファイルの変更点が確認しやすく、レビューを行いやすい
    • nib の xml のように、理不尽な diff が発生することもなく、conflict も起こりづらいです。
  • View の使い回しをしやすい
    • Xib として View を定義した場合、それを他の Xib や Storyboard から利用するには、結局コードによる初期化が必要です。最初からピュアな UIView のサブクラスとして View を定義すれば、オーバーヘッド無しで再利用可能です。
  • デザインや文言を一貫して管理できる
    • 定数やユーティリティクラスを用いて、色、フォント、マージンといったデザイン要素や、各種文言、そのローカライズを一貫して管理できます。
  • ViewController の初期化を厳密に行うことができる
    • Segue を用いた画面遷移を行ったり、Storyboard から ViewController を初期化する場合、値の受け渡しは新しい ViewController インスタンスのプロパティへ値をセットする形で行います。しかしこの方法では値の受け渡しを強制することはできず、すべての Optional なプロパティに対して何らかの対処をする必要があります。UI をコードで記述すると、ViewController の初期化はイニシャライザを用いて行うことができるため、そこで適切に値を渡せばこのような問題は起こりません。また、ViewController 同士が疎結合となり、再利用が容易になります。
// Storyboard から初期化する場合
class ArticleViewController: UIViewController {
    var id: ArticleId!
    var reader: Reader?
    
    ...
}

let articleVC = UIStoryboard(name: "Article", bundle: nil).instantiateViewController(withIdentifier: "Article")
articleVC.id = articleId
articleVC.reader = reader

// イニシャライザを用いる場合
class ArticleViewController: UIViewController {
    let id: ArticleId
    let reader: Reader?
    
    init(id: ArticleId, reader: Reader?) {
        // 実際は ApplicationService 等へ渡していく
        self.id = id
        self.reader = reader
        super.init(...)
        
        ...
    }
    
    ...
}
let articleVC = ArticleViewController(id: articleId, reader: reader)

一方、次のような欠点もあります:

  • 実際にビルドして実行するまで、見た目を確認できない
  • Size Classes への対応が難しい (すみません、調べきれませんでした)

特に Size Classes はどうしても必要となることもあるでしょう。要件に応じて利点欠点を天秤にかけつつやっていくしかなさそうです。
(余談ですが、Size Classes に関しては、Web におけるレスポンシブで行くか別テンプレートでいくか、みたいな判断にも似ているなーと感じます。)

UI の書き方 Tips

いくら利点があるとはいっても、View を愚直にコードで書き起こしていくのは骨が折れる作業です。

それを解決するために弊社で利用しているライブラリと、いくつかの実装を紹介します。

SnapKit

SnapKit は、Auto Layout をシンプルな DSL を用いて記述できるライブラリです。
例として、画面中央に緑の枠線を表示し、その中にラベルとボタンを表示するサンプルを用意しました。

class SampleViewController: UIViewController {
    weak var label: UILabel!
    
    override func loadView() {
        view = UIView()
        view.backgroundColor = UIColor.white
        
        let container = UIView()
        view.addSubview(container)
        
        container.layer.borderWidth = 5
        container.layer.borderColor = UIColor.green.cgColor
        // SnapKit
        container.snp.makeConstraints { make in
            make.centerY.equalTo(view)
            make.left.right.equalTo(view).inset(20)
            make.height.equalTo(view).multipliedBy(0.5)
        }
        
        
        let label = UILabel()
        container.addSubview(label)
        self.label = label
        
        label.font = UIFont.boldSystemFont(ofSize: 20)
        label.textColor = UIColor.red
        label.textAlignment = .center
        label.text = "Some Text!!"
        // SnapKit
        label.snp.makeConstraints { make in
            make.top.left.right.equalTo(container).inset(30)
        }
        
        
        let button = UIButton()
        container.addSubview(button)
        
        button.backgroundColor = UIColor.lightGray
        button.setTitle("OK", for: .normal)
        button.addTarget(self, action: #selector(self.buttonTapped(_:)), for: .touchUpInside)
        // SnapKit
        button.snp.makeConstraints { make in
            make.top.equalTo(label.snp.bottom).offset(30)
            make.left.right.equalTo(container).inset(20)
            make.bottom.equalTo(container).inset(20).priority(UILayoutPriorityDefaultLow)
            make.height.greaterThanOrEqualTo(44)
        }
    }
    
    @objc private func buttonTapped(_: UIButton) {
        label.text = "tapped!!"
    }
}

結果: f:id:y1soga1:20161212062156p:plain

あまりよろしくないレイアウトをしていますが、機能の例としてご覧ください。

コードの階層化

上記の例では、

  • view
    • container
      • label
      • button

という View の親子/兄弟関係がありますが、すべてベタ書きしているため、なんとも構造がわかりづらくなってしまいます。

そこで、以下のような Extension を導入します。

protocol ViewConstructable {}

extension ViewConstructable {
    func addSubview<Sub: UIView, Super: UIView>(_ subview: Sub, toSuperview superview: Super, initializer: (Sub, Super) -> Void) -> Sub {
        superview.addSubview(subview)
        initializer(subview, superview)
        return subview
    }
}

これを用いることで、先程のサンプルは以下のように書き直すことができます:

class SampleViewController: UIViewController, ViewConstructable {
    weak var label: UILabel!

    override func loadView() {
        constructView()
    }

    private func constructView() {
        view = UIView()
        view.backgroundColor = UIColor.white

        let /* container */ _ = addSubview(UIView(), toSuperview: view) { sb, sp in
            sb.layer.borderWidth = 5
            sb.layer.borderColor = UIColor.green.cgColor
            sb.snp.makeConstraints { make in
                make.centerY.equalTo(sp)
                make.left.right.equalTo(sp).inset(20)
                make.height.equalTo(sp).multipliedBy(0.5)
            }

            label = addSubview(UILabel(), toSuperview: sb) { sb, sp in
                sb.font = UIFont.boldSystemFont(ofSize: 20)
                sb.textColor = UIColor.red
                sb.textAlignment = .center
                sb.text = "Some Text!!"
                sb.snp.makeConstraints { make in
                    make.top.left.right.equalTo(sp).inset(30)
                }
            }

            let /* button */ _ = addSubview(UIButton(), toSuperview: sb) { sb, sp in
                sb.backgroundColor = UIColor.lightGray
                sb.setTitle("OK", for: .normal)
                sb.addTarget(self, action: #selector(self.buttonTapped(_:)), for: .touchUpInside)
                sb.snp.makeConstraints { make in
                    make.top.equalTo(label.snp.bottom).offset(30)
                    make.left.right.equalTo(sp).inset(20)
                    make.bottom.equalTo(sp).inset(20).priority(UILayoutPriorityDefaultLow)
                    make.height.greaterThanOrEqualTo(44)
                }
            }
        }
    }

    @objc private func buttonTapped(_: UIButton) {
        label.text = "tapped!!"
    }
}

クロージャ内で child view を生成することにより、コードのインデントと View の階層を一致させることができました。

ViewController と View のファイルを分割する

上の例では、ViewController 内で UI を構築しています。これではファイルが肥大化しますし、ViewController と View の責務を適切に分割できません。

そこで、以下のような ViewController の基底クラスを用意してみました。

class ViewController<ViewType: UIView>: UIViewController {
    private var tempView: ViewType?
    weak var v: ViewType!

    convenience init() {
        self.init(view: ViewType())
    }

    convenience init(view: ViewType) {
        self.init(nibName: nil, bundle: nil)
        tempView = view
    }

    override func loadView() {
        view = tempView
        v = tempView
        tempView = nil
    }
}

これを用いることで、先程のサンプルを以下のように書き直すことができます:

class SampleView: UIView, ViewConstructable {
    weak var label: UILabel!
    weak var button: UIButton!

    required init?(coder aDecoder: NSCoder) {
        fatalError()
    }

    init() {
        super.init(frame: CGRect.zero)
        constructView()
    }

    private func constructView() {
        backgroundColor = UIColor.white
        
        let /* container */ _ = addSubview(UIView(), toSuperview: self) { sb, sp in
            sb.layer.borderWidth = 5
            sb.layer.borderColor = UIColor.green.cgColor
            sb.snp.makeConstraints { make in
                make.centerY.equalTo(sp)
                make.left.right.equalTo(sp).inset(20)
                make.height.equalTo(sp).multipliedBy(0.5)
            }
            
            label = addSubview(UILabel(), toSuperview: sb) { sb, sp in
                sb.font = UIFont.boldSystemFont(ofSize: 20)
                sb.textColor = UIColor.red
                sb.textAlignment = .center
                sb.text = "Some Text!!"
                sb.snp.makeConstraints { make in
                    make.top.left.right.equalTo(sp).inset(30)
                }
            }
            
            button = addSubview(UIButton(), toSuperview: sb) { sb, sp in
                sb.backgroundColor = UIColor.lightGray
                sb.setTitle("OK", for: .normal)
                sb.snp.makeConstraints { make in
                    make.top.equalTo(label.snp.bottom).offset(30)
                    make.left.right.equalTo(sp).inset(20)
                    make.bottom.equalTo(sp).inset(20).priority(UILayoutPriorityDefaultLow)
                    make.height.greaterThanOrEqualTo(44)
                }
            }
        }
    }
}

class SampleViewController: ViewController<SampleView> {
    override func loadView() {
        super.loadView()
        v.button.addTarget(self, action: #selector(self.buttonTapped(_:)), for: .touchUpInside)
    }

    @objc private func buttonTapped(_: UIButton) {
        v.label.text = "tapped!!"
    }
}

View は UI の構築のみに注力し、イベントの受け取りや View の更新は ViewController が行っています。

まとめ

いかがでしたでしょうか。
UI をコードで書く手法は、弊社としてもまだまだ試行錯誤を続けている段階です。またアップデートがあれば記事にできればと思います。 それでは、今回もお読み頂きありがとうございました!

Swift ifとguardのエトセトラ - Re.Ra.Ku アドベントカレンダー day 11

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

こんにちは、神場です。 前回の記事 に引き続き、今回もSwift関連の記事となります。

概要

今回はif文とguard文に関するtipsです。 if文は一般の条件分岐に、guard文は想定外の値が来た場合などの早期リターンに使われるという違いはありますが、使い方が似ているためこの記事ではまとめて紹介したいと思います。

ifとguard両方で使えるもの

今回紹介するのはこの3つです。いずれもifguardの両方で使えます。

  • if let, guard letによるOptional Binding
  • if A, B, ..., guard A, ,B ...のような複数の条件
  • if case, guard caseによるパターンマッチ

順番に見て行きましょう。

if let, guard letによるOptional Binding

まずは基本的なところですが、if let, guard letでオプショナルな変数のバインディングを行うことが出来ます。

if let a = a {
    print(a)
} else {
    print("a is nil")
}

これはオプショナルな変数のUnwrapに使う基本的な方法なので、特に疑問はないかと思います。

if A, B, ..., guard A, ,B ...のような複数の条件

Swiftをやり始めの頃だと(自分も含めて)よくやってしまっていたと思うのですが、

if let a = a {
    if let b = b {
        ...
    }
}

のような文はまとめて

if let a = a, let b = b {
    ...
}

と書くことが出来ます。上記の例のように二つぐらいの条件であればまだ良いのですが、条件が多い時は基本的にカンマ区切りで書いたほうがコードがすっきりします。guard文を使う例を以下に挙げます。

indexが存在し、かつステータスがactiveなユーザーの名前を取得するサンプルコード

enum Status {
  case active, inactive
}
struct User {
  let name: String
  let status: Status
}

func getActiveUserName(users: [User], index: Int) -> String? {
    guard
        index < users.count,
        users[index].status == .active
        else { return nil }
    return users[index].name
}

if case, guard caseによるパターンマッチ

上記二つに比べて日本語ではあまり情報がない印象がありますが、実はifguardでもswitchで使うようなパターンマッチを使うことが出来ます。

enumのAssociated Valueの中身をパターンマッチで取り出すサンプルコード

enum Result<T> {
    case success(value: T)
    case failure(error: Error)
}

func getValue<T>(result: Result<T>) -> T? {
    guard case let .success(value) = result else {
        return nil
    }
    return value
}

上記のように、switchに書くcaseがほぼそのままguardの中に 入ります。

なおこれを応用(?)することによって、Optionalでない変数のバインドを行うことも出来ます。

indexが存在し、かつステータスがactiveなユーザーの名前を取得するサンプルコード(userを一旦変数に入れておくバージョン)

func getActiveUserName2(users: [User], index: Int) -> String? {
    guard
        index < users.count,
        case let user = users[index],
        user.status == .active
        else { return nil }
    return user.name
}

if case, guard caseのさらに詳しい情報については、こちらの方の記事 が非常に参考になるかと思います。

より実践的なサンプル

以上のいくつかを組み合わせて、より具体的なサンプルを見てみましょう。

guard文を使ったAPIレスポンスのハンドリングのサンプル

func parseResponse(result: Result<Any>) -> DTO? {
    guard case let .success(value) = result else {
        print("通信に失敗したよ")
        return nil
    }
    
    guard let json = data as? [String: Any] else {
        print("データがJSONじゃないみたいだよ")
        return nil
    }
    
    guard (200..<300) ~= statusCode else {
        print("ステータスコードが200番台以外だよ")
        return nil
    }
    
    guard let dto = parseJson(json) else {
        print("JSONが期待しているものと違うよ")
        return nil
    }
    
    print("正常なレスポンスが返ってきたよ")
    return dto
}

ただしエラー内容も取得したい場合はsuccessfailureの2つにパターンマッチを使いたいため、成功か失敗かの分岐部分をswitchで書くことになるかと思います。

まとめ

いかがでしょうか。以上の3つ程度があればifguardだけでも比較的読みやすい書き方が出来るのではないでしょうか。若干まとまりのない感じになってしまいましたが、もしご参考になれば幸いです。

参考

Android DataBinding Tips - Re.Ra.Ku アドベントカレンダー day 10

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

こんにちは、安部です。

今回はAndroidのDataBindingのTipsを少し紹介します。

DataBindingの変数に設定するなど基本的なところは省略しています。

includeしたレイアウトに変数を渡す

includeタグを使ったときに変数を渡す方法です。

includeされるレイアウト

通常のレイアウトと同じようにします。

<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>

        <variable
            name="name"
            type="java.lang.String" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{name}" />
    </LinearLayout>
</layout>

includeタグ

先程のレイアウトで定義した変数をincludeタグの属性としてapp:nameに渡したい変数を設定することでincludeされた側のほうでも変数が使用可能になります。

<include
    app:name="@{user.name}"
    layout="@layout/custom_view"/>

EditTextの入力を変数に反映する

two-way bindingの実現方法です。

入力を受け取るクラス/変数

EditTextの入力を受けて取るクラスと変数の例です。

public class User {
    public final ObservableField<String> name = new ObservableField<>();
}

レイアウト

分かりにくいのですが、android:textに設定している値の@{の間に=を入れます。こうするとEditTextに入力された値が変数に反映されます。

<EditText
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:text="@={user.name}" />

制限事項としては、android:textにはStringのものしか設定できません。

EditTextの変更を監視する

変更されるたびに呼ばれるメソッド

この場合ですとonTextChangedが変更されるたびに呼ばれます。

public class Handlers {

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

    public void onTextChanged(CharSequence s, int start, int before, int count) {
        Log.d(TAG, "onTextChanged: " + s);
    }
}

レイアウト

android:onTextChangedに先程つくった処理を設定してあげます。これだけです。

<EditText
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:onTextChanged="@{handlers::onTextChanged}" />

補足

android:onTextChangedがどこで定義されてるかですが、ココ です。

他にもいくつか拡張であるので、ほしいのがないかをココで探すと良いと思います。

よく使いそうな、フォーカスイベントのandroid:onFocusChangeココで定義されてます。

ひとつ問題としては、xmlで属性が定義されてないみたいな警告が出てしまいます。気になる人はコメントで抑制しておくと良いと思います。(tools:ignoreを使った抑制がうまくできなかった…)

<!--suppress AndroidUnknownAttribute -->
<EditText
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:onTextChanged="@{handlers::onTextChanged}" />

注意としてはパッとどこで定義されてるのかが分からないので、何かしらコメントがあると良いかもしれないです。

定義されていないイベント処理する

例えば、SwipeRefreshLayout#setOnRefreshListenerはそのままではレイアウトファイルのみでイベントを設定することが出来ません。

カスタム属性を設定する

次のようなクラスを作って、属性を追加して、それを受け取れるようにします。引数の順番は対象のView、属性に渡す型になります。今回はSwipeRefreshLayout.OnRefreshListenerを受け取るようにします。

public class SwipeRefreshLayoutBinding {

    @BindingAdapter("onRefresh")
    public static void onRefresh(SwipeRefreshLayout view, SwipeRefreshLayout.OnRefreshListener listener) {
        view.setOnRefreshListener(listener);
    }
}

別のパターンとして、@BindingMethodsを使うことも出来ます。こちらはクラスのほうにアノテーションを追加します。

@BindingMethods({
        @BindingMethod(type = SwipeRefreshLayout.class, attribute = "onRefresh", method = "setOnRefreshListener")
})
public class SwipeRefreshLayoutBinding {

}

イベントを処理するメソッドを定義する

SwipeRefreshLayout.OnRefreshListenerで定義されているメソッドと同じシグネチャのメソッドを定義します。メソッド名は変更しても大丈夫です。

public class Handlers {

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

    public void onRefresh() {
        Log.d(TAG, "onRefresh");
    }
}

レイアウトに設定する

先程の作った属性(名前空間はappを使います)とメソッドを設定してあげると、コードを書かずにイベントを処理できるようになります。

<android.support.v4.widget.SwipeRefreshLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:onRefresh="@{handlers::onRefresh}">

    ...

</android.support.v4.widget.SwipeRefreshLayout>

まとめ

頑張ればDataBindingで色々できそうですが、あまりトリッキーなことをしすぎない感じのほうが良いとは思います。そこはうまくバランスを取りながらで。

うまくDataBindingを使って、コードを簡潔にしていきたいですね。

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

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

1回目を読んで下さった皆様、ありがとうございます!
ブックマークや星、facebookシェアもしていただけて、 モデルのタミー共々、大変嬉しく感じています。

今回は、プログラミングしながらできる、肩甲骨のストレッチ方法をお伝えします。

肩甲骨の仕組み

弊社のボディケアでも注目している肩甲骨。簡単に仕組みをお伝えしてからストレッチの話に入っていきます。

1. 17種類の筋肉が付いている

肩甲骨には、17種類の筋肉がついています。その筋肉は、首や背中、骨盤と繋がっています。
そのため、肩甲骨の動きが硬くなると、首や腰を引っ張ってしまい、体の前後左右のバランスが崩れ、姿勢が悪くなってしまいます。
逆に言うと、肩甲骨がほぐれると、首や腰も一緒にほぐれるため、早く効率的に体を整えたいという方には、肩甲骨のケアをお勧めしています。
また、肩甲骨の前には肺があるため、肩甲骨の位置が整うと肺が広がり、呼吸もより大きくなりやすくなります。

2. 褐色脂肪細胞が密集している

肩甲骨の間には、褐色脂肪細胞という細胞が密集しています。この細胞は、体脂肪を燃焼し、熱を生み出す細胞と言われています。
人体には肩甲骨以外に、首、わき下、心臓・腎臓部分にありますが、中でも肩甲骨の周囲は熱量の出し方がとても多く、筋肉の数十倍もあると言われています。
肩を大きく動かすことで、痩せやすい体づくりも期待できます。

肩甲骨のストレッチ

ストレッチは、座りながらできるストレッチを紹介します。
ストレッチを行う時は、綺麗な姿勢を作ってから行うように意識してみてください。
姿勢の保ち方は、前回の講座でお伝えしています。ご確認下さい。
プログラミングしながらできる、健康への近道講座:1回目 - Re.Ra.Ku アドベントカレンダー day 3 - Re.Ra.Ku tech blog

綺麗な姿勢を作ったら、息を吸って、左手で椅子をつかんで、ひじの裏を表に向けて、自然な呼吸で右側に体を倒します。 f:id:nozomiii:20161208145714j:plain

気持ち良く伸びたな、と実感できたら息を吸いながらゆっくり体を戻します。
戻した後は反対側も行います。
側面を伸ばすことで、普段縮こまっている腕の疲れをとります。

次に、
背もたれの後ろで手を組んで、肘を背もたれに引っ掛けて、おへそから体を倒します。
この時、首は下げないように注意してください。
f:id:nozomiii:20161208145730j:plain

1つ目のストレッチと同様、自然な呼吸で伸ばしていき、
気持ち良く伸びたなと実感できたところで息を吸いながゆっくり体を戻します。
前のめりになっている肩甲骨を正しい位置に近づけるストレッチです。

最後は、
両手を机につき、臀部で椅子を行けるところもまで後ろに下げます。
f:id:nozomiii:20161208145748j:plain

肩甲骨を伸ばすとともに、背中もしっかり伸ばします。

どのストレッチも、どこまで出来れば良い悪い、というものではなく、ご自身が一番気持ちが良いと感じる強度が一番良いストレッチです。
ご自身のペースで行ってみてください。

モデルについて

お伝えしそびれていましたが、今回の講座でモデルをしてくれているのは、弊社デザイナーのタミーです。
ドイツ人ですが、日本語も英語もペラペラなトライリンガルです。
弊社の中では、テキストデザインはもちろんのこと、店舗デザインや動画制作等、デザイン全般担当してくれています。
店舗の販促ツールや店舗デザインルール等も、タミーが管理しています。
店舗の近くに寄る機会があれば、ぜひデザイン面も気にかけて見ていただけると嬉しいです。
reraku.jp

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

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

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

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

2日目のリラクのサーバサイド事情 with Scalaにて宣言した通り、今回はサーバサイド事情の中でも中核となるドメインについての話です。

「ドメイン」とは何か

「ドメイン (domain)」はそもそも「領域」という意味ですが、様々な箇所で使われ、文脈によって指すものが異なりやすい言葉だと思います。

今私が掲題している「ドメイン」とは、レイヤードアーキテクチャで言うところのドメイン層で、ビジネス要件の塊です。含まれるものは:

  • ユーザ名は必須かつ20文字以内
  • ログインできるユーザはステータス: Activatedのみに限り、ステータス: Registeredおよびステータス: Suspendedのユーザはログインできない
  • チャットルームはユーザが自由に作成することができ、作成したユーザがオーナーとなる
  • チャットルームのオーナーは任意のユーザーをチャットルームに参加させられる。既に参加済みのユーザーを参加させようとすると、UserAlreadyJoinedというエラーが起きる

などなどで、あくまで「方針」です。

反対に含まれない(含みたくない)ものは:

  • ユーザなどの情報はPostgreSQLに保存する
  • ユーザのアイコンはAmazon S3に保存する
  • 各種操作のリクエストはHTTPにて受け付ける

などの、何によって実現するかという「詳細」です。

ですが、何故詳細を含みたくないのか?それは方針が詳細に引き摺られてしまうからです。

詳細はそれぞれが特有のインタフェースを持っています。PostgreSQLならばSQLにてやりとりしますし、Amazon S3とはHTTP APIでやりとりするでしょう。もしその情報をドメインに持ち込んでしまうと、ドメインはSQLやHTTPという方法に依存し、それらに関するコードを含むようになります。

正直この時点で匂います。関心事が混ざり合っている匂いです。そこに追い討ちとして、「永続化先をPostgreSQLからMongoDBに変更する」という要請が来たらどうなるでしょう?処理のコアとなるドメインには変更がないのに、ドメインのコードを変更しなければなりません。おかしい話ですよね?

また、表現の自由度が低下するので、その点を私たちは気にしています。詳細にはフレームワークも含まれています。フレームワークは設計を制限し、表現を規定します。これによって開発者間のコミュニケーションがしやすくなるなどのメリットもあるのですが、ことドメインにおいてはそれが枷となりやすく、足を引っ張りがちです。よって嫌います。

私はとある漫画が好きなので、思わず「ドメインを設計する時はね、詳細に引っ張られず、自由でなんというか」と続けてしまいたくなるのですが、その通りだと思います*1

DDDの採用

ドメインの設計にはDDD (Domain-driven design)を採用しています。

詳細によって表現の自由度は下げたくないのですが、表現の指針がないのもそれはそれで無法地帯です。そういうわけでドメインの表現としてはメジャーであろうDDDを選びました。よってコード中にはEntityやRepositoryといった概念が出て来ます。

もちろんDDDの核であるユビキタス言語にも気を付けています。週1でチーム内の定例ミーティングがあるのですが、そこでは現在関わっているプロジェクト単位で使用している言葉(日本語/英語共に)に違和感はないかを議論し、常にメンバー間で擦り合わせるようにしています。そのとき「現場ではこういう言葉が使われている」や「こっちの方が実際の要件に即している」ということがあれば、即座にコードに反映していきます。

そんな感じで試行錯誤しつつも結構マジメにDDDを実践していっています。

値オブジェクト

そんなわけでDDDの概念である値オブジェクト、エンティティ、リポジトリなどがどうなっているか、です。

よくあるDDDの説明で、エンティティはいの一番に語られるものだと思っていますが、関連する属性を取りまとめた論理的な単位である値オブジェクトの方が分かりやすいと思っています。ですので、まず値オブジェクトに触れていきます。

値オブジェクトの特徴は:

  • 一度生成したらその値オブジェクトが持つ属性は不変であり
  • 属性すべてが等値であれば、値オブジェクトそのものが等値である

というものです。それ以外にも実践ドメイン駆動設計には「概念的な統一体」などを挙げています。それらは何を値オブジェクトとしてモデリングすべきなのかを示しているのですが、今回はさきほど挙げた2点に絞ってみます。

値オブジェクトの特徴は「不変」で「属性によって等値かを判断する」という点です。Scalaはこれにおあつらえ向きのものがあります。case classです。よって値オブジェクトはすべてcase classを用いて定義しています。以下が値オブジェクトのコード例です。

package domain.user

import java.util.UUID

import org.usagram.clarify._

sealed trait UserId {
  def value: UUID
}

private case class UserIdImpl(value: UUID) extends UserId

object UserId {
  def apply(value: Indefinite[UUID]): UserId = {
    // valueをバリデーションし、パスしたらUserIdImplを生成し、返す
  }
}

case classで定義していると言ったのに、いきなりtraitを用いています。シンプルに書けば:

package domain.user

import java.util.UUID

case class UserId(value: UUID) {
  // valueをバリデーション
}

となり、大分すっきりするのに、なぜ先に掲載した回りくどい書き方をしているのか。それは以下に起因します。

  • バリデーションを強制したい: case class内にバリデーションを書くというのはあくまで任意なので、バリデーション忘れなどのミスには対応できません(まあ今回の例ではjava.util.UUIDなので特にバリデーションすることもないのですが……)
  • case classのapplyを使用禁止にしたい: バリデーションを強制するためにcase classの外、例えばコンパニオンオブジェクトのapplyにてバリデーションを強制する仕組みを導入したとしましょう。しかし、その場合コンパニオンオブジェクトのapplyはcase classに既に使用されているため使えません。じゃあ別のメソッド名を使うか。そうなるとcase classのapplyを使ってバリデーションを擦り抜けることが出来てしまいます

とまあ、ようするに「バリデーションを強制するためにcase classデフォルトのapplyを使用禁止にしたい」という事情があります。ですので、回りくどい書き方をしているのです。

sealed trait UserId {
  def value: UUID
}

まずtraitで値オブジェクトの型を定義します。sealed修飾子によってこのtraitが定義されたファイル以外でUserId型となりえるクラスを定義できなくなります。

private case class UserIdImpl(value: UUID) extends UserId

UserId型の実装です。ここでcase classを使っているため、「不変」かつ「属性によって等値かを判断する」という特徴を備えます。しかし、このcase classはprivateであるため、外から直接扱えません。このままではUserId型のオブジェクトを生成できなくなります。

object UserId {
  def apply(value: Indefinite[UUID]): UserId = {
    // valueをバリデーションし、パスしたらUserIdImplを生成し、返す
  }
}

ですのでコンパニオンオブジェクトに生成のためのメソッドを定義します。ここではapplyとしています。UserIdImpl case classのコンパニオンオブジェクトにapplyを定義することは出来ません(2回定義されているよとコンパイラに怒られます)が、これはUserId traitのコンパニオンオブジェクトなので、問題なくapplyを定義することが出来ます。ここでバリデーションを強制しています。詳細は後日としますが、Indefinite[UUID]という型がそれです。

こうすると:

  • UserId traitはsealed修飾子によって他のファイルにて継承することが出来ない。当然 new UserId { val value = ... } とその場でオブジェクトを生成することも不可能
  • UserIdImpl case classはprivate修飾子によって参照することが出来ない
  • よって必ずUserId objectのapplyメソッドを使用する必要があり、バリデーションは強制される

となり、要望を満たせます。

あとはビジネス要件に応じたバリデーションを書き、値オブジェクトを操作する必要があれば、traitの方にユビキタス言語に則ったメソッド名で定義していきます。

エンティティ

次にエンティティです。エンティティは以下の特徴を持ちます。

  • 識別子を持ち
  • 属性の変更が可能である

しかし、識別子を持つことはともかく、属性の変更が可能であっても、私たちはイミュータブルにモデリングしています。そっちの方がシンプルに考えられ、テストも容易ですしね。ですので、エンティティ自身を操作するすべてのメソッドにおいて、操作された結果の新たなエンティティを返すようになっています(もちろん識別子は同一です)。

以下がエンティティの例です。値オブジェクトのときと同様、バリデーションを強制するようになっています。

package domain.user

import org.usagram.clarify._

sealed trait User extends Entity[UserId] {
  def name: String
  
  def email: String
  
  def updateName(newName: String): User =
    User(Indefinite(id), Indefinite(newName), Indefinite(email))
}

private class UserImpl(val id: UserId, val name: String, val email: String) extends User

object User {
  def apply(id: Indefinite[UserId], name: Indefinite[String], email: Indefinite[String]): User = {
    // id, nameおよびemailをバリデーションし、パスしたらUserImplを生成し、返す
  }
}

エンティティが等値であるかどうかは、識別子が等値であるかによって判断されるべきなので、case classではなくclassとなっています。その代わり、Entity[ID] traitを利用しています。Entity[ID] traitは以下のように定義しています。

trait Entity[ID] {
  def id: ID

  override final def equals(obj: Any) = obj match {
    case that: Entity[_] => id == that.id
    case _               => false
  }

  override final def hashCode = 31 * id.##
}

Entity[ID]型は必ずid属性を持つようになります。これによってエンティティの条件である「識別子を持つ」をクリアします。あとはequalsとhashCodeをオーバライドして、識別子による等値を実現しています。実際のコードではID型パラメータに上限境界を設けたりしていますけどね。

リポジトリ

リポジトリはエンティティ(正確には集約)の永続化のための仕組みです。行うことは:

  • エンティティの取り出し
  • エンティティの格納

が主です。Userエンティティのためのリポジトリ、UserRepositoryは以下のようなコードになっています。

package domain.user

trait UserRepository[X] {
  def find(id: UserId)(implicit session: X): User
  
  def store(user: User)(implicit session: X): Unit
}

これだけです。Repositoryはたしかに永続化するための仕組みなのですが、散々言うように「何を使って永続化するか」がドメインにあってはいけません。よって提供するのは「UserIdを与え、それに対応するUserを返すfindメソッド」と「Userを与え、それを格納するstoreメソッド」というインタフェースだけなのが望ましいです。

なお上記例では定義していませんが、リポジトリにはドメインの処理を実現するための問い合わせ(例: メールアドレスの重複を防ぐために、既に使用済みでないかをリポジトリに問い合わせるなど)用メソッドがあっても構いません。

もちろん、インタフェースだけではアプリケーションとして完成しません。ですので、リラクのサーバサイド事情 with Scalaに書いたよう、インフラストラクチャ層で上記UserRepositoryを実装し、アプリケーション層にて実装されたUserRepositoryを使うように書いていきます。

サービス

あとはサービスですかね。サービスが特定の方法に依存する場合、当然ドメインにはインタフェースのみを定義し、インフラストラクチャ層で実装しています。例えばメールの送信とか。

package domain.user

trait SendActivationEmailService {
  def apply(user: User): Unit
}

サービスが複数のリポジトリやサービスをどうこうする程度のものであれば、サービスの処理自体はドメインに書き、アプリケーション層にてそのリポジトリをサービスに渡すようにしていたりはします。こんな感じ。

package domain.user

import java.util.UUID

trait RegisterUserService[X] {
  def userRepo: UserRepository[X]
  
  def sendActivationEmailService: SendActivationEmailService
  
  def apply(name: String, email: String)(implicit session: X): User = {
    val userId = UserId(Indefinite(UUID.randomUUID))
    val user = User(Indefinite(userId), Indefinite(name), Indefinite(email))
    
    if (userRepo.findByEmail(user.email).isDefined) {
      throw new UserEmailAlreadyUsed(email)
    }
    
    userRepo.store(user)
    sendActivationEmailService(user)
    user
  }
}

class UserEmailAlreadyUsed(email: String) extends Exception(s"$email is already used in other user")

これをアプリケーションサービスで:

package application.user

// 省略

object RegisterUserApplicationService {
  val registerUserService = new RegisterUserService[DBSession] {
    val userRepo = new JDBCUserRepository()
    
    val sendActivationEmailService = new SMTPSendActivationEmailService(...)
  }
  
  def apply(command: RegisterUserCommand): User =
    DB localTx { implicit session =>
      registerUserService(command.name, command.email)
    }
}

こんな感じで組み込みます。

今後やりたいこと

弊社ではドメインを値オブジェクト、エンティティ、リポジトリ、そしてサービスの4種類を用いてモデリングしています。ですので、イベントなどは用いていません。それらにはまだ手付かずですね。

ゆくゆくはイベントを導入し、エンティティ(集約)はイベントの集合(イベントソーシング)で表し、それらによるメリットを自らの手で確かめていきたいです。

しかし、イベントソーシングを導入するとなると、永続化される情報はイベントの集合になり、問い合わせが困難になるため、当然問い合わせ専用のモデル(RDB的な話ならばテーブル)が必要になり、それってもうCQRSじゃん?と、あれもこれもとなります。正直まだ中々踏ん切りが付いていないところですね。まあ小さく始めている途中ということで。

というわけで弊社のサーバサイドのドメイン事情でした。それでは。

*1:ちなみにこの場合の「自由」は何をしてもいいという意味ではなく、要件を満たすための前提としての自由です

SwiftでJSONのマッピングにはUnboxが便利らしい - Re.Ra.Ku アドベントカレンダー day 7

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

こんにちは、Re.Ra.Kuさんのパートナーとして開発に携わっています、神場です。 アドベントカレンダーをやるということで、せっかくならと思い手を挙げたところ参加出来ることになりましたので、書かせていただきます!

概要

今回はSwift製のJSON decoderであるUnboxをプロダクトで採用してみて、かなり使いやすいということが分かりましたので、いくつか使い方のサンプルを紹介するという内容の記事になっています。

Unboxを選択した理由

Swift3でJSONを扱うライブラリにはSwiftyJSONArgoがあると思いますが、今取り掛かっているプロダクトでは

  • マッピング時に型推論が出来る
  • 異なる型への変換が楽に出来る
  • インターフェースが直感的である
  • 実装が軽い
  • Githubのスター数がある程度(300以上ぐらい?)付いている

等の理由からUnboxを採用しました。

この記事で紹介する内容

この記事では

続きを読む