1枚のラベルの向こうには、1人のユーザがいる【連載:クックパッドマート開発の裏側 vol.3】

こんにちは。クックパッドマート連載3日目を担当します、買物事業部エンジニアの今井(@imashin_)です。

去年の10月ごろから、生鮮食品ECサービスクックパッドマートの販売者向けサービスの開発を行っています。クックパッドマートを利用するのは、商品を買うユーザだけではありません。商品を販売する方々にも簡単に利用できるよう開発を進めています。

今回は、どのようにして商品を販売者からユーザまで届けられるように開発しているかを紹介します。

クックパッドマートではどうやって商品をユーザに届けているのか

まず、今どのように商品を届けているのか、商品の注文から受け取りまでの流れを紹介します。

発注

f:id:ima_shin:20190410170609p:plain

販売者は、四六時中クックパッドマートだけを利用しているわけではありません。これまで通りの生産、販売業務が忙しい中で、クックパッドマートも利用していただいています。

そのため、販売者に合わせた方法を開発し、負担にならないようにしています。

みなさんの近くにある精肉店、青果店を思い出してもらうとイメージが湧くかもしれないのですが、販売者はFAXや電話で注文を受けていることが多いです。必ずしもIT、インターネットに慣れているわけではありません。そのためクックパッドマートは、毎日FAXでの発注書の自動送信を行っています(FAX送信にはTwilioを利用しています)。一方でスマホから見たいという要望の販売者向けに、LINE WORKS経由でも発注書をPDFにて送付しています。利用者の多いLINEと同じUIを提供しているLINE WORKSを利用することで、利用障壁を大きく下げることができています。

仕分

販売者は発注で受けた商品の発送準備を行います。この準備段階で、ユーザが受け取り時に目印とするラベルの貼り付けを行います。

  • 07:00 商品に貼るラベルを遠隔で自動印刷する
  • 07:00-14:00 販売者が注文を受けた商品にラベルを貼る。 商品をコンテナごとに仕分けする

f:id:ima_shin:20190410170612p:plain

商品ラベルについても、完全に操作不要で発行できる構成で設置し、必要な時に必要なラベルを発行しています。また、商品へのラベル貼り間違いが発生せず仕分けが素早くできるよう、コンテナ別、商品別でラベルが発行されるようにソートしています。こうすることで、負担をかけないように工夫しています。

配送

  • 11:00-14:00 配送員がコンテナを受け取りにくる
  • 14:00-17:00 受け取り場所にコンテナを配送する

f:id:ima_shin:20190410170536p:plain

配送員は指定のコンテナを受け取り、冷蔵状態を保ちながら商品を集荷し、受け取り場所まで配送します。

受け取り

  • 17:00- ユーザが受け取り場所にて、自分の注文した商品をピックアップする

ユーザには配送が完了すると通知が送られます。受け取り場所に行き自分が注文した商品のIDを確認し、コンテナからピックアップしていきます。

どのようにして今の配送を作ったのか

クックパッドマートはまだまだ完成していないサービスです。今の配送フローがベストだとは考えていません。これからも日々、改良を続けていきます。

ですが、リリース当初の状態からはかなり改良されています。今回はどのように改良、開発を行っているかを商品の受け取りに必要なラベルの発行にフォーカスして紹介していきたいと思います。

クックパッドマートでは、基本的に

  • 初めから実装せず、頑張る運用からやってみる
  • 頑張る運用の知見を元に、プロトタイプを試験運用する
  • 利用者に当てたプロトタイプの知見を元に、スケール可能なプロダクトを作る

