タマネギプログラマーの雑記

たまねぎ剣士的なニュアンス

フロー効率とリソース効率について思うこと

最近、フロー効率、リソース効率という言葉をよく聞くようになってきた。
業界でどの程度流行っているのかは知らないが、少なくとも@i2key御大将の近くのコミュニティに属している関係で、僕は日常的によく聞く。
リソース効率に偏りがちなシステム開発の環境において、フロー効率という考え方が広まっていることは単純に良いことだと思う。

一方で、フロー効率がバズワード的になるにつれて、正しく理解されていないのではないかと思える声もしばしば聞くようになった。
曰く、「リソース効率は悪でフロー効率こそが目指すべき姿」であるとか、「フロー効率こそが価値を最大限に発揮できる方法」であるとかそういう声である。
これはリソース効率というコンセプトの理解として正しくない。

もっとも、これは僕の観測範囲での話なので、実際そう勘違いしている人は少ないのかもしれない。
しかし、もしかしたら多数いるそういう人達へ向けて、そうでなくとも一回自分の考えをまとめる意味も込めて、フロー効率とリソース効率について文章を書いてみる。

但し書き

  • 僕は学生時代の知識から、工程管理的なアプローチでフロー効率とリソース効率について書いていく
  • 僕はリーンやXPの専門家ではない。「こういう解釈もある」程度に思うのがおすすめ
  • フロー効率を銀の弾丸だと思っている人をメインの対象にしている。そうでない人には、焼き直しになって退屈かもしれない
  • この記事は、Recruit Engineers Advent Calendar 201710日目の記事である

フロー効率とリソース効率

まず、フロー効率とリソース効率という言葉についておさらいしてみたいと思う。
これは正直、前述の@i2keyさんのこの記事を読んでしまうのが一番はやいと思う。

簡単に一言で表すならば

フロー効率: それぞれのアイテムが最も効率よく工程を進むことにフィーチャーすること
リソース効率: それぞれのリソースが最大限稼働することにフィーチャーすること

と言える。

とどのつまりこの二つは、何の効率に焦点を当てて考えるかというコンセプトの違いを表す言葉である。

フロー効率は正義なのか

上記の@i2keyさんの記事を読めば、「フロー効率はとても良いものだ」という印象を持つ人が多いだろう。
それ自体は問題ないのだが、「良いものだ」と思いすぎてしまう場合これはまずい。
この記事はとてもいい記事なのだが、バイアスがかかりやすい箇所が多少ある。
例えば、こちらの図などは誤解を生みやすい。

https://cdn-ak.f.st-hatena.com/images/fotolife/i/i2key/20170514/20170514231345.jpg 引用: (http://i2key.hateblo.jp/entry/2017/05/15/082655)

この図だけを見た人は必ずこう思うはずである。
「全てのアイテムが同じ期間で完了するのなら、トータルのリードタイムが短くなるフロー効率の方が良いに決まっているじゃないか」と。
しかし、実際には多くの場合で、フロー効率の方が全てのアイテムが完了するタイミングは遅くなる。
フロー効率側は大体においてオーバーヘッドが発生するため、一つ一つのアイテムが完了するのにかかる時間が長くなってしまうからだ。
@i2keyさんがこの記事の内容を口頭で説明するときには補足してくれるのだが、記事内では充分ではない。

フロー効率に集中するということは、一部のリソースを遊ばせてでもあるアイテムのリードタイムを短くするということである。
そのため、リソース効率に比べオーバーヘッドが発生しやすい。 具体的な例を下に示す。

完了に1単位時間かかる行程が3つずつあるアイテムA・Bを、作業者A・Bの二人で完了させる場合を考える。
ただし、アイテムAの工程2は、同アイテムの工程1が完了しない限り取り掛かれないという制約があるものとする。
フロー効率、リソース効率、それぞれのアプローチで工程を組むと以下のようになる。

f:id:gaoxin-xixxix:20171206231634p:plain
フロー効率時

f:id:gaoxin-xixxix:20171206231843p:plain
リソース効率時

これを見ると、フロー効率の場合、アイテムAのリードタイムは短くなるが、全てのアイテムの完了が遅くなることがわかる。

フロー効率では、個々のアイテムのリードタイムは減るが、フローを優先するために、発揮できる総生産量が低くなってしまうのである。
一方リソース効率は、個々のアイテムのリードタイムは増えるが、極力リソースを遊ばせないため、発揮できる総生産量が高くなる。

