BASE開発チームブログ

フリーミアムなネットショップ構築サービス BASE( https://thebase.in )の開発チームによるブログです。開発メンバ積極募集中! https://www.wantedly.com/companies/base/projects

スタートアップでもSIerの経験はバッチリ役に立つ~ショップコインをリリースしました~

f:id:gimutokenri:20180619154450p:plain こんにちは、BASEのPayment Engineer Groupに所属している柳川です。

先日BASEではショップコインという新機能をリリースしました。ショップコインの説明を簡単にすると、BASEをご利用いただく各ショップさんが、独自にショップで使えるコインを発行することで、ショップさん独自の経済圏を作れる機能です。詳しい説明はこちらをご覧いただけると幸いです。 私はこの機能の開発でサーバーサイドエンジニアとして設計、開発、リリースを行いました。

今回は開発者ブログの記事ということで、開発していて気がついたことをまとめてみたいと思います。

事前情報

プロジェクトの特徴

  • 修正範囲がでかい

    • 新規コード+既存の決済コードに手を入れる
    • 端的にいうと新しく決済方式を作るということ
  • 開発量に対して実装者が少ない

    • サーバーサイドエンジニア:1人
    • デザイナー:2人

私の特徴

大規模開発をしていたSIer出身。

プロジェクトの情報と私の特徴から、SIer時代の開発経験をうまく活かせば、上手にプロジェクトを回せるのではとうっすら考えながら開発に挑むのでした。

やったこと

UMLを書いた

開発が始まる前に、自分の中で以下のことを確認したいと考えUMLを書きました。

  • ヒト
  • モノ
  • コト
  • カネ

このあたりのことを詰めると、実装の漏れが防げると考えてのことでした。

SIerからスタートアップに転職してからは、関係者の人数が少ないことや、スピードが求められることからこの工程を省くことも多かったのですが、今回はそうも言ってはいられない物量であると考えてSIer時代の知識を引っ張り出してきました。このとき書いたUMLが、人に設計を共有する上でも役に立ちました。少人数で開発していると、開発中の内容がブラックボックスと化してしまうきらいがありますが、その点を少なからず軽減できたのではないかと思います。

また図に起こすことで、エンジニア以外も直感的に機能の把握が行えたようです。弊社がドキュメント管理ツールとして使用しているDocBaseにてPlantUMLの記法が使用できるため、そちらを使用してUMLを記述しました。文章でダイアグラムの作成ができるので、通常のドローイングツールに比べ、あとからの変更が非常に容易でした。おすすめです。

設計書を書いて設計書のレビューをした

転職後はあまり行ってこなかった、設計書自体のレビューをしました。

小さな機能開発であれば
頭の中や手元のメモで設計→実装→コードレビュー→リリース
この流れで、進めて大きな問題はないように思います。

しかし今回は開発量の多さが懸念されたため、SIer時代を思い返して、設計書をある程度書いて、なおかつ設計書のレビューもしようと考えました。致命的な指摘等炙り出せたので、本当にやってよかったと思いました。

ちゃぶ台返しポイントを積極的に作った

SIer時代の経験を思い出して辛いのが大幅な手戻り、いわゆるちゃぶ台返しというやつです。

残念ながら自社プロダクトの開発でもちゃぶ台返しは起こります。大切なのはいかにちゃぶ台返しのインパクトを少なくして、プロダクトのクオリティの向上に活かすか。ちゃぶ台返しは敵ではなく、味方なのです。今回はちゃぶ台返しへの対抗手段として、いろいろなところで細かくちゃぶ台返しを起こさせるという作戦を取りました。

  • 設計書のレビュー
  • 複数回のコードレビュー
  • 出来ている場所までをデプロイして操作してもらいながらのレビュー

上記のように開発期間中に数々のレビューをはさみました。

ちゃぶ台返しの回数こそ多くなったような気はしますが、結果的に工数が収まった上でクオリティを上げられたのではないかと思います。ポイントとしてはSlackでレビューを依頼をするだけではなかなか見てもらえないので、必要と思うタイミングで必要な人には明確に時間を取ってもらいレビューを行うことです。他力本願は駄目!

リリース前にQAプロセスを入れられたこと

今回少人数での開発、かつ開発量が多かったこともあり、明示的にQA期間を設け、リリース前の検証を行いました。機能の開発に関わっていないメンバーの新鮮な目で、QAをおこなってもらうのは、実際にバグが潰せるのはもちろん、精神的にも大きな助けとなりました。

うまくやれなかったこと

リリースの単位がでかくなってしまった

この発表資料を読みながら反省したのですが、リリースの単位がでかくなってしまったのは反省点かなと思います。後で確認する範囲がでかくなるだけなので、できるだけ細かく早く出すべきだったと思います。ただすぐには本番稼働しないコードを本番に組み込むのも影響確認とロールバックという点でどうだろう、というのはあるので難しいところかなとも思います。

リリース前の忙しい時期がうまく回らなかった

開発自体は、少ない人数でも段取りをしながら進めることが出来たのですが、リリース直前に立て込んでくると、開発作業とそれ以外作業の帽子をかぶり直すことが出来ませんでした。特にリリース前のQA作業を行うことと、バグを修正することを同時に行うのが厳しかったです。リリース直前に忙しくなることがわかっているなら、極力帽子をかぶり直すことなく進められるように、人をアサインしてもらうなどで準備するべきでした。

開発範囲が少ない場合は、できるだけ一人でやったほうがスピーディーに進められますが、ある程度開発範囲が大きくなると、各人が各ロールに分かれた体制を作ることに強みが出てくるのだろうなと思いました。このあたりは今後の課題かなと思います。

また、自分が今何に集中すべきかを客観的に考えるのが大切だと感じました。テンパらない。

まとめ

大きめの機能開発を、主導する立場として担当させていただき、非常に勉強になりました。まるごと任せていただけたからこその気付きが多かったように思います。特に設計書を作る、開発プロセスを明確化する等のSIerで培った技術が役に立ったのはいい経験でした。

SIer時代は、最初から決まりとしてあった開発の手順に窮屈感を感じることもありましたが、実際になんのために行うのかということを考えて、開発中の課題と照らし合わせていくと、力となる部分は無数にあるなと感じました。

当たり前ですがSIerでもスタートアップでもアプリケーションを作るという上では同じです。アプリケーション開発の道具として使えるソフトウェアの開発手法を、積極的に取り入れていくべきだと感じました。すべての開発工程が最初に描いたとおりに進んだわけではありませんでしたが、それも含めいい経験でした。忙しく開発作業を終えたあとは、知識の吸収が早い気がするので、振り返られなかった分をしっかり振り返って、学ぶべきものを見定め学んで、次に活かしたいです。日々勉強。

最後に

BASEでは、様々な経験を活かしながら一緒に未来を作っていく仲間を募集しています。前向きかつ自由にやっていけるのは間違いない環境です。 ご興味を持たれた方は以下の採用情報から!

jobs.binc.jp

リードエンジニアにおけるサービスリードという役割

こんにちは、CTOの藤川です。

これまでエンジニアの肩書において、エンジニア専門職の上級職としてリードエンジニアという役割を設定しておりました。 リードエンジニアと言うと、世間の見方としては「技術力に優れたエンジニア」というやや漠然とした役割と想像しているのではないでしょうか?

続きを読む

stylelintとBackstopJSで安全にcssを書ける環境を作った

20180605164447

こんにちは。BASE で Design Group に所属している三佐和です。主に ネットショップ作成サービス「BASE」 のフロントエンドを担当しています。

背景

BASE のデザインチームはここ最近で人数が急激に増え、活動が活発になってきており、その中のプロジェクトの一つとして、現在スタイルガイドの刷新に取り組んでいます。

しかし、人数が増えていく一方で、コーディングのルールの統一をコードレビューや個人の裁量に任せていたり、マークアップからリリースするまでに時間がかかってしまうことが問題になってきていました。 そこで、新しいスタイルガイドでは、デザイナーやエンジニアの作業工数を短縮し、より効率よく開発を進めるために、コーディングルールの整備とリグレッションテストを導入することにしました!

やったこと

  1. stylelint を使ってコーディングルールを管理
  2. BackstopJS でテストを行うことでデグレを防ぐ

前提として、 nodejs や yarn などの一般的なフロントエンド開発環境が整っているものとします。

stylelintを導入

stylelint を使って、これまで個人の裁量で保たれていたコーディング規約への準拠を機械的に行えるようにします。

導入

  • stylelintをインストール
  • その他必要なプラグイン等をインストール
  • yarn を使ってインストールします。

    $ yarn add -D stylelint stylelint-scss stylelint-order stylelint-config-standard prettier-stylelint
    

    設定

    既存のコーディングルールに合わせて、.stylelintrc を設定していきます。 BASE の場合はプロパティをアルファベット順にソートしたかったので、stylelint-order を使用したりしています。(私はいままで abc の歌を歌いながら順番にプロパティを並べていました........)

    // .stylelintrc
    {
      "parser": ["css"],
      "plugins": ["stylelint-scss", "stylelint-order"],
      "extends": ["stylelint-config-standard", "./node_modules/prettier-stylelint/config.js"],
      "rules": {
        "indentation": 4, // インデント
        "order/order": [
            "custom-properties",
            "declarations"
        ], // アルファベット順でソートする
        "order/properties-alphabetical-order": true, // アルファベット順でソートする
        "length-zero-no-unit": true, // 値が「0」なら単位を省略する
        "number-leading-zero": "always", // 小数点の頭の「0」は省略する
        "color-hex-length": "short", // HEX形式のカラーコードは3文字で表記する
        "shorthand-property-no-redundant-values": true // ショートハンドでプロパティを書く
      }
    }
    

    設定ができたら、package.jsonscripts にコマンドを追加して、 yarn lint-sass で実行できるようにします。

    // package.json
    // ...
    "scripts": {
       "lint-sass": "prettier-stylelint --quiet --write src/sass/**/*"
    }
    // ...
    

    実際の使用方法

    まずは思うがままに sass(scss) を記述します。

    .btn--submit {
        color: $white;
         background-color: $green;
     border: none;
    }
    

    めちゃくちゃですね!でも大丈夫です、lint を実行すると...

    $ yarn lint-sass
    

    こうなります。

    .btn--submit {
      background-color: $green;
      border: none;
      color: $white;
    }
    

    完璧ですね!もう歌は歌わなくても大丈夫になりました!

    結果

    常にコーディングルールを意識する必要がなくなったので、マークアップのスピードも上がりました!

    2. BackstopJSを導入

    BackstopJS を使って、戻りが発生しないよう、動作チェックする仕組みを作ります。

    導入

    • BackstopJSをインストール

    stylelintと同様に、yarn を使ってインストールします。

    $ yarn add -D backstopjs
    
    • backstop.json にオプションを設定し、スナップショットを撮る
    • スナップショットを使ってテストする

    設定

    backstop.json に必要なオプションを設定していきます。

    // backstop.json
    {
      "viewports": [
        {
          "label": "sp",
          "width": 320,
          "height": 480
        },
        {
          "label": "pc",
          "width": 1024,
          "height": 768
        }
      ],
      "scenarios": [
        {
          "label": "reference", // スナップショットの名前
          "url": "テストするURL",
          "hideSelectors": [],
          "removeSelectors": [],
          "selectors": [
            "#test-sandbox" // スナップショットを撮影する部分
          ],
          "readyEvent": null,
          "delay": 500,
          "misMatchThreshold" : 0.1,
          "onBeforeScript": "",
          "onReadyScript": ""
        },
        {
          "label": "test", // スナップショットの名前
          "url": "テストするURL",
          "hideSelectors": [],
          "removeSelectors": [],
          "selectors": [
            "#test-sandbox" // スナップショットを撮影する部分
          ],
          "readyEvent": null,
          "delay": 500,
          "misMatchThreshold" : 0.1,
          "onBeforeScript": "",
          "onReadyScript": ""
        }
    
      ],
      "paths": {
        "bitmaps_reference": "./backstop_data/bitmaps_reference",
        "bitmaps_test": "./backstop_data/bitmaps_test",
        "compare_data": "./backstop_data/bitmaps_test/compare.json",
        "casper_scripts": "./backstop_data/casper_scripts"
      },
      "engine": "phantomjs",
      "report": ["CLI", "browser"],
      "cliExitOnFail": false,
      "casperFlags": [],
      "debug": false,
      "port": 3001
    }
    

    設定ができたら gulpfile にタスクを追加します。

    // gulpfile.coffee
    // ...
    exports.backstopref = () =>
      connect.server({root: './build', livereload: true})
      backstopjs('reference').then () ->
          connect.serverClose()
    
    exports.backstop = () =>
      connect.server({root: './build', livereload: true})
      backstopjs('test').then () ->
          connect.serverClose()
    // ...
    
    
    

    そして、package.jsonscripts にコマンドを追加して、 yarn backstop-ref で参照用画像の作成、 yarn backstop-test でテストが実行できるようにします。

    // package.json
    // ...
    "scripts": {
        "backstop-ref": "gulp backstopref",
        "backstop-test": "gulp backstop",
    }
    // ...
    

    実際の使用方法

    はじめに、テストの元データになる参照用の画像を作ります。

    $ yarn backstop-ref
    

    20180605170855

    BackstopJS は、この画像をもとに比較テストを行ってくれるので、予期せぬ変更を行ってしまった場合にテストが失敗し、それに気づくことができます。

    試しに、このボタンの border-radius8px から 0px へ変更してみましょう。 そしておもむろにテスト実行用のコマンドを叩きます。

    $ yarn backstop-test
    // ...
          compare | ERROR { requireSameDimensions: false, size: isDifferent, content: 0.32%, threshold: 0.1% }: reference bbq_reference_0_test-sandbox_0_sp.png
          compare | ERROR { requireSameDimensions: false, size: isDifferent, content: 0.32%, threshold: 0.1% }: test bbq_test_0_test-sandbox_0_sp.png
          compare | OK: reference bbq_reference_0_test-sandbox_1_pc.png
          compare | OK: test bbq_test_0_test-sandbox_1_pc.png
           report | Test completed...
           report | 2 Passed
           report | 2 Failed
           report | Writing jUnit Report
           report | Writing browser report
           report | jUnit report written to: ~~/backstop_data/ci_report/xunit.xml
           report | Resources copied
           report | Copied configuration to: ~~/backstop_data/html_report/config.js
          COMMAND | Executing core for `openReport`
       openReport | Opening report.
           report | *** Mismatch errors found ***
          COMMAND | Command `report` ended with an error after [0.51s]
          COMMAND | Error: Mismatch errors found.
                        at ~~/backstopjs/core/command/report.js:113:17
                        at <anonymous>:null:null
    
          COMMAND | Command `test` ended with an error after [5.371s]
          COMMAND | Error: Mismatch errors found.
                        at ~~/backstopjs/core/command/report.js:113:17
                        at <anonymous>:null:null
    
    [15:02:59] 'backstop' errored after 5.38 s
    

    差分が発生したので、テストが失敗し、レポート用の HTML が出力されました。

    20180605164934

    これを開いてみると、差分のある箇所がピンク色になっています!分かりやすいですね!

    結果

    作業を開始する前にスナップショットを作成しておくことで、行った作業でのデグレに早く気づいたり、防ぐことできるようになりました。

    まとめ

    今回、stylelintやBackstopJSを利用したことで、実装者の負担が以前よりも軽くなりました!

    実装者が増えても差分が生まれにくく、デザインの一貫性を保つことができる仕組みを考え、引き続きスタイルガイドを作成していきたいと思います。また同時に、それらをメンテナンスしやすい環境も整えていきたいと思います!

    Go言語勉強会を始めたら学習のペースメーカーになった話

    f:id:khigashigashi:20180521090446j:plain

    こんにちは!BASE Product Division サーバーサイドエンジニアの東口です。主にEコマースプラットフォーム「BASE」の決済領域の開発をしています。本ブログでは、PHPerKaigi 2018での登壇記事等も書いています。

    devblog.thebase.in

    BASEのサーバーサイドの多くはPHPで書かれているので普段触る機会の多い言語はPHPなのですが、先月からGoを書きたい有志が集まって定期的にGo言語勉強会を行っています。

    きっかけ

    初めたきっかけは2018年4月15日に開催されたGo Conference 2018 Springでした。スポンサー企業のラインナップや実際に会場の熱気に触れて、改めてGo言語の勢いや実用性を強く再認識しました。それまでGoを書きたいと思っていたこともありカンファレンスに参加していた同僚と勢いで始めることを決めました。

    やり方

    対象書籍として、『Goならわかるシステムプログラミング』を進めていくことにしました。この書籍を選んだ理由は、Goという言語自体の書き方ではなくGoを使って目的のあるコードを書ける点でした。また、個人的にPHPを触ってると意識しにくい低レイヤの仕組みについて知っておきたいという意図もありました。

    f:id:khigashigashi:20180522163147j:plain

    Go Conference 2018 Springの翌週から毎週1回2時間定期的に時間を取って実施しています。

    集まってやるということで発表の時間を作ろうかという検討もありましたが、身内でやる分にはまずは各自好きなペースでもくもく進めていく方法で進めることにしています。

    f:id:khigashigashi:20180522163303j:plain

    また、読書会中の会話や詰まったときの内容をGo言語について話すslackの #golangチャネルにて話しています。ここで各自詰まったところについて話していたりしているので後続の人がそこを見て参考にできるという利点が有りました。

    良かったこと

    学習のペースメーカーになる

    定期的に時間をとることで途中で止まりがちな本の写経をする習慣ができ、週一回Goを触るペースメーカー的な存在になっています。

    議論ができる

    全員が同じ書籍をベースにやっているので、「このページで使われている関数の内部実装がどうなっているか」等、一人で進めているとスルーして先に進めがちな箇所について疑問を深掘りする機会になっています。

    Goに触れた経験のあるエンジニアが増えた

    本読書会を期に、「実はGoやりたかった」というエンジニアがGoを書き始めました。

    元から社内slackに#golangチャネルというGo言語について話すチャネルがあったのですが参加者が7人くらいで更新頻度もあまり高くありませんでした。

    しかし、読書会を通じてGo言語に触れたエンジニアが増え#golangチャネルに人が増えGoの話題について話す機会が増えました。Go言語のイベントの共有や「MySQLドライバをGoで書いてみたら面白そう」といった、こういうチャレンジをしたら面白そうと言った会話がslackでされています。

    まとめ

    プロダクションでGoを採用している箇所はスポット部分に限られるので、この読書会を通じてGoをプロダクション環境や趣味のプロダクトに適用するきっかけになればと思っています。

    読書会やもくもく会といった勉強会を始める際どういう風に運営するかみたいなことを考えて止まってしまうかもしれないですが、やってから考えるくらいの気持ちで始めてみてはいかがでしょうか?

    Embedded Frameworkを導入して、iOS アプリのビルドを爆速にした話

    f:id:yuhei_kagaya:20180509103406j:plain

    iOSエンジニアの大木です。

    日々の開発で、ちょっとした微修正でメソッドを追加・削除すると、差分コンパイルが効かずビルド10分待ちとなり、開発効率の低下が問題となっていました。それを解決するためEmbedded Frameworkを導入したところ、差分ビルドが成功し1-2分になったというお話です。

    私が入社したのは、2017年の2月でした。そして、アプリはもともとObjective-Cで書かれていて、Swiftコードの占める割合は10%以下だったようです。それから1年近くで、90%以上Swiftコードに置き換えました。

    その時点で、ビルドにかかる時間の長さが目立つようになり、ちょっとした不具合の調査や微調整しているだけなのに10分近く待たなければいけないケースが目立つようになってきました。

    問題としては以下の2つがありました。

    • 1つのメソッドを追加しただけなのに、何故か10分近くかかるため、微調整するのに不向き
    • タイプミスしたので一旦途中キャンセルして修正し再ビルドしたが、差分ビルドが効かず10分待ち

    前提

    日々の開発で行うDebugビルドを対象としています。そのため、AdHoc配信などでビルドする場合のケース(Archive)はCircle CIでおこなっているため特に対象にしていません。差分ビルドが効くようにして開発効率を改善していきます。

    環境

    • MacBook Pro (15-inch, 2016)
    • macOS High Sierra(10.13.4)
    • Xcode 9.2
    • Swift 4.0
    • Swift:Objective-Cの比率は、9:1
      • ビルド対象のSwiftファイルは約850ファイル
    • 一般的なビルド設定やSwift-Objective-Cのブリッジングに対する改善はすでに行なっていた
    • New build systemを使っている

    改善の流れ

    1. ビルド時間計測
    2. ビルドが遅くなっている原因コードを探して修正する
    3. これ以上速く出来ない?
    4. コードをモジュール単位に分割
    5. 差分ビルドが効くようになり、開発効率改善!

    ビルド時間計測

    パイク: Cプログラミングに関する覚え書き にもある通り、推測するのではなく、どこで時間がかかっているか計測してみます。

    XcodeのBuildSettingsの OTHER_SWIFT_FLAGS-Xfrontend -debug-time-function-bodies を追加すると、閾値以上のビルド時間がかかっているメソッドを、Xcode上でWarningレベルで報告してくれます。

    ちなみにBASEアプリでは、App Extensionsを使用していることもあり、設定の抜け漏れを防ぐため、XcodeのGUI上ではなく、xcconfigファイルで設定しています。

    // Debug.xcconfig
    
    #include "Shared.xcconfig"
    #include "DebugPlugins.xcconfig"
    
    SWIFT_ACTIVE_COMPILATION_CONDITIONS = $(inherited) DEBUG;
    // 計測用フラグ
    OTHER_SWIFT_FLAGS = -enable-bridging-pch -Xfrontend -debug-time-function-bodies -Xfrontend -debug-time-compilation -Xfrontend -warn-long-function-bodies=200 -Xfrontend -warn-long-expression-type-checking=200;
    GCC_PREPROCESSOR_DEFINITIONS = $(inherited) DEBUG=1;
    

    計測結果が含まれるビルドログはXcode上で全ては見えないのと、出力されたファイルを読み解く時間が惜しいという場合は、BuildTimeAnalyzer-for-Xcode を使うとよいでしょう。

    ビルドが遅くなっている原因コードを探して修正する

    さて計測結果を眺めてみたところ、注文内容と支払い情報を送信するためのHTTPBodyの生成structを返す処理で10秒前後ビルドにかかっていました。 その処理は、Structに (String, Any) のタプルの配列で必要なパラメーターをセットしているのですが、型情報がわからないままなため時間がかかっていたようです。

    そこで、一旦変数に格納することによって、型推論が効くようになり、ビルド時間も大幅に改善しました。

    diff --git a/CartClient.swift b/CartClient.swift
    index 9e2b4c060..93a882f19 100644
    --- a/CartClient.swift
    +++ b/CartClient.swift
    @@ -42,6 +42,11 @@ class CartClient: APIClient {
             case .checkout(paymentType: let `type`, payment: let payment, cart: let cart):
                 let orderRequestInfo = cart.orderRequestInfo
    +            // 一旦変数に格納して型情報を用意する
    +            // 住所とメールアドレスは必須なのでforce unwrappingで取り出しても問題ないはず
    +            let familyName = payment.shippingContact!.name?.familyName ?? ""
    +            let givenName = payment.shippingContact!.name?.givenName ?? ""
    +            let tel = payment.shippingContact!.phoneNumber?.stringValue ?? ""
                 return Parameters([
                     ("payment", type.paymentMethod),
    @@ -49,14 +54,14 @@ class CartClient: APIClient {
                     ("item_id", orderRequestInfo.itemId),
                     ("shop_id", orderRequestInfo.shopId),
                     ("variation", orderRequestInfo.variationId),
                     ("amount", orderRequestInfo.amount),
    -                ("last_name", payment.shippingContact!.name?.familyName ?? ""),
    -                ("first_name", payment.shippingContact!.name?.givenName ?? ""),
    +                ("last_name", familyName),
    +                ("first_name", givenName),
                     ("postal_code", payment.shippingContact!.postalAddress!.postalCode),
                     ("state", payment.shippingContact!.postalAddress!.state),
                     ("address", payment.shippingContact!.postalAddress!.city),
    -                ("address2", payment.shippingContact!.postalAddress?.street ?? ""),
    -                ("tel", payment.shippingContact!.phoneNumber?.stringValue ?? ""),
    -                ("mail_address", payment.shippingContact!.emailAddress ?? ""),
    +                ("address2", payment.shippingContact!.postalAddress!.street),
    +                ("tel", tel),
    +                ("mail_address", payment.shippingContact!.emailAddress!),
                     ("version", appVersion())])
             default:
                 break
    

    改善結果は以下の通りです。

    コンパイル時間 内容
    9323.63ms 元のコード
    265.4ms(-97%) 追加する値を一旦変数に保存(Optionalのまま)
    16.6ms(-99.8%) さらにNonOptionalにして保存

    他にも修正していくらか改善しましたが、劇的な改善には至りませんでした。

    • if boolValue = true { のような比較をやめて if boolValue { に変更して中身の判定を行う
    • 条件に一致したコレクションの要素を取得するメソッド filter() を使うだけで条件を満たせるのに compactMap() 使って余計にビルド時間がかかっていた。

    100ms以内に収まり喜んだのもつかの間、再度ビルドすると200ms以上時間がかかってしまう問題もあり、これ以上は誤差でしかないと考え修正を打ち切りました。

    これ以上速く出来ない?

    Swiftコードコンパイル時間短縮も大事なのですが、最初に述べた 差分ビルドが効くようにして開発効率を改善 達成するのが第一の目標です。

    考えてみると、全てのソースファイルをアプリケーションターゲットでビルドするのは無駄です。APIからデータを取得するコードやデータモデルなどは、最初に定義してしまえばほとんど修正することはありません。

    モジュールとして切り出せば、関係ないコードのビルドはFramework側で行うことになり、ビルド時間の大幅な改善が見込めそうです。

    iOSには、Cocoa Touch Frameworkとしてコードを共有する仕組みがあります。さらに、Embedded Frameworkという仕組みを使ってXcodeのワークスペースに直接組み込むこともできます。

    コードをモジュール分割

    Embedded Frameworkを使って、コードをモジュールとして分割していきます。

    レイヤー化アーキテクチャー等で、各々理想のアーキテクチャーはあると思いますが、ビルド時間の解決に使うことにします。

    • iOS SDKにのみ依存し単体で動作する共通コード
    • アプリで定義したデータモデルやAPIクライアントコード等、複数箇所で利用し変更が頻繁に発生しないコード

    上記両方に共通していえることですが、設定ファイルの読み込みが必要な部分はアプリケーションターゲット側で読み込み、その結果オブジェクトをフレームワーク側で定義したデータモデルに変換して渡すようにした方がテストもしやすいですし、後々依存関係のエラーで悩む必要も無くなります。

    また、再利用可能にすることを考えると、APIのエンドポイントやストレージのパスなども、アプリケーションターゲットから渡すようにした方が良さそうです。

    最終的には下記のようにモジュール分割しました。

    UIExtensionKitとAppKitという名前のフレームワークを作成し、アプリケーションターゲットはそれをインポートして使うようにしました。

    frame "Application Target" {
        [AppContext]
        [App]
    }
    
    package "UIExtensionKit" {
        [Extensions]
    }
    
    package "AppKit" {
        [AppContext Interface]
        [Registory]
        [Data Structures&Logics]
    }
    [App] . [AppContext] : Init
    [App] ... [Extensions] : Use
    [AppContext Interface] <|.. [AppContext] : Implement
    [Registory] *- [AppContext Interface]: DI
    [Data Structures&Logics] . [Registory] : Use
    [App] . [Data Structures&Logics] : Use

    図のApplication Targetというのはアプリケーションのエントリーポイントとなるターゲットです。アプリを起動する場合はこのターゲットを起動します。

    UIExtensionKitは、iOS SDKでのみ実装してアプリの業務ロジックは含まないそれ単体で動作するコードです。カラーパレットの定義やViewコンポーネントが配置されているセルのIndexPathを取得する便利メソッドなどがあります。

    AppKitは、アプリで定義したデータモデルやそれらを操作するビジネスロジックなどを含みます。 APIリクエストのクライアントだったり、APIのレスポンスを加工したりローカルのストレージにシリアライズしたデータを保存したりするコードがあります。

    結果

    改善結果は以下の通りです。差分ビルドの結果は、 UIViewControllerに viewWillAppear(_:) を追加してビルドした結果です。

    コンパイル時間 ビルド種別 内容
    494.4s<約8分> フルビルド 分割していない元のプロジェクト
    247.055s<4分8秒> フルビルド フレームワークを使ってモジュール分割したプロジェクト
    232.8s<3分52秒> 差分ビルド(成功時) 分割していない元のプロジェクト
    86.924s<1分27秒> 差分ビルド フレームワークを使ってモジュール分割したプロジェクト

    まとめ

    差分ビルドが失敗してビルドに時間がかかるという場合は、Frameworkを使ってモジュール分割してみたら改善できたというお話でした。これによって、無駄に残業して仕事をすることもなくなり、効率よく開発できるようになったと思います。

    おまけ(モジュール分割に関して)

    モジュール分割に対応するためには、技術的な課題がいくつかありそれを解決しなければなりません。 ざっと挙げますと

    • アプリケーションターゲットでしか使えないもしくは実体が存在しないオブジェクトやメソッドが存在し、Frameworkやテストターゲットから利用したい場合に簡単にアクセスできそうにない
    • フレームワーク内のコードを動作させるには、アプリケーションターゲットに配置した設定ファイルを読み込む必要がある
    • Objective-Cで書かれた広告や解析SDKを利用する場合、複数のモジュールでインポートすることができない

    今回の場合フレームワーク側は、一方的に利用される側なので、何らかの方法でこういった依存関係を解消する必要があります。

    それには、依存関係逆転の原則(DIP)という方法を使いこの問題を解決しました。フレームワーク側にAppContext Interfaceという抽象インターフェースを定義することにより、アプリケーションの詳細に立ち入らず依存関係のエラーを防ぐことができるようになりました。

    フレームワーク側に用意する抽象インタフェースの例は、下記のようなコードです。

    これを、アプリケーションターゲットやテストターゲットが実装すれば、フレームワーク側は抽象インタフェースを通して処理を行えるようになりました。不必要にSDKをインポートして依存関係のエラーになることもありません。APIのエンドポイントも渡すようにすれば、切り替えのたびにフレームワーク側をビルドし直す必要もありません。

    // AppKit.frameowrk
    public protocol AppContext {
        var apiConfig: APIConfig { get }
        var tracker: Tracker { get }
        var legacyImageSliceFetcher: LegacyObjCImageSliceFetchable { get }
        var bundleResolver: BundleResolvable { get }
        var keychainResolver: KeychainResolvable { get }
        var realmContainer: RealmContainer { get }
        var legacyAPIBridging: LegacyObjCAPIBridging { get }
        var appConfig: AppConfig { get }
        var traits: Traits { get }
        var userDefaultsResolver: UserDefaultsResolvable { get }
        var activity: Activity { get }
    }
    
    public enum TrackerActions {
        case setUser(userId: String?)
        case error(Error)
        case errorWithAdditionalUserInfo(Error, userInfo: [String: Any]?)
    }
    
    // FabricのCrashlyticsはFrameworkの中に重複してリンクできないので、アクションを通じて処理を行わせる(処理の詳細実装自体はこのフレームワークを利用する側にある)
    public protocol Tracker {
        func dispatch(action: TrackerActions)
    }
    

    フレームワークを利用する側(アプリケーションターゲットやテストターゲット)は、下記のようにこのインタフェースを実装すれば良いことになります。

    // アプリケーションターゲット
    import AppKit
    
    final class AppContext: AppKit.AppContext {
        var apiConfig: AppAPIConfig = APIConfig()
    // [...]
    }
    
    // テストターゲット
    import AppKit
    
    final class MockAppContext: AppKit.AppContext {
        var apiConfig: AppAPIConfig = MockAPIConfig()
    // [...]
    }
    

    フレームワーク側で解決できない依存関係は、利用する側で抽象インタフェースを実装することにより解決できるようになりました。

    しかしこの実体をどう渡せば良いのでしょうか?

    下記のようなコンテナをフレームワーク側に用意し、それを利用するようにすればどこからでも呼び出せるようになります。

    public struct Registory {
        private static var context: AppContext!
        public static func setContext(_ context: AppContext) {
            self.context = context
        }
        static func resolve<D>(keyPath: KeyPath<AppContext, D>) -> D {
            return context[keyPath: keyPath]
        }
    }
    

    これでフレームワークの外側から必要な処理を行えるようになりました。

    参考文献