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

CHANGELOG.mdを書き始めた

雑記

チビチビ開発しているライブラリの1.0.0をリリースしたのを機にCHANGELOGというものを書き始めた。その際にCHANGELOGについて調べていた。

そもそもCHANGELOGは何のために必要なのか考えてみた。CHANGELOGは各バージョンの変更点をざっくり把握するためにあると思う。例えば、Railsの変更点を見たいと思ったとき、まず最初にCHANGELOGを見ると思う。いきなりPull requestやコミットログは見ないだろう。それらは各変更の実装や議論といった詳細を見るのに使われると思う。CHANGELOGは変更の詳細ではなく大まかな変更点の一覧を把握するために使われるんじゃないだろうか。

CHANGELOGを構成する要素はなんだろうか。まず、バージョンとそこに含まれる変更点が挙げられる。そして、変更点の詳細が載ったページへのリンクがあると便利だと思う。変更の種類によってグルーピングするとより見やすくなると思う。よく見かけるのはAdded, Changed, Fixed, Removed, Deprecatedなどといったラベルで変更点をわけている。リリース前の変更点についても書いておくと、今後の変更予定が分かって便利だと思う。

書き方としては、変更点を上から追記していくスタイルと、バージョンごとに書き換えるスタイルがあると思う。歴史の長いソフトウェアの場合、ひとつのファイルにそれらをすべて載せるのは見にくいので、後者のやり方が合うんじゃないかと思う。Railsなんかはこのスタイルだったと思う。

以上のようなことを踏まえて、このようなフォーマットに至った。

# Change Log

## Unreleased

### Added
* `changed(year:month:day:hour:minute:second:nanosecond:)`, which creates a `Date` instance by changing receiver's date components. [#77](https://github.com/naoty/Timepiece/pull/77)
* `changed(weekday:)`, which creates a `Date` instance by changing receiver's weekday. [#77](https://github.com/naoty/Timepiece/pull/77)

## 1.0.2
Released on 2016-12-20.

