ミュータブルな部分をできるだけ避けようとすると思いますが、しかし、セッターを一度も呼ばずにどのように物事を進めていけるでしょうか? ユニットテストを書くと思いますが、しかし、ユーザのインタラクションはどのようにテストすればいいのでしょうか? 可読性の高いコードを書こうとすると思いますが、しかし、どのようにして巨大な ViewController を分解していけるでしょうか?
Andy Matuschak は Khan Academy の iOS 開発者です。そして、以前は iOS 4.1 ~ 8.0 の UIKit チームに属していました。今回の発表は「状態をもつシステムでの複雑性のコントロールの仕方」についての発表となります!
複雑性について (00:00)
複雑性は非常に幅の広いトピックとなります。しかし、非常に重要な用語でもあります。なぜなら、私たちは少し前まではなかったソフトウェアを作っているからです。あるソフトウェアが完成したとします。しかし、その要求は1週間後や次のリリースサイクルで変更されたり、顧客から変更があったりします。変更と複雑性はいつも対立しあっています。なぜなら、複雑性というのはいつでも変更を加えるのを難しくします。
経験 vs 能力 (1:13)
時に、好みや能力の食い違いから問題が起こります。いつも私たちは、自分たちが今までに書いてきたコードを読みます。そして、これはひどく、もろく、いつかは壊れるだろうと思ったりすることがあるかと思います。しかし、それを修正する能力がなかったらどうでしょうか? 特に今日は Swift での複雑性という観点から、もう少しこの辺りについてでできることがないか見ていきましょう。
考えていくこと… (1:50)
- システムで通る経路: チームの同僚にシステムについて説明するとき、ホワイトボードなどを使い樹形図を書いて説明するときがあると思います。その図は、アプリの通る道筋を表しており、その道筋についてより注意深く考えていく方法について説明します。
- コントロールフロー: メソッドやコードのブロックについて考えるとき、それが何通り実行のされ方があるか考える必要があると思います。それらはオブジェクトのオーナーによって、呼ばれるか、または、他の通知やタイマーによって呼ばれます。そのメソッドにはどれくらい経路がありますか?
- 依存する経路: コードまたはインターフェイスについて考えるとき、それらがやり取りする要素がどれくらいあるのか考えると思います。今書いてるソフトウェアの構成の依存関係のグラフを書いたとき、綺麗な木になりますか? それとも、散らかった木になりますか?
- 影響する範囲: これは、アプリの中でツリー操作のようなものです。あるメソッドについて考えたとき、それは他のシステムにどれくらい影響しますか? そして、その影響は十分に信頼できるものですか?
アプリにおける様々な経路や以上のようなことを考えることにより、新しい思いつきがあったり、より理解が深まったりすると思います。
値型 (03:57)
アプリケーションの中にある複雑性を取り除くためのある方法があります。Swift では、値型を使うことがそれを行う簡単な方法です。値型とは数値型や String 型などで普段からとても馴染みがあるものです。
1. 値型の価値 (04:30)
なぜ Swift では新しく値型が追加されたのでしょうか? Objective-C はオブジェクト指向言語です。今までの典型的なやり方では新しいモジュールを追加するときは、新しいクラスが追加されます。しかし、Swift の標準ライブラリの中を見てみると、以下のような結果です。
$ grep -e "^struct " stdlib.swift | wc -l
78
$ grep -e "^enum " stdlib.swift | wc -l
8
Swift の標準ライブラリではほとんど全てが値型となっています。そして、これはかなり強力なものになります。すべての型で変数に割り当てられる時にコピーされます。また、所有しているオーナーは一人だけです。 Objective-C でも数値型と Boolean 型は値型です。しかし、String 型や Array 型や Dictionary 型などは違います。以下のコードは、1, 2, 3 を持った Array を作成しています。そして、新しい変数 B に割り当て、4 を追加しています。しかし、A はそのままです。これは Swift での新しい振る舞いです。Swift では構造体や Enum もこのような振る舞いをします。
var a = [1, 2, 3]
var b = a
// a = [1, 2, 3]; b = [1, 2, 3]
b.append(4)
// a = [1, 2, 3]; b = [1, 2, 3, 4]
その他のものとして、参照型があります。参照型を、複数の変数に割り当てると同じオブジェクトを共有することになります。参照型では、オーナーが複数になる時があり、これらはオブジェクトを持ちます。UIView を作り、それを他の変数に割り当て、alpha の値を変更したとします。ここでは、両方の変数が同じ UIView を参照してることになります。それらは同じインスタンスを共有しています。Swift でのクロージャも同様に参照型となります。
var a = UIView()
var b = a
// a.alpha = 1; b.alpha = 1
b.alpha = 0
// a.alpha = 0; b.alpha = 0
直感的に (07:06)
オブジェクトはまさしく現実のオブジェクトのようです。たとえば、現実の世界に Fido という名前の犬がいるとします。その犬は走ったり、寝転んだりします。そして、私が Fido がとったおかしな行動の話をしたとします、そうすると、みなさんはきっと同じ犬について頭の中で想像することかと思います。こういった具合に、そのオブジェクトを正確に表す、名前やラベルのみを伝えていくこととなります。オブジェクトとは、その名前の受け渡しを行っているようなもので、ポインタを理解しているなら馴染みがあるものだと思います。
それとは対照に値型はデータのようなものです。たとえば、私がみなさんにスプレッドシートを送り、みなさんが合計や平均を計算したりするとします。そして、私が家に帰りそのスプレッドシートに変更を加えたとしても、あなたたちのスプレッドシートには何も影響しません。スプレッドシートは単なるスプレッドシートで、そこにある数字もただの数字です。値型というのはそれと同じようなものです。それを非力だと捉える方もおられるかもしれませんが、時にその非力なものが便利に感じる時もあるのです。値型の簡潔さはとても素晴らしいです。なぜなら、それを使うときに余計なことを考える必要が無くなるからです。
Swift の値型では、かなり簡単に様々な表現ができます。構造体にメソッドを持たせたり、Enum を含む値型なども定義することができます。以下の Tool 型のようなネストした型も定義できます。この場合 Enum は DrawingAction 型の子となります。また Enum ではそれぞれに値を含めることができます。Tool の場合は、color
を持った Pencil か width
を持った Eraser どちらかになります。ここで、覚えておくべき重要なことは、Swift はこれらを想定して作られたということです。先ほど見たように Swift 標準ライブラリでも様々な場面で使われています。
struct Drawing {
var actions: [DrawingAction]
}
struct DrawingAction {
var samples: [TouchSample]
var tool: Tool
enum Tool {
case Pencil(color: UIColor)
case Eraser(width: CGFloat)
}
}
struct TouchSample {
var location: CGPoint
var timestamp: NSTimeInterval
}
なぜ値型は重要なのか? 3つの “I” について (09:53)
-
Inert (不動) (10:09)
値型に振る舞いを持たせることは難しいです。スプレッドシートのデータなどに対して保存や計算をするメソッドについてです。コントロールフローは値型のオーナーによって厳密にコントロールされることになります。値型のオーナーが一つであり呼び出し元が特定なことで、プログラムがより簡単になります。それとは対照的なのがビューのセッ���アップです。以下の MyView には
numberOfBloops
プロパティがあります。numberOfBloops
を変更したときに、center.x
の位置が変わるアニメーションが動きます。そして、アニメーションが完了したときに、center.x
が 0 に戻ります。しかし、ここで疑問が出てきます。もしアニメーションが動いている時にcenter.x
に変更があったらどうなるでしょうか? また、連続でこのメソッドが呼ばれたときはどうなるでしょうか? 実装からはどういう挙動をするかは明らかではありません。構造体のような、値型では、このようなコードは書けません。なぜなら、アニメーションのタイマーシステムに構造体の参照を渡す必要があり、そういったことはできないからです。この値型の非力さは、���ーナーシップが一つであるところから来ています。
class MyView {
var numberOfBloops: Int {
didSet {
UIView.animateWithDuration(1, animations: {
center.x = numberOfBloops * 10
}, completion: { _ in
center.x = 0
})
}
}
}
-
Isolated (独立性) (12:09)
参照型では、暗黙的にアプリケーションの構造上での依存ができます。そして、それははっきりとは分かりません。 以下の例では、参照型の User 型のモデルがあります。ViewController
はuser
をプロパティとして持っていて、変更を加えていきます。そして、NetworkOperation
はuser
とともにインスタンス化します。これらは全て、同じ Userクラスのインスタンスを参照しています。NetworkOperation
が作られるときViewController
から Userインスタンスが渡され作成されたので、これらは全て、同じ Userクラスのインスタンスを参照していることになります。ここで、NetworkOperation
はViewController
の振る舞いに暗黙的な依存関係が生まれました。これは、たとえば、他のチームメートがこのViewController
にいくつかのメソッドを追加した場合、NetworkOperation
の振る舞いに影響してきます。しかし、それはコードからはあまりはっきりとはわかりません。
class User { ... }
class ViewController {
var user: User
}
class NetworkOperation {
init(user: User)
}
-
Interchangeable (交換可能) (13:46)
そして最後は、値型は交換可能です。Fido のことを思い浮かべる人がおかしくならない限り Fido 自体は変わらないという点に戻って考えてみましょう。値型が新しい変数に割り当てらときその値はコピーされアサインされます。そして、そのコピーされたものは全て交換可能です。もしそれらが全く同じデータである場合、それらに違いは何もありません。これは、安全にデータを取り替えれることを意味します。交換可能であることは、どのように値ができるのかは重要ではなく、equal で比較したときに等しければ同じであることを意味するのです。これは、UITouch のインスタンスを作れないことと関連があります。UITouch を引数として期待しているところにそれを渡すと、同じデータであったとしても、同じことが必ず起きるとは限りません。このことは UITouch でできることを制限しています。データを保存することもできないし、ネットワークで受け渡ししたりすることもできません。複数のユーザーが同時にお絵描きをするアプリを作っている場合、それらを入れ替えたりすることができないので、多くのことはできないのです。自由自在に扱うことができないことを意味します。
struct Drawing {
var actions: [DrawingAction]
}
struct DrawingAction {
var samples: [TouchSample]
var tool: Tool
enum Tool {
case Pencil(color: UIColor)
case Eraser(width: CGFloat)
}
}
struct TouchSample {
var location: CGPoint
var timestamp: NSTimeInterval
}
ユニットテストの観点から言うと、UI のユニットテストというものは以前から本当に難しいものです。一つの理由として API は、交換可能でないからです。単純なデータで入力/出力のテストを行うとき、モックやスタブをする必要はありません。値型なら関数にそれを渡し、その結果を確認することでできます。
これらの3つの I はなぜ値型がそんなに便利かについてでした。値型は不動で、独立していて、交換可能だからです。しかし、すべての UI の部品はこのように振る舞いません。そして、これらのことを参考にしながら考えていきましょう。値型の場合は、これらの特性があることを前提にプログラムが書けます。しかし、参照型の場合、これらについて勝手に決め込むことはできません。
2. オブジェクトである目的 (21:25)
しかし、必ずしもすべてのプログラムを値型で書き換えるべきだと言ってるわけではありません。システムの中では、オブジェクトの方が良い部分もあります。オブジェクトには振る舞いと反応があります。気をつけるべきことは、それとは対照に値型は死んでいるということです。これらのバランスが重要になり、システムの中でステーブルな部分をどこにするか考える必要があります。また、これのたとえとして私がよく使うのは “回転のぞき絵” です。回転のぞき絵は静止画が動いているように見える伝統的なアニメーションの装置です。絵の外側は何も変わりません、しかし、スリットの中を覗き込んでみるとあたかもその静止画が動いているように見えるのです。たくさんの死んだもので一つの生きているものを表現しているようです。
このモデルでは、値は不動です。しかし、システムの中の値の一連の集まりから一つのアイデンティティを持つものを作り出すことができます。描画の例に戻ってみると、キャンバスの概念があり、そこに書き込みが行っていきます。currentDrawing
は値型で表され、一つのインスタンスはある状態のスナップショットとなります。ここでのアイデンティは、値の一連の流れとなり、それぞれの値はその瞬間のスナップショットとなります。currentDrawing
は現在の描画しているものか最後に描画されたものになっていると思います。Rich Hickey の言葉を借りると、値型を不動なデータとして考えることができます。そして、それは気軽にくっつけて新しいものを作ることができます。そして、各状態をわかりやすくするために時間の一連の流れの中で、それぞれにラベルを付けることができるのです。
class CanvasController {
var currentDrawing: Drawing
/* ... */
}
struct Drawing { /* ... */ }
オブジェクトの場合、このキャンバスの中ではどのように当てはまるでしょうか? Swift のオブジェクトはデフォルトでアイデンティを持っています。複数の場所で変数に割り当てると、それらは同じものを共有して指すようになります。先ほどアプリでやったような、複数の状態を持つピクチャを作るのにオブジェクトを使います。状態の遷移が管理でき、各要素が振舞っているようなサイドエフェクトを表現できます。それらは不動ではなく、使い方は不変でないと考える必要があります。以下はどのように currentDrawing
を扱っていくかのコードとなります。ViewControlelr である “CanvasController” があり、構造体の Drawing は値として機能します。描画の位置を変えた時に、Drawing
の mutating メソッドを呼ぶかに関わらず、また新しい drawing を割り当てたに関わらず、値の不動性や死んでいる状態というのは常に成り立っているのです。たとえ、CanvasController が複数の場所からイベントを受けとり、変更を加え続けたとしても、先ほどの3つの不動、独立、交換可能であることは、Drawing
で保ち続けます。
class CanvasController {
var currentDrawing: Drawing
func handleTouch(touch: UITouch) {
currentDrawing = ...
}
}
3. 全てのものはふさわしい場所がある (27:19)
3つの “I” についてとどのように一連の値型を使って、一つのアイデンティティを作るかについて見た後は、今度はどこでこれらが使えるでしょうか? 私の提案としては、プログラムを2つのレイヤーで分けます。オブジェクトとレイヤーと値型のレイヤーです。オブジェクトレイヤーは、全てオブジェクトで構成します。そのレイヤーは、イベントのやりとりをしたり、値型のレイヤーとやりとりをしたりする役割で薄い板のようにしておきます。そして、アプリのビジネスロジックは全て値型のレイヤーに描いていきます。もしこの方法で考えると、オブジェクトのレイヤーは薄くなり、値型のレイヤーがアプリケーションの大部分を占めることになると思います。
どうして分離しますか? オブジェクト指向プログラミングの一つの信条として、アクションとロジックを分離することがあります。ここで紹介した方法は、これを達成するとても良い方法です。Drawing の例に戻ってみると、Drawing プログラムで実装しなければいけないことがいくつかあります。 - タッチ感知し描画に変換する - フィルターなどを使ってタッチの速度を計算する - 60Hz で値を取っている場合など、それをスムーズな曲線に変換する - 速度やタイミングやその他のことに依存しているストロークの形を計算する - 描画する
今までだったら UIView を使用する方法が一般的だったと思いますが、それよりもよりよい方法を提案します。以下のようなタッチの速度を計算する関数は、値型のレイヤーに書く関数です。これは、値型のタッチのサンプルを受け取り、速度を表すポイントを返す関数となります。そして、パスをスムーズな曲線にするためにベージュ曲線を利用している関数などもあります。これらは問題なくテストすることができ、他のシステムから独立しています。
// estimate touch velocity
extension TouchSample {
static func estimateVelocities(samples: [TouchSample])
-> [CGPoint]
}
// smooth touch sample curves
extension TouchSample {
static func smoothTouchSamples(samples: [TouchSample]) -> [TouchSample]
}
// compute stroke geometry
struct PencilBrush {
func pathForDrawingAction(action: DrawingAction)
-> UIBezierPath
}
// incorporate touch into drawing (+ update state)
extension Drawing {
mutating func appendTouchSample(sample: TouchSample)
}
今回紹介したことは、私たちのアプリのコアで作った機能です。それらをきちんと機能するコードを書き、分離して書くことが大事になります。 私が言いたいのは、できるだけオブジェクトで構成する部分を薄く作り、コアの部分をしっかりと作るべきだということです。今回説明した方法は、オブジェクト指向を普段よく使う人に馴染みがあることで、関数型プログラミングの人にはあまり馴染みがないことかもしれまんせん。
参考 (34:57)
- Boundaries - Gary Bernhardt
- Are We There Yet? - Rich Hickey
- SICP - Abelson & Sussman
- Enemy of the State: ビデオ & スライド - Justin Spahr-Summers
Q&A (35:55)
Q: 値型について話すときミュータブル性やイミュータブル性について何か前提する仮定がありましたか?
Andy: 特にそれといった前提する仮定はなかったです。なぜなら、Swift では、値型もミュータブルとして働きます。Swift で値型に変更を加えるとき、値型に変更を加えているわけではありません。しかし、変数の値は変わっています。本当の意味では、値型を変えることはできません。
Q: “値型である価値” のセクションのイミュータブルな値とは対照となるような構造体や enum についてもう少し説明していただけますか?
Andy: 参照型のオブジェクトを作ったとすれば、システムの他の部分にも参照を渡していくことになります。イミュータブルなフィールドだけをもつオブジェクトを使って行うと、構造体を使ってやるよりも不動性や独立性や交換可能性に欠けることになります。C や C++ や Objective-C では、構造体や enum にはいくつかパフォーマンスに影響がある部分があると言えます。通常 C で構造体を作ったとき、スタックに作成され、コピーされ渡されます。Obj-C や C でイミュータブルな参照を作った場合、ヒープで領域を確保し作成することになり、目的がわかりにくくなります。それにパフォーマンスに少し影響があります。Swift ではこのような影響はありません、Swift では構造体は必ずスタックにあるわけではなく、オブジェクトも必ずしもヒープにあるとは限りません、これらの違いがなくなったと言えます。
Q: 値型に移行するメリットについてお話しされましたが、何かデメリットはありますか?
Andy: Swift はまだできたばかりの言語です。なので、はっきりいってそこまで最適化がなされていません。たくさん構造体を作り、それをあらゆるところで使ったり、新しい構造体を古い構造体からコピーして作るなどしていると、今はパフォーマンスが悪くなると思います。しかし、Swift は今後の最適化のことを考えて作られた言語です。スタックに確保された値型がその参照によってやりとりされたり、今しているようなコピーを避けれるようになると思います。Array は値型のコンテナであるので、Array は少しこれを避けるのが難しいです。新しい変数を作りそれを Array に割り当て、それに変更を加えます。そして、それをコピーすると、Arrayの裏側で共有されることになると思います。その部分は明らかにもう少し最適化がされるべき部分であります。
Q: 値型としてデータモデルを定義している場合、何か変更がある度に毎回再描画することになると思います。これを行う何か効率的なやり方を知っていますか?
Andy: React という Web フレームワークがあります。その考え方は、データを元に生成した要素があり、それは DOM ツリーのようなものですが、実際は DOM ツリーを表現したものに過ぎません。React ライブラリは、要素を返し、変更を加える部分だけの diff を適用することが可能です。実際の Web ページの DOM に変更するのはとてもコストがかかることです。
Q: ときどき値型に変更を加える mutating 関数にするか、新しい値を返す関数にするべきかどうか判断することが難しい場合はあります。それについて何か良いガイドラインはありますか?
Andy: 新しい値を返す関数か変更を加えるかの大きな違いは、交換可能かと扱いやすいかどうかです。もし定数があったとして、コピーすることなしにはこれを使うことができません。そのような点で変更を加える mutating 関数は少し扱いづらい点があります。
Q: 値型に変更するべき箇所はどのようにして割り出せば良いですか?
Andy: 値型のものを一度、ボックスの中に置いてみてください。ここでいうボックスというのはプロパティで値型を持つクラスです。複数のオーナーで共有したい場合は、そのボックスを共有するようにしてください。また、他のところではそのボックスを使わないでみてください。そうすると、どこが安全でどこが安全でないのか正確に理解できることかと思います。
Q: コードを混ぜることについてはどう思っていますか? Swift に移行していると思いますが、あなたの視点から見て、互換性などについてどう思いますか?
Andy: 実際のところは私は楽観的に考えすぎていました。しかし、先ほどお話ししたやり方を現在すでにあるプロジェクトに適用するのはとても難しいことだと思います。経験からすると、大きなコードがあり、このアプローチを取ることを考えているなら、チームメンバーが同意���ているかどうか確認する必要があると思います。なぜなら、とてもコストがかかることだからです。
Q: どのように依存関係を取り除きますか? モデルレイヤーを値型にすることはかなり良い方法のように感じますが、しかし、おそらくそのレイヤーはアプリの他の部分と依存していたり Obj-C です。
Andy: 私たちの経験では、以前 CoreData を扱うレイヤーがありました。明らかに CoreData は独立性とはかけ離れたものです。私たちがとった方法は、クエリを実行したり値型を返したりする CoreData のレイヤーを間に挟むことにしました。ビューのレイヤーで値型を使おうとすると実際、たくさんのことをしなければいけません。たとえば、アイコンやラベルを描画するために TableView のセルがあるとします。セルにコンテンツを表示させるために ManagedObject をセルに渡すのが一般的なやり方だと思います。そこで、ビューが必要とする値型を提供するレイヤーを作りました。ビューのオーナーはビューに表示するものをそのレイヤーで計算し、ビューに渡します。その計算を行い、ビューにデータに渡すレイヤーを私たちはプレゼンタと呼んでいます。
Q: MVVM についてはどう思いますか?
Andy: MVVM が答えではありません。MMVM では、モデルレイヤーとプレゼンテーションデータとの間に ビューモデル と呼ばれるレイヤーがあります。そして、それらが不動性を持つ部分だと思います。ネットワークリクエストを投げたりそういったことです。これら二つのことを混ぜることには賛成できません。
Q: 今説明にあったことを違うスキルセットや能力のメンバーと上手くやっていこうとしたときに、どのようにすれば良いと思いますか?
Andy: 値型のレイヤーに書くものなどについてのルールを定めた場合、他のチームメンバーが間違いをしにくくなると思います。型システムである場合、彼らが要件を推測するのがより簡単になります。文化的に、かなりチャレンジングなことだと思います。 また、私の他の発表で Functioning as a Functionalist というものがあり、こちらでどのように企業文化にの改善に取り組んでいくかについて触れています。
Q: ジェネリクスを使うことについて何か Tips などありますか? たとえば、ジェネリクスを持った関数があるとき、オブジェクトか値型どちらにすべきだと思いますか? そのあたりについて何かオススメできることはありますか?
Andy: ジェネリクスを使う一番のメリットはそれが何か考える必要がなくなることです。これは Obj-C ではできなかったことです。また、すぐに実感するメリットは Array を使うときだと思います。私は、ジェネリクスで必ずしも値型を使えとオススメするのは少しためらいがあります。ジェネリクスの興味深い例は、テーブルビューのデータソースを書いたときです。Swift での Json のデコードなどでも皆さん使うと思います。
About the content
This content has been published here with the express permission of the author.