94
90

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

名前空間を利用した Swift 開発

Last updated at Posted at 2015-12-22

これはなに

Embedded Framework を使えば Swift で名前空間を利用できます。
それによりレイヤーを意識した設計になりクリーンな開発をしていきましょう。

また、今日書こうと思っていたネタが別のアドベントカレンダーの1日目に書かれていたのでリンクを貼ります。まとまっていて大変良い記事です。
http://qiita.com/mono0926/items/e29cd17789fd1d1548aa

MVC? DDD?

さて、アプリの設計の話です。
スピード重視・可読性無視の個人開発ならば設計などない方が早く完成するし、自分が全部知っているので保守もできます。
しかしチーム開発となると設計は重要なファクターになってきます。
自分と考え方の違う大勢の開発者が同時に開発を進めていくため、ルール決めが必要になります。
コード規約や Pull Request のレビューの仕方、継続的インテグレーションなどがそれらのルールに当てはまるでしょう。
そして実際に書くコード。どこに何が記述されていて、どの部分に影響するのか。それをわかりやすくするために先人たちは様々な手法を考案してきました。

iPhone アプリの開発を考えてみましょう。ユーザへ入出力を行う View があり、View を出し分ける ViewController があり、その ViewController は API を叩いたり CoreData へアクセスしたりする…
これらの操作をどのように切り分けるか。
それが Model-View-Controller だったり、 Domain Driven Development だったりするわけです。もちろん他にもあります。

私自身もつい最近、仕事で iPhone アプリの設計を一から考える機会があったので様々な手法を調べました。
その結果、「DDD で、レイヤードアーキテクチャにする」ということで やっていくという気持ち で開発を始めました。

この記事は DDD でレイヤードアーキテクチャの場合に使える Embedded Framework の話です。

DDD

DDD、と聞いてなんだか怖いなあ、とか糖質制限をするのかなあ、とか思う人もいるとは思いますが、私もそうでした。
実践DDD 本を読んだり、エリックエバンス本を読んだりと、普段はインターネットで調べて済ます私が本を読みました。

DDD、ドメイン駆動開発、とは、ある関心ごとを一単位としてコードを切り分けて開発していくこと、とあります。
ありがちなメッセンジャーアプリを例にとると、「ログイン」「ユーザ」「メッセージ」などがドメインにあたります。要は機能ですね。

そして、エリックエバンス本ではアプリケーションを以下の4層に分ける例があります。

  • User Interface
  • Application
  • Domain
  • Infrastructure

私はこれにもう一層加えました。"Environment層" です。

  • User Interface
  • Application
  • Domain
  • Infrastructure
  • Environment

このように層を分け、その中でドメインを分割して開発を行うというルールにしました。

レイヤードアーキテクチャ

なぜもう一層加えたかというと、レイヤードアーキテクチャを使いたかったからです。
レイヤードアーキテクチャというのは、層(レイヤ)同士の参照関係が決まっているものを言います。
DDD の概念だけだと、例えば User Interface層 が Domain層 を直接見てオブジェクトを取ってきても問題ありません。
しかしレイヤードアーキテクチャはそれを許しません。
それぞれの層は一つ下の層しか参照できない
というのがルールです。
そうすると、「アプリ全体で使いたい値」「環境によって異なる値」をどこが持つかということになるので、それを Environment 層に振り分けました。
Environment 層はすべての層から参照されますが、どこの層も参照しません。
いい加減わかりづらいので図に表すと以下になります。
時間がなくて手書きになってしまい、かなりお見苦しい点をご承知ください。。

ddd.png

これはユーザが非同期で入力を行った際にどのようなフローになるかという図です。
赤矢印が参照で、青矢印がそのコールバックになります。
古典的なレイヤードアーキテクチャですが、私が開発する規模だとこれで十分であろうという見通しの元進めました。
それぞれの層がどのような役割を担うのか書きます。

UserInterface層

ユーザからの入力を受け取ったり、ユーザに何か表示したりする層。ユーザはこの層しか見ません。
ViewController や View がそれに相当します。
いわゆる「ロジック」と呼ばれるものはここには書きません。
ここでは ViewController は View を出し分けるのに徹して、入力を受け付けたら下の層に丸投げします。
そうすることで肥大化しがちな ViewController をスリムにすることを心がけます。

Application層