### Fixed
* Fix testDateInISO8601Format() availability. [#74](https://github.com/naoty/Timepiece/pull/74).
* Specify Swift version for the compilation of watchOS target. [#79](https://github.com/naoty/Timepiece/pull/79).

iOSアプリの開発でもCHANGELOGを書くようになった。CHANGELOGの読者としては、開発者自身もそうなんだけど、ベータ版を配信するテスターを主に想定している。ベータ版配信では、fastlaneからFabricを使って配信しているんだけど、その際にCHANGELOG.mdをパースしてリリースノートを自動生成するようにしている。このパーサーはrubygemとして公開している。

2016年振り返り

今年書いたコード

  • 仕事のiOSアプリを0からSwiftで書いた。
  • Timepiece: Swift 3に対応し、1.0.0をリリースした。
  • cocoapodsにcontributeした。
  • AnyQuery: Swiftでのリポジトリパターンを設計していた。
  • clr: Swiftのアプリ開発以外のユースケースを見つけた。
  • ABKit: なんとなくA/Bテストツールを作ってみた。

感想

今年はiOSの一年だった。新卒以来、久々に仕事でiOSアプリをフルスクラッチで書いてリリースした。SwiftとiOSを勉強し、コードをたくさん書いた。

プライベートでは、ずっと続いてるTimepieceをメンテナンスしていた。try! SwiftでTimepieceを使ってくれている海外の開発者と会い、自信がついた。

来年は、Timepieceのメンテナンスを粛々と続けつつ、仕事で関わっているプロジェクトの成功を第一に考えていきたい。

コード署名・証明書

Objective-C/Swift/iOS

iOS開発で頭を悩ます問題のひとつに、コード署名や証明書の問題がある。Code Signing Errorなどのような単語で検索すると、どのように問題を解決するのか、どのような手順で証明書を発行しXcodeに設定するのか、といった情報がたくさん返ってくる。しかし、コード署名とは何か、証明書とは何か、といった根本的な疑問に答えているサイトは少ないように思える。コード署名や証明書はチーム開発において致命的な問題につながることもあり、十分な理解の上で慎重に運用すべきものであると思う。

そこで、僕はこちらの本を読み、署名や証明書といったものを理解した。

暗号技術入門 第3版 秘密の国のアリス

暗号技術入門 第3版 秘密の国のアリス

本書ではiOS開発における署名や証明書についての記述はないが、自分の中でiOS開発におきかえて読んでいた。そこで得られた理解をここに書いておこうと思う。

まずコード署名とは、証明書を使ってアプリケーションにデジタル署名を施すことを指している。デジタル署名とは秘密鍵を使ってメッセージに施された署名のことで、署名は秘密鍵の対となる公開鍵を使って検証することができる。デジタル署名によって、メッセージが署名を施した人物のものであること、またメッセージが改ざんされていないことを保証することができる。

iOSアプリケーション開発の文脈では、開発したアプリケーションがAppleによって認証された開発者によるものであること、そして開発したアプリケーションが改ざんされていないことをコード署名によって保証していることになる。

一方、証明書とは認証局によってデジタル署名が施された公開鍵のことである。証明書を使わずに公開鍵の受け渡しを行う場合、man-in-the-middle攻撃という手法でなりすましされる危険性がある。攻撃者が公開鍵の受け渡しの間に入り、攻撃者の公開鍵を代わりに受け渡すという手口だ。こうした危険性を排除するため、公開鍵自体に認証情報が必要となる。そこで、認証局が電話番号などさまざまな情報を使って公開鍵を発行した主体を認証し、証明書を発行する。

iOSアプリケーション開発の文脈では、キーチェーンアクセス.appでCSRファイルというものを作成している。このファイルはコード署名の検証に利用する公開鍵と公開鍵を発行する主体の認証情報を含んでいる。このファイルをMember CenterにアップロードすることでAppleから証明書をダウンロードすることができる。

アプリケーションをApp Storeにアップロードする際、開発者は自身の秘密鍵を使ってコード署名を行い、Appleはアップロードされたアプリケーションを証明書に含まれる公開鍵で検証する。

エンジニア立ち居振る舞い:生産性を計測する

雑記

お題「エンジニア立ち居振舞い」

僕は開発以外の立ち居振る舞いとして、いくつかのツールと習慣によって開発の生産性を計測している。

自分のチームではスクラムを採用していて、各タスクにはストーリーポイントが割り振られている。相対的な作業量のようなものだ。また、個人的にポモドーロテクニックを採用している。25分開発したら5分休憩する周期を1ポモドーロと呼んで、そのリズムを繰り返すヤツです。

毎日、完了したタスクのストーリーポイントと開発に費やしたポモドーロを計測している。Google Spreadsheetに書いている。やっていることはそれだけ。

そうすることで見えてくることがいくつかある。まずは、曜日ごとの開発できる時間だ。会議が多い曜日はせいぜい4ポモドーロだなーとか、リモートワークできる曜日はこれくらいだなーとか。曜日ごとのポモドーロの平均をSpreadsheetで計算して見ている。そして、これは見積もりのときに利用できる。来スプリントに開発できるポモドーロを平均値から見積もれる。

あとは、完了したタスクのストーリーポイントとポモドーロから、1ポモドーロあたりのストーリーポイントが分かる。自分のなかでは、この値を生産性の指標として考えてる。なんとなくグラフにしたりして、生産性を上げるモチベーションにしている。

一番良いことは、上で話した「来スプリントのポモドーロの見積もり」と「1ポモドーロあたりのストーリーポイント」から「来スプリントで完了できるストーリーポイントの見積もり」が計算できることだ。この計算に基づいて、来スプリントでこなすタスク量を決めている。

実績に基づいた見積もりができるようになったおかげで、マネージャー側からは安定したスケジュールが組めるようになるし、開発側からは無理のない仕事ができるようになる。さらに、無理のない仕事ができるようになると、無理のない生活ができるようになる。このことは最近結婚した僕にとって一番重要なことなのだ。

Timepiece 1.0.0をリリースした

Objective-C/Swift/iOS

最初のバージョンはSwiftが出た2014年だった。それ以降、細かな機能追加やバグ修正を繰り返し、0.5.0でSwift 2.3、0.6.0でSwift 3に対応した。どこかのタイミングで多くのStar、issueがつくようになり、多くのユーザーが使ってくれるようになった。

そして昨日、ようやく安定版となる1.0.0をリリースした。1.0.0では破壊的な変更を行った。

メソッド名の変更

// 0.6.0
Date.date(year: 2016, month: 10, day: 31)
"2016-10-31T12:00:00+0900".dateFromFormat("yyyy-MM-dd'T'HH:mm:ssZ")

// 1.0.0
Date(year: 2016, month: 10, day: 31)
"2016-10-31T12:00:00+0900".date(inFormat: "yyyy-MM-dd'T'HH:mm:ssZ")

よりSwiftらしく、またSwift 3の命名規則に沿ったものに変更した。

タイムゾーンのサポートをやめた

個別の日付オブジェクトについてタイムゾーンをサポートするようにしていたが、バグになりかねない部分であり、後述するメンテナンスコストを下げていく方針に合わないため、この機能を削除した。

Durationの内部表現を変更した

これまでのバージョンでは、期間を表す概念としてDurationというstructを用意していたが、これをやめた。本来、期間を表す概念としてDateComponentsというものがあるため(これまでDateComponentsが期間を表すということをちゃんと認識してなかった)、これを最大限利用した。この変更によって、1.0.0では新たに以下のような計算や処理が可能になった。

// 1.0.0
Date() + (3.hours - 30.minutes)
(3.hours - 30.minutes).string(in: .abbreviated) //=> "2h 30m"

固定フォーマットでの出力をやめた

// 0.6.0
1.weeks.later.stringFromFormat("yyyy/MM/dd")

上のようなメソッドをやめた。DateFormatterで簡単に実装できるが、この機能は暦やロケールを考慮していない実装なのであまりオススメできない。その代わりにDateFormatter.Styleを指定して出力できるようにした。

// 1.0.0
1.weeks.later.dateString(in: .short)

望ましい機能は実装しやすくする。望ましくない機能は実装しにくくする。というのが理想的なAPIインターフェイスであるように思う。

変更の背景

OSSにかけられる時間が減った。メンテナンスコストを下げたかった。バグが出やすい機能はできるだけ削った。機能が豊富な日付操作ライブラリは他にもある。そんな中、最小限の機能でバグがなく正確で少し読めば何をしているか分かるようなライブラリが理想的だとおもった。

CocoaPodsにコントリビュートした

Objective-C/Swift/iOS Ruby/Rails

開発中のiOSアプリでCocoaPodsでインストールしたライブラリのライセンス表示を実装する際に、とある理由でライセンスに表示したくない状況があった。いろいろ調べたところ、CocoaPodsが出力するPods-{ProjectName}-Acknowledgements.plistMITといったライセンスタイプが含まれていないことがわかった(ライセンスのテキストはあるけど、そこから抽出するのは大変)。podspecにはライセンスタイプを記載する必要があるため、内部表現としてライセンスタイプをもっているはずだと思った。そこで、それをplistファイルに出力するようにするPull requestを送って、そしてmergeされた。

Pull requestしてみた感想としては、RSpecのようなよく知らないテスティングフレームワークを使っており、自力ではどこをテストすればいいのか分からず困惑した。コミッターの方が修正してくれたようなのでよかった。

1.1.0.beta.1に含まれているので、今後はMITなどライセンスタイプを基に表示するライブラリをフィルタリングできたりできると思う。よかったら利用してください。

通信周りの処理をミドルウェアで整理する

Objective-C/Swift/iOS

課題感

APIリクエストの送信前、APIレスポンスの取得後にさまざまな処理をはさみたいことがある。例えば、こんな処理だ。

  • ネットワークインジケータの表示・非表示
  • リクエストとレスポンスのロギング
  • 二重送信の防止
  • ログイントークンが有効期限切れだったときに、リフレッシュトークンを使ってログイントークンを更新した後、再送
  • HTTPリクエストのスタブ

ただ、こういった処理をAPIクライアントにそのまま実装していくとAPIクライアントが肥大化するし、かと言ってViewControllerに実装するといろんな箇所で似たようなコードを書くことになる。

解決策

APIクライアントをラップして機能を拡張するミドルウェアをつくる。ミドルウェアAPIクライアントを呼び出して通信処理を実行しつつ、リクエストの送信前とレスポンスの取得後に処理をはさむ。

例えば、APIClientというオブジェクトで本来の通信処理を実行するとする。ロギングを行うミドルウェアはこんな感じになる。

extension Middleware {
    struct Logger: RequestSendable {
        let client: RequestSendable

        func send<T: RequestType>(request: T) -> Task<Void, T.Response, ErrorType> {
            print(request)
            return client.send(request)
                .success { response -> Task<Void, T.Response, ErrorType> in
                    print(response)
                    return Task(value: response)
                }
                .failure { error, _ in
                    print(error)
                    return Task(error: error ?? ApplicationError.Unknown)
                }
        }
    }
}

そして、こんな感じで初期化する。

let client: RequestSendable = Middleware.Logger(client: APIClient())

だけど、ミドルウェアが増えると、以下のように初期化が大変になってくる。

let client: RequestSendable = A(client: B(client: C(client: D(client: APIClient()))))

そこで、ミドルウェア群を簡単に組み合わせるための仕組みをつくる。

extension Middleware {
    struct Stack {
        let middlewareTypes: [RequestSendable.Type]

        init(_ middlewareTypes: [RequestSendable.Type]) {
            self.middlewareTypes = middlewareTypes
        }

        func buildClient() -> RequestSendable {
            let client = APIClient()
            return middlewareTypes.reverse().reduce(client) { (result: RequestSendable, middlewareType: RequestSendable.Type) in
                return middlewareType.init(client: result)
            }
        }
    }
}

これによって、こんな感じで直感的にAPIクライアントを初期化できる。

let client = Middleware.Stack([A.self, B.self, C.self, D.self]).buildClient()

たいていの場合、利用するミドルウェアは同じなのでデフォルトで利用するミドルウェアスタックを簡単に初期化できるようにする。

extension Middleware {
    struct Stack {
        // ...

        static func defaultStack() -> Stack {
            var middlewares: [RequestSendable.Type] = []

            middlewares.append(A.self)

            if someCondition {
                middlewares.append(B.self)
            }

            middlewares.append(C.self)

            reeturn Stack(middlewares)
        }
    }
}

そして、APIクライアントの初期化はこうなる。

let client = Middleware.Stack.defaultStack().buildClient()

まとめ

通信周りのさまざまな処理をミドルウェアという形で実装することで、疎結合なモジュールに分離することができた。将来的に新たな処理を追加する場合でもミドルウェアを新たに実装してスタックに追加するだけでよく、既存のAPIクライアントやミドルウェアに手を加える必要はない。テスト時のみ不要なミドルウェアを除くといった柔軟な設定も可能になるだろう。