の段階を踏んでサービス開発を行っています。(ex サービスリリース初期の話

今回は商品ラベルの発行にフォーカスして、実際に行った開発をお伝えしたいと思います。

頑張る運用をやる

初期の段階ではコストをかけてでも(後々自動化可能な)配送を行えるのか検証を行いました。商品ラベルの発行は人の頑張りで次のような運用をしていました。

  • 注文の締めとともに社内に設置されている複合機でラベルを印刷する
  • 配送業者にラベルを配送してもらう

f:id:ima_shin:20190410170808p:plain
複合機でのラベル印刷

検証結果としては、商品へのラベル貼りを販売者が問題なく行えることを確認できました。加えて、ラベルに印字したIDを元にユーザが自分の受け取るべき商品をピックアップできることも確認できました。

プロトタイプを販売者にあてる

ラベルを毎日郵送するにはコストが莫大にかさみますし、スケールさせることも困難になります。そこで次の段階として、販売者にラベルを印刷してもらう方向でプロトタイプを作成しました。

安価に、素早く開発できることを基準に技術選定を行い、iPadとiOS用のSDKを提供しているラベルプリンターを採用しました。

  • ラベル発行用iPadアプリを開発し、ラベルプリンターにて印刷できるようにする
  • プロトタイプを店舗に設置し、試験運用してみる
    • ただし、問題発生時にはバイク便にてすぐラベルを届けられるようにバックアップを用意

f:id:ima_shin:20190410171717j:plain

f:id:ima_shin:20190410171833p:plain
ラベルの印刷フロー

このラベル発行アプリとiPadとラベルプリンターを販売者に提供することで、ラベルの配達をなくすことに成功しました。しかしながら、多くの問題点も浮き彫りとなりました。

  • ラベルプリンターの紙詰まりによる故障
    • 耐久性に特化したラベル発行機でないと長期の運用は保守が大変だった
  • 操作可能な画面は不要
    • 導入当初は、発注内容の確認や商品の情報入力をiPadからできるのではと思われたが、実際には設置場所の狭さや操作する余裕がないことがわかった
  • 通信環境の不良
    • iPadが安定してIPアドレスを払い出せない
    • iPadとプリンターの接続状態を安定させることが難しかった
  • OS、アプリの管理
    • 販売者の操作なしにOS、アプリを常に最新状態に保たせる仕組み、運用を作ることが難しかった
  • 販売者ごとのITリテラシーの差異
    • 必ずしも全ての販売者がiPadやプリンターの操作に慣れているわけではなかった

このように実際にプロトタイプで試験運用した結果、多くの問題点を洗い出すことができました。ラベルは商品を販売者からユーザに届けるために必要不可欠なものです。毎日必ず発行できる安定性を実現させる必要があります。

スケールできるプロダクトを作る

安定してサービスを運営するためには、ラベル発行に高い安定性が必要だということを認識することができました。また広くスケールをさせるためには、誰でも簡単に設置、管理できる必要があります。そこで、以下のような要件を元にスケール可能なプロダクトの開発を行いました。

  • 安定してラベルを発行できる構成と設計
    • 完全に遠隔でラベル発行をコントロールできる
    • ラベル発行が可能か把握するための死活を監視する
    • ラベル切れ等によるラベル発行不可能状態になる事前に検知する
  • 簡単に導入、運用できる設計
    • 電源を刺すだけ利用できる
    • 複雑な操作なしに運用できる

以上を満たすように開発を行い、つい2週間ほど前に新たな構成でプロダクトをリリースしました。

今の状態

では、今の構成がどのようなものかを紹介しようと思います。

ハードウェア

安定した稼働を実現するために、以下の機器でラベル発行機を組みました。

  • LTE ルーター UD-LT1/EX + SORACOM Air
    • 定期リブート
    • ネットワーク断絶時のリブート
    • Syslog
    • 外部ネットワークからの設定変更
    • SNMPによる状態監視
  • TSP743IIE3-24J1 JP
    • 通電すると常にONの状態に固定可能
    • 紙詰まりしにくい
    • ネットワーク経由でコントロール可能
    • カバーが開いている、紙が詰まっている、ラベルが切れかかっている等の状態を取得可能
    • SNMPによる状態監視
  • Raspberry Pi Model B+
    • デバッグ、キッティング、監視用

f:id:ima_shin:20190410171946p:plain
ラベル発行の構成

各機器の安定性、死活監視を利用することで、ラベル発行を安定して行うことができるようになりました。Raspberry PiでLTE通信を行うこともアイデアとしては挙がっていましたが、リリース速度を重視し、一旦既存のルータ製品を採用することにしました。

ソフトウェア

以上のハードウェアを稼働させるために、主に3つの開発を行いました。

star_ethernet

スター精密製プリンターを制御するiOSやAndroidのSDKは提供されていたのですが、サーバから直接利用するケースが少ないのか、Rubyはサポートされていませんでした。しかし、ソケット通信によるプリンターのプロトコルについて、細かな仕様が提供されていたため、Rubyからスター精密製プリンターを制御するgemを作成しました。

基本的にはTCPソケットで制御コマンドを送信し、プリンターを制御します。公開されている全てのコマンドをラップし、ラベル発行に必要なハンドリングを可能にしています。

例えば、文字を大きくしたりレイアウトを変えたりする、QRコードを印字する、線を引くといった印字内容の操作もこれを用いて行います。ラベル台紙のカットやラベル送り、ビープ音を出すこともできます。プリンターの細かな状態を取得することもできます。

f:id:ima_shin:20190410172040p:plain
https://www.starmicronics.com/Support/Mannualfolder/UsersManual_IFBD_HE0708BE07_EN.pdf

mart_server

プリンターへのラベル発行命令はECSから送信します。

mart_server(クックパッドマート全体を支えるRailsアプリケーション)に発行すべきラベルの情報を集約し、日次バッチにて発行するラベル情報をstar_ethernetを利用してプリンターに送信します。

バッチにはkuroko2を利用し、barbequeで各プリンターへのラベル発行ジョブの管理しています。何かしらのトラブルでラベル発行に失敗した時は、原因を調査しジョブを再実行することで全てのプリンターで確実にラベルが発行されるようにしています。またラベル残量が少なくなっていたり、紙詰まりの発生を検知しています。

mart_shepherd

配布端末、ネットワークの管理を新たなのアプリケーションとしてmart_shepherdに切り出しました。

mart_shepherdはSORACOMプラットフォームとの間に立ち、mart_serverからgRPC経由のリクエストに応じて端末の管理を行います。また、ルーター、プリンター、ラズパイ各端末との通信時にはプロクシを行い、通信路を確立します。

アセンブル

実際にこれらの構成を設置するためには、機器を一つの什器にまとめてコンパクトにする必要があります。また、電源を刺すだけで簡単に運用を開始できるようにすることを目指しました。

そこで、一つのボックスに全ての機器を収納し、プラグを刺すだけで全ての機器の電源がONになり、即座に運用状態になるようにしました。

弊社には、ハードウェアを加工できる「工房」と呼ばれるスペースが存在し、そこで全ての加工、組立を行いました。

f:id:ima_shin:20190410172304j:plain
加工中の様子

f:id:ima_shin:20190410172230j:plain
加工済みのボックス

f:id:ima_shin:20190410172532p:plain
アセンブル済のボックスとプリンター

改善点

以上のように、安定して稼働するプロダクトを完成させることができました。2週間運用している限りでは、何かトラブルが発生しても遠隔で復旧することに成功していて、ラベルの発行ができない問題にぶつかったことはありません。

しかし、まだ改善点は残っています。

  • 低コスト化、小型化
  • 輸送可能な構成、耐衝撃性
  • 熱制御

これらを実現するために、引き続き開発を行っています。

まとめ

たかが商品のラベル1枚と思いがちですが、ラベルが発行されないとユーザーに正確に商品を届けることができません。1枚のラベル発行に失敗すると、1人のユーザが料理を作れない状態に陥ってしまいます。

そのようなことが決して起きないよう、たかがラベルの発行であっても真剣に開発に取り組んでいます。

これからもクックパッドマートは、素早いサイクルでの開発の元、安定したサービスの提供と、スケールを実現していきます。

この記事を通して、クックパッドマートのサービス開発にご興味を持っていただけた方がいらっしゃいましたら、ぜひ一緒にサービスを作りましょう!

www.wantedly.com

お知らせ

クックパッドマートでは、4/24(木)に、買物事業部のエンジニアによる発表とエンジニアとのミートアップを開催予定です。

cookpad.connpass.com

今回の記事のような生鮮ECそのものの仕組みや、流通の仕組みの開発に興味がある方、クックパッドマートのエンジニアと直接話してみたい方はぜひご応募ください!お待ちしています!

クックパッドマートiOSアプリを楽しく新規開発した話【連載:クックパッドマート開発の裏側 vol.2】

こんにちは。
連載シリーズ2日目を担当します、クックパッド買物事業部 iOSアプリエンジニアの中山(@LimiterJP)です。
早いもので入社して一年が経ちました。

私は去年4月にクックパッドへ入社しました。
その後6月にアプリ開発を始め2018年9月に「クックパッドマート」のiOSアプリをリリースしました🎉

クックパッドマートは
生鮮食品をスマホアプリから簡単に注文することができる生鮮食品ECサービスです。
従来のネットスーパーや生鮮宅配サービスとは異なり、街の精肉店や鮮魚店などの販売店や地域の農家といった生産者など、小規模事業者や個人事業主が参加できるプラットフォームです。

今日はクックパッドマートのiOSアプリの立ち上げを爆速で行った舞台裏について、

  • 新規開発で大切にしたこと
  • 開発速度を上げるための考え方
  • 新規開発に特化した具体的な手法

の観点でお話します。

新規開発で大切にしたこと

ここでは、私が普段から新規開発を行う際に心がけていたことをいくつか紹介します。

開発スピードは大切

開発スピードは早ければ早いほど失敗から機能改善・成功までの速度も上がると考えます。

「ユーザーが実際にアプリを使用し、内容に共鳴・共感しリテンションを保てるか?」
という仮説検証を行う際、
「実際にアプリをリリースし、ユーザーが使用して操作してみないとわからない」
ということを認識する必要があります。

ユーザーがアプリをダウンロードしたときの印象にはレベルが存在すると考えます。
レベル1 使えない・よくわからないアプリ
レベル2 何に使うのか?を理解できる内容のアプリ
レベル3 なかなか使えるので使いつづけようと感じるアプリ
レベル4 神アプリ!シェアしよう!拡めたいと思うほどのお気に入りのアプリ

f:id:degikids:20190408134227p:plain

そのうち最低でもレベル3以上を目指さないと結果を出すことは難しいでしょう。
そして瞬間的にレベル4でも使い続けてもらえない内容だと別の問題にも悩むことになります。
アプリ開発って本当に難しい。

アプリリリース前にしっかり使用するシーン・ストーリー・コンセプト・カスタマージャーマップを組み立て入念な企画を立案します。
デザインを当ててみて機能を設計するなど頭の中でアプリを開発する。
ここまでの工程を私達のチームでは「論理できた」と呼んでいます。

ところがこの「論理できた」の状態でいざ実際にリリースしてみると
「実際のユーザーが継続的に使用することはなくニーズがずれて失敗する」
ことが多いのではないでしょうか?

これらは機能単位で起こっているなんてこともあります。
時間をかけて作った機能が使われないなんてことよくありますよね。

そのため、速く改善してレベル3, 4を目指せる状態を作ることでリテンション(継続率)を高められる状態になると考えます。

スピード重視とはいえ、雑に実装してバグを出してでもスピードを求めるとか
「コードレビューを全く行わないぞ、テストコードを書かないぞ!」とかそういうことではありません。
行う箇所を選定・判断することが大切です。
大切なのは効率化して工夫できることの選択肢を増やし開発スピードを向上させることです。
(後述する「開発速度を上げるための考え方」など)

チームメンバーとよく笑いよく話す

クックパッドマートのチームでは、基本的に誰かが面白いことを言っている現場なので笑いが絶えません。 (誰か反応するまで喋り続けるスタイルです!)

デザイナー、エンジニア、ディレクターなど職域を超えた人同士の雑談も多く、 Slackでやり取りすれば良いようなこともあえて喋るように、人間関係も良好で良いチームだと感じています。
そのおかげか、メンバーがどういう気持ちで仕事に向き合っているか、何を考えているのかも知ることができています。 業務で疑問に思ったことは身近な人に聞けば一瞬で解決することも多く、様々な仕事が円滑に進むので、会社に来ることに楽しさも感じます。

「普段から高い頻度で雑談ができるチームは強い。」 その結果、開発速度も改善速度も上がり、品質も高くなると考えています。   

圧倒的当事者意識

「圧倒的」とつけるとなんだかすごい感じがするのでつけてみたのですが当事者意識は非常に大切です。
自分が作っているサービスを使い倒す。使わないと問題点や改善点は理解できないですよね。
クックパッドマートで販売されているパンはとても美味しいのです🍞
エンジニア自身がアプリの課題を把握することで、自分が使いやすくするためには?という意識が自然に芽生えるものです。
よってアプリをしっかり普段使いすることは非常に重要です。

開発速度を上げるための考え方

ここでは、具体的にどのように開発速度を上げていくか、私なりの考え方について紹介します。

パレートの法則(2:8の法則)で物事を考える

パレートの法則とは全体の数値の大部分は全体を構成するうちの一部の要素が生み出しているという理論です。

例えば「アプリ利用者のうち8割は、全機能の2割しか使わない」とすると、

  • すべての機能のうち2割の重要な機能に集中する
  • 2割のユーザーしか使わない機能はほどほどに作る

などのヒントが得られそうです。

何が正しいかはチームで決めると良いでしょうし、実際にそのまま採用するのではなく頭で考える作業をします。 明らかに仕様頻度が少なく重要度の低いものに時間をかけない選択やphase分けをして最初のリリース時には実装しないなどの判断をすればいいと思います。

パレートの法則に当てはめることで、

  • 開発の優先順位をつけることでリリースまでの最短距離の見通しが良くなる
  • なにか取り掛かる際にこれらを意識することで開発スピードが目に見えて上がる

などの効果があると考えています。 やる事とやらない事をはっきりさせ、見通しをよくする事が大切ですね。

何事もphaseで分けた考え方を持つ

なぜならはじめから大きなものを作ろうとすると疲弊し、その他がおろそかになる可能性があります。
「段階を経て完成を目指す」というのでしょうか。
この時点ではその「作ろうとしているもの」がユーザーに受け入れられるかはわかりません。

そこで比較的工数がかかる実装はphaseで分けて考える場面があると思います。 具体的には複数の緯度経度と場所情報の情報を持つリスト構造の情報があったとします。

リスト構造の情報を地図上にピンが立てタップし直感的に俯瞰できると見やすいでしょう。 しかしリリース序盤だと受け取り場所の情報が少なすぎて 逆に見づらい上に寂しい感じもします。

f:id:degikids:20190408134619p:plain

そこでリリース時はただのリスト構造からタップして選択するUIを選択し、ロケーションが増えたらMapからピンをタップして選択するUIへ再構築するという判断が生まれます。

新規開発に特化した具体的な手法

ここでは、iOSアプリで新規開発を行う際の具体的な手法について紹介します。

同じコードを極力書かないようにコードスニペットは磨いておく

高い頻度で使用するコード記述をスニペットに登録しておけばいちいちGoogle検索したり、昔書いたソースコードを探してみたりすることなく、 使いたい時に正確な記述をサッと呼び出して使うことが可能です。

存在は知っていてもあまり使用されていないのが現実です。 私はどの言語を書くときでもIDEに付属しているコードスニペットを活用しています。

f:id:degikids:20190408142542p:plain

コードスニペットは定期的に磨いておくと開発効率が抜群に上がります。
iOSではXcodeのsnippetを活用しおり開発をしているとこれらは何度も登場します。

  • TableView, CollectionViewの最低限動くdelegateメソッド一式
  • GoogleMapの最低限動くdelegateメソッド一式
  • 地図からルート探索
  • 緯度経度のリスト構造と現在の位置情報からdistanceが近い順に並べ替え
  • UIActivityやShareなどのイベントハンドラ
  • 位置情報取得(パーミッションdelegate含む)
  • アラートやフルスクリーンの透過ダイアログ・モーダルウィンドウ各種
  • WebView・SafariViewの設置
  • チュートリアルなどのスライドをcollectionViewのpagingを使用して実装
  • カメラ起動 最低限動くdelegateメソッド一式
  • ライブラリから写真選択、アルバムから画像抽出
  • プッシュ通知の実装
  • GCD各種

登録時はCompletion Scopes でしっかり分類しcompletion shortcut は検索性を保った名称設計を心がけます。 たったこれだけでも、手を抜いてしまうと開発効率は落ちます。

人にもよりますが私の場合すべてのsnippet の completion shortcut prefixにsw_をつけています。(sw_はswiftを表していますObjective-C時代からの歴史的経緯もあります)

sw_ から始まるものはすべてsnippetであるという分類ルールを持ち通常の補完と区別しやすいようにします。

f:id:degikids:20190408142347p:plain

普段SwiftでXcodeを使用してコーディングする時は

sw_xxxx で補完

または

 command + shift + 「l」

検索

↑↓cursor

Enter

の順でコードを書いていきます。

その結果、通常のコード補完よりも早く正確に書けるようになります。 初見のコードも一旦雑に書き、使い回せるようにコードレビューを繰り返し、磨き上がったらsnippetに丁寧に放り込みます。

チーム開発するときは
~/Library/Developer/Xcode/UserData/CodeSnippets を共有しておくととても便利です。

例えばアプリ上でGoogleMapを設置しピンを立てて位置情報取得して画面でみて見ましょうか?という場面があったとします。 プロトタイピングツールでは難しい地図のモック作成の場面でも短時間で実装しディレクターやデザイナーと実際に地図を動かし良い悪いの議論することができます。この時点でプロトタイピングツールすら必要なく開発を進めることができます。

Xcodeのプロジェクトテンプレートを磨いておくと結果的にすべての開発効率が向上する

私はxcodeのプロジェクトテンプレートを利用しています。(具体的な用意の仕方はここでは割愛します)

ですが単純に自分の雛形プロジェクトテンプレートを用意しておけば良いと思っています。(新規プロジェクトで雛形アプリを作成)

以下のような機能を雛形の中に含めています。

  • 設定画面
  • TabBarController
  • お問い合わせ(WebView or SafariView)
  • アプリの使い方(WebView or SafariView)
  • プッシュ通知
  • 利用規約(同意の機構)
  • プライバシーポリシー
  • アプリバージョンの表記
  • ライセンスの表記
  • レビュー催促の仕組み
  • ログイン画面
  • Cloud FirestoreのCRUD

その他たくさん

まず上記を含んだ雛形のプロジェクトを複製し、その案件で必要ない機能は削除していきます。
上記の実装がXcodeから新規作成したときに既に実装されていたらどうでしょうか?
それはもう「強くてニューゲーム」です。

f:id:degikids:20190408140326p:plain

最初のスタートダッシュで大きな差がつきます。

Storyboardベースでつくる

Xcode6以前はコードベースで進めるほうが効率的でしたが、
iOS9.0以降からはStoryboardの分割が容易になりコードベースで作るより圧倒的に開発が楽になりました。

Storyboard Reference の登場によりファイルの分割や関連付けも容易に。
以前と比べてConflictの可能性も低くくなったと感じます。

f:id:degikids:20190408140945p:plain

Storyboardを活用しデザイン確認が行えると開発が円滑に進む場面が多いように思えます。
例えばちょっとしたボタンの位置、配色などデザイナーと机を合わせて確認・議論ができます。簡単なモックもコードを汚さず作ることが可能です。

SwiftGenを活用する

SwiftGenとはXcodeで使用される画像イメージ、フォント、 カラー、segue等のリソース名を自動的に生成し型付を行ってくれるライブラリです。 https://github.com/SwiftGen/SwiftGen

Storyboardの遷移はコードで行っています。
理由として再利用率が高い画面を切り離して考えられるからです。

簡易的な値渡し例えばWebViewで開く情報を segue で渡すとシンプルかつ直感的に実現できます。

画面遷移では performSegue を使用しており、prepere で値渡しを行っています。
そのためにはSegueIDをStoryboardから設定する必要がありこれを手作業で管理するのは厳しい。
Segueのデメリットは「いろんな場所に設定が散らばっていて情報が隠れやすく流用がしづらい」ということだと思います。
コードからSegueIDやStoryboardを利用しようとした場合に、補完が効かないためハードコードをするか、手動で管理をする必要があります。

そこでSwiftGenの登場です。
簡単にStoryboardの名前からStoryboardのSegueのIDで名まで自動で生成し型付してくれます。

f:id:degikids:20190408142658p:plain

buildしなくても角丸とかボーダーなどを確認できるようになります。

class CustomView: UIView {
    @IBInspectable var customBool: Bool = false
    @IBInspectable var customInt: Int  = 0
    @IBInspectable var customFloat: CGFloat = 0.0
    @IBInspectable var customDouble: Double = 0.0
    @IBInspectable var customString: String = ""
    @IBInspectable var customPoint: CGPoint = CGPointZero
    @IBInspectable var customSize: CGSize = CGSizeZero
    @IBInspectable var customRect: CGRect = CGRectZero
    @IBInspectable var customColor: UIColor = UIColor.clearColor()
    @IBInspectable var customImage: UIImage = UIImage()
}

まとめ

クックパッドマートのiOSアプリはまだまだ発展途上で完成はしていません。
現在もコツコツと開発を進めており日を重ねるごとに利便性は向上しています。
使用できるエリアの皆さんは是非使っていただけると幸いです。ありがとうございます!
この記事を通して、クックパッドマートのサービス開発にご興味を持っていただけた方がいらっしゃいましたら、ぜひ一緒にサービスを作りましょう!

www.wantedly.com

お知らせ

クックパッドマートでは、4/24(木)に、買物事業部のエンジニアによる発表とエンジニアとのミートアップを開催予定です。

cookpad.connpass.com

今回の記事のような生鮮ECそのものの仕組みや、流通の仕組みの開発に興味がある方、クックパッドマートのエンジニアと直接話してみたい方はぜひご応募ください!お待ちしています!

クックパッドマートにおける実世界での配送を意識した注文の検証処理【連載:クックパッドマート開発の裏側 vol.1】

はじめに

こんにちは、買物事業部の勝間(@ryo_katsuma)です。 今日から5日間は、買物事業部のメンバーで連載記事を書かせていただきます。

買物事業部は、クックパッドの生鮮食品ECサービスの新規事業「クックパッドマート」の開発を行っている事業部です。 クックパッドマートのサービスについての説明や、立ち上げ期の舞台裏については昨年の長野によるエントリをご参照ください。

クックパッドマートは、iOS、Androidアプリのリリース、商品受け取り場所におけるスマートロックの設置、注文当日配送の実現など、 サービスをより多くの人に便利に使っていただくためのいろいろな新しい取り組みを行ってきました。 今回はこれらの取り組みについて多くの方にぜひ知っていただきたいと思い、連載形式で紹介させていただきます。

ちなみに明日以降は、以下のような内容を予定しています。

注文の検証処理

vol.1の本稿では、クックパッドマートにおける注文の検証処理(validation)について紹介します。 注文の検証処理とは、クックパッドマート内での用語になりますが、その名の通りユーザーがカートに入れた商品について、「注文可能かどうか」を判定するための検証処理になります。

iTunes Storeのようなデジタルコンテンツの販売の場合、クレジットカードなど決済手段に問題がない限り「注文できるかどうか」の検証処理に複雑なケースはあまり多くなく、せいぜい販売上限数を考慮するくらいではないかと思います。 一方で、物流が絡むECにおいては、現実世界での非常に細かな制限が多く絡み、注文の検証処理には多くのことを考慮する必要があります。

ここからは、クックパッドマートにおける注文の検証において、どのようなことを考慮しているかをご紹介します。

前提

まず、クックパッドマートにおけるデータの関連性についてご紹介します。 説明を簡略化するために実際の概念とは一部異なるものもありますが、注文処理においては以下の概念が重要な登場人物になります。

product

  • いわゆる「商品」の概念
  • ユーザーが購入し、販売店や生産者の方に用意いただく食材を指す

location

  • クックパッドマートにおける「受け取り場所」の概念
  • ユーザーは常に1つの受け取り場所を指定している

delivery

  • location毎の注文締切、受け取り開始、受け取り終了などの「配送スケジュール」
  • たとえば「4/8 2:00注文締切」「4/8 16:00 ~ 23:00 商品受け取り可能」などのデータを持つ

order

  • いわゆる「注文」の概念
  • 注文毎に配送スケジュールのdeliveryに紐付き、各orderは複数のproductを持つ

データ間の関連性

rails的に表現すると、このような関連性を持っています。

class Delivery
  belons_to :location
end

class Location
  has_many :deliveries
end

class Product
end

class Order
  has_many :products
  belongs_to :delivery
end

また、商品の集荷配送については、後ほど詳細を述べますが下記のような流れになります。

  • 販売店は注文情報に従い、コンテナに商品を入れて準備
  • ドライバーは指示書に従い、複数の販売店から商品が積載されたコンテナを集荷し、温度管理された配送資材(以下、シッパー)に入れる
  • ドライバーは指示書に従い、複数の受け取り場所でシッパーからコンテナを取り出し、冷蔵庫に設置する
f:id:ryokatsuma:20190405192518p:plain

注文の受付可能数

前述の通り、在庫の概念が無いデジタルコンテンツではなく、実在する販売店や生産者の方に用意いただく商品なので扱える数は有限です。 また、商品の多くはクックパッドマートだけではなく、実店舗でも販売されているので、その数には上限が設けられている必要があります。

そこで、商品には購入可能数が設定されてる必要があります。ここではsales_limit_per_dayとすると、購入可能数を超えているかどうかのチェックはこのようになります。

product.sales_limit_per_day < delivery.orders.where(product: product).count

これで購入可能数の確認は十分でしょうか?答えはNoです。

すべての配送場所を考慮

ある販売店の商品は、1つの受け取り場所だけではなく、N箇所の受け取り場所に配送されます。 言い換えると、あるdeliveryの配送日と同じ配送日の別のdeliveryが存在することになります。 たとえば、4/8配送分の販売店Aの豚肉は、受け取り場所1のユーザーで購入されていなくても、受け取り場所2のユーザーで購入されている可能性があります。

そこで、ある配送日における全配送場所のdeliveryを考慮すると、受付可能数のチェックはこのようにする必要があります。

deliveries = Delivery.find_by(date: delivery.date)
product.sales_limit_per_day < Order.where(delivery: deliveries, product: product).count

これで購入可能数のチェックはOKになりましたが、注文の検証としてはまだ不十分です。

受け取り場所のスペースを考慮

受け取り場所において、注文した商品は冷蔵庫の中のコンテナ内に設置されます。

f:id:ryokatsuma:20190405192532j:plain

受け取り場所の冷蔵庫設置面積は基本的に広げることはできないので、冷蔵庫の数は簡単に増やすことがはできません。言い換えると、受け取り場所における冷蔵庫内のコンテナの数も有限になります。 例えば、注文商品の種類が多く、コンテナに空きスペースが存在しない場合は、冷蔵庫に商品を設置することができなくなるので、配送しても冷蔵できない状態になるので注文を受け付けてはいけないことになります。

そのため、「注文しようとしている商品は受け取り場所のコンテナに設置できるかどうか」を判定する必要があります。 そこで、クックパッドマートでは、販売商品1つづつ簡易的に体積を測定し(縦x横x高さ)、注文しようとしている商品の体積は、受け取り場所のコンテナの総体積に収まるかどうかを確認しています。

f:id:ryokatsuma:20190405192553p:plain

実際は、コンテナは配送のオペレーションの観点で販売店や生産者ごとに分けられている(複数の販売店, 生産者の商品が1つのコンテナに混在することは無い)ので、冷蔵庫の中で

  • 空きコンテナを確保できるか
  • コンテナの中に空き容量があるか

を判定しています。

ContainerBox.find_available(
  shop: product.shop,
  delivery: delivery,
  capacity: product.volume
).any?

ちなみに、コンテナの空き容量確認で現在は体積を利用していますが、ここは議論の余地があると考えています。 たとえば、商品は上積み(= 商品の上に商品を載せる)を禁止させたほうが商品が痛むリスクを減らすことができるはずですが、「商品に上積みすれば体積的にはコンテナに積載可能」な状態になってしまいます。そのため、本質的には「商品の接地面積を測定し、その総数がコンテナの設置面積を超えなければ積載可能」とでもしたほうが良いと考えています。 ただ、実際は商品それぞれ接地面積を簡単に測定することは難しいため、オペレーションコストを考えて体積を利用して運用を行っています。

さて、これで注文の検証は十分でしょうか?

配送時に温度を考慮

クックパッドマートで扱う商品は、商品の品質を下げないようにするために「予冷品(肉や魚など)」「未予冷品(野菜など)」「常温品(パンなど)」と複数の温度カテゴリに属し、各温度帯の商品はそれぞれ別のシッパーで配送を行っています。

f:id:ryokatsuma:20190405171806j:plain

たとえば、(これは実際に自分たちで配送実験を行うことで初めて理解したことなのですが、)夏の朝採れ野菜は相当高い温度に上がっています。この野菜を肉や魚など予冷品と同じシッパーで配送した場合、シッパー内の温度が上がってしまい、予冷品の品質に大きな影響が出ることが分かりました。部内では夏の朝採れ野菜温度高すぎ問題として有名な問題です。

また、パンも配送時に冷やしすぎると(約-2度〜5度)、小麦のデンプンがアルファ化老化して品質に大きな影響が出ることが知られています。 このように、商品の配送時にはその商品の「温度帯ごとに分けたシッパー」で配送することが、商品の品質保持の観点で必要になります。

一方で、各ドライバーが配送できるシッパーの積載量は有限です。 先の冷蔵庫の例と同じく、ドライバーも当日急に多く確保することは難しいので、あらかじめ確保したドライバーたちの車の積載量を超えるシッパーは配送できません。 つまり、1ドライバーが配送できるコンテナの総量も有限になるので、注文対象商品の温度カテゴリのシッパーに余裕があるかどうかを判定する必要があります。 注文対象商品の集荷配送を行うドライバーはあらかじめ決定できるので、このように書けます。

Shipper.find_available(
  driver: routing.driver,
  delivery: delivery,
  temperature_category: product.temperature_category
).any?

これらすべての確認を行うことで、「この商品は注文可能かどうか」が判定できます。なかなか道が長いですね。

まとめ

クックパッドマートにおける現在の注文の検証処理について解説を行いました。 実際はさらに細かな考慮がもう少しありますが、概ねこのような確認を注文処理の直前に行っています。 「かなり複雑すぎる処理をしているな、、」と思われた方もいるかと思いますが、実際に実装している僕もそう思います。

また、これらの処理はどんどんアップデートを行っています。 実際、温度カテゴリも最初は全く考慮せずにすべて冷蔵で配送していたものが、パンの扱いが出てきたことで2つの温度カテゴリになり、 夏の朝採れ野菜温度高すぎ問題に対応するために3つの温度カテゴリに対応しました。 このように現実に起きる問題に柔軟に対応するためは、的確にオブジェクト指向プログラミングで設計、実装を行う必要があり、ここは難しくも楽しいところでもあります。

一方で、このような複雑な処理も、おいしい食材をユーザーさんに確実に届けるために必要な条件だと考えています。 実際、リリースしてから半年近く経過していますが、冷蔵庫に設置できないなどの配送事故や、商品が傷んでいたといった事故は起きずに済んでいるので、引き続きおいしい食材を安全に届けるために技術によるサポートを行っていきたいと思います。

この記事を通して、クックパッドマートのサービス開発にご興味を持っていただけた方がいらっしゃいましたら、ぜひ一緒にサービスを作りましょう!

www.wantedly.com

お知らせ

クックパッドマートでは、4/24(木)に、買物事業部のエンジニアによる発表とエンジニアとのミートアップを開催予定です。

cookpad.connpass.com

今回の記事のような生鮮ECそのものの仕組みや、流通の仕組みの開発に興味がある方、クックパッドマートのエンジニアと直接話してみたい方はぜひご応募ください!お待ちしています!

/* */ @import "/css/theme/report/report.css"; /* */ /* */ body{ background-image: url('https://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527163350.png'); background-repeat: repeat-x; background-color:transparent; background-attachment: scroll; background-position: left top;} /* */ body{ border-top: 3px solid orange; color: #3c3c3c; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; line-height: 1.8; font-size: 16px; } a { text-decoration: underline; color: #693e1c; } a:hover { color: #80400e; text-decoration: underline; } .entry-title a{ color: rgb(176, 108, 28); cursor: auto; display: inline; font-family: 'Helvetica Neue', Helvetica, 'ヒラギノ角ゴ Pro W3', 'Hiragino Kaku Gothic Pro', Meiryo, Osaka, 'MS Pゴシック', sans-serif; font-size: 30px; font-weight: bold; height: auto; line-height: 40.5px; text-decoration: underline solid rgb(176, 108, 28); width: auto; line-height: 1.35; } .date a { color: #9b8b6c; font-size: 14px; text-decoration: none; font-weight: normal; } .urllist-title-link { font-size: 14px; } /* Recent Entries */ .recent-entries a{ color: #693e1c; } .recent-entries a:visited { color: #4d2200; text-decoration: none; } .hatena-module-recent-entries li { padding-bottom: 8px; border-bottom-width: 0px; } /*Widget*/ .hatena-module-body li { list-style-type: circle; } .hatena-module-body a{ text-decoration: none; } .hatena-module-body a:hover{ text-decoration: underline; } /* Widget name */ .hatena-module-title, .hatena-module-title a{ color: #b06c1c; margin-top: 20px; margin-bottom: 7px; } /* work frame*/ #container { width: 970px; text-align: center; margin: 0 auto; background: transparent; padding: 0 30px; } #wrapper { float: left; overflow: hidden; width: 660px; } #box2 { width: 240px; float: right; font-size: 14px; word-wrap: break-word; } /*#blog-title-inner{*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-position: left 0px;*/ /*}*/ /*.header-image-only #blog-title-inner {*/ /*background-repeat: no-repeat;*/ /*position: relative;*/ /*height: 200px;*/ /*display: none;*/ /*}*/ /*#blog-title {*/ /*margin-top: 3px;*/ /*height: 125px;*/ /*background-image: url('https://cdn-ak.f.st-hatena.com/images/fotolife/c/cookpadtech/20140527/20140527172848.png');*/ /*background-repeat: no-repeat;*/ /*background-position: left 0px;*/ /*}*/