iPhone アプリはみんな便利なものを作りたいと思うので、同じ操作をいろんな画面からできるようにすると思います。
それをまとめるのがここ。
Domain層 と UserInterface層の橋渡しをするところです。Domain層から受け取ったフクザツで大量のデータを整理して UI が本当に必要な形に直すところです。
なので Application層 は他と比べて薄い層になります。「ロジック」はありません。

Domain層

最も重要な部分です。「ロジック」です。
外界から受け取ったデータを変換したり、追加したり、捨てたり、組み合わせたりします。
DDD ではここのエンティティのライフサイクルを考えるのがキモだとあります。

Infrastructure層

外の世界と通信をします。
具体的には HTTP 通信をしたり、データベースにアクセスをしたりします。
AlamofireRealm といった便利ライブラリはここにいることになります。
そうして外の世界から受け取った JSON Data などを例えば SwiftyJSONArgo などで変換するのが役割です。

Environment層

アプリ全体で使う値、例えばロガーの sharedInstance。
また Config を切り替えると値が変わるもの、例えば API の向き先(開発環境・ステージング環境・本番環境)などはここに書いておきます。

この5層で iPhone アプリ開発ができるように Xcode の設定をしていきます。

Embedded Framework の作成

さて、それぞれの層で Embedded Framework、つまりターゲットを追加していきます。

スクリーンショット 2015-12-22 4.18.39.png

まずこれが Project を作成した初期段階です。
TARGETS の下にある + ボタンを押してフレームワークを作成します。

スクリーンショット 2015-12-22 4.18.59.png

名前をそれぞれの層の名前にします。(Unit Test をそれぞれの層ごとに作るかは好みなんですかね)

スクリーンショット 2015-12-22 4.19.19.png

スクリーンショット 2015-12-22 4.19.36.png

同様に5つフレームワークを作り、ディレクトリを整理しました。

スクリーンショット 2015-12-22 4.22.52.png

Xcode 上でのディレクトリ構成と物理的なディレクトリ構成は異なるので、ここで synx を使って同期させておくと GitHub などで見た時に見やすいです。

$ synx DDDSample.xcodeproj

スクリーンショット 2015-12-22 4.26.14.png

層間の参照関係の作成

さて、これでターゲットが層ごとに分かれました。これから層間の参照関係を設定していきます。

まず Application 層に User 構造体を作ります。
Targets に Application を指定することを忘れないようにしましょう。後から変更できますが。

スクリーンショット 2015-12-22 4.41.00.png

とりあえず中身を書きます。
ここでアクセス修飾子が重要になります。

  • public ターゲットの外から見える
  • internal 同じターゲットから見える
  • private そのファイルでしか見えない

です。適切にアクセスレベルをコントロールしましょう。

さて、Application.User は以下です。

swift
// Application
public struct User {
    public let id: Int
    public init(id: Int) {
        self.id = id
    }
}

UI層からこれにアクセスするには、 import Application と書きます。書かないとダメです。
この import 文が必須になるということが Embedded Framework の強みです。
他の層にアクセスするのに import 文での明示が必要」ということです。
これによりそれぞれの層は見られるものが限定され、それぞれの層をクリーンに保つことができます。

swift
import UIKit
import Application                    // これがないと User 構造体が見れない

// UI
class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        let user = User(id: 3)
        print(user.id)                // 3
    }
}

さて、Domain層でもユーザを扱うことになると思いますが、Domain層でも User 構造体を作ることは可能です。これが名前空間の力です。
DDD だと Application と Domain は特に同じ名前で作りたいことが多いかと思いますが層ごとにフレームワークを分ける利点がここで活きてきます。

swift
// Domain
public struct User {
    public let identifier: Int
    public init(id: Int) {
        identifier = id
    }
}

これでも、UI層はちゃんと Application.User を見てくれるので安心です。

swift
import UIKit
import Application                    // これがないと User 構造体が見れない

// UI
class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        let user = User(id: 3)        // Application.User
        print(user.id)                // 3
        print(user.identifier)        // エラー。Application.User だということがわかる
    }
}

また、現実ではありえませんが UI層 が Application層 と Domain層 両方を見てしまった時はもちろんどちらの User も見れます。

swift
import UIKit
import Application
import Domain

// UI
class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        let user = User(id: 3)                 // エラー。どちらの User かわからない
        let appUser = Application.User(id: 3)
        print(appUser.id)                      // 3
        let domUser = Domain.User(id: 4)
        print(domUser.identifier)              // 4
    }
}