最も、アイテムAの工程間に制約がなければフロー効率でも3単位時間で全てのアイテムは完了するのだが、実際の開発においては多くの場合こういった制約がついて回るはずだ。

このように、基本的には、組織の総生産量の観点で有利なのはリソース効率の方である。

ではフロー効率はいけないのか

上で、組織の総生産量の文脈ではリソース効率の方が優れているという話をした。
しかし、だからと言ってフロー効率がいけないというわけではない。
この文章の主旨はフロー効率disではなく、それぞれの特性を理解し、正しく使うことが大事だということである。

もう一度それぞれの特性をまとめてみよう。

フロー効率: あるアイテムのリードタイムを短くすることができるが、総生産量の面で不利
リソース効率: 組織の総生産量の最大化に有利であるが、個々のアイテムのリードタイムの面で不利

こうして見ると、総生産量とリードタイムのコンテクストにおいては、きれいにトレードオフになることがわかる。
では、それぞれはどのような場合に適材なのだろうか。

フロー効率は、任意のアイテムのリードタイムを短くできる。
ということは、仮説検証型の開発にもってこいと言える。
一回のリリースサイクルのリードタイムを短くすることで、仮説検証の試行回数を増やすことができるからだ。
イテレーティブな開発では、完了しなければならないアイテムの総数は一般的に決まっていないため、総生産量はある程度犠牲にしても良い。

リソース効率は、フィードバックを入れて方向転換することには弱いが、予め決められたアイテム群をより早く完了することができる。
よって、いわゆるプロジェクト型の開発に向いていると言える。
プロジェクト型の開発では、プロジェクト完了までアイテムが価値を発揮する必要がないため、個々のリードタイムの長さは問題にならない。

じゃあGoogleとかはどうなの?

こういう話をすると、フロー効率擁護のニュアンスで 「それでもGoogleは、週の20%をフリーにすることで生産性を上げた」や、「稼働率100%のCPUはまともに動かない」といった意見を言ってくる人がたまにいるが、これは詭弁ないしは勘違いである。
というのも、上記2例は本質的にフロー効率とリソース効率とは違う文脈の話だからだ。

(僕が考えるに)フロー効率とリソース効率という言葉は、工程設計の戦略を表す言葉である。
工程設計とはある種の制約充足問題であって、個々のリソースの稼働率は一定であるという前提が必要である。
そのため、フロー効率的なアプローチでも、リソースである人間は制約上可能な限り最大限に稼働することが前提となる。
100%の稼働による人間の生産性の低下は、議論の埒外の話なのである。

それはどちらかというと、リソースとしての人間の特性の話であり、これは人間工学などの方が対象分野としては近いだろう。
そういった点は、フロー効率だろうとリソース効率だろうと、一日の工数を低めに設定することで実現を目指すことができる。

まとめ

リソース効率は銀の弾丸ではなく、利点と欠点を持った一つのアプローチにすぎない。
トレードオフを理解し、適材適所で使う事が大事だ。
もちろん、理想的な状況は、リードタイムが短くかつ総生産量も損ねない状況であるため、フロー効率的な工程でリソースが遊ばないような工程設計ができるのが一番良い。
しかし、現実には制約があり、そのような理想的な工程設計は多くの場合できない。
そのため、自分たちが置かれた状況を理解し、より良い手段を選択しなければならないのである。

WKWebViewのログイン保持

iOSでWebページを表示するために使用するWKWebView
今回、WKWebViewでのログイン保持でハマったためワークアラウンドを書いておく

今回想定するアプリ

f:id:gaoxin-xixxix:20171004234507p:plain

一度webviewに行った後、webview内でログインを行う。 その後もう一度viewに戻り、再度webviewを開く。
この時、ログインが保持されていないという現象にあった。

どうしてこうなる

どうもセッションcookieをWKWebViewがストアしておかないことによるものらしい。

nsmutableurlrequest - Losing cookies in WKWebView - Stack Overflow

iOS WKWebView Tips | Professional Programmer

必要なcookieを調べて、WKWebViewから抜いて直接NSHTTPCookieStorageに突っ込めたりしないかと考えたが、そもそもWKWebViewからセッションcookieを抜くことができないっぽい。

解決策

解決策というか、ワークアラウンドなのだが、なんならバッドノウハウかもしれないので、ご意見があったらうかがいたいのだが。 今回は、上記二つ目のリンクでも述べられているWKProcessPoolを活用することにした。

WKProcessPoolとは、複数のwebview間でWeb Content Processを共有するためのものだ。

