自己紹介
初めまして!!!マッチングエージェント社のタップルiOSチームでインターンをしている東原秀亮です。今までは主にUnityやiOSを中心にエンジニアとしての経験を積んできました。
今回、サイバーエージェントのインターンに採用して頂き、1ヶ月という期間で、いくつかの改善、開発を行いました。
その中でもちょうどタップルのアーキテクチャを新しくしようということで、アプリのリアーキテクチャに関するタスクに関わることができました。
今までMVC的なプロジェクトにしか関わったことがなく、アーキテクチャの設計から参加できる&新たな技術に触れられるタスクにモチベーションが上がったので、今回はそのことを記事にしたいと思い筆をとりました。
タップルの開発歴史
最初にタップルのアプリについてなのですが、2013年から提供開始したアプリです。
当時はObjective-Cで書いていたそうです。
そんなアプリもSwift化に踏み切っており、現在Objective-Cはほとんど残っていません。(Swift化の経緯)
これからさらなるアプリの進化のためにどうしていくかという課題がある状態です。
やる意義、達成したいこと
そもそもどうしてモダン技術、アーキテクチャを採用する必要があるのでしょうか?
単純に技術的好奇心だけで採用してしまうのはプロダクトにとってはプラスではありません。
極論モダン技術を採用するメリットがないのであれば、Objective-Cだけで開発したほうが変わらない技術で開発できてプロダクトにより貢献できるかもしれません。
アプリをリアーキテクチャしていくことのメリットとしては、
- 改善の早さ、施策の数を増やせる
- 安定性を高める
- スケールしやすくなる
- エンジニアの採用力強化
以上のような点が挙げられます。
逆に上記のプラスを満たせるような設計を採用していく必要があります。
設計ミーティング
設計はエンジニアの皆でミーティングして決めます。インターン生の僕も参加しました。アプリの設計を話し合うというのが初めてだったので戸惑いもありましたが、今まで行っていた会社ではどうだったか思い出しながら臨みました。
議論の内容としてはクリーンアーキテクチャなのかFluxなのかという点が最初に話し合われました。
よくあるMVCやMVVMといったものよりも責務を厳密に分けたいというのが上記を検討した理由です。
結局MVVM + Fluxのようなアーキテクチャを採用する方針に決まりました。
そちらのほうが画面間の情報の受け渡しを安全に行いやすいという理由です。
加えてタップルはマッチングアプリなので複雑に状態を持ち合うことが多く、Fluxの方が状態を管理しやすいという理由もありました。
※ 実際ミーティングで使われたホワイトボード(MVVM + Fluxを検討しています)
ここで先ほど挙げたリアーキテクチャしていくメリットをもう一度確認してみます。
- 改善の速さ、回せる施策の数を増やせる
➡ 今の設計より責務を分割し、より変更しやすくなりそうです。
- 安定性を高める
➡ 前述の責務分割で満たせそうです。
- スケールしやすくなる
➡ 定期的に書き換えることで、そのとき向かおうとしている方向を踏まえた設計にできます。
- エンジニアの採用力強化
➡ Rxも積極的に使っていくのでRxバリバリ書けるような学習意欲高めな人にもアピール出来ます。
実際に運用、計測してみないと厳密には分かりませんが、プロダクトに貢献できる施策になりそうです。
実際に設計ミーティングに参加してみて、インターン生もいちエンジニアとして扱ってもらえる環境にあるなと思いました。もちろんプロダクトに参加したばかりなので疑問点がないかなどのフォローはメンターの方やチームの方が手厚くしてくれて学びがあります。しかし、こういう場に普通に参加できるのはプレッシャーもありつつ、エンジニアとしての視座が上がっていくので楽しかったです。こういう場で議論を引っ張っていけるレベルまで早く到達したいなと思い、新たな目標が出来ました。
Fluxの説明
具体的な設計の説明に移りたいと思います。
まずそもそものFluxなのですが、以下の図で表すことができます。
(引用元)
Viewが何かしらのインプットを受け取ったときにactionに通知します。
actionはdispatcherを用いてstoreに状態を通知します。
View側の更新をするときはstoreの値を見て処理をするという流れになります。
直接書き換えたいところに通知するのではなく、view -> action -> store -> viewという流れになるのが特徴です。
今回はMVVM + FluxということでViewとactionの間にViewModelを挟みます。
こうすることでView側にはViewの処理だけを書く事ができます。
実装例
今回実装したものの大枠だけ解説していきたいと思います。
シンプルなのでこちらを参考に他のプロジェクトでも応用してもらえたらうれしいです。
(公開できる部分だけの紹介になるのでコピペでは動かないです)
ViewController
class ViewController: UIViewController: UITableViewDataSource {
@IBOutlet private weak var tableView: UITableView! {
didSet {
tableView.registerNib(classType: TableViewCell.self)
}
}
private var viewModel = SettingLicenseViewModel()
private var bag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
tableView.delegate = self
tableView.dataSource = self
addObserver()
viewModel.load()
}
private func addObserver() {
viewModel.responses
.subscribe(onNext: { [weak self] _ in
self?.tableView.reloadData()
})
.disposed(by: bag)
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewModel.responsesValue.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(classType: TableViewCell.self, for: indexPath)
cell.prepare(repo: viewModel.responsesValue[indexPath.row])
return cell
}
}
基本的にはViewに関する処理だけ書いていきます。ViewController自身がデータを持つことはなく、ViewModel側から持ってきたものを利用します。
Rxをガンガン使っていきたいところですね!
ViewModel
final class ViewModel {
private let action = Action()
private let store = Store()
private let bag = DisposeBag()
var responses: Observable<[Response]> {
return self.store.list.asObservable()
}
var responsesValue: [Response] {
return self.store.list.value
}
func load() {
self.action.load()
}
}
ViewModelがstoreとactionを保持します。
storeの値を監視しています。
Viewに関係ない処理があれば、こちらに書いていきます。
Action
final class Action: Actionable {
enum Atype: ActionType {
case load([Response])
case error(Error)
}
let dispatcher: Dispatcher
let repo: Repository
let bag = DisposeBag()
init(dispatcher: Dispatcher = .default, repo: Repository = RepositoryImpl()) {
self.dispatcher = dispatcher
self.repo = repo
}
func load() {
repo.load()
.subscribe(
onNext: { [weak self] (res) in
self?.dispatcher.dispatch(type: Atype.load(res))
},
onError: { [weak self] (error) in
self?.dispatcher.dispatch(type: Atype.error(error))
}
)
.disposed(by: bag)
}
}
dispatcherというstoreに状態を更新するためのオブジェクトを持つのが特徴です。dispatcher.dispatchで実際の更新を行っています。
Atypeというのはこのdispatchで送るときにどんな種類の通知かを表しています。
この種類に応じて行う処理を分ける事ができます。
ちなみにdipatchの中身ですが、 後ほど出てくるregisterでtypeと処理を登録し、dispatchの部分でtypeに該当するactionを実行するという中身になっています。
Store
final class Store: Storable {
let dispatcher: Dispatcher
private(set) var list = BehaviorRelay<[response]>(value: [])
private let _error = PublishSubject()
var error: Observable {
return _error.asObservable()
}
init(dispatcher: Dispatcher = .default) {
self.dispatcher = dispatcher
self.dispatcher.register(store: self) { [weak self] (type: Action.Atype) in
switch type {
case .load(let condition):
self?.list.accept(condition)
case .error(let error):
self?._error.onNext(error)
}
}
}
}
registerの部分でtypeに応じて行いたい処理を書いていきます。
今回はlistの値を書き換えています。
ViewModel側でこの値を監視しているのでdispatchされたタイミングでViewModel側に通知がいくのがわかります。
実装は以上になります。
store, actionの部分が特に難しかった印象です。学習コストは少し高いですが、責務が明確に別れているため自分が今書いているレイヤーに集中しやすい、状態更新をstoreを介してやるので安心して実装できる点がメリットだと思います。
僕もまだまだ習熟できているとは言えないので、より理解を深めていきたいです。
やって感じたこと
メリットとしてはFluxでうまく書けると、それぞれのレイヤーの処理に集中できます。
viewにはviewの処理だけ書いてあるのを見るととても気持ちが良いです。
でも慣れてないと時間が掛かるなという印象です。
学生の身で新しいアーキテクチャを学ぶ機会というのは中々なかったので技術的に貴重な学びになりました。加えて、新しい環境でもついていけたのは同じチームの先輩方の存在が大きかったです。質問にも快く答えていただき、とても明るいチームで楽しく仕事ができました。
そういった環境の中でもっとプロダクトに対して意見できるようになりたい、デザイン面での知識をつけたいといった目標も出てきました。エンジニアとしての実力もつけつつ、上記の点でも結果を出せるエンジニアになりたいと思っています。
最後になりますが、暖かくチームに迎えてくださったマッチングエージェント社の方々、丁寧に指導してくださったiOSチームの先輩方、ありがとうございました!