Observer パターンを手軽に実現するためのフレームワーク SteinsKit を作りました

タイトル通り、[SteinsKit](https://github.com/el-hoshino/SteinsKit) を作りました。Qiita は自作ライブラリーの宣伝サイトではないので、ライブラリーを作ったモチベーション、使い方、そして制作に利用したテクニックを紹介したいと思います。

# モチベーション

RxSwift や ReactiveSwift などに慣れた人、特に MVVM などの設計で `bind` に慣れた人でしたら、ある変数の値が変わったら、その変数に結びついてる別の変数や動作が自動で行われる書き方[^reactive]がとても便利と感じてるでしょう。

[^reactive]: 「リアクティブプログラミング」や「宣言型プログラミング」とも呼ばれたりします

ところが RxSwift が完璧だということでもありません；まず一番すぐ体感できるデメリットはビルド時間の長さではないでしょうか。RxSwift は RxCocoa などの周辺ライブラリーも含まれ、なおかつ実装自体も非常に複雑であるがゆえに、ビルド時間が非常にかかります；CocoaPods ならキャッシュクリアすると 15 分以上、Carthage なら bootstrap に 20 分以上かかります[^buildtime]。そして `BehaviorRelay`[^behavior relay] を使うために、どっちかというと比較的により UI 層寄りの RxCocoa を import する必要があります。また、筆者個人的には、そもそもの話、RxSwift にしろ ReactiveSwift にしろ、ましては SwiftBond にしろ、彼ら全て「Functional Reactive Programming」という非常にマッシブなコンセプトの実装ですので、Stream や Hot/Cold Observables などの概念や、それらに基づいて作られた数多くの operators は学習コストを上げるだけでなく、コードレビューやデバッグの難易度も上げている側面があります。

[^buildtime]: もちろんビルド環境のスペックにも左右されるので一概には言えませんが

[^behavior relay]: `Error` と `Completion` がない `Observable` で、一つ前のメジャーリリースでの `Variable` とほぼほぼ同じようなものです。

しかし我々エンジニアは、多くの場合はガッツリした FRP 設計でプログラムを組みたいわけではなく、単純に「この値が変わったら、この動作を実行したい」という、言わばただの「Observer パターン」の実現だけです。極端な話、Swift の `didSet` が外からでも組み込めればそれでいいくらいのノリです。それだけのために、わざわざ FRP の概念を実装した RxSwift や ReactiveSwift を導入するのは、ちょっと言い過ぎた例えですが一人の兵士を殺すために巡航ミサイルを使うみたいな利用法だと筆者は感じます。

また、RxSwift に限って言うと、どこにでも現れる `disposed(by: disposeBag)` 解放処理もただのノイズでしかなくてウザい[^dispose]ですし、`subscribe` を使った Closure 購読ですと `[weak self]` 書いて `guard let self = self else` 入れないといけないのも面倒いです。

[^dispose]: あくまで個人の感想です

なので、これらの問題点を解決すべく、作ったのがこの SteinsKit です。SteinsKit を使った購読の構文は下記のように非常にシンプルです：

```swift
model.count
    .map({ "あなたは \($0) 番目の訪問者です！" })
    .beObserved(by: label, .asyncOnQueue(.main), 
                onChanged: { $0.text = $1 })
```

SteinsKit の根本的な考え方としては、Cocoa MVC 設計としては ViewController が Model を監視する必要があります[^mvc]。Objective-C の時代では KVO というランタイムのオブザーバ機能が標準に備えていますが、Swift は静的な性格が強くてそういった機能がありません。なので筆者は Cocoa MVC のこの監視用件だけさえクリアできれば OK だという考えで SteinsKit を作ったため、SteinsKit には余計な機能が一切ないですし、逆にいうと例えば「ボタンの押下を購読してモデルを更新する」という本来の Cocoa MVC の使い方から踏み外した機能も実装していないので、人によっては貧弱だと感じるかもしれません。

[^mvc]: 参考：https://developer.apple.com/library/archive/documentation/General/Conceptual/CocoaEncyclopedia/Model-View-Controller/Model-View-Controller.html#//apple_ref/doc/uid/TP40010810-CH14-SW9

# 使い方

まず SteinsKit には `Variable<Value>` と `LazyVariable<Value>` の 2 種類の変数の箱があります。大まかの考え方は `RxSwift` に昔存在した `Variable` とさほど変わらないのですが、この 2 つの区別としては `Variable` は必ず初期値が存在するのに対し、`LazyVariable` は初期値がなくても宣言できます（ただし値の代入操作は必ず何かしらの `Value` 型の値を代入しなければなりません）。部品の初期化時に値がないけど途中から必ず何かしらの値が入ってくる `lazy var` の代わりに使う箱だと考えれば分かりやすいかと思います。

そしてこれらの箱を所持する側から、内部の値を変更するときは `accept` メソッドを使います。直接値を与える `func accept(_ newValue: Value)` メソッドもあれば、現在の値を参照して新しい値を作る `func accept(_ calculation: (Value) -> Value)`[^new value calculation] メソッドもあります。また、彼ら現在の値を確認だけしたい時は `var currentValue: Value`[^current value] プロパティーもあります。

[^new value calculation]: `LazyVariable` の場合は `func accept(_ calculation: (Value?) -> Value)` になります
[^current value]: `LazyVariable` の場合は `var currentValue: Value?` になります

これらの箱を外に公開する時は、基本そのまま公開するのではなく、`AnyObservable<Value>` として公開します。`AnyObservable` は `protocol Observable` の型消去です。Swift の `protocol` はジェネリクスが使えないので、型消去で具体的な型に落とし込む必要があります[^type erasure]。

[^type erasure]: Swift 自身もこの「型消去」というアプローチを多用しています、例えば `Sequence` を型消去した `AnySequence` や `Hashable` を型消去した `AnyHashable` などがあります

`Observable` には `associatedtype Value` という具体的な値の型の関連以外に、購読側用のメソッドが 4 つ用意されています：

1. `func runWithLatestValue (_ execution: (Value) -> Void)`
これはとにかく今現在の値を利用して何かの処理をするだけのメソッドです（ただし `LazyVariable` でまだ値が初期化されていない場合は処理がスキップされます）；購読はしませんし、値が変更した時に動くわけでもないので、とにかくいますぐ何かやりたい時に便利です。

2. `func map <NewValue> (_ transform: @escaping (Value) -> NewValue) -> AnyObservable<NewValue>`
これは変換メソッドです。変換する瞬間と、値が変わるたびに `transform` が動いて、変換されたものは別の `AnyObservable` の型消去として渡されますので、元の箱には影響ありません。RxSwift の `map` とほぼほぼ同じ使い方です。

3. `func beObserved <Observer: AnyObject> (by observer: Observer, _ method: ExecutionMethod, onChanged handler: @escaping (Observer, Value) -> Void)`
これは購読するメソッドです。購読する瞬間と、値が変更されるたびに `handler` が動きます。`observer` に渡される購読する部品は、`handler` の最初の引数にも渡されるので、`handler` の `$0` が購読オブジェクトとして使えてわざわざキャプチャーリストで `weak` 宣言する必要がありません；また、この購読オブジェクト自体がそもそも弱参照として箱に保持されますので、購読オブジェクトが解放されたら購読自体も解放され、`disposed(by: disposeBag)` のような解放処理をわざわざ書く必要もなくなります。そしてここの `method` ですが、これは `.directly`、`.asyncOnQueue(DispatchQueue)` と `syncOnQueue(DispatchQueue)` の 3 つのオプションがあります。`.directly` は値を変更したスレッドがそのまま `handler` を実行します、`.asyncOnQueue` は `handler` を渡されたキューで非同期で実行されます、最後の `.syncOnQueue` は `handerl` を渡されたキューで同期で実行されます。

4. `func beObserved <Observer: AnyObject> (by observer: Observer, onChanged handler: @escaping (Observer, Value) -> Void)`
これは上のメソッドとほぼほぼ同じですが、 `method` 引数が省略されてますが内部では `.directly` を使っています。

`Variable<Value>` と `LazyVariable<Value>` のインスタンスに対して、`.asAnyObservable()` メソッドを使えば、`AnyObservable<Value>` という型消去されたインスタンスが得られます。

これらを合体させると、ライブラリーに組み込まれている Playground と同じように書けます：

```swift:Playground.swift
import UIKit
import PlaygroundSupport
import SteinsKit

// MARK: - Model
final class Model {
    
    private var _count: Variable<Int> = .init(0)
    
    private func _countUp() {
        _count.accept({ $0 + 1 })
    }
    
}

extension Model: ModelProtocol {
    
    var count: AnyObservable<Int> {
        return _count.asAnyObservable()
    }
    
    func countUp() {
        _countUp()
    }
    
}
// MARK: Model End -

// MARK: - View Controller
protocol ModelProtocol: AnyObject {
    var count: AnyObservable<Int> { get }
    func countUp()
}

final class ViewController: UIViewController {
    
    private lazy var label: UILabel = {
        let label = UILabel()
        label.frame = CGRect(x: 25, y: 200, width: 320, height: 100)
        label.numberOfLines = 0
        return label
    }()
    
    var model: ModelProtocol!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .white
        view.addSubview(label)
        startObservation()
    }
    
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        addCount()
    }
    
    private func startObservation() {
        model.count
            .map({ "Welcome!\nWe have \($0) visitors visited\nour playground!" })
            .beObserved(by: label, .asyncOnQueue(.main), onChanged: { $0.text = $1 })
    }
    
    private func addCount() {
        model.countUp()
    }
    
}
// MARK: View Controller End -

// MARK: - Main
let vc = ViewController()
let model = Model()
vc.model = model

PlaygroundPage.current.needsIndefiniteExecution = true
PlaygroundPage.current.liveView = vc
```

# 実装に使ったテクニック紹介

## 型消去（Type Erasure）

こちらは上記の使い方の説明にも出てきてますが、これは要するにジェネリクスが使えない `protocol` を、ジェネリクスが使える何かの具体的な型に落とし込む手法です。SteinsKit に関していうと、`Variable<Value>` を外側には `AnyObservable<Value>` という型で公開[^observable exposure]する時に、内部で用いた手法で、実装は[こちら](https://github.com/el-hoshino/SteinsKit/blob/v0.1.1/SteinsKit/Observations/AnyObservable.swift)から確認できます。また、購読する側も、購読している変数は `Variable` なのか `LazyVariable` なのかも意識すべきではないと思います。

[^observable exposure]: もちろん `Variable` としてそのまま公開するのも、`Observable` としては使えますが、問題は `Variable` には外側に公開したくないメソッド（`accept` などの、保持する側が使うメソッド）も公開されてしまうのでよろしくないです

型消去するにはいくつかの方法がありますが、SteinsKit では `AnySequence` の実装にも使われている継承 Box の方法で実装しています。この方法の詳しい実装の仕方は [omochimetaru さんの記事](https://qiita.com/omochimetaru/items/5d26b95eb21e022106f0#type-erasure-継承-box-方式)で書かれていますので、ここでは簡単にだけ紹介します：

継承 box は 3 つの型が必要です：まずは外側に公開するための `AnyObservable<Value>` はもちろん必要ですが、内部にはさらに `AnyObservableBox` という内包の抽象型と、この抽象型を継承した `ObservableBox` という具象型が必要です。継承が必要ですので Box の 2 つは `class` ですが、型消去の `AnyObservable` は `class` でも `struct` でも構いません、筆者は実行効率のために `struct` の方を選びましたが[^value type]。

[^value type]: `struct` は値型ですが、そもそも内包している `AnyObservableBox` は `class` で参照型ですので、代入による大元の本体のインスタンスの増加はありませんので心配する必要はありません

さて継承 Box の面白いところは、`class` の継承によるジェネリクスの上書きを使っているところです。まず、大元の抽象クラスである `AnyObservableBox` のジェネリクスは `Observable` の関連型であり、`Variable` のジェネリクスでもある `<Value>` です。しかし、`AnyObservableBox` は内包するプロパティーもなければ、具体的な実装もありません。さらには `Observable` を継承もしていません。ただし `protocol Observable` として宣言した全てのメソッドの「宣言」だけはしてあります、中身の実装は全て `fatalError` で落としていますが。

次にこの `AnyObservableBox` を継承して作った具象クラス `ObservableBox` ですが、今回ジェネリクスが `<O: Observable>` という、`Observable` を準拠した `<O>` に変わりました。この `<O>` は `ObservableBox` がプロパティーとして持つ `base` の型になるので、つまり `ObservableBox` に `Variable` や `LazyVariable` のインスタンスを渡せば `base` として内包されます。そして親クラスから継承した全てのメソッドは、オーバーライドして全てこの `base` に渡してしまいます。ただし `<O>` は `<Value>` ではないので、ジェネリクスも変わって、`ObservableBox<O>` は `AnyObservableBox<O.Value>` の継承です。この `<O.Value>` というジェネリクスの上書きが、継承 Box による型消去のキモです。

最後は外側に公開する `AnyObservable` ですが、これはもちろんジェネリクスが `<Value>` になります。`<Value>` ですので、同じジェネリクスを使っている `AnyObservableBox` を自分自身の内包するオブジェクト `base` の型になりますが、しかし実際内包しているオブジェクトは `AnyObservableBox` を継承した `ObservableBox` のインスタンスです。これでさらに `AnyObservable` を `Observable` として準拠し、`Observable` の全てのメソッドを `base` にそのまま渡せば、`Observable` の正体を隠したまま `<Value>` のジェネリクスが使えるようになります。

## メタプログラミング

メタプログラミングとは、実際のプログラムのコードをそのまま書くのではなく、何かのテンプレートを書いて機械でプログラムのコードを吐き出してもらう手法です。通常のプログラムと違って、同じようなコードを大量に量産できるので非常に便利です。

同じく使い方の章でも出てきていますが、 `Variable` と `LazyVariable` の 2 種類の変数箱があります。彼らはほぼほぼ同じようなものでして、違いは単純に片方が初期値が必ずかるがもう片方は初期値がなくても作れるということと、それによって片方は確実に最新の値があるがもう片方は必ずしも最新の値がないだけの区別です。この微妙の動作の違いを実現するには基本 3 通りの方法があります：

1. そのまま手で両方実装します。メリットは特に思い当たりません、デメリットは書くの面倒くさいのと、DRY 原則に違反し、将来保守するときにどっちかの不一致が生じる可能性があります。

2. `class` で継承を使って作ります。メリットは `Variable` を継承して `LazyVariable` が必要な動作を実装すれば大抵のものは 2 度手間なしに実装できるので DRY 原則も守りやすいですが、デメリットは `Variable` の実装でサブクラスである `LazyVariable` を常に考慮する必要があるので、アプローチとしては間違っているし、`Variable` に不必要な Optional が入ってしまうのでスマートとも言い難いです

3. メタプログラミングでそれぞれの違いをスクリプトで書き分けます。これも実際の SteinsKit が取り入れてる方法です。メリットは DRY 原則がきちんと守られて継続開発による情報の不一致が生じないのと、継承による不必要な考慮がないためスマートで安全なコードが実現されます、デメリットはメタプログラミングはやはりハードルが少々高めです。

SteinsKit ではメタプログラミングの実現に [Sourcery](https://github.com/krzysztofzablocki/Sourcery) を利用しています。今回の場合は、`Variable` と `LazyVariable` を同じ [`Variable` というテンプレート](https://github.com/el-hoshino/SteinsKit/blob/v0.1.1/SteinsKit/Variables/Variable.stencil)で生成しています。

Sourcery は [Stencil](https://github.com/stencilproject/Stencil) というテンプレートファイルを使ってコード生成します。Stencil の基本的な使い方は、テンプレートファイルで特殊な文法構文がなければ書いたものをそのまま生成します；特殊な文法構文があったらその構文に応じて処理します。Stencil の文法構文は 3 種類あります：`{{` `}}` で囲む変数構文、`{%` `%}` で囲む制御構文、そして `{#` `#}` で囲むコメント構文です。

まず変数構文について紹介します、これは非常に単純で、変数構文は要するにこの変数の値をそのまま生成ファイルに書き出すものです、例えば `a` という変数があって、この変数の値が `Qiita` なら、テンプレートファイルで `{{ a }}` を書けば、生成ファイルに `Qiita` が吐き出されます。

次にちょっと飛ばしてコメント構文です、これも非常に単純で何を書いてもただの読み手用のコメントですので何も吐き出されません。例えばテンプレートファイルで `{# これはコメントです #}` と書いても、生成ファイルには何も出てきません。

最後は戻って制御構文についてですが、これは少し複雑になります。Stencil には制御のための演算子やタグがたくさんあります、例えばループ演算用の `for` タグ、条件分岐用の `if` タグ、別のテンプレートファイルを導入する `include` タグ、比較に使う `==` や `>` 演算子などなどがあります。例えばテンプレートファイルで下記のようなコードを書いてみます：

```stencil
{% for i in 1...5 %}
  {% if i == 3 %}
// ٩( ᐛ )و
  {% else %}
// {{ i }}
  {% endif %}
{% endfor %}
```

これでは生成ファイルには下記のようなコードが吐き出されます：

```swift
// 1
// 2
// ٩( ᐛ )و
// 4
// 5
```

`Variable` と `LazyVariable` の実装で、まずは Stencil で `["Variable", "LazyVariable"]` の配列を持つ `variableTypes` という変数を定義します。独自変数の定義は `{% set variableTypes "Variable,LazyVariable"|split:"," %}` で実現しています。これは `"Variable,LazyVariable"` という文字列を `","` で分割して配列にし、`variableTypes` という変数に値として保存するという命令です。これができたら、次は `{% for type in variableTypes %}` だけで、`Variable` 型と `LazyVariable` の型をループで実装できます。そして実装するときに、`Variable` と `LazyVariable` で違う実装が必要なときに、`{% if type|contains:"Lazy" %}` で、これは `LazyVariable` なのか、それとも普通の `Variable` なのかを判別できます。

テンプレートファイルを書いたら、次は Sourcery の設定をしないといけません。まずは `sourcery` コマンドを打つディレクトリー（基本は `.xcodeproj` もしくは `.xcworkspace` ファイルと同じディレクトリー）で設定ファイル `.sourcery.xml` を作る必要があります[^sourcery xml]。このファイルではプロジェクトファイルのパス（もしくは全てのソースファイルのパス）、テンプレートファイルのパス、そして出力するディレクトリーを書いておけば最低限の情報が揃います。[SteinsKit の設定ファイル](https://github.com/el-hoshino/SteinsKit/blob/v0.1.1/.sourcery.yml)はこのように書かれています：

[^sourcery xml]: `$ sourcery --option parameter` の感じで使うことも可能ですが、設定の一覧ができた方が後から保守もしやすいので、設定ファイルを作って `$ sourcery` だけ打った方がいいと筆者は思います

```xml:.sourcery.xml
project:
  file: SteinsKit.xcodeproj
  target:
    name: SteinsKit

templates:
  - SteinsKit/Variables/Variable.stencil

output:
  SteinsKit/Variables
```

ただし注意しないといけないのは実はこの出力ディレクトリーの書き方はあまりよろしくないです。テンプレートファイルは複数設定できますが、出力ディレクトリーの設定は一つだけです。これはすなわち全てのテンプレートはその出力ディレクトリーにファイルが出力されます。ですのでどっちかというと、テンプレート依存しない出力ディレクトリーの方が望ましいでしょう。

そして、このように設定ファイルが作れたら、最後は Build Phases に行って、*Compile Sources* の前に Sourcery を動かすフェーズを追加すればいいです。

Sourcery についての詳しい情報は [Sourcery のドキュメント](https://cdn.rawgit.com/krzysztofzablocki/Sourcery/master/docs/index.html)でご確認できます。

# この名前はどういう意味？

特に意味はない。