WKProcessPool - WebKit | Apple Developer Documentation

これを活用すると、cookieが複数のwebviewで(確かめてないから本当かわからないが)リアルタイムに共有されるらしい。
が、リアルタイムというのは今回はあまり重要ではない。
今回重要なのはWeb Content Processが共有されるという点だ。
つまり、同じWKProcessPoolを使うwebview間であれば、その存在が時系列上ずれていたとしてもセッションcookieを共有できるはずと考えた。
なので

extension WKProcessPool {  
    static let shared = WKProcessPool() 
}  
  
class ViewController: UIViewController {  
    override func viewDidLoad() {  
        let configuration = WKWebViewConfiguration()  
        configuration.processPool = WKProcessPool.shared
        let webview = WKWebView(frame: .zero, configuration: configuration)  
#中略  
    }  
}  

というようにしてやると良い。
複数のViewControllerで使わないのであれば、WKProcessPoolはシングルトンっぽくしなくても、propertyとしてもってやればいいだろう。

ここで注意しなければいけないのは、WKWebViewConfigurationはWKWebViewのinitializerに渡してやらなければならないという点だ。

let webview = WKWebView()
webview.configuration.processPool = WKProcessPool.shared

のようにやった場合はうまくいかなかった。
今度時間がある時にWKWebViewのコードを呼んで理由を探そうと思う。

RxWebKitがいい感じだったよ

RxSwiftの関連ライブラリーを調べていたところRxWebKitなるものを発見。

github.com

要はWebKitのRxSwiftラッパーです。
いい感じにWKWebViewを扱えてとても楽しい! 結構素敵な感じに使えたので、シンプルなサンプルアプリを作ってみます。

導入

まずは普通に

pod 'RxWebKit'

ってな感じにしてpod install。 Carthageでもできますが、例のごとく割愛いたします。

普通にViewを構築

WKWebViewはコードで追加するので、タイトルやロードビュー、進む戻るボタンなどをテキトーに配置します。 f:id:gaoxin-xixxix:20170531215605p:plain

IBOutletでViewControllerに引っ張ってきます。

import UIKit
import WebKit

class ViewController: UIViewController {
    @IBOutlet weak var progressbar: UIProgressView!
    @IBOutlet weak var refreshButton: UIBarButtonItem!
    @IBOutlet weak var forwardButton: UIBarButtonItem!
    @IBOutlet weak var titleLabel: UILabel!
    @IBOutlet weak var backButton: UIBarButtonItem!
    @IBOutlet weak var toolbar: UIToolbar!
    @IBOutlet weak var containerView: UIView!
}

次に、viewDidLoad内で、WKWebViewを追加します。 navigationDelegateはぶっちゃけ今回いりませんが書いちゃってます。

setUpConstraints(for webView: WKWebView )では、WKWebViewをcontainerViewにぴったり貼り付ける制約を加えています。 webview.loadで、githubをロードし始めます。

override func viewDidLoad() {
        super.viewDidLoad()
        
        // set up WKWebView
        let webview = WKWebView()
        webview.navigationDelegate = self
        webview.translatesAutoresizingMaskIntoConstraints = false
        containerView.addSubview(webview)
        setUpConstraints(for: webview)

        // load github.com
        let request = URLRequest(url: URL(string: "https://www.github.com")!)
        webview.load(request)
}

private func setUpConstraints(for webview: WKWebView) {
        webview.topAnchor.constraint(equalTo: containerView.topAnchor, constant: 0.0).isActive = true
        webview.bottomAnchor.constraint(equalTo: containerView.bottomAnchor, constant: 0.0).isActive = true
        webview.leadingAnchor.constraint(equalTo: containerView.leadingAnchor, constant: 0.0).isActive = true
        webview.trailingAnchor.constraint(equalTo: containerView.trailingAnchor, constant: 0.0).isActive = true
}

これでViewパーツの配置は終わりです。

webviewのプロパティをバインディングしていく

ここからRxWebKitを活用する作業に入っていきます。

webview.rx

からWKWebViewに拡張されたRX系のプロパティにアクセスできます。

まずは、RxSwift、RxWebKitをimportします。

import RxWebKit
import RxSwift

そして、webviewのtitleをtitleLabelのtextにbindします。 こうすることで、webviewが遷移する度に表示されているページのタイトルがtitleLabelに自動的に反映されます。 driveを使うか迷ったのですが、どっちが良いんでしょうねこれ。

