TECH PLAY

dely株式会社

dely株式会社 の技術ブログ

236

こんにちは。クラシル開発のAndroidチームに所属しているnozakingです。 先日、Androidチームに新しい仲間が加わったので、 wevox values card を用いてメンバー間で価値観を共有しました。 実施した様子や結果を載せてみましたので、少しでもクラシルのAndroidチームに興味を持っていただけたら嬉しいです☺️ wevox values cardとは? wevox values cardとは、カードを使って価値観を引き出せるものです。 wevox.io オンラインでもできるようですね。 今回はオフラインのカードで実施しましたが、メンバー同士顔を合わせながらやることでカードを引いたときや捨てるときの表情や発言からも個性が感じられて良かったです。 目的 今回チームに新しい仲間が加わったということで、 メンバー間で価値観を共有してお互いに理解を深めること が目的です。 以前もメンバーが新加入したタイミングでやりましたが、今回もチームメンバー全員でやります。 改めてやると、前回と違う面が知れたり理解が深まるので楽しいです。 いざスタート! 下記のような感じで進めていきます。 色々な価値観が書かれたカードを5枚ずつを配り、残ったカードは山札に。 山札からカードを1枚引いて、手札の中から自分の価値観から一番遠いと思う1枚を捨てる。 ↑を山札がなくなるまで繰り返す。 最後に手元に残った5枚がその人が大事にしている価値観となる。 何を捨てるのかもその人の個性が出ていて興味深かったです。 結果 今回実施した結果をAndroidチーム6名分載せちゃいます! まずは今回の新メンバーである みうらさん の結果はこちら。 なんとなく明るいカラーリングも みうらさん のやわらかさを表現している気がしますね! 次に、我らがAndroidチームの長である うめもりさん の結果はこちら。 "愛" っていいですね! その他のメンバーはこんな感じでした。 それぞれ個性を感じますね。 おわりに この記事を通じてクラシルのAndroidチームに興味を持っていただけたでしょうか? さらに興味を持っていただくため、今後もいろいろと発信していけたらと思っています。 クラシルAndroidを知るならアーキテクチャに触れている下記の記事も参考になるので是非読んでみてください! tech.dely.jp クラシルのAndroidチームでは、生産性の高いアーキテクチャを維持・アップデートしつつも楽しくプロダクト開発できる人、したい人を求めています。 今回の記事を読んで興味が出てきた方、是非お話ししましょう! 下記の採用リンクから応募いただいてもOKですし、Twitterから気軽にDM頂けてもOKです! dely.jp Tweets by dely_developers twitter.com どうぞよろしくお願いします☺️
アバター
iOSエンジニアの石田です。 WWDC2021では、ConcurrrencyにまつわるSwiftの新しい文法やSwiftUIの新しいViewが紹介されました。 本記事では、それらを使ったアプリを作りながら、新機能を紹介したいと思います。 作るアプリは、GitHubの API を使って、ユーザの画像をUICollectionViewのような形式で表示するものです。 ユーザ情報は https://api.github.com/users から取得でき、ユーザ画像は avatar_url で定義されています。 async/await まず、API経由でユーザ情報を取得します。 URLSessionを使って非同期通信を行う場合、コールバックでレスポンスを処理していました。 しかし、Swift5.5で追加されたasync/awaitを使うことでコールバックが不要になります。 async/await自体は他の言語でも実装されている文法で、非同期処理をより簡潔に記述することができます。 以下が例となります。 簡単のためにエラーハンドリングなどは全て無視しています。 func fetch () async { let users = try ! await fetchUsers() } func fetchUsers () async throws -> [User] { let url = URL(string : "https://api.github.com/users" ) ! let (data, _) = try await URLSession.shared.data(from : url ) let users = try JSONDecoder().decode([User]. self , from : data ) return users } 関数の返り値として非同期処理の結果を返すことができます。 URLSessionの処理をawaitをつけて呼び出し、関数自体をasyncで定義しています。 asyncをつけた関数を呼ぶ際は、awaitをつけて呼びます。 async/awaitを利用することでコールバックを使うことなく、同期処理と同じ記述で非同期処理を書くことができます。 AsyncImage もともとSwiftUIにはImageという画像表示のためのViewがありましたが、URLを直接入力することはできず、HTTP通信で画像をダウンロードする場合はひと手間加える必要がありました。 Xcode13で登場したAsyncImageではそういった手間は不要で、URLを入力すると画像が自動でダウンロードされます。 使い方は極めてシンプルで、例えば以下のようになります。 AsyncImage(url : URL (string : "https://example.com/icon.png" )) プレースホルダーを指定し、画像をリサイズする場合は以下のようになります。 AsyncImage(url : url ) { image in image.resizable() } placeholder : { ProgressView() } .frame(width : 100 , height : 100 ) AsyncImageの引数はURL?型なので、unwrapすることなく気軽に使えるのも良いところだと思います。 async/awaitとAsyncImageを使ったサンプル 上述したasync/awaitとAsyncImage、そしてLazyVGridを使ってGitHubのユーザ画像をUICollectionViewのように2カラムでユーザ画像を表示します。 import SwiftUI struct User : Decodable , Identifiable { let id : Int let avatarUrl : String } struct ContentView : View { @State var users = [User]() let columns : [GridItem] = Array(repeating : . init (.flexible()), count : 2 ) var body : some View { ScrollView { LazyVGrid(columns : columns ) { ForEach(users) { user in AsyncImage(url : URL (string : user.avatarUrl )) { image in image .resizable() .scaledToFill() } placeholder : { ProgressView() } } } } .task { users = try ! await fetchUsers() } } private func fetchUsers () async throws -> [User] { let url = URL(string : "https://api.github.com/users" ) ! let (data, _) = try await URLSession.shared.data(from : url ) let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase let users = try decoder.decode([User]. self , from : data ) return users } } API通信など含めても非常に簡潔に書くことができました。 エラーハンドリングを全て無視しているので実際はこんなに少ない行数にはなりませんが、コールバックがないため見通しが良くなっていると思います。 まとめ 簡単なアプリの実装を通して、Xcode13で追加された機能を紹介しました。 iOS15からの機能なので、iOS14をサポートしている間は使うことができないのですが、使うのが非常に楽しみな機能になっています。 async/awaitの登場によってRxSwiftやCombineがどのように捉えられ、使われていくのかもウォッチしていきたいところです。 TRILLでは、今後さらにサービスを大きくすべくアップデートを進めており、エンジニア、デザイナー、PdMを積極採用中です。 もし興味がありましたら、気軽にアクセスしていただければと思います。 dely.jp
アバター
こんにちは、今回は技術的な話ではなく、TRILL開発部で行っている勉強会などの紹介をしたいと思います。 社内勉強会 TRILL開発部では週1で社内勉強会を行っております。 参加メンバーはTRILL開発部メンバーということで、サーバサイド、Webフロントエンド、スマホネイティブアプリ、PdMなど、領域をまたいだ参加者での勉強会です。 (もちろんこれとは別でクラシル・TRILL合同のiOS勉強会など領域特化の勉強会もあります) 勉強会で扱う題材も様々で、 BigQueryを使ったワークショップ 実装した機能の設計について 最近導入した計測ツールの話 dockerについて など、特定の領域にそれほど特化しない内容なのですが、聴講して終わりではなく議論の時間も多く取っているのが特徴だと思います。 技術的な会話は、同じ領域のエンジニア同士 (例えばWebフロントエンドエンジニア同士) だと日常的に行われると思うのですが、領域を超えるとなかなか普段の会話では機会が少なかったりします。 この勉強会では、領域をまたいだエンジニアが参加することで、チームにおける広い知見の獲得を目指しています。 英語ランチ また、delyはグローバルチャレンジを行っており、外国人採用も進めております。 TRILLにはまだ (2021年6月現在) 海外出身の社員はいないのですが、今後積極採用していくにあたり、昼食の時間などを使って英語縛りランチを行ったりしています。 これは開発部というよりは、TRILL内有志での開催を行っております。 英語は話せないよりは話せるほうが良いのですが、機会がないとなかなか勉強しなかったりするので、こういう機会を設けてモチベーションを上げています。 まとめ 今回はTRILL開発部で行っている社内勉強会などの紹介をしました。 TRILLは月間利用者数が数千万人に及ぶ巨大なサービスなのですが、エンジニアは5人以下と少数精鋭チームで運用しております。 少人数のチームということで領域間の垣根も低く、勉強会などを通してチーム全体のスキルアップに励んでいます。 TRILLでは、今後さらにサービスを大きくすべくアップデートを進めており、エンジニア、デザイナー、PdMを積極採用中です。 もし興味がありましたら、気軽にアクセスしていただければと思います。 dely.jp
アバター
こんにちは!dely 開発部でクラシルのサーバーサイドエンジニアをやっています @_kobuuukata です!👩🏻‍💻 コロナの緊急事態宣言の影響で、 dely もリモートワークを採用しています🏡 そこで今回は、クラシルのサーバーサイドエンジニアがリモートワーク時、どんな1日を過ごしているのか紹介したいと思います! リモート時の1日のスケジュールはこんな感じです↓ リモート時のスケジュール 9:00 出社 dely では「フレックス制」を採用しています。 フレックス制と聞くと、コアタイム以外は自由に出勤していいというイメージがあるかもしれませんが、dely では、何時から何時の間で働きます!というのを事前に申告する形をとっています。(なお、前日までなら変更OK!) ちなみに私はリモートの時は9時から、出社の時は10時からにしてます。 出社後は、Slack チャンネルに今日のやることを書いていきます(以下イメージ) 2021/05/31 - 現在取り組んでいるメインissue:〇〇機能改修(〜5/31) - TODO - [ ] 〇〇機能改修の実装 - [ ] 問い合わせの不具合調査 - [ ] PRレビュー - MTG   - [ ] 朝会   - [ ] サーバーサイド採用定例   - [ ] サーバーサイドMTG   - [ ] 1on1 9:00 PRレビュー/レビューコメントの対応 始業開始はまず、溜まっているPRレビューを見たり、自分が出したPRレビューのコメントが来ているものを対応してます。 朝は比較的 Slack 通知が飛んでくることが少ないので、この時間で集中して確認することが多いです。 最近はサーバーサイドのメンバーが増えてきたこともあり、1日で 3~5 件くらいの新規 PR が飛んできます。 10:00 Squad 朝会 各 Squad の朝会は 10:00 開始が基本ルール。メンバーが Squad を兼務しないという原則に基づくようにしています。 リモートの日は meet を使って朝会を実施しています。 私の所属する Squad では、PdM/デザイナー/Android/iOS/サーバーサイド/フードメンバーが参加し、朝会を行っています。朝会では、細かいタスクの進捗状況を確認するというよりは、リリースに向けた仕様の共有や分析結果の共有など Squad 全体に関わることを共有します。 Squad 体制とは?dely 開発部がなぜ Squad 体制を採用したのか?についてはこちらの記事をご覧ください↓ blog.tsubotax.com 10:15 Squad サーバーサイド内での職種朝会 私の所属する Squad は少し他 Squad よりもメンバーが多く、現在インターン生も含め5名のサーバーサイドエンジニアが1つの Squad に所属していることもあり、Squad 朝会が終わったあとに Squad 内のサーバーサイドメンバーでタスクの進捗状況を共有します。設計や実装で困ったことがあれば、ここで相談します。 10:30 サーバーサイド仕様書の作成 新規機能開発を行うときは、まずサーバーサイド仕様書を作成していきます。 本番環境で発生したバグ改修以外はこの仕様書を作成するルールとなっています。 サーバーサイド仕様書はサーバーサイド(Rails)以外にも、AWSの環境設定などが必要になる場合もあるので、設計漏れがないかSREメンバーにもレビューしてもらいます。 API エンドポイントのレスポンスは Android/iOS のメンバーと相談し、合意をとっておきます。 単にどう実装するのかを記載するだけではなく、なぜその機能を実装する必要があるのか?といった観点も含まれており、なるべく後戻りなく負債として残らないようにしています。 サーバーサイド仕様書テンプレート 13:00 お昼 リモートの時は自炊することが多いです。 クラシルでは、リモートの合間にササッとできるレシピも紹介されているので、ぜひみなさん作ってみてください👩🏻‍🍳 お昼を食べたあと、天気がいい日はベランダで日向ぼっこするのにハマっています😎🌴 14:00 コードの実装 サーバーサイド仕様書のレビューが通ったら、いよいよ実装していきます。 API を実装する際は、API 作成ルールがドキュメントにまとめられているので、入社したてでもあまり迷うことなく実装できると思います! また、リモートの際は、discord というツールを使っていて、何か困ったことや相談ごとあれば気軽に相談できるようにしています。 16:30 Rails エンジニア採用定例 dely では、採用も各職種ごとに KPI を持ち、メンバー全員が採用に携わっています💪 サーバーサイドでは Rails エンジニアの新卒・中途採用の進捗状況を確認し、KPI 達成に向け、改善を繰り返しています。 17:00 サーバーサイドMTG Squad 体制になり、サーバーサイドメンバーで集まって話す機会が減ってしまったため、サーバーサイドメンバーで課題や共有事項などを話す会です。 なぜこれをやることになったのか?具体的にどんなことをやっているか?については、こちらの記事に書かれているので、是非ご覧ください! tech.dely.jp 17:30 1on1 チームの上長によって頻度は異なりますが、私のチームでは2週間に1度のペースで 1on1 をやっています。 事前アンケートで、どんなテーマを話したいか?体調が悪いところはないか?目標の進捗状況はどうか?などの項目を記入し、その内容に沿ってざっくばらんに話してます。 18:00 退社 お疲れさまでした〜!帰宅前に Slack に今日やったことを書きます📝 2021/05/31 - 現在取り組んでいるメインissue:〇〇機能改修(〜5/31) - TODO - [x] 〇〇機能改修の実装 - [ ] 問い合わせの不具合調査 - [x] PRレビュー - MTG   - [x] 朝会   - [x] サーバーサイド採用定例   - [x] サーバーサイドMTG   - [x] 1on1 残業は1日1時間程度で、基本的に定時で帰れることが多いです。 私も入社前は本当に?と思っていましたが笑、本当に 20 時にはほとんど開発メンバーはいません! みんなメリハリをつけて業務を行っているのがクラシル開発部の特徴かもしれません。 おわりに いかがでしたか? 少しでも dely で働くイメージの参考になれば嬉しいです! delyではエンジニアを募集しています dely.jp 22・23卒の方はこちらから dely.jp
アバター
TRILL開発部の石田です。 2段階認証の設定で、QRコードを読み込んで30秒ごとに変わる6桁の数字が生成される、という仕組みをよく見かけます。 今回はSwiftでその2段階認証の仕組みを実装してみました。 2段階認証 (TOTP) とは 2段階認証にはいくつか種類があり、SMS認証やGoogle Authenticatorのようなアプリ認証、YubiKeyのようなデバイス認証の3種類が一般的です。 この記事ではGoogle Authenticatorのようなアプリ認証を実際にiOSアプリとしてSwiftで実装し、中身を紐解いていきます。 Google Authenticatorのようなアプリ認証をTOTP (Time-based One-Time Password) と言うのですが、仕組みとしてはQRコードで16文字の英数字を読み取り、その英数字と現在時刻を使って6桁の数字を生成します。 外観は以下の図のようになります。 16文字の英数字と書きましたが、正確にはBase32文字列であり、自由に設定可能な文字列ではありません。 TOTPの仕組み 2段階認証用のQRコードを解析すると以下のようなURLになっています。 otpauth://totp/Twitter:@username?secret=SH2BZBC3H7DISN6Z&issuer=Twitter QRコード形式の場合は、QRコードと16文字の英数字 (秘密鍵) が表示されるのですが、URLのsecretにあたる部分が秘密鍵となります。 この秘密鍵を使ってワンタイムパスワードを生成します。 具体的には以下のようにパスワードを生成します。 UnixTimeを30で割った値 (小数点切り捨て) をCounterとする 秘密鍵とCounterを使ってHMAC-SHA-1で20byteのハッシュ値を計算する ハッシュ値の下位4bitから符号なし整数Offset (0〜15) を生成する ハッシュ値20byteのうち、Offset番目のbyteから4byteを取り出す 4byteの上位1bitを取り除いたデータを数値にする 得られた数値のうち下位6桁がワンタイムパスワードとなる ちなみに、パスワード生成の過程でハッシュ化やデータの切り取りを行っているので、6桁の数字から秘密鍵を逆算することは不可能です。 TOTPをSwiftで実装してみる 実装にあたり、 CryptoSwift と Base32 を利用しました。 TOTPのコードは以下のようになります。 import Base32 import CryptoSwift // Base32に変換 guard let key = base32Decode( "XXXXXXXXXXXXXXXX" ) else { return } // 1. 現在時刻からカウンターを生成 let unixTime = Int(Date().timeIntervalSince1970) let timeSteps = unixTime / 30 // 8byteのデータに変換 let counter = withUnsafeBytes(of : timeSteps.bigEndian , Array. init ) // 2. HMAC-SHA-1でハッシュを生成 let hash = try ! HMAC(key : key , variant : .sha1).authenticate(counter) // 3. hashの下位4bitを整数に変換 let offset = Int(hash.last ! & 0b00001111 ) // 4. hashのoffset番目のバイトから4バイト取得 var slicedHash = Array(hash[offset ... offset + 3 ]) // 5. 4byteの上位1bitを取り除いたデータを数値にする slicedHash[ 0 ] = slicedHash[ 0 ] & 0b01111111 let num = Data(slicedHash).withUnsafeBytes { $0 .load( as : UInt32.self ).bigEndian } // 6. 下位6桁を取得 let totp = String(num).suffix( 6 ) print(totp) 普通のiOSアプリの実装ではあまり見かけない文法がありますが、文字列に秘密鍵を入力すると、QRコードを読み込んだ2段階認証のアプリと同じ値となると思います。 まとめ Swiftで2段階認証 (TOTP) を実装しました。 普段何気なく使っている2段階認証ですが、中身を紐解くと色々と気付きがあり、またSwiftでのビット演算など普段あまり使わない文法も知れて勉強になりました。 delyでは全方面でエンジニアを積極採用中です。 興味のある方は是非お声がけください。 dely.jp
アバター
こんにちは delyサーバーサイドエンジニアの望月 ( @0000_pg )です 4月になり、春の季節がやってきました 新入生・新社会人の皆さん、おめでとうございます🌸🌸 今回は技術的な内容ではなく、せっかく春なので 新社会人となり、エンジニアとして働く皆さんや これからエンジニアとして働いてみようかなと思っている皆さんに向けて ポエム的な内容でお送りします😉🌸 はじめに delyにおける新卒採用 エンジニアという仕事 "技術力" がなくてもできることはある "窓口" になる リポジトリのドメイン知識をつける とにかく模倣する コードを読む、動かす、つくる 他のメンバーを頼る 色んなことに首をつっこみ、チャンスは掴み取る やってみてげんなりすることを繰り返す おわりに はじめに 2020年の7月頃から サーバーサイドエンジニアにおける中途採用のカジュアル面談や 1次面接を担当してきました また、今年からは ー部、新卒採用でも話しをさせてもらったりしています 多くのエンジニアの皆さんとお話しさせてもらい 対話のなかで、自分自身の考えもアップデートさせてもらっている感覚があり とても有意義な時間だなと感じています delyにおける新卒採用 delyにおいても20・21卒 (そして22卒) の方が入社を決めてくれて パワーや可能性や希望のようなものに日々、圧倒されています✨✨✨ 自分もdelyに中途入社した時は彼ら、彼女らと そこまで変わりない年齢だったのですが 当時の自分と比べて、本当にしっかりしているな・・と感じます💦 エンジニアという仕事 エンジニアという職業は本当に面白いなと思います 自分がエンジニアになろうと思ったのも、モノづくりを通して 社会に対して、直接影響を与えられる ということに面白味を感じたからです どんなプロダクトをつくっていたとしても それを利用する"者"がいて、 エンジニアリングであらゆる問題を解決、効率化していくのが 仕事というのは面白いなと思います クラシルで言うと、SNSなどを通してユーザーさんの声が届きます そういった自分 (たち) がつくったモノへの嬉しい言葉が励みになったりします delyにおいては、エンジニアは "実装者" で終わることはなく 自らが主体性を持って意見や要望を述べ プロダクトを開発していく必要があり、それも面白い所かなと思います "技術力" がなくてもできることはある エンジニアになりたての頃は、 「なんで自分はこんなこともできないのだろう」 「 こんなことも知らない自分は・・」 などと自分に失望したり、落胆する瞬間が必ずあります でも、それが当たり前というか それでいいんじゃないかと思います なので開き直っていくのが大切だと思います 自分は藤倉成太さんのこの記事がとても好きで いまでもふと思い出したときに読んでいます type.jp 一部を引用します 「世界を変えたいとかいうのに、なぜか『修行宣言』をする人が多いんですよ。いつか技術が身に付いたらやります、とか、20代のうちは修行します、なんて。少しでもコードが書けるなら、10年後じゃなくて今始めればいいじゃないですか。確かに技術力は必要だけど、それがなくてもできることはいくらでもある。例えばチームで開発しているなら、こぼれ落ちた簡単なタスクをどんどん拾って、チームにどう貢献するかを考えることの方が大事です。 技術力だけが世の中を変える武器だというのは大間違いだし、いつまでもアマチュア気分でのんきなことを言っている場合じゃない。先輩のコードを読み書きしたり、コードレビューのやり取りを復習したり、現場で飛び交う言葉を調べることからだって始められるわけです。『エンジニアは技術力がなければ、何者にもなれない』なんて、まずスタート地点が違うんじゃないかと思います」 "技術力" というものは "経験" によって培われるものなので、 はじめから、技術力がある人はいないし そこまで気にせずやればいいと思います そして 自分のなりたい理想像と、現在の自分にギャップがあるなら 徐々にそのギャップを埋めていけばいいと思います "窓口" になる 上の話につながるのですが、 自分はdelyに入社して、とにかく "窓口" になることにしました 日々の業務のなかでくる依頼をすべて自分に集約するということです (もちろん属人化という意味ではないです) 自分が窓口になることで、 他部署の人とも強制的にコミュニケーションが必要になり 接点ができて自分にもメリットがありました また、困ったときに頼ってもらえる存在になりたいな という気持ちも同時に生まれました リポジトリのドメイン知識をつける ドメイン知識を付けることはやったほうがいいなと思います 前述の自分が窓口になることで 更にドメイン知識を付ける機会がありました 現状クラシルのサーバサイドにおけるコードがどうなっているか 全体の70%ぐらいは把握しています ドメイン知識を付けることで困りごとが生じた際に なにか手助けができる存在にもなれるし、実装の拡張を行う際にも役立ちます とにかく模倣する これはもう散々聞いた話ではあると思うのですが 周りに自分のレベルを遥かに超える人がいたりします そういう人の技術をとにかく模倣したり、盗むことが大切だと思います コードレビューや実装をみてどんどん自分のものにすればいいかなと思います 自分もクラシルは経験上、最も大きなプロダクトだったので delyに入社して、学ぶことがたくさんありました コードを読む、動かす、つくる 上の話の続きなのですが、やっぱりコードを読むことが 一番いいかなと思います GitHubのOSSを色々検索してコードリーディングしたり 自分はAPIを使って色々つくるのがすきなので OSSのAPI Client のコードを読むことが多いです ライブラリの中身をみるのもいいです (例えばRubyであれば、gemの中身など) gem特有のメソッドが、中身をみたら とても簡単な処理であることもあります また、OSSコントリビュートするのもいいと思います コントリビュート手順を読まないといけなかったり CLAに署名したり、適切に追従したり 英文でPRを出したり 派生元ブランチを適切に指定して切ったり PRを出すだけでも学ぶことがたくさんあると思います 他のメンバーを頼る 自分が失敗したなぁと思うのが、周りに頼ることができなかったという点です 入社したての頃は 何もできないのに、周りに頼ることもできませんでした ただ、当時は他のメンバーはもっと難易度の高い課題に取り組んでいたり チームとしても小規模でした (完全な自走が求められるなかで、自分自身の技術力や経験が不足していた😥) いまはサーバーサイドチームも2倍の人数になったので tech.dely.jp サーバーサイドMTGをおこなって、 色々なコミュニケーションが取れる機会をつくっています 結局のところ、自分ができることは限られているし 色んな人の助けを借りながらやったほうが 最終的に早く、うまくいったりします 色んなことに首をつっこみ、チャンスは掴み取る なるべくチャレンジする機会があれば、 手を挙げるように心がけています 多少失敗しても、得られることのほうが圧倒的に多いです 自分も入社して初めて挑戦したことが多くあります 色んなことに、首をつっこむと多くの人と関わることになって 自分の存在を知ってもらえるし、 "信頼を得る" ことができます この、"信頼を得る" ということが大事かなと思っていて 「この人ならこの業務を任せてもいいだろう」 とか 「この人に聞けばわかる」 という存在になることです 自分も "信頼" を稼いでる途中です😉💰 やってみてげんなりすることを繰り返す やってみてげんなりすることを繰り返して成長するというか老成する.やらないで斜に構えて自分の陣地を守っていると老化する.圧倒的成長! とか言ってればきっと若いまま何もしないでいられる.|落合陽一 @ochyai #note https://t.co/P0nTDala0Q — 落合陽一 Yoichi OCHIAI (@ochyai) 2020年3月1日 落合陽一さんのnoteのタイトルからとりました この言葉が好きで、本当にその通りだなと思います 自分も、いま思い出すとあまりに無様すぎて 笑うしかないような瞬間、瞬間が多くありました ですが やらないで斜に構えて自分の陣地を守っていると老化するし、 圧倒的成長! とか言っていれば きっと若いまま何もしないで終わってしまうのだと思います やってみて、色んな失敗をして、 自分自身の実力の無さに、げんなりしたり 絶望的な気持ちになることを 繰り返していくことで、"成長" できるのだろうと思います おわりに delyではエンジニアを募集しています dely.jp 22・23卒の方はこちらから dely.jp
アバター
こんにちは、開発部の高橋です。 2020年10月頃から「クラシルサーバーサイドMTG」と呼ばれる、クラシルのサーバーサイド内で定期的に集まって話しあう取り組みを行っています。 今回はこの取り組みの経緯や取り組み方などについてご紹介します。 経緯 開催方法 話すトピックに関して やってみた所感 新たに生まれた課題 終わりに 経緯 クラシルサーバーサイドMTGを始めた経緯に話すには、まずクラシルの開発体制の変化について説明する必要があります。 開発体制変化の内容や意図などに関して知りたい方は、以下の記事を読んでもらえると分かりやすいかと思います。 blog.tsubotax.com クラシルの開発体制は左の集中型組織での開発から、2020年4月頃に右のSquad体制と呼ばれる職能横断的な開発体制に変化しました。 この体制変化により、元々は職能別に決定されていた座席や目標設定などがSquad別に決定され、業務時間のコミュニケーションの大半はSquad内で行われる形になりました。 これにはプロダクト開発という面で見ると、解決したい課題に対する把握や深い議論でき、チームワークが発揮しやすいというメリットがあると思います。 しかしながら、この開発体制の変化によって発生した課題もあります。 Squad体制化されたことにより座席も離れ、以前より職能別チームとしての感覚が薄れたことで、サーバーサイド同士のコミュニケーションが以前にも増して減ったと自分は感じていました。 これにより、サーバーサイド同士でお互いが何をやっていかを把握しづらくなったり、 各々が抱えている課題を相談しづらくなったりしているのではないかという危機感が芽生えました。 そこで他のサーバーサイドにも聞いてみたところ共感を得られたのでやってみようということで始めた、というのが大まかな経緯です。 開催方法 現状では、サーバーサイドメンバーがそれぞれトピックを持ち寄って、そのトピックについて30分という時間の中で話し合う形で行っています。 持ち寄り方としては、週に1回トピックを投稿するためのSlackスレッドを用意し、そこに書き込んでもらう方式にしてます。 (ただ、この方式だと新しく参画したメンバーが過去のログを追いにくいなど課題が出始めているため、別の方法を検討中です。) スレッドの様子(モザイクばかりになってしまった...) 投稿フォーマットは自由で、情報共有ツールで書いてそのURLを共有するパターンもあれば、スレッドに内容を直接書くようなパターンもあります。 話す順番はスレッドをトピックに書き込んだ順番にしており、 もし全員話しきれなかった場合は、次回は話せなかった人から優先的に話すというルールにしています。 話すトピックに関して 設計・実装など技術的な話題 プロダクトの方針や施策的な話題 プロジェクトの進め方などの話題 その他困っていること・話しておきたいこと など、トピックに関しては開発やチームに関することであれば何でもOKとしてます。 実際に話されたトピックの例を外に出せる範囲で紹介すると、例えば以下のようなものがありました。 各Squadの今後のロードマップの話 新機能のDB設計で困っている話 参加した技術イベントで得た知見共有 古の実装について有識者に聞く Pull Requestレビュールールに関する相談 プライベートでグラフデータベース触ってみた話 発生した障害に対する振り返り リファクタリングの方針相談 Railsに追加された新機能について話す など やってみた所感 始めてから今4〜5ヶ月ほど経ちましたが、新たな課題は発生しつつも今の所うまくいっているように見えます。 以前よりも各々の状況を把握しやすくなりましたし、サーバーサイド内で相談しやすい雰囲気づくりに貢献できてるかなと思ってます。 とはいえ自分の感想だけだと単なる自己満足になってしまうので、他のサーバーサイドメンバーにも感想を聞いてみました。 各自のやっていることが可視化される 各自の課題に感じていることが可視化される 週一で相談できる場があるという安心感 サーバーサイドエンジニア同士でコミュニケーションを取る機会になり、それ以外の場面でも相談しやすくなる Squad体制になってから参画した身としてはこのMTGがあったおかげで馴染めた 全員の意見を聞きたい軽い提案や相談がしやすい(全員のスケジュールを別途抑えるのはハードル高いので) ここで各Squadの共有がされる場合が多いので、PRレビュー時のコンテキストの理解がしやすい 新たに生まれた課題 現在クラシルは各種エンジニアを大募集中で、それに伴ってサーバーサイドエンジニアの人数も急拡大しています。 これによって30分という時間だと相談しきれなかったり、MTG話せないメンバーなどもでてきてしまっているなどの問題も発生しています。 かといって単にMTGの時間を延ばせばよいかというと、必然的に30分のときよりも消費コストに対するパフォーマンスが落ちやすくなってしまうため悩ましいところです。 ここに関しては正直まだ模索中で、メンバーと相談しつつ改善できたらよいと思っています。 終わりに 先にも書きましたが、クラシルではサーバーサイドを始めエンジニア・デザイナーを絶賛募集中です! 興味があれば是非以下を覗いてみてもらえると嬉しいです! https://join-us.dely.jp join-us.dely.jp
アバター
こんにちは。dely開発部にてクラシルのAndroidエンジニアを担当しているnozaです。 月日の流れは早いもので、前回の記事から間があいてしまいましたね。 先月、Material Components for Androidのバージョン1.3.0 *1 が公開されましたね。 主な内容として下記のComponentが追加されてます。 MaterialTimePicker ProgressIndicator 個人的にはProgressIndicatorを待ち望んでいました。 というのも、Android SDKで提供されているProgressBarだと、思った通りに手軽にプログレス表示するのが難しかったからです。 今回はProgressBarのモヤっとポイントと、ProgressIndicatorだとどんなプログレス表示が実現できるのかを紹介していきます。 ProgressBar *2 Android SDKで提供されるもので、標準の設定で円形にしたり水平バーにしたりできます。 画面内のコンテンツを取得するための通信中などに用いることが多いと思います。 Android版クラシルでは、画面内のコンテンツ表示の邪魔をしないようにToolbarの下にバーの形状で設置しようとしました。 こんな感じにしたいの図 しかしながら、お手軽に実現できないモヤっとポイントがいくつかあったのです。 ProgressBarのモヤっとポイント バーの上下の余白 indeterminate=true の場合、標準の設定で下記のようなアニメーションが実現できます。 しかし、このアニメーションで設定されるVectorDrawable(API Level21未満だとpng画像)自体に上下の余白が入っています。 Viewの範囲をわかりやすくするために 背景色#dddddd を入れてみると・・・ このようにViewの描画領域とバーの間に余白があることがわかります。 意図した場所に配置するためにはこの余白を考慮して工夫する必要がありました。 工夫の例をいくつか挙げてみます。 例1:自前でDrawableを用意して、余白をコントロールする。 標準で用意されているDrawableに余白が含まれているなら、自前で作成してしまえば良いです。 が、VectorDrawableやアニメーションなど用意するものが多く、ある程度知識も必要になるため面倒くさいです。 ※ progressDrawable 、 indeterminateDrawable というattributeで設定可能 例2:上下のマージンに程よいマイナス値を設定してごまかす <ProgressBar style = "@style/Widget.AppCompat.ProgressBar.Horizontal" android : layout_width = "match_parent" android : layout_height = "wrap_content" android : layout_marginTop = "-6dp" android : layout_marginBottom = "-6dp" android : indeterminate = "true" /> marginにマイナス値を設定することで余白分を無くしています。 が、小手先で対応している感じがあってモヤっとします。 例3:layout_heightとscaleYでごまかす <ProgressBar style = "@style/Widget.AppCompat.ProgressBar.Horizontal" android : layout_width = "match_parent" android : layout_height = "4dp" android : indeterminate = "true" android : scaleY = "4" /> 高さ 4dp にすることで、バーの表示が縮んでしまうので scaleY="4" で引き伸ばしています。 上下に余計な余白は入っていませんが、縮めて引き伸ばしているためボケてしまっています。 かっこ悪いですね。 といった感じで、お手軽でしっくりくる解決ができずモヤっとしていました。 表示/非表示時の切り替え 例えば通信中にはProgressBarを表示し、通信完了したら非表示にしたいと思います。 よくやる方法としてはvisibilityを操作する方法ですが、それだとパッと切り替わってしまうためチカチカした印象になります。 通信状態に応じてProgressBarが突然出たり消える例 突然何かが表示されたり消えたりすると潜在的な違和感を与えてしまうので、もっと滑らかに出たり消えたりさせたいものです。 表示状態を切り替える時にアニメーションをうまいこと実行させるのも手ですが、お手軽に実現したいというモヤっと感がありました。 そこで、Material ComponentのProgressIndicatorの出番です。 Material ComponentのProgressIndicator 前述したモヤっと感を、Material ComponentのProgressIndicatorを利用して解決してみましょう。 水平バーで表現できる LinearProgressIndicator を使います。 <com . google . android . material . progressindicator . LinearProgressIndicator android : layout_width = "match_parent" android : layout_height = "wrap_content" android : indeterminate = "true" app : hideAnimationBehavior = "outward" app : showAnimationBehavior = "inward" app : indicatorColor = "#ffaa4e" app : trackColor = "#eeebe7" /> 上記のxmlで定義したLinearProgressIndicatorを linearProgressIndicator という変数名で取得しておきつつ・・・ // 表示したい時に呼び出す linearProgressIndicator.show() // 非表示にしたい時に呼び出す linearProgressIndicator.hide() ※Material Componentの導入 *3 は省略しています。 LinearProgressIndicatorで実装した例 はい、これだけでできました! 滑らかで美しい! ProgressIndicatorで設定可能なattributes Material DesignのProgress indicators *4 にどんな風に扱えるのか記載がありますが、どんな見た目になるのかやってみましょう。 色 進捗を表す部分は indicatorColor で、その下地は trackColor で色を設定できます。 また、 indicatorColor には色の配列を設定可能です。 <!-- colors.xml に下記を定義 --> <array name = "progress_colors" > <item> #f00 </item> <item> #ff0 </item> <item> #0f0 </item> <item> #00f </item> </array> <com . google . android . material . progressindicator . LinearProgressIndicator android : layout_width = "match_parent" android : layout_height = "wrap_content" android : indeterminate = "true" app : indicatorColor = "@array/progress_colors" /> indeterminateな水平バーを使用する場合、 indeterminateAnimationType というattributeで、色配列がどのように適応されるのかを指定できます。 indeterminate AnimationType 見た目 disjoint contiguous ただし、"contiguous"という設定を使用するには条件がいくつかあるので注意です。 trackCornerRadius (後に説明)は設定不可 indicatorColor で設定した色が3色以上 配列が3つ以上でも、色が3色以上である必要がある ※設定を無視してくれればいいのですが、レイアウトxmlの読み込みでエラーになりクラッシュしました。 形 trackThickness で高さ(太さ?)を、 trackCornerRadius で、indicator部分に丸みを設定できます。 "丸み"とはなんなのかは、実際の表示をみてもらうとわかりやすいです。 (よりわかりやすくするために trackThickness を1増してます) 見た目 trackCornerRadius設定なし trackCornerRadius設定あり 表示アニメーション 表示時は app:hideAnimationBehavior 、非表示時は app:showAnimationBehavior のattributeで設定できます、 ProgressIndicatorは標準では none が設定されていて、アニメーションがありません。 設定値 非表示時(hide) 表示時(show) none 標準で設定されているもの。アニメーションしない。 標準で設定されているもの。アニメーションしない。 outward 下端から上端に向かって折りたたまれる 下端から上端まで拡大 inward 上端から下端に向かって折りたたまれる 上端から下端まで拡大 それぞれを設定してみた時の見た目はこんな感じです。 hide show 見た目 outward outward outward inward inward outward inward inward ちなみに表示アニメーションが実行された後はvisibilityが変更されます。 show() の後には View.VISIBLE が、 hide() の後には View.INVISIBLE か View.GONE となりますが、これは setVisibilityAfterHide(int visibility) というメソッドで設定できます。 アニメーション方向 標準では左から右へ満たされたり流れたりするアニメーションですが app:indicatorDirectionLinear というattributeで変更可能です。 determinateな場合 indicatorDirectionLinear 見た目 leftToRight rightToLeft indeterminateな場合 indicatorDirectionLinear 見た目 leftToRight rightToLeft ※ startToEnd 、 endToStart も設定可能だが省略 ※LinearProgressIndicatorだけに用意されたattribute 他にも CircularProgressIndicator 用に用意されたattributeもあるのですが、もりもりになってきたのでひとまずここまで。 ProgressBar、ProgressIndicatorの使い所 ProgressIndicatorの紹介をしてきましたが、どんなプログレス表示をしたいかによってProgressBarを使うのか、ProgressIndicatorを使うのかは変わってくると思います。 ProgressIndicatorはMaterialDesignな見た目の表示はお手軽に実装できますが、独自のアニメーション(例えばロゴがグルグル回ったり跳ねたり・・・)を設定するattributeは用意されていません。 凝ったプログレス表示をしたい場合は、Drawableを自前で作成してProgressBarに適応した方が良さそうです。 筆者は今回ProgressBarの中身も覗いてみたのですが、アニメーションの設定とか楽しそうだなと思いました。 おわりに MaterialComponentのProgressIndicatorについて紹介してきましたが、Android版クラシルでも実際に導入しています。 Android版クラシルでは、読み込み中の表示から通信中、コンテンツ取得後の表示までがなめらかでユーザーの目に優しいものになっていると思います。 それはサービスに大きく関わるような効果ではありませんが、アプリ全体を通じて「安心感」や「温かみ」のようなものをユーザへ与えられているのではないかなと思います。 極端な例を用意してみましたが、どう感じますか? 新しい機能を通じてユーザーに最適で最大の価値を提供する事はもちろん大切なことですが、このような細かなところへの配慮を重ねることでも快適さを実現していきたいですね。 *1 : Material Components for Android v1.3.0: https://github.com/material-components/material-components-android/releases/tag/1.3.0 *2 : ProgressBarの公式ドキュメント: https://developer.android.com/reference/android/widget/ProgressBar *3 : Getting started with Material Components for Android: https://github.com/material-components/material-components-android/blob/master/docs/getting-started.md *4 : Material Design Progress indicators: https://material.io/components/progress-indicators/android
アバター
TRILL開発部の石田です。 先日Twitterで以下のツイートが流れてきました。 ある検査は精度95%で正しい結果を出すが、5%の確率で間違えた結果を出す。 実際に病気である人は全体の5%だ。 その検査で陽性反応がでた。この人が本当に病気である確率は何%か? — 加納裕三 (Yuzo Kano) (@YuzoKano) 2021年1月5日 ある検査は精度95%で正しい結果を出すが、5%の確率で間違えた結果を出す。 実際に病気である人は全体の5%だ。 その検査で陽性反応がでた。この人が本当に病気である確率は何%か? 非常に興味深い内容なので、こちらについて言及していきたいと思います。 ちなみに元ツイートを参照すると分かりますが、正解は95%ではなく50%です。 問題の整理 まず、ツイート中の以下の文について考えてみます。 ある検査は精度95%で正しい結果を出すが、5%の確率で間違えた結果を出す。 これは言い換えると以下のようになります。 病気の人が検査をしたとき、陽性と出る確率が95%で、陰性と出る確率が5% 健康な人が検査をしたとき、陽性と出る確率が5%で、陰性と出る確率が95% また、問いは以下となっています。 その検査で陽性反応がでた。この人が本当に病気である確率は何%か? これも言い換えると以下のようになります。 陽性と出たとき、病気である確率は何%か このツイートは一見当たり前のことを問うているようなのですが整理すると、 与えられた確率: 病気 or 健康な人が検査をしたときの結果の確率 求めたい確率: 検査結果が陽性のときの病気の確率 となり、与えられた確率と求めたい確率が違うことが分かります。 例を挙げると、仮に検査結果が病気でも健康でも全て陽性と出るとき、「検査結果が陽性のときの病気の確率」は100%でも0%でもなく、実際に病気である人の割合と同じになることが分かると思います。 ベイズの定理 さて、上記の問いを解く前にベイズの定理について説明します。 まず条件付き確率について説明します。 条件付き確率とは「Aが起こったときにBも起こる確率」であり、例えば「病気のときに陽性と出る確率」のようなものです。 「Aが起こったときにBも起こる確率」は と表現されます。 は、Aが起こった確率 のうちで、AとBが起こる確率 なので、 \begin{equation} P(B | A) = \frac{P(A \cap B)}{P(A)} \end{equation} と表現できます。 ちょっと分かりづらいので具体例を挙げてみます。 サイコロを振って「偶数が出たとき出目が4以上である確率」を考えると、確率は と表され、偶数が [2, 4, 6] でありその中で4以上なのは [4, 6] となるので と求められます。 同様に出目が偶数の確率 は [2, 4, 6] の で、偶数かつ4以上の確率 は [4, 6] の となります。 式に当てはめてみると、 \begin{align} P(4以上 | 偶数) &= \frac{P(偶数 \cap 4以上)}{P(偶数)}\\ &= \frac{\frac{1}{3}}{\frac{1}{2}}\\ &= \frac{2}{3} \end{align} となり、等式が成り立つことが分かります。 と同様に も \begin{equation} P(A | B) = \frac{P(A \cap B)}{P(B)} \end{equation} と表現でき、2つの式の分母を払って を削除する等式を書くと、 \begin{equation} P(B | A)P(A) = P(A | B)P(B) \end{equation} となり変形すると、 \begin{equation} P(B | A) = \frac{P(A | B) P(B)}{P(A)} \end{equation} という式になります。 これがベイズの定理です。 問題の解法 ベイズの定理だけでは何のことかよく分からないので、ベイズの定理を使って実際に先のツイートの問題を解いてみます。 求めたい確率は「陽性と出たとき、病気である確率」でした。 これにベイズの定理を当てはめると、 \begin{equation} P(病気 | 陽性) = \frac{P(陽性 | 病気) P(病気)}{P(陽性)} \end{equation} となります。 値を埋めていきます。 実際に病気である人は全体の5%だ。 とあるので、 となり、病気の人が検査をしたとき陽性と出る確率が95%なので となります。 陽性の人の確率 はちょっとややこしいのですが、陽性の人が全体の何%か考えると、病気の人 (5%) の中で陽性と出た人 (5% x 95%) と、健康な人 (95%) の中で陽性と出た人 (95% x 5%) の合計なので、 \begin{align} P(陽性) &= 0.05 \times 0.95 + 0.95 \times 0.05\\ &= 0.095 \end{align} となります。 以上より、ベイズの定理を使って を求めると、 \begin{align} P(病気 | 陽性) &= \frac{P(陽性 | 病気) P(病気)}{P(陽性)} \\ &= \frac{0.95 \times 0.05}{0.095} \\ &= 0.5 \end{align} と計算でき、50%となります。 まとめ ベイズの定理を具体例を交えて紹介しました。 分かったようで分からない感のあるものだと思っており、自分もよく混乱しています。 この記事を読んで少しでも理解の一助となれば幸甚です。 delyでは全方面でエンジニアを積極採用中です。 興味のある方は是非お声がけください。 join-us.dely.jp
アバター
こんにちは、delyでクラシルiOSアプリ開発を担当している稲見 ( @inamiy )です。 この記事は「dely #2 Advent Calendar 2020」の25日目の記事です。 dely #1 Advent Calendar 2020 dely #2 Advent Calendar 2020 昨日は、delyのSREチームのjoooee0000(高山)さんによる delyのSREチームがオンコールトレーニングを導入する3つの理由 の記事でした。 オンコール対応できるエンジニア、強くてカッコいい・・・ 私の方からは、メリークリスマス🎄🎅🔔 にふさわしい Elm Architecture による unidirectional なプレゼントをお届けします🎁 (2020/12/26 EDIT: タイトルを「なぜ MVVM は Elm Architecture に勝てないのか」から「なぜ MVVM + FRP は Elm Architecture に勝てないのか」に変更しました) iOS開発における MVVM + FRP 2020年現在、多くのiOSアプリ開発の現場では、RxSwift等を用いた関数型リアクティブプログラミング (Functional Reactive Programming, FRP) によるMVVM (Model-View-ViewModel) 設計が主流だと思います。 MVVMは、テストしにくい UIViewController からアプリの表示ロジックを別クラス (ViewModel) に切り出し、複雑なビュー構造(=可変参照と副作用の悪夢)から解放されて、コードの可読性の向上と、テストをよりシンプルに書くための基本的な設計パターンとなっています。 従来のiOS開発では、ViewController〜ViewModel 間のデータのやり取りについて、 データの保持に「可変状態 (ViewModel内のvar変数など)」 データの送信に「委譲(デリゲート)」や「コールバック」 を使った方法が考えられてきました。 // Before: 従来のViewModel public class ViewModel { // 可変状態 public var state : String // コールバック付き関数 public func doSomething (callback : (String) -> Void ) { let result = state + " world" // 何か計算する callback(result) // コールバックで返す } } // Before: 従来のViewController class ViewController : UIViewController { let viewModel : ViewModel ... func viewDidLoad () { ... viewModel.state = "hello" // 1. 状態をセットする // 2. ビュー側から手動でメソッドを呼び出して、計算結果をコールバックで受け取る viewModel.doSomething(callback : { text in print(text) // 3. hello world が出力される }) } } しかし、FRPの登場によって、これまでの pull方式 (状態の更新とコールバックの呼び出しタイミングが異なる2ステップ)から push方式 (状態の更新と同時にコールバックが自動的に呼び出される1ステップ)に一変しました。 可変状態として BehaviorRelay ( BehaviorSubject )、コールバックとしてそのストリーム機能部分である Observable が用いられるようになりました。 // After: FRPを使ったViewModel public class ViewModel { // 可変状態 public let state : BehaviorRelay <String> // ストリーム出力 public func doSomething () -> Observable <String> { return state.asObservable().map { $0 + " world" } } } // After: FRPを使ったViewController class ViewController : UIViewController { let viewModel : ViewModel ... func viewDidLoad () { ... // 1'. 先に購読(監視)して準備する viewModel.doSomething().subscribe(onNext : { text in print(text) // 3'. hello world が出力される }) // 2'. 状態をセットすると、3が自動的に呼ばれる(同じコールスタック内) viewModel.state.accept( "hello" ) } } 一見すると、1と2の順序が逆転しただけに見えますが、 After の2' を毎回呼び出す度に、3' 内の購読のクロージャが自動的に呼び出される のに対して、Before 1 の場合は何度呼び出しても、追加で 2 を呼ばないと最新の状態を用いた計算を実行することができません。 つまり、 FRPの購読機能(オブザーバーパターン)によって、メソッドを毎回手動で呼び出す手間が省ける ことがFRPの利点の一つです。 iOSのUI開発においては、ビュー側でViewModel内の表示用データの Observable を購読することによって、データバインディングという形で「1回の購読だけでUIの連続更新が可能」になります。 以降の話では、「iOS開発における MVVM + FRP パターン」をまとめて「MVVM」と呼ぶことにします。 一般的なViewModel 上述の2つのコードは、ViewModel内部の状態がどちらも public なので、外部から直接アクセスできてしまう=将来の状態の予測可能性が簡単に壊される懸念があります。 通常の開発では、内部状態を private に隠蔽して、代わりに PublishRelay 等を用いた入力用のデータフローを追加することが一般的です。 // After 2: FRPを使ったViewModel (状態隠蔽) public class ViewModel { // private な可変状態 private let state : BehaviorRelay <String> = . init (value : "!!!" ) // public な入力 public let input : PublishRelay <String> // ストリーム出力 public func doSomething () -> Observable <String> { return input.asObservable() // 入力をトリガーに、現在の状態を取得 .withLatestFrom(state) { input, state in return input + "world" + state } } } class ViewController : UIViewController { let viewModel : ViewModel ... func viewDidLoad () { ... viewModel.doSomething().subscribe(onNext : { text in print(text) // hello world!!! が出力される }) viewModel.input.accept( "hello" ) } } この頻出パターンでは、状態が withLatestFrom 経由で取得された簡易的なデータフローとなっていますが、ここに(UI)アーキテクチャーを考える上での本質情報が隠されています。 それは、ViewModelの基本的な役割が 「入力ストリームと内部状態をもとに、新しいストリームを外部に出力する」 ということです。 「入力」と「内部状態」、そして「出力」という3つの要素は、まさに計算機理論の基礎的モデルであるステートマシン(状態機械)そのもの といえます。 複雑化しすぎたViewModel しかし残念ながら、多くのiOS開発現場におけるMVVM設計は、このような単純な作りにはなっていません。 もちろん、業務が発展していくに従って、ビジネスロジックが複雑にならざるを得ない事情がありますが、私たちiOS開発者が FRPを過信しすぎて複雑なデータフローを構築してしまう ことにも大きな問題があります。 具体的な例を挙げると、ViewModelはしばしばこのように肥大化しがちです: // After 2: FRPを使いすぎたViewModel public class ViewModel { // private な可変状態の集まり private let state1 : BehaviorRelay <String> private let state2 : BehaviorRelay <String> private let state3 : BehaviorRelay <String> private let state4 : BehaviorRelay <String> private let state5 : BehaviorRelay <String> ... // public な入力ストリームの集まり public let input1 : PublishRelay <String> public let input2 : PublishRelay <String> public let input3 : PublishRelay <String> public let input4 : PublishRelay <String> public let input5 : PublishRelay <String> ... // public な出力ストリームの集まり public let output1 : Observable <String> public let output2 : Observable <String> public let output3 : Observable <String> public let output4 : Observable <String> public let output5 : Observable <String> ... // 初期化と同時にデータフローのグラフを構築 public init () { // output1 は input1 と state1 に依存 output1 = input1.asObservable() .withLatestFrom(state1) { input, state in return input + "world" + state } // output2 は、input2 と state1, state2, state3 に依存 output2 = input2.asObservable() .withLatestFrom(state1) { ( $0 , $1 ) } .withLatestFrom(state2) { ( $0 , $1 , $2 ) } .withLatestFrom(state3) { ( $0 , $1 , $2 , $3 ) } .flatMap { ... } // output3 は、input3、input4 と output2 も使いつつ、state4 に依存 // 何なら追加で別の副作用も同時に行う output3 = Observable.combineLatest(input3, input4, output2) .map { ... } .withLatestFrom(state4) { ... } .flatMap { ... } . do (onNext : { ... }) // output4 は、以下略 ... // 結果的に、withLatestFrom, combineLatest等を多用した、 // 入力と状態が複雑に絡み合うカオスなデータフローのグラフが出来上がる } } これはあたかも、多層なニューラルネットワークを頑張って一から手書きしているようなものです。 init() 内部のコードがFRPのパイプラインで埋め尽くされ、数百行のコードに膨れ上がることも少なくありません。 このようなFRPの過剰使用とコードの複雑化のことを、個人的に リアクティブ・スパゲティ と呼んでいます。 (もちろん、FRPが存在しなかった頃に比べれば、パイプライン化によって可読性は随分と高まった方なのですが) なぜリアクティブ・スパゲティは起きるのか リアクティブ・スパゲティが発生する原因は明確です。 「(いつの間にか) state2 、 input2 、 output2 が生え始めた」 からです。 個々の Observable が存在することは、それぞれがデータフローを増やしてしまう要因になります。 そして、チーム全体を通してFRPをよく理解していないと、簡単にストリームの分岐や合流、余分に追加された状態とその手動ハンドリング(例: disposeBag 以外の Disposable をViewModelが所有している)が大量に発生してしまい、循環的複雑度が爆発的に増加します。 よりコードの可読性を高く保ち、簡潔に書くためには、 入力・内部状態・出力それぞれが一本化されてシンプルさを維持しなければなりません。 Elm Architecture ここで、 Elm というプログラミング言語に目を向けてみましょう。 細かい言語仕様についてはここでは触れませんが、フロントエンドエンジニアの方であれば、 The Elm Architecture を知っている方も多いと思います。 プログラミング言語全般のUI設計思想に大きな影響を与え、JavaScript Redux、Swift Composable Architecture、Rust Yew、PureScript Halogen、Haskell Miso など、事例を挙げると枚挙に暇がありません。 ざっくり言うと、Elm Architecture は「入力 Msg と現在状態 Model から次の状態 Model を出力する」という基本に忠実な Unidirectional UI設計のことです。 純粋関数である update (または Redux でいう reducer )関数を定義し、プログラム実行の際に使用します。 // Swiftで書いた例 enum Msg { case increment, decrement } typealias Model = Int // update : msg -> model -> model func update (msg : Msg , model : Model ) -> Model { switch msg { case .increment : return model + 1 case .decrement : return model - 1 } } プログラムの実行については、次のような形の関数を呼び出します: /* sandbox : { init : model , view : model -> Html msg , update : msg -> model -> model } -> Program () model msg */ func makeProgram ( init : Model , // 初期状態 view : Model -> Html <Msg> , // ビューのレンダリング update : (Msg, Model) -> Model // reducer ) -> Program <Model, Msg> FRP 時代の Elm Architecture (〜v0.16) ところで、Elm Architecture は v0.17 以前にはFRPを使っていた ことをご存知でしょうか? Signal と呼ばれる、おおよそ RxSwift.BehaviorRelay ( BehaviorSubject ) と同じデータ構造を使って、副作用を含むイベントのストリームをパイプライン処理していきます。 そのElm + FRP時代のオペレータの中でも特に有名なのが foldp (fold from the past) と呼ばれる、過去を畳み込む関数です。 // foldp : (a -> state -> state) -> state -> Signal a -> Signal state func foldp <Input, State> ( update : (Input, State) -> State , initial : State ) -> (Signal < Input > ) -> Signal <State> 「過去を畳み込む」というと、なんだか中二心がくすぐられる思いがしますが、なんてことはない、 RxSwift.scan と同じ意味です。 実はこの foldp は、前節の MVVM と同じく、 「入力ストリームと内部状態をもとに、外部に新しいストリームを出力する」 という計算のエッセンスが随所に散りばめられています。 update: (Input, State) -> State :畳み込み計算 (= reducer) initial: State :初期状態 Signal<Input> :単一の入力ストリーム Signal<State> :単一の出力ストリーム (NOTE: この出力ストリームはその後、 makeProgram 内で view を使って Signal<Html> に変換されて画面に出力されます) (NOTE: (Signal<Input>) -> Signal<State> の部分を Signal Function と呼び、 Arrow と呼ばれる構造を持ちます= Arrowized FRP。これがいわゆる Mealy Machine の話へとつながります) 従来のFRPでは、「入力・内部状態・出力」のエッセンスを実現するために、FRPパイプラインを真面目に実装する必要がありました。 一方で、 foldp が教えてくれるのは、 複雑奇怪なパイプラインを作る代わりに update 関数一つを用意 すればそれで済むということです。 これが、Elm v0.17 で A Farewell to FRP になったきっかけとも言えます。 MVVM vs Elm Architecture それでは早速、MVVM と Elm Architecture を比較していきましょう。 今回は話を簡単にするため、MVVMの場合の入力と出力のストリームがそれぞれ2つずつあると仮定します。 また、各 Observable ストリームは無限時間存在するものとし、 onError / onCompleted を行わないものとします。 すると、Elm Architecture ( foldp ) によるストリームの一本化の場合、 MVVMのような Observable を複数構成する代わりに、一本化された Observable の型パラメータに入る「状態」と「アクション」の型を複数に細分化する構造を取る ようになります。 // アプリ全体の2アクション // Action ≅ Action1 + Action2 。足し算はEitherで書くことができる。 enum Action { case action1(Action1) // 子アクション1 case action2(Action2) // 子アクション2 } // アプリ全体の2状態 // State ≅ State1 × State2 。掛け算はタプルで書くことができる。 struct State { var state1 : State1 // 子状態1 var state2 : State2 // 子状態2 } 通常、アクションは enum (直和型)、状態は struct (直積型)を使う場合が多いので、一旦その形にならうものとします。 直和型と直積型については、簡単に言うと、 「代数的データ型 = 型で足し算と掛け算ができる」 というものです。 足し算は Either 型、掛け算はタプル型 だと考えることができます。 代数的データ型 (Algebraic Data Type = ADT) の詳細はこちらをご参考下さい。 Algebraic Data Type in Swift - Speaker Deck ここまでの話を一旦整理すると、 MVVM では複数のObservableを入力・出力に持つ 複数を表現するこの場合、掛け算(タプル)で考える Elm ( foldp ) では、入力・出力に1つずつの Observable のみを使い、その値を代数的データ型で細分化する MVVM Elm 入力 Obs<Action1> × Obs<Action2> Obs<Action> ≅ Obs<Aciton1 + Action2> 出力 Obs<State1> × Obs<State2> Obs<State> ≅ Obs<State1 × State2> ( Obs は Observableの略) Observable の代数的性質 ここで天下り式になりますが、 Observable の重要な性質として、以下のことが成り立ちます。 (ここでは、データフロー=川と呼ぶことにします) Obs<A> = Aが流れる川 Obs<B> = Bが流れる川 Obs<A> × Obs<B> = Aが流れる川と、Bが流れる川 は、一つにまとめて、 Obs<A + B> = AまたはBが流れる川 に置き換えることができる(その逆も成り立つ) この仮説は直感的にも正しそうに見えますね。 証明は、片方からもう一方に変換するコードを実装できるかどうかで決まります。 // Obs<A> × Obs<B> → Obs<A + B> func fromMvvmToElm <A, B> (a : Observable <A> , b : Observable <B> ) -> Observable <Either<A, B> > { return Observable.merge(a.map(Either.left), b.map(Either.right)) } // Obs<A + B> → Obs<A> × Obs<B> func fromElmToMvvm <A, B> (aOrB : Observable <Either<A, B> >) -> (Observable < A > , Observable < B > ) { return ( aOrB.compactMap { if case let .left(a) = $0 { return a } else { return nil } }, aOrB.compactMap { if case let .right(b) = $0 { return b } else { return nil } }, ) } この相互の関係性から分かることとして、2つの関数を交互に呼ぶと fromElmToMvvm(fromMvvmToElm(a, b)) = (a, b) fromMvvmToElm(fromElmToMvvm(aOrB)) = aOrB と、どんな入力値を代入しても元の入力値に戻ります。 この「相互変換して元に戻せる」性質のことを 同型 (isomorphic) といい、 Obs < A > × Obs < B > ≅ Obs < A + B > と書くことができます(「≅」はイコールではなく、同型を意味します) 結局、何が言いたいかというと、 MVVM Elm 入力 Obs<Action1> × Obs<Action2> Obs<Action> ≅ Obs<Action1 + Action2> MVVM と Elm の「入力」の構造については、どちらも同じことを言っているに等しい と結論付けることができます。 余談: Observable は単なる「足し算の型の圏」から「掛け算の型の圏」への強モノイダル関手だよ なお、余談ですが、 Observable<Never> についても思いを馳せると、面白いことに気づきます。 Observable<Never> は(今回の前提においては)「無限に続く絶対に値を流さないストリーム」を意味しますが、実際に実装してみると: let never : Observable <Never> = Observable.create { observer in // 何もしない、というかできない return Disposables.create() } の書き方一通りしかありません。( Observable.never と同じ) 一方で、Swiftの Void (Unit型) もまた () ただ一つのみを値として持ちます。 つまり、 Observable<Never> ≅ Void が成り立ちます。 ここで、おもむろに圏論(数学)という飛び道具を持ち出すと、 1 = Void , 0 = Never とおいて、 μ: Obs<A> × Obs<B> → Obs<A + B> ε: 1 -> Obs<0> が同型射(逆方向の関数もある)であることから、 Observable が モノイダル圏 (型, +, 0) から (型, ×, 1) への強モノイダル関手 (strong monoidal functor, nLab ) であることが分かります。 何を言っているのか良く分からないかもしれませんが、要するに Observable は強かったのです。 出力ストリームの合成の限界 さて、入力に関して MVVM と Elm Architecture の構造は同じであることが分かりました。 それでは、出力についてはどうでしょうか? MVVM Elm 出力 Obs<State1> × Obs<State2> Obs<State> ≅ Obs<State1 × State2> 結論を先に言ってしまうと、出力は 相互に変換して元に戻すことができません。 試しに、変換関数として次の実装を考えてみましょう。 // 1. Obs<A> × Obs<B> → Obs<A × B> func fromMvvmToElm <A, B> (a : Observable <A> , b : Observable <B> ) -> Observable <(A, B)> { return Observable.combineLatest(a, b) { ( $0 , $1 ) } } // 2. Obs<A × B> → Obs<A> × Obs<B> func fromElmToMvvm <A, B> (ab : Observable <(A, B)> ) -> (Observable < A > , Observable < B > ) { return (ab.map { $0 . 0 }, ab.map { $0 . 1 }) } 一見すると、この対応関係は成り立ちそうに見えますが、残念ながら上手くいきません。 例えば、Rx marble diagram で適当なデータフローを考えてみると: a : a0--a1----------a2--> b : b0------b1--b2------> // fromMvvmToElm(a, b) ab : a0--a1--a1--a1--a2--> b0 b0 b1 b2 b2 // fromElmToMvvm(fromMvvmToElm(a, b)) a' : a0--a1--a1--a1--a2--> b' : b0--b0--b1--b2--b2--> a != a' , b != b' なので元に戻らないことが分かります。 他にも combineLatest を zip や withLatestFrom などの他の合成オペレータに置き換えたり、 distinctUntilChanged 等を用いてフィルタリングしてもロジックが複雑化するのみで、同型であることを導くことは困難です。 2.の実装がとても自然なストリーム分解の導出に見える一方、1.の実装で 2つのObservableの(掛け算を使った)合成による不可逆性が発生している と言えます。 この原因の根本について、筆者は次のように考えます: combineLatest や zip , withLatestFrom 等を使った Observable の掛け算の合成は、(時間的同期を取るために)内部で発行した値を一部メモリキャッシュするという「副作用」が発生し、これがFRPのストリームの計算結果に対しても不可逆性を生じさせている もし、この仮説が正しいとするなら、次の点についても言えそうです: fromElmToMvvm は自然な導出(純粋な map のみを使っているので) Elm Architecture から MVVM への出力の変換は容易 MVVM への出力変換で、各々のストリームが前回の値を重複して発行してしまう問題があるが、 distinctUntilChanged を使えば、MVVMのように差分更新のみを抽出することも可能 fromMvvmToElm は、どのような掛け算的合成を行っても、不可逆な結果に終わる MVVMからElmを構成することはできない Q. combineLatest を使えば、全ての出力をかき集められるのでは? ところで、上述の 「MVVMからElmを構成することはできない」は、やや飛躍した結論に思われるかもしれません。 実際、数学がどうこうという謎めいた話を無視すれば、 combineLatest を使って散らばった各出力の Observable を一点に集めることが可能だからです。 しかし、この単純な方法は「限られたケース」においてのみ可能であるだけで、一般的には成立しませんし、また非効率的です。 主に、次のような課題があります。 combineLatest 引数の各 Observable は subscribe 時点で 初期値を持っていなければならない 初期値がないと、 combineLatest の onNext がなかなか始まらず、Elmにおける状態更新のタイミングを再現しない combineLatest は、掛け算的合流計算のために メモリキャッシュを消費 し、Elm に比べて非効率になりやすい Reactive glitch(同じ上流元の同期的合流問題) 2つの異なる出力が、同じ1つの入力をトリガーとして派生した場合、タイミング問題が生じて、中間の変更状態が反映される ちなみに Reactive glitch の根本問題は、 2つの出力が「同時に更新」される場合に、個々の Observable に分解できない ことが原因です: // aとbが両方同時に更新 ab : a0------a1--> b0 b1 // fromElmToMVVM(ab) a : a0------a1--> b : b0------b1--> // fromMvvmToElm(fromElmToMvvm(ab)) ab': a0-----a1-a1--> b0 b0 b1 一瞬だけ (a1, b0) (場合によっては (a0, b1) )という余計な中間状態が発生している ことが分かります。 この問題点として、もし ab' をVirtual DOMに渡してUI差分レンダリングした場合、不要な計算が走ることにつながります。 Reactive glitch に対する解決策としては、FRPの中で トポロジカルソート を用いたQueueによる管理の方法が挙げられます。 詳しくは、こちらのURLをご参考下さい。 Reactive programming (Glitches) - Wikipedia RxSwift a.withLatestFrom(a) 同じ上流元の同期的合流問題 - Qiita Understanding Reactive Glitches - Swift Talk - objc.io なお、RxSwift や ReactiveSwift を始めとする、ほとんどのFRPライブラリは、Reactive glitch 問題に対応していません。 もし対応しているフレームワークがあれば、ぜひ教えて下さい。 まとめ いかがでしたか? この記事では、MVVMに対するElm Architectureの優位性について、 FRP (Observable) の持つ数学的構造 に着目して仮説を立ててみました。 Virtual DOM (差分レンダリング) フレームワークの有無や良し悪しに関係なく、結論を導ける という点が、この話の一番の面白い点だと思います。 さらに学びたい方のために、Swiftで解説した Elm Architecture について、こちらのスライドをご参考いただければ幸いです。 Reactive State Machine (Japanese) - Speaker Deck Elm Architecture in Swift - Speaker Deck SwiftUI 時代の Functional iOS Architecture 今回のブログを書くにあたり、参考にした文献・Webサイトはこちらになります。 Elm: Concurrent FRP for Functional GUIs Asynchronous Functional Reactive Programming for GUIs Elm: Concurrent FRP for Functional GUIsを読んで - The curse of λ おわりに delyでは現在、 「 クラシル 」「 TRILL 」を一緒に開発しながら共に成長していけるメンバーを絶賛大募集 しています。 join-us.dely.jp もし、この記事を読んで「私も strong (monoidal) になりたい」と思いましたら、ぜひ私が先日書いた入社ブログも合わせて読んでみて下さい。 会社のカルチャーや中の人の雰囲気、事業内容について紹介しています。 note.com また、2021/01/21 19:00〜にクラシルiOSチームのオンライン雑談会を開催します。 こちらもぜひ奮ってご参加ください! bethesun.connpass.com 最後までお読みいただきありがとうございました!
アバター
こんにちは! AWSのカオスエンジニアリングの新サービスもリリースされ、オンコールトレーニングへの関心が高まっているのを感じています。delyのSREチームのjoooee0000(高山)と申します。 この記事は「 dely #2 Advent Calendar 2020 - Adventar 」の24日目の記事です。 昨日は新規事業開発をしている おっくー (@okutaku0507) さんによる 「 KPI自動通知Botで始める 数字に執着するプロダクトマネジメント|奥原拓也 / PdM / anynote 」でした。 KPIの必要性から具体的なBot化の知識まで具体的に解説されているのでぜひ参考にしてみてください! note.com adventar.org adventar.org はじめに 今回は、delyのSREチームがオンコールトレーニングを導入する3つの理由を紹介したいと思います。 delyのSREチームはこれまで、元々在籍していたメンバーと、2019年の10月に入社したメンバー2人体制で運用してきました。そして今年(2020年)の10月から、新しくSREとして入社したメンバーと育休から帰ってきたメンバー(育休前はサーバーサイド担当)の2人を足して4人体制になりました。現在、4人になった新生SREチームをチームとして再スタートさせるべく、チームミッションの再定義や役割スコープの決定、チーム体制の整備を行なっています。 その中で、delyのSREチームはオンコールトレーニングの導入を決定しました。なぜ導入することになったかを3つの項目に分けて紹介していきたいと思います。 (なお、この記事のオンコールトレーニングがさす具体的なトレーニングの内容はSRE本の28章 「SREの成長を加速する方法: 新人からオンコール担当、そしてその先へ」で紹介されているトレーニング内容をベースとしたものです) オンコールトレーニングを導入する3つの理由 1. オンコール体制強化が急務 まずはシンプルに現状の体制でのオンコール対応に限界があるためです。 現在は元々在籍していたシニアエンジニアがメインでオンコール対応をしており、他のメンバーは補助という形でオンコール対応にあたっています。 そのため、メイン担当者がオンコール対応において単一障害点となってしまっています。さらに、delyのSREチームが運用を担当しているサービスはレシピ動画サービスの「クラシル」に加え、最近GCPからAWSへのリプレースを行なった「TRILL」という女性向けメディアも追加され2つになりました。なので、特に障害発生タイミングが被った場合に地獄がおこります。同じAWSのサービスを使っているため、AWS障害時などには容易におこりうることが想像できます。 「はい!その通りなのですが....」 delyのSREチームのメンバーは比較的新しいメンバーで構成されていることに加えて、サーバーサイドチームから異動したメンバーや、前職でがっつりオンコール対応をしていたわけではないメンバーも在籍しています。 そのような状況で、他のメンバーがオンコールのメイン対応を担えるようになるにあたり以下のようなハードルがありました。 オンコールのメイン担当者になるための3つのハードル ⑴ 新しいメンバーがオンコール対応をぶっつけ本番でやるのは危険 delyのSREチームが運用を担当している2つのサービスは、それぞれ月間利用者数千万人を超える巨大サービスです。 障害によりサービスが停止した際には大きな損害が出てしまうため、新しいメンバーがぶっつけ本番で対応するのは危険です。 ⑵ シニアSREが優秀すぎて他のメンバーが実績を積む機会がない この現象はオンコール対応にはよく発生する問題のようです。システム障害対応についての本にはこのようなオンコール対応の教育に関する問題について言及されています。 システム障害は、多くのシステムにおいて最優先の対応事項です。そのため、障害対応メンバーについては、社内で最もできる人間がアサインされます。あなたよりも経験が長いメンバーがいるのであれば、おそらく、あなたが障害対応を指揮する「インシデントコマンダー」になることはないでしょう。このような現場の場合、あなたはいつ経験を積むのでしょうか?それは、他にできる人間がいないときです。 (木村 誠明. システム障害対応の教科書.より) これでは、メイン担当者の負荷が一向に減らず、永遠に単一障害点になり続けます。 ⑶ 通常のプロジェクト業務だけではシステム全体像把握の効率が悪い/把握度が属人的になる オンコール対応にはサービスが稼働しているシステムについての知識が必要になります。業務をこなしていく中でだんだんとシステムに関する深い知識が身についていくものだと思います。 しかし、アサインされたプロジェクトによってどこに知識がついていくか偏りが出てしまいます。 例えば、現在検証環境周りの再構築をやっているのですが、そこにがっつり関わっているメンバーは本番環境システム側に触れる機会が相対的に少なくなってしまいます。 また、自主的なリバースエンジニアリングや自主学習を通してシステムを理解することもできますが、自学のためどれほどの習熟度なのか判断する材料がないこと(どの程度の習熟度でオンコール対応できるという判断基準をもうけられない)と、効率が悪いことが問題としてあります。 この3つのハードルを越えるために、GoogleのSRE本を元にオンコールトレーニング導入を検討しました。 そこで疑問なのは、「そもそもオンコールトレーニングってどれくらいの効果があるの?どれくらいのチーム規模で導入するべきなの?」という話です。NetflixがオンコールトレーニングをやってAWSの障害に強かった話やGoogleのSREチームがオンコールトレーニングを導入している話などは有名ですが、あの規模だからできたことなのではと考えてしまいます。 しかし、Googleが出しているサイトリライアビリティーワークブックには、delyのチーム構成に似たオンコールトレーニングにおけるGoogleのモデルケースが存在していました。これは直接的な導入の理由になるわけではありませんが、似たチーム構成で成功例が存在していることは導入を後押しする理由になりました。 こちらはサイトリライアビリティワークブックのオンコール対応の章で紹介されていたモデルケースです。 マウンテンビューの Google SREチームの事例 <チーム構成> ● SREテックリードのSara ● 他のSREチームから来た経験豊富なSREのMike ● SREになるのは初めてのプロダクト開発チームからの異動者 ● 4人の新規採用者(「Nooglers」) <ストーリー> 数年前、マウンテンビューのGoogle SREのSara は、3 ヶ月以内にオンコールにならなければならないSRE チームを立ち上げました。 ----中略---- チームが成熟していても、新しいサービスのオンコールになるのは挑戦であり、新しいマウンテンビューのSREチームは相対的に若いチームです。にもかかわらず、この新しいチームはサービスの品質やプロジェクトの速度を犠牲にすることなくサービスをオンボードできました。彼らは即座にサービスに改善を加えましたが、その中にはマシンのコストを40%下げたり、リリースのロールアウトをカナリアやその他の安全性チェックと合わせて完全に自動化したりといったことが含まれていました。新チームは 99.98%の可用性、言い換えれば四半期ごとにおよそ 26 分のダウンタイムをターゲットに、信頼性あるサービスを提供し続けてもいました。 新SREチームは、これほどのことをどのように自分たちで達成したのでしょうか?それは、スタータープロジェクト、メンタリング、トレーニングを通じてでした。 (サイトリライアビリティワークブック P149) このチームは、オンコールトレーニングを実施することでたくさんのシステム変更を加えているにも関わらずSLOを達成し、サービスの信頼性を担保しています。そしてそれは、オンコールトレーニングによって成し遂げられたことということです。 また、この事例において具体的にどのようなトレーニングをおこなったかについても本に記載されています。 このような背景と最後の後押しが、オンコールトレーニングをdelyのSREチームに導入する意思決定につながりました。 2. SREの運用改善業務のアウトプット最大化 オンコールトレーニングは、シンプルにオンコール対応ができるようになることに加えて別の役割も果たすと考えています。それは、SREの通常業務をする上でも非常に重要な知識が習得できるという点です。 delyのSREチームの本来注力するべき業務はシステムの設計と運用の 改善 にあります。 改善には、下記のような工程が存在します。 ①課題を発見する ②課題に適切な優先順位をつける ③適切な解決策を考えて実行する ここで重要なことは、①と②の工程ができていなければ、どんなに③が優れていても影響は小さいということです。 よって、改善業務において①と②の工程がとても大事になってきます。つまり、チームのアウトプットを最大化するには、まず課題発見と優先順位づけの精度を高めていく必要があります。 そこで課題となってくるのが、delyのSREチームは比較的新しいメンバーが多いためまだシステムに対する知識にムラがあることです。特にシステムの全体を俯瞰して課題の優先順位づけを行うことにはまだハードルがあります。また、システムに内在する既知の課題を網羅的に把握する機会がありません。全員の課題発見のスピードを上げ、優先順位づけを行えるようになることで①と②の質を高めていく必要があります。 オンコールトレーニングを行うことにより課題発見と優先順位づけをする上で重要な知識が身につくと考えています。 オンコールトレーニングにはいくつかの種類ややり方が存在しています。それを大別すると、 システムについての網羅的な学習 と 障害をベースにした学習 に分類できます。 ● 網羅的な学習 課題を発見するには、そもそもシステムについて知る必要があります。システムについて学習をすることにより、課題の発見/理解がしやすくなります。また、網羅的な学習により全体像が見えることで課題の優先順位づけの精度を上げることができます。 ● 障害ベースの学習 障害ベースの学習を行うことにより、システムの現状の弱点を把握することができます。「システムの現状の弱点 = 課題」です。障害ベースの学習を行うことで、現状潜んでいる課題を知ることができます。 このように、オンコールトレーニングをすることが課題の発見と優先順位づけをスムーズにできる手助けをしてくれます。結果的に、オンコールトレーニングをすることでSREチームが出すアウトプットの価値を上げることにつながると考えています。 3. 新規社員がスムーズに戦力になれる仕組み作り これまで、オンコールトレーニングを整備するとオンコール対応強化やSREの運用改善業務をする上でメリットがあるという話をしてきました。それにより、今後入社するメンバーにとってもスムーズに戦力になれる環境が整います。 さらに、新規社員を迎えるにあたりもう一つオンコールトレーニングを行うメリットがあります。それは、システムに関するドキュメントが豊富になるという点です。 delyにはもともと、在籍日数や立場に関係なく誰でも 活躍することができるうように情報の透明性を高くし、情報の非対称性をなくすという文化があります。 オンコールトレーニングで習得した知識をメンバーが ドキュメンテーションすることで現状のシステムの構成やモニタリング、ロギング、ロールバックの手順など豊富な知識が可視化されることになります。現状もドキュメントはかなり豊富なほうですが、オンコールトレーニングの導入によりさらに豊富になっていく予定です。これは、SREチームだけではなくサーバーサイドチームなど他のチームにとってもいい影響を及ぼすことが考えられます。 オンコールトレーニングを導入することで、入社後の戦力になるスピードを上げることができ、さらにドキュメントがそろうことでストレスなく業務にあたる環境作りが望めます。 まとめ これらより、delyのSREチームではオンコールトレーニングを導入することになりました。オンコールトレーニングをすることはオンコール対応ができるようになるだけではなく様々なメリットがあります。 また、大規模なチームではなくても導入をすることができると考えています。 導入の仕方や実際になにをやっているかなどは今後のブログでまた共有していこと思います! delyではSREメンバーを募集しています。@gomesuit @bababachi @akngo22 など愉快なメンバーが揃っていますのでご応募お待ちしております! https://join-us.dely.jp/ join-us.dely.jp また、「クラシル Tech Talk」などのイベントも多数行っています。 エントリー前に開発部の様子を知りたいという方はぜひ覗いてみてください。ご応募をお待ちしております! bethesun.connpass.com
アバター
こんにちは、delyコマース事業部サーバーサイドエンジニアの小川です。 最近クラシルにて、ネットスーパー機能のリリースができました! (以下 クラシルネットスーパー) 入社して1年くらいたちますが、とってもエキサイティングな毎日を過ごしています。 この記事は「 dely #1 Advent Calendar 2020 - Adventar 」の24日目の記事です。 前日は仁多見さんの記事でした!↓ 思った以上に大変だったクラシルでの Scoped Storage 対応 - クラシル開発ブログ はじめに みなさんはElasticsearchを利用して、開発中のサービスに検索機能を導入したことはありますでしょうか。 今や様々なサービスで利用されているかと思います。 クラシルネットスーパーでは、キーワード検索以外の部分でもElasticsearchを活用しています。 レシピに紐づく食材を取得したり 商品を分類・識別したり 普通だったら、玉ねぎカテゴリに玉ねぎの商品を紐付ける運用をすると思いますが、 ネットスーパーの商品は1店舗当たり何万件も商品があり、それが日々入れ替わるので上記の運用が現実的ではありません。 商品の分類・識別の自動化が必須だったので、それを今回は自然言語ベースで行いました。 分類・識別や一般的なキーワード検索のどちらも行える、Elasticsearchがとても相性よかったです。 記事メディアとはまた違った検索を提供する特徴的な事例だと思いますが、 検索エンジンの最適化はコツコツとやっていくことがマストです。 最適化についてどんな機能があるか、また運用のポイントなどをまとめていこうと思います。 Elasticsearchについて基本だけは知っているけど使ったことない方々 検索最適化したいけどやり方がわからない方々 の参考になれば幸いです! 最適化の際に必要なElasticsearchの知識 スコアリングの式を知る Elasticsearchは特定のクエリに対して、類似度を測りスコアリングしています。 スコアリングは主に、 tf-idf を利用して算出されています。 参考: https://lucene.apache.org/core/8_6_0/core/org/apache/lucene/search/similarities/TFIDFSimilarity.html q がクエリ、 d がドキュメント、 t がターム(ドキュメントの内容を形態素解析した得られた単語)として見てみましょう。 根拠を持って検索最適化を行うためには、スコアリングの式についての理解が必須です。 同義語について知る Elasticsearchでは「同義語」をanalyzerに設定することができます。 analyzerの概念図 例えば、表記揺れやタイポなどは同義語として設定し、同じものとみなす独自のanalyzerを作成することができます。 例 (トマト,とまと,tomato)など 同義語はanalyzerの概念図のToken Filtersのレイヤーに属します。 以下の図は、同義語の概念について表しています。 内部のユースケースは、外部のユースケースより単純です。 外部に行くにつれ複雑になりますが、検索エンジンとしての洗練度・パワーが増します。 様々な「同一性」に対して同義語を使うことは不正解ではありませんが、 これらを理解せずに無作為に同義語の設定を行うと、予期せぬ挙動をする検索エンジンが爆誕します。 同義語はとても強力です。 うまく使いこなせば、様々なユースケースに対応できる検索エンジンができるでしょう。 しかしうまく使いこなせないと、気付いた時には運用の難易度がとても高い検索エンジンが誕生してしまうかもしれません。 参考: https://opensourceconnections.com/blog/2018/12/07/synonyms-by-any-other-name-part-1/ さらに精度を高めたい 他にもfunctionクエリやword2vecなどを組み合わせて最適化を行うことができます。 実は画像検索でさえできてしまいます。 詳しい説明は省きますが、そう言った手段もあることを把握しておくだけでとても有益です。 個人的にはword2vecについて興味を持っていて、より有益に使えないかなとソワソワしています。 手段の目的化はヨクナイ 最適化の運用・設計のポイント 検索最適化の正解を明確にすること これは意外と大切です。最初にざっくりでいいので、「想定されるクエリ」と「クエリに対する理想の検索結果」を整理しておきましょう。 例えばネットスーパーで言うと、 ユーザーは「トマト 2個入り」や「トマト 〇〇県産」とは検索しないと想定します。 おそらく「トマト」と検索して出た結果の中から、目当ての「トマト」を選択してカートに入れるでしょう。 となったとき、トマトに付随する「2個入り」や「〇〇県産」と言う単語は、無視できる事になります。 余計な単語は無視することが検索精度向上には大切です。 また、「トマト」と検索した時は、「トマトジュース」や「トマトケチャップ」が結果に出ることは望ましくありません。 「トマトジュース」が欲しいユーザーは「トマトジュース」と検索するからです。 何も最適化せずにElasticsearchを使うと、「トマト」で「トマトジュース」なども出てきてしまいそうですね。 以下のように検索最適化の正解を明確にすることが大切です。 想定されるクエリ:トマト クエリに対する理想の検索結果:原体のトマト(トマトジュースや、ケチャップはでない) それにより、 「2個入り」や「〇〇県産」と言う単語は、無視できる。 トマトジュースなどが出てこないようにするにはどういったらいいか と言う事項を初めから考慮に入れられるようになります。 最適化の作業の難易度・影響範囲・作業量のバランス 検索最適化の正解が明確になって初めて、最適化の作業が始まります。 想定されるクエリ:トマト クエリに対する理想の検索結果:原体のトマト(トマトジュースや、ケチャップはでない) を達成するためにたくさんのアプローチがあると思います。最適化の手法に正解はありません。 どのアプローチを選択するかの判断基準として、「難易度・影響範囲・作業量のバランス」を見ると良いです。 アプローチ① プロダクトの初期においては必要なアプローチかと思います。 例えば、 検索クエリが複数渡された場合、or検索だったのをand検索にする タイトルとディスクリプションでスコア計算の重みが同じだったのをタイトル重めに調整する ストップワードを追加する などです。 初期はリソースが少ないかつ、影響範囲についてあまり考えなくても良い場面(リリース前など)があると思います。 100点の検索精度ではなく、70~80点くらいの検索精度を目指せれば良いかなと個人的に思います。 アプローチ② アプローチ①の手法でだいたいの精度が出てきたら、移行して良いと思います。 1つ例をあげると、前述した同義語の概念のうち、Alt Labelsレベルの同義語の登録であれば、誰でもできるかと思います。 運用や設計次第ではもっと高レベルの最適化を専門知識なしに行えるかもしれません。 ここで大切なのは 「いかに影響範囲を狭くできるか」 になります。 チューニングするたびに現状の検索結果に影響してしまうと、 一方の精度は伸びるけど一方は落ちてしまうなんていうことになりかねません。 着実に70~80点から100点に近づけていける運用が組めると良いと思います。 結局は プロダクトのフェーズ・特性・リソースなど様々な面から選択していくと良いと思います。 個人的には、「特定の分野における自然言語は限られている」かつ「長期的運用が必要なものが属人化するとヤバイ」と考えているので、 よほどの事がない限り、早い段階で作業内容がストックする前提で、アプローチ②が行えるような設計・運用を試みます。 最後に 以上Elasticsearchのポイントについてざっとまとめてみました。 クラシルネットスーパーでは、割と綺麗な運用で色々な要件を達成できました。 ネットースーパーの商品に手作業でラベルをつけなくても、分類・識別できるようになるのは大きいです。 技術に使われるのはなく、技術を使いこなすことが大切ですね。 レシピ連携機能を作る時、検索について何も知らない自分が弊社CTOに、 「レシピの食材とネットスーパーの商品を手作業で紐付けてくしかない」と言った時に、 「ぼくたちがこれをトマトだと判別するのもアルゴリズムだから」 と言われて一蹴されたのはとても昔に感じます。 明日はそのCTOである、たけさんの記事です。 お楽しみに! delyではエンジニアの採用をひたすらやっていますので、興味ある方はぜひのぞいてみてください! join-us.dely.jp bethesun.connpass.com
アバター
本記事は dely #1 Advent Calendar の 23 日目の記事です。 adventar.org dely #2もあるのでこちらも見てみてください! adventar.org こんにちは、Android エンジニアの tummy です。 昨日はうめもりさんの「 Androidも宣言的UI(が当たり前になりそうな)時代に非宣言的UIライブラリでこの先生きのこるには 」でした。 絶賛このアーキテクチャで実装中ですが、以前より diff が少なくなったのとどこに何を書けばいいのかが明確になってとても捗ってます。ただ、まだ悩みポイントもあるのでこれから潰していきたいと思ってます 😎 さて、タイトルにある通りクラシルでも先日 Scoped Storage 対応をしました。 (今回は Scoped Storage についての説明は省きます、ドキュメントをぜひ参照してください 💁‍♀️) developer.android.com MediaStore 使ったりいい感じに権限つければいいんじゃないの?と軽く考えてたんですが、思ったより大変だった のでやったことを書いていこうと思います! クラシルでの画像使用部分 クラシルでは以下の 2 箇所で画像を用います。 たべれぽ プロフィール カメラで撮影したものをそのまま使用できる他、ギャラリーから選択できるようになっています。 たべれぽのみ、フィルターをかけたりクロップすることもできます。 流れとしては以下の図のとおりになります。 起きていた事象 切り抜きの画面でファイルが見つけられずに無限にローディング状態になっていました。(お問い合わせしていただいたユーザーさんのおかげです、ありがとうございます!) またこの状態になると、謎のグレーな画像が端末内に保存されることがわかりました。 ローディングが終わらない 謎のグレーな画像たち 当初は Intent の渡し方が悪かったのかな?と思ったのですが、よくよく調べていくと Scoped Storage に対応しないとだめだということがわかり、着手しました。 行った対応 カメラかギャラリーか判定見直し ギャラリーを起動する際のコードを以下のようにしました。 val intentGallery = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI) intentGallery.type = "image/jpeg" こうするとギャラリーの Intent の情報に data がセットされるので、これがあるかないかで判定ができるようになります。 今までは変数として保持している Uri があるかないかで判断していたので、このロジックを変更しました。 FileProvider の活用 Uri.fromFile() を使うと共有の制限にひっかかるのと、権限をアプリ内で使うパスに渡すために FileProvider を使います。 qiita.com <? xml version = "1.0" encoding = "utf-8" ?> <paths> <external-path name = "external_files" path = "." /> <cache-path name = "cache" path = "." /> </paths> private fun getUriFromFile(context: Context, file: File): Uri { return FileProvider.getUriForFile( context, BuildConfig.APPLICATION_ID + ".provider" , file ) } キャッシュを作ってそれの Uri を使い回す Google Photos 経由で画像を選ぶと Uri が以下のような形式になり、Google Photos の ContentsProvider に対して権限をもらえないためエラーになります。 content://com.google.android.apps.photos.contentprovider/-1/1/content%3A%2F%2Fmedia%2Fexternal%2Fimages%2Fmedia%2F19018/ORIGINAL/NONE/~} stackoverflow.com そのためキャッシュを作り、その Uri を使い回すことで対応しました。 OkHttp の RequestBody に content スキームの Uri をのせるためのクラスを作る 普通のファイルであれば MultipartBuilder などを使って渡せましたが、content スキームの場合は渡せないのでひと手間加える必要がありました。 そのため以下のような RequestBody を拡張したクラスを作り、渡すようにしました。 class ImageRequestBody( private val context: Context, private val uri: Uri ) : RequestBody() { override fun contentType(): MediaType? = MediaType.parse( "image/jpeg" ) override fun writeTo(sink: BufferedSink) { val parcelFileDescriptor = context.contentResolver.openFileDescriptor(uri, "r" ) ?: return Okio.source(FileInputStream(parcelFileDescriptor.fileDescriptor)).use { sink.writeAll(it) } parcelFileDescriptor.close() } } 初期対応で足りなかったところ ドキュメントを開く Intent も投げる 画像の Intent のみだと、Galaxy などでデフォルトで入っているギャラリーアプリが認識されない ことがわかりました。そのためドキュメントの Intent を使い、type を画像にしぼって開くことで認識してくれるようになったので、この対応を行いました。 val intentDocument = Intent(Intent.ACTION_OPEN_DOCUMENT) intentDocument.type = "image/*" intentDocument.addCategory(Intent.CATEGORY_OPENABLE) できてないこと カメラで撮った際に画像を端末内に反映させていたのですが、その処理を消したのでできてません。冒頭に少し触れた、実装中のリアーキテクチャでできるといいな…と思っています。 さいごに delyではエンジニアを絶賛募集中です。ご興味のある方はぜひこちらのリンクを覗いてみてください。 カルチャーについて説明しているスライドや、過去のブログもこちらから見ることができます。 join-us.dely.jp delyの開発部では定期的にTechTalkというイベントを開催し、クラシル開発における技術や組織に関する内容を発信しています。 クラシルで使われている技術や、エンジニアがどのような働き方をしているのか少しでも気になった方はぜひお気軽にご参加ください! bethesun.connpass.com
アバター
こんにちは。dely株式会社でAndroidチームのマネージャーをやっているうめもり(Twitter: @kr9ly )です。 この記事は「dely #1 Advent Calendar 2020」の22日目の記事です。 21日目の記事は、 kazkobay さんによる「デザイナーからPdMになる時に役に立った本と方法まとめ」でした。エンジニアがPdMになるときにも役に立ちそうな本なので、暇を見つけて読んでみようと思いました。 note.com 「dely #2 Advent Calendar 2020」もありますので、是非そちらもご覧ください。 Jetpack Compose前夜 皆さん、Jetpack Compose使ってますか?直近ですと12/2に1.0.0-alpha08、12/16に1.0.0-alpha09がリリースされ、開発が大分活発なのがうかがえますね。正式版リリースは来年末か再来年でしょうか?さすがにまだalpha版なので、業務で本格導入してる方はほとんどいないのではと思いますが、Reactから始まった宣言的UIの流れがとうとうAndroidにも来るのかという感じですね。 試しに触ってみたところすでになかなかの完成度で、React(+React Hooks)のような宣言的UIで普通にAndroidアプリを作る、という体験が2年後くらいには当たり前になっているような気がしました。 React(VirtualDOM)がUIプログラミングの世界にもたらしたもの VirtualDOMを用いたReactというフレームワークがWebフロントエンド界隈にもたらしたものがなんだったか、ということはWebで調べればたくさん出てきますが、かいつまんで言えば以下の1点に集約されるでしょうか。 プログラマーを UIコンポーネントの状態の差分管理 から解放した UIコンポーネント(DOM)の状態の差分検知&更新処理をVirtualDOMという仕組みが一手に担うことで、プログラマーは常にアプリケーションの最新の状態を反映するVirtualDOMを構築するコードを書くだけで済むようになりました。その上、VirtualDOMが最小の差分を自動的に検出してUIを更新してくれるため、UIを更新するコストをあまり意識せずともパフォーマンスの高いコードが書くことができるようになった、ということはとても大きいでしょう。(もちろん差分検出処理自体の重さもアプリケーションが大きくなれば意識する必要が出てくることだとは思いますが、ここでは触れないでおきます) もちろんWebフロントエンドではない世界なのでDOMもVirtualDOMもAndroidの世界には存在しませんが、Jetpack Composeでも同じような仕組みでUIコンポーネントの状態を差分更新することからプログラマーを解放してくれます。UIを組み立てる参照等価な関数を組み合わせるだけで、Jetpack ComposeはUIコンポーネントツリーの差分を検知してUIを更新してくれるので、プログラマーはアプリケーションの状態からUIコンポーネントツリーを組み立てることだけを意識してプログラミングすることができます。 プログラマーをUIの差分更新から解放したことで生じたもう一つ大きな変化は、 アプリケーションの状態を一か所に集約しやすくなった ことです。状態の差分をプログラマーが管理しなければならなかった時は、前の状態をどこかに持っておいて比較しながらUIを更新する必要があるため、どうしてもアプリケーションの状態がコードの中にバラバラに点在してしまいがちでした。 それを一か所に集約できるようになったため、 状態をUIコンポーネントとして組み立てるコード と、 集約された状態を更新するコード にアプリケーションコードを明確に分離することが出来るようになりました。集約された状態からUIコンポーネントに状態を反映する仕組みは、Reactでは "single source of truth" という言葉を用いて説明されています( https://ja.reactjs.org/docs/forms.html#controlled-components )。今ではAndroid開発者にもお馴染みだと思いますが、Flux(Redux, etc...)のような単方向データフローアーキテクチャは、この二つのコードが明確に分離しやすくなったからこそ生まれたアーキテクチャです。Fluxのような単方向データフローアーキテクチャーをAndroidアプリに適用しようとする試みは過去色々な場所で行われてきたことだと思いますが、VirtualDOMの仕組みのないAndroidの世界でFluxを実現しようとするのは、どうしても無駄なボイラープレートコードの増加や、暗黙的なアーキテクチャの制約などを生み出してしまいがちで、結果的に大きな生産性の改善には繋がりにくかったのではと思います。 UIコンポーネントの状態の差分管理からの解放と、UIコンポーネントの更新のコードとアプリケーションの状態の更新のコードの明確な分離は、確実にAndroidのプログラミングにおいてもコードの保守性を高め、UIプログラミングの生産性の向上に繋がるはずです。 ……Jetpack Composeさえ導入できれば。 だがまだ本格的にJetpack Composeを導入するわけにはいかない我々がこの先生きのこる方法 Jetpack Composeはまだalpha版、下手すれば本格的に導入できるのは2年後でしょう。我々は2年後まで指をくわえて待っているのがよいのでしょうか?一応Androidの世界にもAndroid Data Bindingがあります(Android Data Bindingはプログラマーが状態の差分を意識しなければいけないのは何も変わらないですし、大きなコンパイル時間のペナルティがあるという問題もありますが…)し、今まで通りアプリケーションを作ることは問題なくできます。 しかし、Reactがもたらした UIコンポーネントの状態の差分管理からの解放 UIコンポーネントの更新とアプリケーションのデータの更新の責務の明確な分離 については、部分的にその利点を取り入れながらAndroidアプリケーションの作り方をアップデートすることはできます。この記事では、その方法の要点だけ説明したいと思います。 非宣言的UIライブラリでsingle source of truthを実現する 差分更新と一括更新が同一のコードで行うことができるような仕組みを用意する VirtualDOMのような差分検知&差分更新のライブラリがない世界では、何らかの方法で差分検知&差分更新の仕組みを実現し、UIコンポーネントの差分を管理する仕事からプログラマーを解放する必要があります。クラシルのAndroidアプリでは、以下のようなコードでUIコンポーネントの状態の更新を実現できるような仕組みを作りました。 override fun view( context: Context, state: RecipeItemState, updater: ViewUpdater<BaseBinding> ) { ...(省略)... updater.update(state.title, state.isPr) { layout, title, isPr -> val titleBuilder = SpannableStringBuilder() titleBuilder.append(title) if (isPr) { titleBuilder.setSpan( LeadingMarginSpan.Standard( 32 .dpToPx(context), 0 ), 0 , title.length, 0 ) } layout.titleLabel.text = titleBuilder } updater.update(state.thumbnailUri) { layout, uri -> layout.image.bind(imageLoaderFactories.thumbnail(uri).build()) } updater.update( state.isPremiumRecipe, state.isPremiumUnlocked ) { layout, isPremiumRecipe, isPremiumUnlocked -> layout.premiumLabel.visibility = isPremiumRecipe.visibleOrGone() layout.lockedLabel.visibility = (isPremiumRecipe && !isPremiumUnlocked).visibleOrGone() } updater.update( state.isFavorite, state.id, state.favoriteStateUpdatedTimestamp ) { layout, isFavorite, _, _ -> layout.favoriteButton.isActivated = isFavorite } updater.update(state.isPr) { layout, isPr -> layout.prLabel.root.visibility = isPr.visibleOrGone() } } updater.update関数にパラメータを渡して、その中でAndroidの通常のViewの操作を使ってUIを更新しています。 最初にこの関数を呼んだ時と、updater.updateに渡したパラメータのいずれかが更新されることでViewが更新される仕組みになっており、プログラマーが差分の粒度を意識する必要はあるのがVirtualDOMを使ったコードとのプログラミングとは違うところですが、最低限一括更新と差分更新の両方がこのコードだけで実現することはできています。 内部処理は結構単純で、以下のようなコードで実現されています。 class DiffDetector { private object EndOfState private var dirty = false private var index = 0 private var diffBuffer = mutableListOf< Any ?>() inline fun update(callback: () -> Unit ) { startUpdate() callback() finishUpdate() } fun startUpdate() { dirty = false index = 0 } fun detect(value: Any ?): Boolean { if (dirty) { append(value) return true } if (index >= diffBuffer.size) { append(value) return true } if (diffBuffer[index] === EndOfState) { dirty = true append(value) return true } if (value == null || value:: class .javaPrimitiveType != null ) { if (value != diffBuffer[index]) { append(value) return true } } else { if (value !== diffBuffer[index]) { append(value) return true } } index++ return false } fun finishUpdate() { append(EndOfState) } private fun append(value: Any ?) { if (index < diffBuffer.size) { diffBuffer[index] = value } else { diffBuffer.add(value) } index++ } } このクラスをラップしたのがViewUpdaterですが、内部的にはパラメータの配列を持っておいて、呼ばれた順にindexを動かしてパラメータを突き合わせているだけです。非プリミティブ型の場合は参照を突き合わせていますが、後述のStateをイミュータブルにすることで特に問題が発生しないようになっているのと、差分の検知処理がパフォーマンス上の問題にならないように気を付けています。 もちろんupdater.update関数をネストされるなどされると状態がおかしくなってしまうのですが、そこはネストした場合に例外を吐くなどの処理を追加することでおかしな呼ばれ方ができないようになっています。 Stateを集約し、イミュータブルにする クラシルのAndroidアプリでは基本的にコンポーネント(Activity, Fragmentくらいの粒度で管理しています)内では単一のオブジェクトでStateを管理しています。Stateは原則イミュータブルで、State内のフィールドのいずれかが更新されるたびにStateのインスタンスが更新されるような仕組みになっています。 以下はクラシルのmodel関数(コンポーネントの状態を更新するロジックをmodelというfunctionにまとめています)内から一部抜き出したコードですが、基本的にStateはdata classとして定義しており、dispatcher.dispatch内でstateオブジェクトをcopyすることで新しいStateを作るという形で状態を更新しています。単一のオブジェクトであるStateが更新されるたびに、view関数が呼ばれて状態がUIコンポーネントに反映される仕組みになっています。 override fun model( action: Action, props: Props, state: State, dispatcher: StateDispatcher<State> ) { when (action) { is RecipeDetailListSnippet.Actions.UpdateRecipeSummary -> { dispatcher.dispatch { state.copy( summary = action.summary ) } } is RecipeDetailListSnippet.Actions.UpdateRecipeDetail -> { favoriteFeature.addBrowsingHistory(props.id) dispatcher.dispatch { state.copy( detail = action.video ) } } is RecipeDetailListSnippet.Actions.UpdateQuestions -> { dispatcher.dispatch { state.copy( questions = action.questions ) } } is RecipeDetailListSnippet.Actions.UpdateTaberepoInfo -> { dispatcher.dispatch { state.copy( taberepos = action.taberepos, taberepoCount = action.taberepoCount ) } } } } コードの責務を分割し、Stateの更新によらないViewの更新を抹殺する ここまでのコードで気づいた方もいらっしゃるかもしれませんが、クラシルのUIコンポーネント周りのコードは cycle.js のModel-View-Intentというアーキテクチャからの発想でコードを3つの関数に分割しています。 Android ViewからActionを発行するintent関数 ActionとStateをもとにアプリケーションの状態を更新するmodel関数 StateをもとにAndroid Viewを更新するview関数 Intentのコードの雰囲気だけ共有しておくと、こんな感じです。 override fun intent( layout: ComponentViewBinding, dispatcher: StatelessActionDispatcher<Argument> ) { layout.clearButton.setOnClickListener { dispatcher.dispatch { SearchInputSnippet.Actions.UpdateInputText( "" ) } } layout.voiceButton.setOnClickListener { dispatcher.dispatch { SearchInputSnippet.Actions.StartVoiceInput } } layout.searchInput.addTextChangedListener { text -> if (layout.searchInput.hasFocus()) { dispatcher.dispatch { SearchInputSnippet.Actions.UpdateInputText(text.toString()) } } } } 責務を3つのコードに分割して、view関数だけでUIの状態を更新するようになっています。原理上はintentでViewを更新できてしまいますが、Actionが受け取れないこと、Stateが参照できないことで通常の用途では更新しにくいことで抑止になっていることと、あとはコードレビューでそういったコードが混入しないように気を付けています。 クラシルのAndroidアプリでは、single source of truthの考え方をこれらの工夫で実現しています。どうしてもAndroid Viewが元々そういった発想で作られていないこともあり、たまにワークアラウンド的なコードが必要になることもありますが(layout.searchInput.hasFocus()はそのワークアラウンドの一部だったりします)、基本的にはすっきりコードを書くことが出来ています。 コンポーネントツリー全体でもsingle source of truthを実現する クラシルのAndroidアプリではFragmentの採用をやめたのですが、一方でコンポーネントツリーの分割統治はUIアプリケーションを作るうえでコードの再利用性などを考えても避けて通れない問題なので、独自の仕組みでコンポーネントツリーを分割統治できるようにしています。その詳細については今回は触れませんが、Fragmentなどを採用する場合でも考え方は一緒だと思いますので参考になればと思います。 ReactのパラメータがState/Propsに分かれている理由 クラシルのAndroidアプリを今のアーキテクチャにした際に、最初は全て単一のStateで作っていたのですが、途中からReactのように内部でプライベートで持つ状態をState、外部から渡される内部からは変更できない状態をPropsと分けて管理するようになりました。最初はなんらかのパラメータを外から渡してStateを更新する仕組みだったのですが、例えばディープリンクなどの処理によって親からコンポーネントの状態を変更することが発生すると、Stateと分離してPropsという状態を導入したほうが都合がいいことが分かったためです。 具体的には、親から渡されたパラメータをStateに反映し、かつ内部でそのパラメータを変更する場合に問題が発生しました。内部で親から渡されたパラメータを更新するようになると、上のコンポーネントはその変更を把握していないため、おかしなタイミングでStateが親から渡されるパラメータで上書きされてしまうという問題が発生しました。結局内部のパラメータを変更したいタイミングで上のコンポーネントに対してActionを投げるようにする、という形で対応したのですが、それは結局Propsという内部から変更不能なパラメータでUIの状態を更新しているのと同じことだということに後で気づき、その形に落ち着きました。 "一つ上のComponent"からStateを更新することを意識する State/Propsの仕組みにアーキテクチャを移行する際に、コンポーネントツリー全体でsingle source of truthを実現する際に意識すべきことが一つあることがわかりました。それは、 親が知っているべきことは必ず親がStateを持ち更新すること ということです。single source of truthに則ったアーキテクチャを採用すると、基本的には親から子供のコンポーネントに状態を問い合わせることがなくなるため、自然にそういった考え方になっていくのではないかとは思いますが。 限界はある、やりすぎないことも大事 今回クラシルのAndroidアプリのアーキテクチャについて全てのことをご紹介できたわけではないですが、以上のような工夫で宣言的UI時代も戦えるようなアーキテクチャにアップデートをはかっています。今後も生産性が高く、素早くユーザーに価値を提供できるアーキテクチャを維持していきたいですね。 とはいえ、基本的には非宣言的UIライブラリでやっていることなので、限界やちょっとした癖もあります。例えばRecyclerViewのスクロールのようなStateで管理すると困るような状態もあったり、アーキテクチャの統一感を意識しすぎると無理が出てくるような局面もあります。 95%位のユースケースは自然に書けるが、いざという時の5%くらいの抜け道は残しておくという発想で、開発に著しい無理が出ないような形を保っていきたいですね。 We're Hiring! クラシルのAndroidチームでは、生産性の高いアーキテクチャを維持・アップデートしつつも楽しくプロダクト開発できる人、したい人を求めています。今回の記事を読んで面白そうだな、もっと話を聞いてみたいなと思った人、クラシルの開発に興味が出てきた人、是非お話ししましょう。クラシルの採用に興味なくても、記事が面白いと思ってくれた人とは是非お話してみたいのでTwitterなどで連絡いただけると嬉しいです。 きのこる先生
アバター
こんにちは。 TRILL開発部で、webプロダクトのPO兼デザイン周りも担当しています、 yuaoです。 この記事は「dely #2 Advent Calendar 2020」21日目の記事です。 adventar.org dely #1もありますので、こちらも是非ご覧ください。 adventar.org 昨日は akina.mさんの記事 「システム管理者に贈る「運用改善に役立った!」AWSの機能4選」でした! tech.dely.jp akina.mさんにはTRILLでもとってもお世話になっています。 私にとってシステムの領域はまだまだまだまだ未知の部分が多いのですが、 システム管理者さんの視点で書かれていて、とても勉強になりました。 akina.mさんいつも本当にありがとうございます! さて、初めてこういったものを書かせていただきますが、 今回は、 デザインをデザイナーさんにお願いしたら ちょっとイメージとちがうものが上がってきた、 そんな時に、どんな風に修正指示を考えるか… というのをまとめてみました。 尚、今回指している「デザイン」とは 主に表層デザイン、ビジュアルデザインのことを扱っています。 どうしているか 結論から言いますと、タイトルの通りでして デザインを要素(「色」「線」「形」などのこと)に分解して、 どこに違和感があるか、を なるべく具体的に伝えるようにしています。 修正指示の話をする前に まずは大前提として、制作をお願いする時は、 デザイナーさんとしっかり情報の認識合わせをする というのが必須です。 上がってきたデザインが、イメージに合わせて貰えているかどうかは、 オーダー時の情報共有に、抜け漏れ無いようにする必要があります。 デザインで何を伝えたいのか、 どう言った用途で使うのか 納期、画像サイズなどの規約 この辺りは基本です。 そして、TRILLに関するデザインを作成して貰う場合で言うと、 もう1つ大事な共有事項があって、 それがブランドイメージです。 TRILLではブランドガイドラインなるものがあり、 ブランドを表現するための、考え方や、 具体的なデザインルールなどがまとまっています。 例えば色やフォント、画像の加工方法など、 細かく言語化されています。 なので、新しくデザイン制作に関わっていただくデザイナーさんには 最初に展開する、大事な資料の1つとしています。 (補足として、ブランドガイドラインは全てそのルールに則って作れ!ということではなく、 世界観を統一する為の推奨であり、 それを踏まえて考えてね、という感じのものです) デザインサンプルを見せてもらう 前置きが長くなりましたが、ここからが本題です。 デザインをお願いする時の情報は揃えた。 認識合わせもした。 そして制作を進めて頂いて、さあサンプル確認だ。 「…あれ?」 ってなることがあります。 勿論、イメージ通りのものを上げて頂く事多々あります。 が、「うーん?」となることもある。。 つまり違和感を感じる。 違和感を感じるものをOKには出来ないので、 ここから修正をお願いする事になります。 そこで「どこをどんな風に修正してもらうか」 を考えなければなりません。 私個人的に…ですが、 デザインにおける違和感を「ダサい」という一言で言わないようにしています。 その言葉は、その人の「好み」が根底にある場合が多く ロジカルな指摘ではないと思っているので… (メンバーの皆様、うっかり口を滑らせてたらほんとすみません) 自分が感じる違和感がどこからくるのかを突き止める 上がってきたデザインに違和感を感じたら、 その違和感を言語化することが、 とっても大事だと思っています。 そして違和感を言語化する時に、 私が頭の中に引っ張り出すのが 先述しました「デザインの要素」です。 グラフィックになる部分というのは、要素に分けることができます。 デザインの要素 線 色 形 テクスチャ 空間 フォーム フォント こちらの タカハマ ケンタ さんの記事がめっちゃ分かり易くて、 個人的には何度も読み直して、 とても参考にさせていただいています。 感覚派デザイナーも知っておいて損はない「デザインの要素と原則」 webnaut.jp 表層デザインやグラフィックデザインは、 大前提として「伝えるべき事」という骨格があって それをいかに「的確に」伝えるかを司る部分です。 その「的確」の中には、サービスのブランドイメージも勿論含まれているので、 TRILLの表層デザインやグラフィックデザインを作るときというのは即ち、 「TRILLとして、情報を伝える」為に、 厳選したデザイン要素を用いて、構成すること を言います。 違和感の分解の仕方例 (あくまで私の中のプロセスなので一例として) ではデザインを見た時に感じた「違和感」はどこからくるのか。 「ブランドイメージに合っていないのかな?」 「伝えなければいけないことに合っていないのかな?」 まず大枠で分けるとこんな感じになる事が多いです。 そして、 それなら「合ってない」と感じるのは どの要素が影響しているんだろう? みたいな考え方をしていきます。 例 例えばこの記事のタイトルサムネイルのデザインが こんな感じで上がってきたら。 ※シンプルに説明する為要素限定しています。 実際はもっと複合的に使う場合もあり(フォントと色とテクスチャ、背景画像、装飾など) より複雑なことが多いです。 違和感を感じるところはざっとこんな感じ (今回はTRILLブランドがどうかというのは省きます) 【大枠】 伝えなければいけない事を、スムーズに伝えられない可能性がある 何故か↓ 【フォント 】 フォントサイズは的確か。読み始めの「デザインの」の部分に、視点が最初にいかないのではないか。 【色】 テキストに使っている色にはどういう意図があるのか。意図した強弱になっているか。 【フォント】 選択しているフォントは的確か。途中でフォントを変更する意図は何か。こちらも読み始めの部分に視点を持っていけないのではないか。 【空間】 文字間は的確か。「がいいかも」「とい」「う話」で分かれて読めることで、読み手のリズムを崩さないか 【空間】 文言全体が画面に対して左に寄っていないか。左右の余白を変える意図は何か。重心が偏ることで、バランスが悪くなっていないか。 こんな感じで違和感を要素毎に分けて、 作り手の意図を聞いたり、修正の提案をしたりするのに使っていきます。 まとめ 表層デザインやグラフィックデザインは、 それが好きか嫌いかみたいな感情に先に触れることもあり、 違和感を言語化するのが難しい部分だなと思います。 私自身デザインを作ることもあるので、 自分が指示を受ける側だとしたら 具体的に言ってもらえた方が修正し易く、 効率的に対応できるなと思うので、 こんな進め方をするようになりました。 要素に分解していくと、 フラットに、気になる部分が見えてきて、 改善を考えるきっかけにし易くなりますし、 先ほどご紹介した記事に掲載されている他の「原則」なども併せていくと、 より、いろんな応用ができるので、 何かの参考になれば幸いです! 尚、要素分解は自分でデザインを作る時にも有効で、 自分が扱ってる要素は、 芯にある情報を、的確に伝えるためのものに出来ているかな? と、デザインを俯瞰して見る時に使えたりするので、 個人的にはオススメです。 おわりに リップサービスも含まれていると重々承知していますが、 一緒にお仕事をしている他のデザイナーさんに、 指示が分かり易いと言って頂いたことがあったので、 こんな感じで記事にまとめてみようと思い至りました。 デザイナーさん達、本当にいつもありがとうございます! プロダクトはあらゆる分野のものが複合的に重り 1つのものになっていて、 表層・ビジュアルデザインはその一部分です。 delyの中には、その各分野を解像度高く見れるメンバーがたくさんいて、 それぞれがより良いパフォーマンスをすべく、日々走っています。 私も自分の担当領域でのパフォーマンスを上げていくべく、 デザイン (今回は表層・グラフィックだけでなく、情報設計や体験設計も含めた意味) や プロジェクト推進の為に、これからも精進していきたいなと思います。 積極募集中 delyでは一緒にサービス成長させるエンジニアさんや デザイナーさんを積極採用中です。 興味のある方はぜひカジュアルにお話しさせてください! join-us.dely.jp また、delyではTechTalk という社内のメンバーがテーマ毎に話すイベントも開催していますので、 こちらもぜひチェックしてみてください! bethesun.connpass.com 明日は、クラシルのUIデザイナーkassyさんによる 「2020年 UIデザイナーが読んで良かった本 9冊」 です! 私自身、読まなきゃと思っている本が溜まっているのですが、 ご紹介されているものが興味深いものが多く、 さらに積まれそうな予感がします。。 是非こちらもご覧ください!
アバター
こんにちは!SREチームの松嶋です。 こちらは「dely #2 Advent Calendar 2020」の20日目の記事です。 adventar.org delyのアドベントカレンダーは#1もあるので、こちらもぜひ。 adventar.org 昨日は、maseoさんの「Google Optimizeでテストをしてる話」という記事でした。A/BテストでGoogle Optimizeを導入するか検討しているフロントエンジニアの皆さんはぜひ読んでみてください! tech.trilltrill.jp はじめに 私は昨年の11月にdelyへ入社しましたが、もう既に1年が過ぎてしまいました。体感的にまだ半年しか経っていない気持ちですが、そんな時間が過ぎるスピードの速さもdelyならではかもしれないと感じる今日この頃です。 delyのSREチームは、2020年9月までの約3年間、最大2人体制でなんとか頑張ってきました。10月以降は、新しくdelyにジョインしてくれたり、育休から復帰したメンバーが戻ってきたことによって、チームメンバーが4人に倍増しました。今までは足元課題を解決するために時間を取られていたのですが、現在は中々着手できていなかった課題にもアプローチができつつあるので、今後システムの改善速度が右肩上がりになる予感しかありません。なんだかとっても良い感じです。 少し前置きが長くなりましたが、現在delyでは、皆さんご存知の「クラシル」という動画レシピサービスの他にも「TRILL」という女性向けメディアの運営にも携わっています。最近、TRILLのバックエンドをGCPからAWSに移行したのもあり、delyが運営しているサービスの大部分はAWS上で動いています。 この記事では、クラシルやTRILLの実運用において、システム管理者の視点で「これは結構良いかも」と感じたAWSの機能を4つPickupして紹介しようと思います。 1. ECSサービス状態がCloudWatch Eventsで取得可能になった システム管理者としていち早く知りたいと思うのは、サービスの異常状態ではないでしょうか。従来まではECSサービスの状態変化を取得するためには、ECS APIを経由する手段しかありませんでした。そのため、サービスイベントの状態を検知する仕組みを自前で実装し、運用する必要がありました。 昨年11月のリリースによって、ECSがCloudwatchEventsに向けてECS Service Action イベントを発行するようになったので、ECSサービスの状態変化がとても簡単に検知できるようになりました。 aws.amazon.com このアップデートをうけてクラシルやTRILLでは、ECSサービスの異常を出来るだけ早く検知したかったので、「ERROR」と「WARN」イベントが発生したときはSlack通知するように設定しました。 例えば、実際どのような場合に助かったかというと、ECSのタスクがスケールアウトができない状態を早い段階で検知できたときです。この原因は、暫くデプロイされていないECSサービスのコンテナイメージがECRのライフサイクルポリシーによってイメージが削除されていたためでした。 ECS サービスアクションの通知 この「SERVICE_TASK_START_IMPAIRED」というWARNイベントは、タスクが正常に立ち上がることができないことを意味しています。 タスクがスケールアウトできなくてユーザーに影響が出始めてから気づく・・・というような恐ろしい状態になる前に気づいて対応できたので、本当に良かったです。 2. Systems Managerオートメーション 続いて2つ目は、Systems Managerのオートメーションです。一昔前のクラシルのデプロイは、ジョブスケジューラーのRUNDECKを使ってEC2サーバーにSSHし、デプロイスクリプトを実行するというやり方でした。 しかし、RUNDECKサーバーは、本番環境と開発環境のどちらにも接続できる状態になっていたり、サーバー自体がコード管理されておらずブラックボックスになっていたりと課題を多く抱えていました。 何か代替手段がないかと検討したときに、候補としてあがったのが「Systems Managerのオートメーション」です。 docs.aws.amazon.com オートメーションの良いところは、JSONドキュメントを書いてしまえば簡単にデプロイを自動化することができるところです。さらに、RUNDECKようにサーバーを自前で立てる必要もなし、SSHしないので鍵管理も不要なためセキュリティリスクも防げて、サーバーをメンテナンスするコストも削減できるので、今まで抱えていた課題をオートメーションによって解決することができました。 3. FireLens 続いて3つ目は、こちらも昨年11月に発表されたFireLensです。皆さんはコンテナログの収集はどのように管理していますか。小規模なアプリケーションであれば、コンテナから直接CloudwatchLogsにログを送るのが一番楽だと思いますが、クラシルやTRILLのような規模になってくるとログ収集だけで毎月のコストが無視できないほどになってきます。コスト面を考慮するなら、サイドカー方式で自前でFluentdコンテナを立ててログをElasticsearchやS3に転送する等が考えられますよね。 FireLensの何が良いかというと、サイドカー方式でFluentdやFluent Bitを利用するときのログ収集を楽にしてくれるところです。また、Fluent Bitを使うときはAWSが管理しているマネージドなDockerイメージを使うことができますし、タスク定義の中でログの出力先を指定すれば、FluentdやFluent Bitの設定ファイルをいじらなくてもログ転送が可能になります。 Filterプラグイン使ってログ加工したい場合は、カスタム設定ファイルが必要になりますが、それでも自前でコンテナ立てる場合よりシンプルになると思います。 aws.amazon.com さらに、FirelensだとデフォルトでECS メタデータがフィードに追加されているので、どのクラスターのどのタスク定義から送られてきたログなのか識別することも容易です。このECS メタデータは、タスク定義内のオプションで有効/無効にすることが可能なので、FluentdやFluent Bitの設定ファイル側で何か設定する必要もありません。 赤枠のフィードを出力するか否か「enable-ecs-log-metadata」オプションで指定できる TRILLでは、FireLensを使いコンテナログをKinesis Data Firehoseを経由してElasticsearchに転送しています。TRILLは月間利用者数が4000万を超えており、日々大規模なトラフィックを捌いているのですが、現状ログ周りがボトルネックならずに運用できています。 prtimes.jp 4. AWS Chatbot 最後に紹介するのは、AWS Chatbotです。暫くの間ベータ版でしたが、今年の4月に一般提供が開始されました。弊社もChatOpsの仕組みを自分たちで作っているものもありますが、AWS Chatbotを使えばとても簡単にSlack連携ができるようになります。 aws.amazon.com 例えば、CloudwatchAlarmやCodeシリーズの通知(実行開始、成功、失敗)などを簡単にSlack通知することが可能となります。CloudwatchAlarmの場合は、アラートになっているメトリクスのグラフを添付してくれたり、CodeBuildが失敗した場合は、失敗したフェーズのエラーメッセージを通知してくれるので結構便利だなと感じました。 CodeBuildの失敗通知例 しかし、通知内容のカスタマイズはできないので、自分たちが欲しい情報が取れない場合はChatbotは使えないですが、そんな手間かけずに知りたい情報に関してはChatbotで十分事足りるなと思います。 まだTerraformが対応していないのが1つの難点ですが、皆さんで"Thumb up👍🏻"してリリースされるのを後押しして待ちましょう。 github.com まとめ 今回は、運用改善につながったと感じるAWSの機能をクラシルやTRILLの実例を交えて紹介しました。気になったものがあれば、ぜひ導入を検討してみてください。 明日は、TRILLの可愛くて素敵なデザイン周りを担当しているyuaoさんの「デザインの指示に迷った時は、 「要素に分解」がいいかもという話」です!お楽しみに! 最後に また、dely ではエンジニアを絶賛募集中です! ご興味ある方はこちらのリンクから。 join-us.dely.jp まず、dely開発部の雰囲気を知りたい方は、TechTalk というイベントを定期的に実施していますので、お気軽に参加いただけると嬉しいです。カジュアル面談もこちらから応募いただけます! bethesun.connpass.com
アバター
こんにちは。 delyコマース事業部のJohn( @johnny__kei )です。 この記事は「dely #1 Advent Calendar 2020」の20日目の記事です。 adventar.org adventar.org 昨日はtakaoさん( takaoh717 )の「 エンジニアが始めるプロダクトマネジメント最初の一歩 」という記事でした。 はじめに 今回は、iOS開発で試しに使っている Tuist というツールを紹介したいと思います。 TuistはXcodeプロジェクトの生成、管理を楽にしてくれるコマンドラインツールです。 似たようなツールで XcodeGen があります。 大きな特徴は、 Project.swift というファイルでの中でプロジェクトの定義を Swift で記述するところです。 ここがイケてるやんと思って使い始めました。 (↓サイトトップに記載されている例) import ProjectDescription import ProjectDescriptionHelpers let project = Project.featureFramework( name : "Home" , dependencies : [ .project(target: "Features", path: "../Features"), .framework(path: "Carthage/Build/iOS/SnapKit.framework") .package(product: "KeychainSwift") ] ) また、 *.xcodeproj や *.xcworkspace はgitignore前提なので、コンフリクトして辛い思いをすることはありません。 SoundCloudでの開発にも使われているようです。 developers.soundcloud.com Tuistはドキュメントもいい感じで、さくっと試してみるのも簡単です。 Getting Started tuist.io ドキュメントに沿って雰囲気をお伝えします。 tuist init tuist init で始めると以下のようなフォルダ構成が生成されます(tuist ver 1.28.0) |--.gitignore |--Project.swift |--Targets | |--MyApp | | |--Resources | | | |--LaunchScreen.storyboard | | |--Sources | | | |--AppDelegate.swift | | |--Tests | | | |--AppTests.swift | |--MyAppKit | | |--Sources | | | |--MyAppKit.swift | | |--Tests | | | |--MyAppKitTests.swift | |--MyAppUI | | |--Sources | | | |--MyAppUI.swift | | |--Tests | | | |--MyAppUITests.swift |--Tuist | |--Config.swift | |--ProjectDescriptionHelpers | | |--Project+Templates.swift tuist edit tuist edit で定義用Xcodeプロジェクトを開きます。 Project.swift はこんな感じです。 let project = Project.app(name : "MyApp" , platform : .iOS, additionalTargets : ["MyAppKit", "MyAppUI"] ) Project.app(xxx) というのは Project+Templates.swift に記載されているHelperメソッドです。 MyApp というアプリケーションと MyAppKit と MyAppUI というEmbeddedFrameworkがあるプロジェクトを定義しています。 tuist generate tuist generate で Project.swift の定義から MyApp.xcworkspace や MyApp.xcodeproj が生成されます。 MyApp.xcworkspace を開くとこんな感じです。 実践編 他にどんな感じかで使うかの例を少し挙げます。 WorkSpace.swift で複数のプロジェクトをまとめる 複数の Project.swift を作成して Workspace.swift でまとめることもできます。 Getting Startedのときから、フォルダ構成も若干変えています。 |--.gitignore |--Projects | |--MyApp | | |--Project.swift | | |--Resources | | | |--LaunchScreen.storyboard | | |--Sources | | | |--AppDelegate.swift | | |--Tests | | | |--AppTests.swift | |--MyAppKit | | |--Project.swift | | |--Sources | | | |--MyAppKit.swift | | |--Tests | | | |--MyAppKitTests.swift | |--MyAppUI | | |--Project.swift | | |--Sources | | | |--MyAppUI.swift | | |--Tests | | | |--MyAppUITests.swift |--Tuist | |--Config.swift | |--ProjectDescriptionHelpers | | |--Project+Templates.swift |--Workspace.swift MyApp/Project.swift はこんな感じです。 let project = Project.app(name : "MyApp" , platform : .iOS, dependencies : [ .project(target: "MyAppKit", path: .relativeToManifest("../MyAppKit")), .project(target: "MyAppUI", path: .relativeToManifest("../MyAppUI")) ] ) tuist generate で生成された MyApp.xcworkspace はこんな感じになります。 特定のプロジェクトにフォーカス tuist focus XXX でとその依存関係を解決した xcworkspace が生成されます。 機能単位でXcodeプロジェクトがわかれているといじらないコードやターゲットも表示されないのでプロジェクトファイルがシンプルになるし、Indexingも早くなり、ファイル検索も膨大な中から探さなくて良くなるので、開発が捗ります。 SwiftがちょっとわかるデザイナーがUIComponentの確認や調整をするときは、UIのみのプロジェクトを動かす、などができます。(Xcode Previewを使用など) tuist focus MyAppUI をした時はこんな感じです。 依存関係が一目でわかる tuist graph で依存関係のグラフを生成できます。 プロジェクト間にどのような依存関係があるか一目でわかるので便利です。 μFeatures tuistはμFeaturesという考えた方をしていて、僕たちの開発において、だいぶ参考にしています。 tuist.io ほかにもいっぱい ほかにもいっぱい機能があるので、気になった方はぜひ触ってみてください。 さいごに delyでは、定期的にイベントを行っています。 僕も中の人ながら、聴くのを楽しみにしています。 メンバーがどんな感じの雰囲気かもわかるので、ぜひお気軽にご参加ください。 bethesun.connpass.com 各ポジション募集中なので、気になるなぁとか思った方はぜひイケてる募集サイト見てみてください。 join-us.dely.jp 明日はこばさん( @kazkobay )による記事です。お楽しみに!
アバター
こんにちは、delyでクラシルのiOSエンジニア兼PdMをしているtakao( takaoh717 )です。 この記事は「 dely #1 Advent Calendar 2020 」の19日目の記事です。 昨日はデザイナーredさんの「 Material DesignでUIデザインをブーストしよう 」という記事でした。 adventar.org adventar.org はじめに delyに入社して3年が経過し、僕は今iOSエンジニア兼PdMとしてプロダクトに関わっていますが、今の役割になるまでにこれまでいろいろな立ち回りをしてきました。 最近弊社で開催したPdMに関する イベント で「コードを書くことを主としているエンジニアがどうやってPdMとしての能力を磨くべきか」という質問をいただきましたが、イベント内では具体的な話をあまりできなかったので本記事で僕自身の事例を交えながら紹介したいと思います。 この記事がPdMとしてのキャリアを検討しているエンジニアの方やそういった動きを今後していきたい方の参考になる話ができればと思います。 プロダクトマネジメントのスタートは社内のボール拾いから プロダクトマネジメントの最初の一歩としては社内で転がっているボール拾いをすることから始めるのが良いかなと思っています。 ボール拾いというのは、誰もディレクションする人がいなかったり宙に浮いてしまっている状態のプロジェクトや課題がある場合にそれらを自分から巻き取りに行くようなことを指しています。 自分からボールを拾って物事を進めることができれば、PdMに必要なスキルが徐々に身につけられると思います。 ボールを拾うためにはプロダクトに関するあらゆる物事を自分ごとに捉える必要がある 普段コードに向かっているとなかなか他のことに目を向けられない なんてことはエンジニアであればよくあることだと思います。 もちろん集中してコードを書かないといけないときはありますし、それはそれでとても良いことだと思います。 しかし、社内でボールを拾っていくためには受け身の状態でいるのではなく主体的に動いていく必要があります。 どこに課題があるか、どこが改善できそうか、なにかおかしいところがないか など常にアンテナを張って、情報収集を続けることで気付けることがあると思います。 そうしていると、自分が介入することでうまく回る部分を見つけられ、次のアクションに繋げられるようになります。 部署間のステークホルダーを繋ぐハブになる 誰かが舵取りをしたほうが良さそうだけれど、誰も舵取りをしなくてなんとなく現場メンバーでふわっと進めてしまったり、全体を把握できている人がいないことで無駄なコミュニケーションが大量に発生する 、なんてことはないでしょうか。 関わっているプロジェクトがもしそういった状況に陥っている場合は率先して舵取りをしていけると良さそうです。 delyでは今でこそ スクワッド体制 になって各プロジェクトの舵取りをするPdMがいるのであまりそういった問題は起こりにくくなっていますが、PdMをそもそも明確に立てていなかったときやPdM1人体制だった頃などはプロジェクトの振り返りをしたときに責任者不在による問題の発生についてよく話が上がっていました。 スクワッド体制 PdMになるまで 僕は2017年11月にdelyにiOSエンジニアとして入社し、今も(頻度はかなり減りましたが)iOSのコードを書いていますが、どちらかというとPdMとしての役割をメインで担っています。明確にPdMという役割になったのは今からちょうど一年前くらいだったと思いますが、それまでもPdMっぽい動きを度々することがありました。 今考えると、僕の場合はボール拾いを何度も繰り返すことで少しずつエンジニアリング以外のスキルの幅を広げられたのかなと思っているので、その際にやっていたことや意識していたことなどを紹介します。 CSチームとの連携の改善 入社して1ヶ月くらいが経過したころ、ユーザーからの問い合わせの対応に課題を感じました。 当時感じた課題としては以下のようなものがありました CS対応が属人化していた 開発部のサーバーサイドエンジニア1人 ⇔ CS担当だけでクローズドにコミュニケーションしていた 伝言による無駄なコミュニケーションが発生していた 問い合わせ内容はアプリに関することがほとんどだけどサーバーサイドエンジニアが対応していたので、都度アプリエンジニアに質問や確認をするコミュニケーションが発生 無駄なコミュニケーションが発生することにより返答に時間がかかってしまうことがあった 開発チームが不具合を認識するまでに時間がかかっていた 開発チームがユーザーの声を拾えていなかった CS担当者が対応するためのドキュメントやテンプレートなどが整備されていなかった 改善に向けて起こしたアクション こういった課題を解決するためにまずはドキュメントの作成と周知を行い、改善に向けて以下のようなアクションをしました。 CS担当の人にプロダクトの基本的なことに関しては内部の仕組みも理解してもらえるようにする 一次回答はなるべくCSチームで対応できるような対応集を用意する 開発チームで他のメンバーも対応できる仕組みを作る 問い合わせの状況、開発の状況をお互いに見やすい環境を作る 問い合わせの一次回答で必要な情報はできるだけユーザーに聞いておく アプリ内によくある質問を掲載する 当時はまだステークホルダーもCS担当の方と当時数名のエンジニアだけだったのでそんなに難しいことはしていませんでしたが、こういったことをやり始めた結果その後も1年くらいはCSチームとの連携を続けることができ、ゆくゆくはプロダクトに変更を加えるような施策やCSチーム全体の運用の改善につながるような施策も色々と進められました。 (今は別のメンバーが担当しています) 上記のようなアクションを通して、例えば以下のようなスキルを身に着けていくことができました。 ドキュメントを使った言語化力 部署を跨いだステークホルダーとのコミュニケーション力 ユーザの行動を知るためのデータ分析力(+SQL) これらのスキルはPdMとして課題を解決していくためにどれも必要なものだと思いますが、職種に閉じたエンジニアリングをやっているだけではなかなか身につけるのは難しいと思います。 しかし、少し視野を広げて自分から動いていけば自然と身についていくことが多いと思います。 まとめ 今回この記事で特にお伝えしたかった内容は以下の2点です。 プロダクトマネジメントはボール拾いから始める プロダクトに関わるあらゆる出来事を自分ごととして捉えることが重要 今現在エンジニアとしてガリガリコードを書いていて、キャリアとしてPdMとしての道を考えている人の参考になれば幸いです。 おわりに 明日のアドベントカレンダーの記事はジョンさんの「Xcodeプロジェクト管理ツール「Tuist」を試している」です。ぜひ御覧ください! また、dely ではエンジニア/デザイナーを絶賛募集中です。 ご興味ある方はこちらのリンクからお気軽にエントリーください! delyではエンジニアを絶賛募集中です。ご興味のある方はぜひこちらのリンクを覗いてみてください。 カルチャーについて説明しているスライドや、過去のブログもこちらから見ることができます。 join-us.dely.jp delyの開発部では定期的にTechTalkというイベントを開催し、クラシル開発における技術や組織に関する内容を発信しています。 クラシルで使われている技術や、エンジニアがどのような働き方をしているのか少しでも気になった方はぜひお気軽にご参加ください! bethesun.connpass.com
アバター
ごきげんよう! TRILL開発部のWebを担当しています、maseoです。 この記事は「dely #2 Advent Calendar 2020」19日目の記事です。 dely #1 Advent Calendar 2020 - Adventar adventar.org dely #2 Advent Calendar 2020 - Adventar adventar.org 昨日は HPdM (ハイパー プロダクト マネージャー)のRiceさんの記事「 初心者PdMに贈る「"伝書鳩"が意思を持つために意識すべきこと 」でした。 エンジニアはHPdMさんに本当に助けてもらってます。 こんなに色々考えてくださっているのは本当にありがたいです。いつもありがとうHPdMさん達! さて 今回はTRILLのWebで行っているGoogle Optimize *1 を使ったA/Bテストはこんな感じ!というお話を書きたいと思います。 TRILLでは、以下のドキュメントを参考に、Google Optimize JavaScript API を利用しています。 developers.google.com TRILL Webでこの方法を採用している理由は色々ありますが、A/Bテストを行う上でサーバーサイド側の操作がGoogle Optimize上では厳しいとか、テスト内容によってはDBから取得する値を変えたいとか、テスト用の広告を入れたいとかです。 もう少し具体的に言うと、 記事詳細でA/Bテストがしたい!となった時に、URLが記事のID毎に異なっているため、「 このタイプの記事はテスト対象……、このタイプの記事は非対称…… 」なんてことがGoogle Optimizeのビジュアルエディタでは大変そうだな……ってなったからです。 実装 今回は過去に実際に実施したテストを書いてみます。 ※仮説の設定とか、テストパターンの作成とかは割愛します。 ■テスト内容 新しいモジュールを追加するにあたって、 「 どのタイプの表示がいいかな〜?そもそも追加しても他のモジュールに影響はないかな〜? 」 を確認したい ■テストパターン オリジナル(現行パターン) サムネイル小 サムネイル大 の3パターン ざっくりこんな感じです。 それでは、早速ですが、実装コードはこちら( ドンっ ) export default class OptimizeUtil {  constructor() {   this .getElement();   }  init() {   gtag( 'event' , 'optimize.callback' , {    callback: (value) =&gt; {      this .callback(value);     }    } );   }  getElement() {    this .nextArticleList = document .querySelector( '.js-next-article-list' );    this .nextArticleItems = document .querySelectorAll( '.js-next-article-item' );   …   }  changeClassToSmall3() { // テストパターンの表示を実現する操作    if ( this .nextArticleList) {     this .nextArticleList.classList.remove(`Articles_List`);    }    if ( this .nextArticleItems) {     this .nextArticleItems.forEach((el) =&gt; {     el.classList.remove(`Articles_Item`);     el.classList.add(`Articles_Item-small3`);     } );    } …   }  changeClassToBig3() { // テストパターンの表示を実現する操作   …   }  callback(value) {    switch (value) {     case '0' :      break ;     case '1' :      this .changeClassToSmall3();      break ;     case '2' :      this .changeClassToBig3();      break ;     default :      break ;    }   } } 一部割愛してますが作りはシンプルです。 テストパターン毎に適切な関数を呼び出して、あとはCSSとかで見た目を整えてあげます。 このテスト方法では、Google Optimizeでそれぞれのパターンの効果が見られるのはもちろん、新しいクリックイベントの計測もできますし、 今回は実装されていませんがパターン毎にリンクにクエリパラメータを簡単に付けれたりします。 例えば、Adjustを使ったアプリストアへの送客なんかもパターン毎の計測が簡単です。 また、同じ "記事" だけど、Aカテゴリーの記事ではテストして、Bカテゴリーの記事でテストしない!みたいなこともHTMLの出しわけとかで簡単に制御できます。(トラフィックには影響ない範囲で) 全部まとめて実装できるのも良いですね。レビューなども普段のフローでできるので、安心です。 表示確認 そして見た目はこんな感じになります。( ドドンっ ) ① <figure class="figure-image figure-image-fotolife" style="border: 1px solid #000; title="display_result_1"> 表示結果1 ② 表示結果2 ちゃんと表示が変わっていますね! あとはテストを見守るのみです。 他のモジュールに悪い影響がないかに注意して日々を過ごします。 テスト終了 時は流れ、 オプティマイズの結果はこんな感じになりました。 オプティマイズの結果 差が出てますね。(よかった) 計測データで他のモジュールにもマイナスな影響は確認されなかったので、テストしたパターンは無事採用されました!(よかった) ただ、このテストはGoogle Optimize上では全ユーザーに出てることになっておりまして、 Optimizeのトラフィックの割り当てを使ってないです。(別途ゴニョゴニョして数%のユーザーをテスト対象としています) なので、トラフィック割り当てを使う場合は、 テストパターンの対象になっていないユーザーのこともしっかり考える必要があります。 まとめ ということで、複雑なタイプのABテストも実施できるようになりました。 エンジニアの実装コストなどの問題はありますが、今のところはいい感じに運用できています。 今後もより良いテスト環境が作れるように、そしてしっかりPDCA回せるように頑張ります。 おわりに 明日は akina.m さんの「システム管理者に贈る 運用改善に役立った!AWSの機能4選」です!お楽しみに!( \\ 楽しみ!! // ) delyではエンジニアを積極採用中です。ご興味がありましたら、是非お気軽にお話させてください! join-us.dely.jp また、delyについて詳しく知りたいよって方は、TechTalk という社内のメンバーがテーマ毎に話すイベントもあるのでこちらも是非お気軽にご参加ください! bethesun.connpass.com *1 : Googleが提供している、A/Bテストを行うツール
アバター
こんにちは! TRILL開発部PdMの米田( @rice_ynd )です。 この記事は「 dely #2 Advent Calendar 2020 」18日目の記事です。 昨日はTRILL Android担当 永井さんの記事「 Merged Manifest を使って uses-permission を調査した話 」でした。 dely #1 Advent Calendar 2020 - Adventar adventar.org dely #2 Advent Calendar 2020 - Adventar adventar.org さて今回の記事ですが、 プロダクト開発においてPdMが果たすべき役割 について書いてみたいと思います。 「 PdMに任命されたけど、何をしたらいいの? 」な人や「 言われたことを言われたとおりにしかできない。やばい。 」な人にぜひ読んでみてほしいです。 そもそもPdMって? 一口にPdMといっても、プロダクトやチームの規模やフェーズ感、そのチームが担う責務によって細かく役割は異なるかと思います。 ただどんなプロダクトやチームにおいても本質は変わらないと考えていて、「 プロダクトをより良くするために、方針を指し示し舵を切ること 」がPdMの役割だと思っています。 いまチームがどこを向くべきで、そのために何が必要かをメンバーに明示し、それをマネジメントすることができている状態 が、PdMとして正しい役割を果たせている状態だと言えると思います。 そもそもPdMがどうあるべきかという包括的な話は、弊社で新規事業開発をしている奥原さん( @okutaku0507 )の記事「 プロダクトマネージャー1年目の教科書 」が参考になりますので、興味がある方はぜひ読んでみてください。 note.com "伝書鳩"になっている状態 では、どのような状態がPdMとして正しい役割を果たせていない状態なのでしょうか。 PdMに限らず、チームを推進する立場にある人が陥りがちなのが" 伝書鳩 "になってしまう状態です。 これはわかりやすく、PdMとして正しく機能していない状態であると思います。 具体的にどういうことかというと、例えばPdMにあたる役割の人が 他部署や他チームからの依頼や相談をそのまま自チームのメンバーに伝えている 経営層や上長の提言をそのまま方針としてチームに指し示している 物事の優先度が"言われた順"になってしまっている に当てはまる状態は、怪しいです。 これらが具体的にどう正しくないのか、ひとつずつ見ていきます。 ◯ 他部署や他チームからの依頼や相談をそのまま自チームのメンバーに伝えている これ自体が悪いというわけではありませんが、依頼の意図や相談によって解決したいことなど、本質を理解せずにチームに落とすだけではPdMを介する意味がありません。 むしろフローがひとつ増える分、コストが増えてしまいます。 その依頼を遂行すること or 相談を解決することで 何が改善するのか どんな価値が生まれるのか どんな負が生まれるのか などを理解し、優先順位を決定したり、足りない情報を補完したり、時に別の提案を返したりと、 プロダクト開発をスムーズに推進するための付加価値を生むこと がPdMの役割として適切です。 ◯ 経営層や上長の提言をそのまま方針としてチームに指し示している 前述の項目とほぼ同様ですが、意思を持たないPdMはチームに必要ありません。 他者の意見やアドバイスを咀嚼し、それがプロダクトにとって本当に必要かどうか、有効な打ち手かどうかを見極め、施策に取り入れるなどの判断をするのがPdMの役目です。 ◯ 物事の優先度が"言われた順"になってしまっている 「プロダクトをより良くする」ためには、数多ある問題の中からインパクトの大きいものを課題化し改善していく必要があります。 "インパクトの大きいもの"を測る観点として重要なのは、 事業KPIに与える影響 や ユーザーに与える体験の質 などです。 このインパクトの大小を根拠を以て判断し、対応コストを考慮した上でどこから手を付けるべきかを意思決定します。 言われた通りに物事を進めたら、何も進んでいなかった過去 偉そうにあれこれ語っていますが、これらはすべて過去の経験から学んだことで、自戒の意味もありこのテーマにしてみました。 まさに伝書鳩が原因で失敗した話ですが、とあるサービス課題を解消するために企画領域の担当者から施策の相談を受けたことがありました。 それを言われるがままタスクとして積み進行しようした際に、実装担当のエンジニアから指摘を受け、様々な観点において考慮すべき点が考慮されていないことが発覚しました。 発覚した時点ではすでに要件も仕様もfix、スケジュールもほぼ確定。 結局漏れていた観点を再考し、施策内容自体が変更になりスケジュールも引き直すはめに。 この失敗においては、あらかじめ施策の意図を理解し、プロダクトへの影響範囲や仕様や設計上の懸念点、リソース状況などを把握した上でどう進めるかを判断すべきでした。 とはいえ、例えば非技術者のPdMが実装についてすべてを把握したり、管轄外で走っている施策への影響を考えたりというのも現実的に難しかったりします。 ひとりで考え込まず、早い段階で有識者を巻き込み意思決定するというのもプロダクトマネジメントにおいては重要なポイントです。 担当プロダクトにおける「良し」を定義しておくこと 冒頭で「いまチームがどこを向くべきで、そのために何が必要かをメンバーに明示し、それをマネジメントすることができている状態」をPdMの果たすべき役割と述べました。 基本的に"改善"は、 理想と現実のギャップをひたすら埋める作業 だと考えています。 理想が存在しないプロダクトにギャップは存在しません。つまり、改善は行えません。 PdMは何よりもまず、 プロダクトがどうなることが「良し」なのかを知っておくこと が重要です。これがあらゆる意思決定における指針となります。 先に挙げた「"伝書鳩"になっている状態」は、プロダクトに対するPdMとしての意思が存在しないために他者の意見や依頼をそのまま受け入れざるを得ないことでそうなってしまっているというパターンが多いように思います。 月並みですが、プロダクトを理解し、ユーザーに寄り添い、どうなることがそのプロダクトにおいて理想かをひたすら思い描き、そこにチームを導く意思を持つことが脱"伝書鳩"の第一歩です。 note.com まとめ 今回は、"伝書鳩"が意思を持ってプロダクト開発を推進するために気をつけるべきことをお伝えしました。 プロダクトの理想形を思い描く(「良し」を知る) 現状と理想とのギャップを知る ギャップを埋めるために、施策の優先度を根拠を以て正しく整理する 自分がチームを理想形に向かわせる意識を高く持つ かくいう自分も初心忘るるべからず、これらの意識を念頭に置いて今後もTRILLを推進していきます。 明日は、TRILL開発部 フロントエンドエンジニアのmaseoさんによる「Google Optimizeでテストをしてる話」です。お楽しみに! 積極募集中 delyでは一緒にサービス成長させるエンジニアを積極採用中です。 興味のある方はぜひカジュアルにお話ししましょう! join-us.dely.jp また、delyではTechTalk という社内のメンバーがテーマ毎に話すイベントも開催していますので、こちらもぜひチェックしてみてください! bethesun.connpass.com
アバター