名前空間を明示すればいいだけです。
実際はこんなめちゃくちゃな参照をしないようにレビューなどで弾いていきましょう。

実践編

実際に開発する上で良かったことや困ったことを書きます。

困ったこと

AppDelegate をどこに置く?

だいたいのクラスはそれぞれの層に分かれますがアプリのコアになる AppDelegate 、これはメインターゲットである必要があります。
メインターゲットは開発する場所ではないので、メインターゲット+どこか ということになりますが、アプリケーションにすべきか、UI にすべきかという議論がありました。最初は Application にいたのですが、(Storyboard を呼ぶとはいえ)rootViewController を指定していること、Application のメソッドをいろいろ呼びたいこと、という理由から UserInterface になりました。

DI をどうする?

DDD には Dependency Injection(依存性の注入)というものがあります。別の層へアクセスするときは別の層との依存性をなるべく減らしたいので、インターフェース(Swift の Protocol)を参照するようにするためのものです。
最初は Environment層 で注入を行いたかったのですが、それを行うには Environment層 がすべての層を参照する必要があり、また Environment層 はすべての層から参照されるので相互参照になってしまいダメでした。
最終的には各層が DIContainer+Application というように DIコンテナを持ち、「DIコンテナは例外だ!」とすることでやっていけています。
DI とレイヤードアーキテクチャはあまり相性が良くないようですが、そこはすべてが理想的に行くとは思わずに割り切ることも大切かと思います。

導入コストが高い

最初は少人数での開発でしたが、それでも導入コストは高かったです。
「DDDをやるぞ!」と言っても「なんでそんな大層なことやる必要があるの?」とか「Rx を導入するぞ!」と言っても「なんだそれは…よくわからんから関係ないところだけやる」と言われたり…
これについては簡単で、「やっていくという気持ち」が大事でした。仮にメンバがついてこれなくなったとしても自分で責任を持って全部やる、という気持ちです。もちろんその気持ちは今も持ち続けていますし、そもそも Objective-C でなくて Swift を選択した時からこの気持ちは持ち続けています。
DDD、レイヤードアーキテクチャといった今まで現場になかった概念は受け入れられがたいものではありましたが利点を説明したり自分で実装してみせたりと布教活動が大事です。これは自分場合だけでなく多く当てはまるんじゃないかなと思っています。新規開発だと特に参考になるコードも自分で作っていかないといけないので最初は大変ですが例を作れればそれをお手本にやってもらうだけなのでうまくいきます。
また自分としても DDD の基本的な理解に一週間ほどかかりその間に開発が本格的に始まってしまわないかドキドキしたものです。
DDD はまだ全然理解できておらず、日々勉強が続いています。

良かったこと

クリーンな構造(ただしApplication以下)

ユーザのアクションというのはいつ行われるのかわかりません。
非同期で予期せぬ行動もあるかと思います。
そのイベントのキューイングを UI 層でうまく収めることで、Applicaiton 以下の層はユーザのインタラクションを気にせずに処理を進めることができます。
またそれの実現のために RxSwift を採用しました。
まあ PromiseKit でも 素の closure でもなんでも良いのですが、処理が追いやすく見やすいということは大切なことです。

作業分担の明確化

これを実践する前までは一人が View から Model まで作るようになっておりコンフリクトも多く作業待ちも発生していたのですが、レイヤードアーキテクチャにしたことで「君は UI を作って。俺がロジック書くから。」という分担に変わりました。あらかじめインターフェースだけ切っておけばどちらもお互いの作業の進捗を気にすることなく作業に取りかかれるし、まずコンフリクトしません(ファイル追加での xcodeproj/projectpbx のコンフリクトはしょっちゅうですが)。
また、それぞれの層でやることが少なく、決まった動作をするので後からプロジェクトに参加するメンバがいた時も参考になるコードがポツリポツリと書かれているのでコードリーディングに頭を悩ませる頻度が減ったかと思います(もちろん初めての Swift や Rx の概念の理解は大変ですが)。

まとめ

  • Swift の Embedded Framework のおかげで名前空間を利用した
  • 「やっていくという気持ち」が大切
  • DDD 難しいので日々勉強である

以上です。ありがとうございました。

参考リンク

94
90
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
94
90

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?