// bind title to title label
webview.rx.title.bind(to: titleLabel.rx.text).addDisposableTo(disposeBag)

次に、進む戻るボタンのenabledを制御します。
WKWebViewには、canGoForwardとcanGoBackというプロパティがあり、それぞれ、進めるページがあるか、戻れるページがあるかを取得できます。
丁度ブラウザの進む戻るボタンと同じです。
これもRX化されているので、こちらで用意した進む戻るボタンのisEnabledにbindします。(こちらはdriveを使いました)

ただし、UIBarButtonのisEnabledはRxのobserverが用意されていません。
なので、自分で実装することになります。
今回は、RxSwift公式のサンプルに倣い、BindingExtension.swiftというファイルを作って実装しました。

import UIKit
import RxSwift
import RxCocoa

extension Reactive where Base: UIBarButtonItem {
    var isEnabled: UIBindingObserver<Base, Bool> {
        return UIBindingObserver(UIElement: base) { button, isEnable in
            button.isEnabled = isEnable
        }
    }
}

こんな具合に、送られてきたBoolをUIBarButtonItemのisEnableに対応させるようなObserverを作ります。 これに、先程のcanGoForwardとcanGorBackをbindすることで、自動的にUIBarButtonItemが有効化、無効化するようになります。

// handle back and forward buttons availablity
webview.rx.canGoForward.asDriver(onErrorJustReturn: false).drive(forwardButton.rx.isEnabled).addDisposableTo(disposeBag)
webview.rx.canGoBack.asDriver(onErrorJustReturn: false).drive(backButton.rx.isEnabled).addDisposableTo(disposeBag)

実際にこれらがタップされたときの処理と、 リフレッシュボタンがタップされたときの処理は、RxWebKitの機能を用いていないので割愛します。

最後に、プログレスバーの処理を設定します。
RxWebKitではページの読み込みに関係するプロパティとして、loadingプロパティとestimatedProgressが存在します。
前者は現在読み込みを行っているかを表しており、
後者は読み込みがどの程度完了しているか推定値で表しています。

プログレスバーにはUIProgressViewを用い、loadingプロパティを表示非表示に、estimatedProgressを進行度合いにbindしました。 なお、UIProgressViewのisHiddenと進行度合いを表すprogressはRX対応されていないので、またも自作observerを実装します。

先程のBindingExtensionに下記を追加します。 isProgressingは進行中かどうかisHiddenは消えているかを表しているため、 notを入れることでちょうど進行中に表示されるようになります。

extension Reactive where Base: UIProgressView {
    var progress: UIBindingObserver<Base, Float> {
        return UIBindingObserver(UIElement: base) { progressbar, progressRate in
            progressbar.progress = progressRate
        }
    }
    
    var isProgressing: UIBindingObserver<Base, Bool> {
        return UIBindingObserver(UIElement: base) { progressbar, isProgressing in
            progressbar.isHidden = !isProgressing
        }
    }
}

これらに前述のプロパティをbindします。

コメントの下の行では、WKWebViewより前に来るように、bringSubViewで最前線に持ってきています。

// set up progressbar
containerView.bringSubview(toFront: progressbar)
webview.rx.loading.asDriver(onErrorJustReturn: false).drive(progressbar.rx.isProgressing).addDisposableTo(disposeBag)
webview.rx.estimatedProgress.map { progress in
     return Float(progress)
}.asDriver(onErrorJustReturn: 0.0).drive(progressbar.rx.progress).addDisposableTo(disposeBag)

ここで気をつけなければいけないのはestimatedProgressのbindです。 estimatedProgressはDouble型、UIProgressViewのprogressはFloat型なので、一度mapを噛ませるなりして型を揃えてやる必要があります。

以上で必要な処理は全てです。

結果

f:id:gaoxin-xixxix:20170531224656g:plain

すっきりとしたコードでこんな感じのwebviewが実装できちゃいます!素敵!!

今回のサンプルのコードです。

github.com

Moyaでお手軽API通信

ブログ始めてみたんですけど、特に抱負も何もないので、しれっとMoyaで遊んだ事について書きます。

Moyaって?

github.com

APIとか叩く時に作られがちな、"APIManager"とか"NetworkModel"みたいな、 ネットワークを抽象化するレイヤーの役割をしてくれるライブラリ。 内部的にはAlamofireを叩いていて、言うならAlamofireのラッパーとも言えるかもしれない(言えないかもしれない)。

何が便利?

公式が言うには

  • 正しいAPIのエンドポイントにアクセスしてるかコンパイル時にチェックできる
  • enumの連想値使って異なるAPIの使い方をちゃんと定義できる
  • testする時stub的な役割を果たせる

個人的に思ったのは

  • siestaとかよりhttp通信の層をよく隠してくれるので楽
  • 仕組みが割とわかりやすいので使う上でリーズナブル

といったところでしょうか。

使い方

導入

Podfileに

pod 'Moya'

を記入し、pod installで簡単にインストールできます。 なお、Moyaを入れるとAlamofireも一緒に入るので別途記述する必要はありません。 ちなみにSwift Package ManagerやCarthageでも導入可能です。

今回は、 github.com を使って基本的な使い方をなぞってみます。 MoyaのレポジトリにあるBasicUsageとほぼ同じ内容です。

enumを作成

Moyaでは、自分が使いたいAPIの情報をenumに持たせます。 まずは、それぞれのAPIターゲットをenumとして列挙します。 また、それぞれのターゲットに対し渡すパラメータがある場合は、連想値でこれを定義します。

import Moya

enum HackersNewsAPI {
    case item(id: Int)
    case user(id: String)
    case maxItem
    case topStories
    case newStories
}

また、このenumTargetTypeプロトコルに準拠する必要があります。 具体的には

  • var baseURL: URL
  • var path: String
  • var method: Moya.Method
  • var parameters: [String: Any]
  • var parameterEncoding: ParameterEncoding
  • var sampleData: Data
  • var task: Task

のプロパティを設定しなければなりません。 今回は以下のように準拠させます。

extension HackersNewsAPI: TargetType {
    // ベースURL
    var baseURL: URL { 
          return URL(string: "https://hacker-news.firebaseio.com/v0")! 
    }
    
    // それぞれのターゲットごとのpath
    var path: String {
        switch self {
        case .item(let id), .user(let id):
            return "/\(id).json"
        case .maxItem:
            return "/maxitem.json"
        case .newStories:
            return "/newstories.json"
        case .topStories:
            return "/topstories.json"
        }
    }
    
    // それぞれのターゲットごとのhttpメソッド
    // 今回はgetしかないが、postなどある場合はswitch selfなどで適切な値を返す
    var method: Moya.Method {
        switch  self {
        case .item, .user, .maxItem, .newStories, .topStories:
            return .get
        }
    }

    // 今回はパラメーターとして渡さないのですべてnil
    var parameters: [String: Any]? {
        return nil
    }
    
    // パラメーターのエンコーディング指定
    // リクエストボデイにjsonとしてセットすることもできる
    var parameterEncoding: ParameterEncoding {
        return URLEncoding.default
    }
    
    // テスト時に使われる(なんでマストなのかわからない)
    // めんどくさかったので今回はずるします
    var sampleData: Data {
        return Data()
    }
    
    var task: Task {
        switch  self {
        case .item, .user, .maxItem, .newStories, .topStories:
            return .request
        }
    }
}

以上でenumの準備は完了です。

ここまで書いて、題材にしたAPIがあんま良くなかったことに気づく。 が、面倒くさいので強行します。

MoyaProviderを使ってAPIリクエストする

enumの準備が整ったら、MoyaProviderを生成し、リクエストを送ることができます。

let provider = MoyaProvider<HackersNewsAPI>()
provider.request(.newStories) { result in
    
}

provider.requestの第二引数となっているクロージャが受け取っているresultは、 .success(Moya.Response).failure(MoyaError)を持つenumです。 なので基本的には

provider.request(.newStories) { result in
    switch result {
    case let .success(moyaResponse):
        do {
            try moyaResponse.filterSuccessfulStatusCodes()
            let data = try moyaResponse.mapJSON()
            // do something with the data
        }
        catch {
           // failed to convert to JSON
        }
    case let .failure(error):
           // failed at api access
    }
            
}

のようにしてエラー検知すると良いと思います。

filterSuccessfulStatusCodesは、ステータスが200~299でなければ例外を投げるメソッド。 mapJSONは名前の通りデータをJSONマッピングしてくれるメソッドです。

どちらもMoya.Responseに組み込まれたメソッドです。

まとめ

今回は本当に触りしかやりませんでしたが、Moyaは、APIリクエストの際に挟み込めるクロージャーや、 テストを意識した機能、RX対応など、まだまだ魅力的な機能を持っています。 特にRX対応は割とAPIもイケてると思うので是非触ってみてもらえたらなと思います。

一応今回のソース

github.com