TECH PLAY

dely株式会社

dely株式会社 の技術ブログ

236

こんにちは。delyでAndroidエンジニアをしているkenzoです。 この記事は「 dely #1 Advent Calendar 2020 」の17日目の記事です。 昨日はサーバサイドエンジニア高松さんの「 バンディットアルゴリズムをライトに解説 」という記事でした。 A/Bテストとバンディットアルゴリズムを用いた施行が進む様子を並べて見比べられるのが面白かったです。ご興味ある方はぜひこちらも御覧ください! adventar.org adventar.org 今回はFirebaseのRemote Configを用いて一定割合のユーザーに値を振り分ける(A/Bテストをする)ときの設定方法についてお話します。 delyではクラシルを使用してくれているユーザーにより良い料理体験を届けられるよう、日々の機能開発・改善やキャンペーン施策等に対して開発部・マーケティング部のメンバーがFirebase Remote ConfigやA/B Testingを使ってユーザーに値を振り分け、機能の出し分け等を行うことが頻繁にあります。 今回少し複雑な設定を試す必要があり、改めて設定方法について検証したので、その内容をご紹介します。 今回は主にFirebaseのコンソール画面におけるRemote ConfigのConditionsタブでの値の振り分けの設定の仕方と反映のされ方についてご紹介します。 Remote Configと組み合わせて使用するA/B Testingも便利な機能でよく利用していますが、本記事ではあまり触れません。 また、今回はアプリ側の実装についても触れません。 こちらの順で説明していきます。 今回主に用いる設定 シンプルに振り分ける例 2分割 3分割以上 どのように振り分けられるのか 具体的なユースケースでの設定例 注意事項 キーが異なる条件の組み合わせだとうまくいかない 条件の順番を間違えるとうまくいかない まとめ おわりに 今回主に用いる設定 Remote Config画面のConditionsタブ右上の「条件を追加」ボタンを押して表示されるポップアップにおいて、 「ユーザー(ランダム %)」と、 「%」の右のボタンで設定できる「キー」を利用してユーザーを分けていきます。 シンプルに振り分ける例 2分割 50:50のユーザーに振り分ける場合はこちらのように <= 50% と > 50% の条件を作成します。 キーにはどちらの条件にも同じ値をセットします(ここでは test_a )。 (条件を1つだけにしてデフォルトを利用することもできます) 作成した条件を用いてRemote Configのパラメータを作成します。 作成したパラメータを公開して少し経つとパラメータの値が振り分けられたユーザーの割合が表示されます。 このように50:50のユーザーに値を振り分けることができました。 3分割以上 3つ以上のグループに値を振り分けたい場合はこちらのように複数の条件を作成します。 もちろんこれらの条件のキーは揃えます。 この場合は <= > のどちらかに統一するのがわかりやすくておすすめです。 こちらの設定ではデフォルトも含め4グループに値を振り分けています。 少し複雑になるので、ユーザー群に対してどのように値が振り分けられるのか説明します。 どのように振り分けられるのか 引き続き上記の3分割以上の例について見ていきます。 Conditionsにある条件は 上にあるものが優先される ので(画像だと A_3 > A_2 > A_1 )、下記の順に条件を満たしたユーザーに値が振り分けられていきます。 *1 random_user_test_A_3 で対象のユーザー100%のうち25%以下に当たる25%のユーザーに値 3 が振り分けられる random_user_test_A_2 残りの75%(25~100%)のうち50%以下(25~50%)に当たる25%のユーザーに値 2 が振り分けられる random_user_test_A_1 残りの50%(50~100%)のうち70%以下(50~70%)に当たる20%のユーザーに値 1 が振り分けられる 残りの30%のユーザーにはデフォルトの空の文字列が振り分けられる 図にするとこんな感じです。 その結果このように値が振り分けられています。 具体的なユースケースでの設定例 今度は架空のユースケースにおける設定を試してみます。 新機能をリリースし、その機能を30%のユーザーに対してのみ表示させる 新機能が表示されているユーザーの中でもプレミアムユーザーにのみ、新機能の特別な使い方をお知らせするページへ飛べるバナーを表示させる プレミアムユーザーかどうかはユーザープロパティを使用して判定 *2 この検証の環境ではプレミアムユーザーの割合は50%程度 Conditionsにてこのような条件を作成します。 作成した条件を用いてRemote Configのパラメータを設定します。 これで上記の仕様通りにパラメータが function banner に割り振られますが、今回はRemote Configの画面を見るだけでは正しく割り振られたことが確認できません。 クラシルではRemote Configで設定した値がどのように割り振られたのか知るために、ログ基盤にどんな値が割り振られたのかを送るようになっています。 今回はこのようにログ基板に送られたログを確認することで、仕様通りにパラメータが割り振られたのかを確認します。 このような感じのSQLで実際に振り分けられた結果のログを確認します。 AB_TEST_LOGテーブルにユーザー毎に割り振られた値が入っているものとします。 実際に上記の仕様と同様の設定をしてログに溜まった値を計測した結果がこちらです。 *3 新機能はおよそ30%(0.87 + 13.29 + 15.86 = 30.02)がonで、およそ60%(33.15 + 36.83 = 69.98)がoff バナーは新機能がonのユーザーのみがonで、かつ、プレミアムユーザーのみがon となっており、上記の仕様を満たしています。 (プレミアムユーザーで新機能がon、バナーがoffのユーザーが少しの割合存在していますがこれはログ送信のタイミングによって生じている誤差です。 *4 ) 注意事項 設定の際に気を付けておくことをご紹介します。 キーが異なる条件の組み合わせだとうまくいかない 下記のように設定されたキーが別の条件を組み合わせてパラメータを作成すると、 条件ごとにユーザーのマッピングが変わってしまうため、このように意図しないユーザー群に振り分けられてしまいます。 実際に反映された値はこちらのようになっていました。 条件の順番を間違えるとうまくいかない 3分割以上の場合、 上の説明 のように値が割り振られていくため、条件に設定するユーザーの割合は 狭い範囲の条件から 順に反映されるように設定します。 Conditionsにある条件は 上にあるものが優先される ので、狭い範囲の条件から順に並べておきます。 逆に、こちらのように広い範囲の条件が優先されるように設定してしまうと、 このように広い範囲の条件のみに値が割り振られてしまいます。 まとめ Firebase Remote ConfigのConditionsを用いた少し複雑な設定をする方法をご紹介しました。 ユーザーに値を振り分けるのは今回の方法の他にも同じFirebaseのA/B Testingや他社サービスでも実現できますが、今回のように具体的なユースケースとして紹介した振り分け方も知っておくと、選択肢が1つ増えると思います。 また、今回の方法は設定が少し複雑になるため運用上のミスも発生しやすい箇所となりますので、実際に振り分けられても影響のない値で検証してから利用することをおすすめします。 今回の内容が皆様の日々の改善の一助になれば幸いです。 おわりに 明日はデザイナーredさんの「Material DesignでUIデザインをブーストしよう」です。ぜひ御覧ください! また、dely ではエンジニアを絶賛募集中です。 ご興味ある方はこちらのリンクからお気軽にエントリーください! delyではエンジニアを絶賛募集中です。ご興味のある方はぜひこちらのリンクを覗いてみてください。 カルチャーについて説明しているスライドや、過去のブログもこちらから見ることができます。 join-us.dely.jp delyの開発部では定期的にTechTalkというイベントを開催し、クラシル開発における技術や組織に関する内容を発信しています。 クラシルで使われている技術や、エンジニアがどのような働き方をしているのか少しでも気になった方はぜひお気軽にご参加ください! bethesun.connpass.com *1 : 参照: Remote Config のパラメータと条件 *2 : 参照: Remote Config とユーザー プロパティ *3 : 実際はプレミアムユーザーではなく50%くらいとなる条件を設定し、そのログを計測した結果です *4 : 「非プレミアムユーザーがプレミアムユーザーになったタイミング」と「それがFirebaseにユーザープロパティとして反映されて値を再取得するまで」の間にログ送信のタイミングがきてしまったことによるものです
アバター
どもです、TRILLのAndroid担当してます永井です。 この記事は「dely #2 Advent Calendar 2020」の17日目の記事です。 adventar.org 「dely #1 Advent Calendar 2020」はこちら↓ adventar.org 昨日は @MeilCli さんの C# 9.0時代のnull判定解剖 という記事でした。 様々なnull判定の比較検証がまとまってますので、こちらもぜひ御覧ください! さて 今回は APK で要求している uses-permission の手軽な解析方法について話したいと思います。 先日、新しく広告SDKを実装したAPKをビルドしていたところ、心当たりのない uses-permission が付与されていることに気づき、要求元を調査していました。 そこで直近実装したものを一つづつ外して追いかけようとしていたところ、 メンバーに Merged Manifest 使うと便利ですよーってアドバイスをもらい即解決できたのでこの感動と Tips を忘れないうちにまとめました。 やったこと は超簡単で、まず AndroidStudio 内でプロジェクトの AndroidManifest.xml を開きます。 下タブに [Text] [Merged Manifest] とあり、[Text] には開いたマニフェストで宣言している定義値が並んでいますが、今回使うのは [Merged Manifest] の方です。 Android の APK に含めることのできる AndroidManifest は一つだけなので、外部ライブラリや Flavor などのマニフェストはビルド時に一つにマージされます。 [Merged Manifest] ではその一つにマージされたマニフェストの定義値を確認することができます。 少しみづらいですが色が使ってるライブラリと対応していて、uses-permission をクリックすると使用しているライブラリのマニフェストを表示することができます。 例えば選択行の READ_EXTERNAL_STORAGE および WRITE_EXTERNAL_STORAGE は leakcanary (デバッグ時のリーク検出ライブラリ)で定義されていることがわかります。 参考リンク developer.android.com 解析結果 今回謎だった uses-permission は android.permission.READ_EXTERNAL_STORAGE android.permission.WRITE_EXTERNAL_STORAGE android.permission.READ_PHONE_STATE の3つで、今回広告を実装するにあたってデバッグ用に追加した AdMob のテストスイートの消し忘れによるもので、呼び出しコードを削除していたが build.gradle に依存が残っていて権限要求されていました。 また開発用デバッグ Flavor で有効になる leakcanary も android.permission.READ_EXTERNAL_STORAGE android.permission.WRITE_EXTERNAL_STORAGE を要求していることがわかりました。 なのでテストスイートを外し改めてリリースビルドすることで無事不要な権限を要求することのないAPKをビルドすることができました。めでたしめでたし。 まとめ 出処不明な uses-permission やその他定義は AndroidManifest.xml の [Merged Manifest] から簡単に追える。 使わないコードは依存も忘れず削除しよう。 以上です! おわりに 明日はプロダクトマネージャーの Rice さんの 初心者PdMに贈る「"伝書鳩"が意思を持つために意識すべきこと」 です。ぜってぇ見てくれよな! delyでは一緒にサービス成長させるエンジニアを積極採用中です。 興味のある方は気軽にお話しましょう〜! join-us.dely.jp delyについて詳しく知りたいよって方は、TechTalk という社内のメンバーがテーマ毎に話すイベントもあるのでこちらも是非ご参加ください! bethesun.connpass.com
アバター
どうもC#erの @MeilCli です。仕事ではAndroidエンジニアしてますがC#erなのでアドベントカレンダーではC#について書きます 今回参加してるアドベントカレンダーはこちらです。16日目の記事になります adventar.org あと同様なカレンダーがもう1つあります adventar.org また、この記事の一部を クイズにしたもの も投稿していますのでよろしければそちらもご覧ください 祝: C# 9.0リリース さて、つい先日 .NET 5 と共に C# 9.0 がリリースされました。C# 9.0の新機能は多々あるのですがその中でパターンマッチングの強化の一貫で value is not null のようにnot条件が追加されました。この新機能によってC# 8.0のようにnot null判定をするために value != null や value is T nonNull や value is {} を書かずとも自然言語的な文章で書けるようになりました C#では前述のようにバージョンアップに連れ様々なnot null判定ができる構文が追加され、どの構文を使えばいいのか迷うところでもありました。null判定も同様に様々な構文があり、 場合によっては特定の構文は使わないほうがパフォーマンス的に良い ということさえありました というわけで今回はC#における歴代のnot null・null判定の構文紹介とC# 9.0時代の最適な判定方法を探していこうと思います 様々なnull・not null判定方法 null判定 null判定はC#バージョンによっての判定方法の追加が少なく、よく使われるものだと2種類あるかと思います *1 // 素直に==比較 bool isNull = value == null ; // C# 7.0のパターンマッチング(定数パターン) bool isNull = value is null ; それ以外のものだと以下のような方法が考えられると思います *2 bool isNull = object .Equals( value , null ); // 参照型の場合 bool isNull = object .ReferenceEquals( value , null ); bool isNull = EqualityComparer<T>.Default( value , null ); null判定に関しては方法が少ないためパフォーマンスが同じならば書き手の好きな方を選べばいいとなるかと思いきや、 == 演算子がオーバーロード可能なため型によってはnullと==比較しているのにfalseを返すような邪悪なことをされる恐れがあります *3 。より意図した通りのコードにしたいならば value is null の判定方法が一択になるでしょう not null判定 not null判定はnull判定よりも方法が多く、自分が知っているだけでもよく使われるもので5種類あります // 素直に!=比較 bool isNotNull = value != null ; // is演算子 bool isNotNull = value is string ; // string?の場合、int?ならばvalue is intになる // C# 7.0のパターンマッチング(型パターン) bool isNotNull = value is string notNullValue; // C# 8.0のパターンマッチング(プロパティーパターン) bool isNotNull = value is { }; // C# 9.0のパターンマッチング(not expression + 定数パターン) bool isNotNull = value is not null ; それ以外にもnull許容値型の場合はHasValueプロパティによる判定もできます // null許容値型の場合 bool isNotNull = value .HasValue; さて、not null判定でもより意図した通りのコードにしたいならば前述のように != 演算子を避けた判定方法を取るといいのですが、それ以外の選択肢がたくさん存在しています。これは実際にパフォーマンスを計測してみるしかありませんね ベンチマーク パフォーマンスを測るためのベンチマークツールはいつも通り BenchmarkDotNet を使います 計測対象のプロジェクトでは.NET5でnull許容参照型を使ったりするので以下のようなcsprojにします <Project Sdk = "Microsoft.NET.Sdk" > <PropertyGroup> <OutputType> Exe </OutputType> <TargetFramework> net5.0 </TargetFramework> <Nullable> enable </Nullable> </PropertyGroup> <ItemGroup> <PackageReference Include = "BenchmarkDotNet" Version = "0.12.1" /> </ItemGroup> </Project> また計測対象となるクラスの基本構成はこんな感じです [SimpleJob] [MeanColumn, MinColumn, MaxColumn] [MemoryDiagnoser] public class Bench { [Params( null , "" )] // or null, 1 public string ? Value { get; set; } // or int? [Benchmark] public bool Method() { bool result = false ; for ( int i = 0 ; i < 10 ; i++) { result = Value is null ; // ここを計測したいケースごとに変える } return result; } } 本来ならば計測対象のメソッドにforループで演算回数の水増しをしないほうがいいのですが、単純に1回限りの演算だと実行時間が早すぎて計測できないため10回ループさせています *4 ベンチマークケース 今回は値型と参照型の基本的な想定ケースとして string? と int? をnullと空文字または1の値のときを計測しました まとめると4回の計測になります 参照型(string?)のnull判定するとき 参照型(string?)のnot null判定するとき 値型(int?)のnull判定するとき 値型(int?)のnot null判定するとき また、null・not null判定は ! 演算子やfalse比較で反転した結果にすることで同様な判定方法を記述できるため計測パターンは ! 演算子を使いつつnull判定とnot null判定でほぼほぼ同じ判定式になるようにケースを作成しました 参照型・null判定 メソッド名 判定式 EqualOperator Value == null IsOperator !(Value is string) PatternMatchNull Value is null PatternMatchNotNull7 !(Value is string notNullValue) PatternMatchNotNull8 !(Value is { }) PatternMatchNotNull9 !(Value is not null) ObjectEquals object.Equals(Value, null) ObjectReferenceEquals object.ReferenceEquals(Value, null) EqualityComparer EqualityComparer<string?>.Default.Equals(Value, null) 参照型・not null判定 メソッド名 判定式 EqualOperator Value != null IsOperator Value is string PatternMatchNull !(Value is null) PatternMatchNotNull7 Value is string notNullValue PatternMatchNotNull8 Value is { } PatternMatchNotNull9 Value is not null ObjectEquals !(object.Equals(Value, null)) ObjectReferenceEquals !(object.ReferenceEquals(Value, null)) EqualityComparer !(EqualityComparer<string?>.Default.Equals(Value, null)) 値型・null判定 メソッド名 判定式 EqualOperator Value == null HasValue !(Value.HasValue) IsOperator !(Value is int) PatternMatchNull Value is null PatternMatchNotNull7 !(Value is int notNullValue) PatternMatchNotNull8 !(Value is { }) PatternMatchNotNull9 !(Value is not null) ObjectEquals object.Equals(Value, null) EqualityComparer EqualityComparer<int?>.Default.Equals(Value, null) 値型・not null判定 メソッド名 判定式 EqualOperator Value != null HasValue Value.HasValue IsOperator Value is int PatternMatchNull !(Value is null) PatternMatchNotNull7 Value is int notNullValue PatternMatchNotNull8 Value is { } PatternMatchNotNull9 Value is not null ObjectEquals !(object.Equals(Value, null)) EqualityComparer !(EqualityComparer<int?>.Default.Equals(Value, null)) 結果 BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042 Intel Core i9-10900K CPU 3.70GHz, 1 CPU, 16 logical and 8 physical cores .NET Core SDK=5.0.100 [Host] : .NET Core 5.0.0 (CoreCLR 5.0.20.51904, CoreFX 5.0.20.51904), X64 RyuJIT DefaultJob : .NET Core 5.0.0 (CoreCLR 5.0.20.51904, CoreFX 5.0.20.51904), X64 RyuJIT 計測環境はこんな感じです *5 参照型・null判定 参照型・not null判定 値型・null判定 値型・not null判定 また、計測コードなどの詳細はGitHubに公開しているのでそちらを参照ください github.com 要約 ちょっと結果が多すぎるためそれぞれの平均値を取ってみます *6 メソッド名 参照型 値型 EqualOperator 3.106 3.282 HasValue N/A 3.25 IsOperator 3.129 226.055 PatternMatchNull 2.65 3.746 PatternMatchNotNull7 2.621 6.292 PatternMatchNotNull8 3.097 3.274 PatternMatchNotNull9 2.631 3.275 ObjectEquals 14.37 248.335 ObjectReferenceEquals 3.075 N/A EqualityComparer 16.75 52.655 総合的にはPatternMatchNotNull9がよく、それ以外の場合ではそれぞれ参照型・値型で長短があったりそもそも遅かったりという感じでしょうか ちなみに EqualityComparerのケースでは EqualityComparer<T>.Default を取得する時間が影響を与えてる可能性があったため、string?とint?それぞれのインスタンスを取得する時間のベンチマークを取りました [SimpleJob] [MeanColumn, MinColumn, MaxColumn] [MemoryDiagnoser] public class EqualityComparerBench { [Benchmark] public IEqualityComparer< string ?> StringEqualityComparer() { return EqualityComparer< string ?>.Default; } [Benchmark] public IEqualityComparer< int ?> IntEqualityComparer() { return EqualityComparer< int ?>.Default; } } 結果としては計測できないほど早い処理が行われてそうという感じでした。 .NET Core 2.1におけるDevirtualization関連の最適化 によってランタイム側で EqualityComparer<T>.Default をすり替えて仮想メソッド呼び出しのコストを回避したという話もあるようなのでそのあたりが影響してるのではないかなと思います こういうこともあったり、どこからどこまで *7 をベンチマーク対象として捉えればいいのかややこしくなってくるということもあるので今回の計測では IEqualityComparer<T>.Default.Equals(Value, null) を計測することにしました ベンチマーク結果の解剖 さて、ベンチマークを出して終わりではありません。.NET 5(on Windows)の結果はわかりましたのでそれぞれのベンチマークケースでなぜ差が生じたのかを紐解いていきます C#でこのようなベンチマークケースの差を探るにはまずC#のコンパイル結果となるCIL *8 を見るのが手っ取り早いです。今回は ILSpy を使ってデコンパイルしました また、CILは中間言語ということもあってコードが長くなる傾向になります。この場ではできる限り省いたものを載せるので全文を読みたい方は GitHubリポジトリー を参照してください 参照型・null判定 // Value == null // !(Value is string) // Value is null // object.ReferenceEquals(Value, null) ldarg . 0 call instance string NullCheck .Benchmark.ReferenceNullBench :: get_Value () ldnull ceq object.ReferenceEquals(Value, null) がコンパイル時の最適化によってあとかたもなくなっていることには驚きですね。 ldarg.0 でスタックから引数0(つまりこのクラスのインスタンス)を読み込んで call instance string NullCheck.Benchmark.ReferenceNullBench::get_Value() でプロパティの値を読み込み、 ldnull で読み込んだnull参照とプロパティの値を ceq で等値比較するという感じです // !(Value is string notNullValue) // !(Value is { }) // !(Value is not null) ldarg . 0 call instance string NullCheck .Benchmark.ReferenceNullBench :: get_Value () ldnull cgt .un ldc .i4 . 0 ceq こちらは cgt.un で大小比較し、 cgt.un でint32の1か0がstackにpushされているので ldc.i4.0 (つまりint32の0)と ceq で等値比較しています。前述の Value == null などのケースより命令数が多くなってるのでパフォーマンス的に不利かと思いきや、ベンチマーク結果的にはあまり差がないようです // object.Equals(Value, null) ldarg . 0 call instance string NullCheck .Benchmark.ReferenceNullBench :: get_Value () ldnull call bool [ System .Runtime ] System .Object :: Equals ( object , object ) object.Equals の場合はメソッドの呼び出し結果をそのまま使うという感じでした。あまり面白みがないのですがあとで object.Equals の実装を探ってみます // EqualityComparer<string?>.Default.Equals(Value, null) call class [ System .Collections ] System .Collections.Generic.EqualityComparer ` 1 < !0> class [System.Collections]System.Collections.Generic.EqualityComparer`1<string>::get_Default() ldarg . 0 call instance string NullCheck .Benchmark.ReferenceNullBench :: get_Value () ldnull callvirt instance bool class [ System .Collections ] System .Collections.Generic.EqualityComparer ` 1 < string >:: Equals ( !0, !0) EqualityComparer<string?>.Default.Equals(Value, null) に関してはCIL的にはcallvirtで仮想メソッド呼び出しを行っている箇所がコストになりそうなものの、ランタイム側で最適化されるという話もあるためCILレベルではあまり判断できそうにないですね。ここに関しては実装を深堀っていこうと思います 参照型・not null判定 // Value != null // Value is string // Value is string notNullValue // Value is { } // Value is not null // !(object.ReferenceEquals(Value, null)) ldarg . 0 call instance string NullCheck .Benchmark.ReferenceNotNullBench :: get_Value () ldnull cgt .un こちらはnull判定の時とは逆に cgt.un で大小比較するという形になっていますね。ベンチマーク結果的には Value != null と Value is string と Value is { } が他のケースよりちょっと遅いかな(?)という印象もありましたがCIL的には同じ結果にコンパイルされているので誤差の範囲なのでしょうか…(MinとMaxでも明らかに差がついているのでランタイム側でなんらかの最適化が入ってそうな気がしないこともないですがこれ以上はわかりませんね) // !(Value is null) ldarg . 0 call instance string NullCheck .Benchmark.ReferenceNotNullBench :: get_Value () ldnull ceq ldc .i4 . 0 ceq このケースのみ他のパターンマッチングなどと違い、 ceq でnull参照と等値比較し、さらにその結果を ldc.i4.0 と等値比較しています。否定演算子を正直に変換している感じがしますね。こちらはCILレベルでは命令数的に不利ですが前述のケースとの差はなさそうです !(object.Equals(Value, null)) と !(EqualityComparer<string?>.Default.Equals(Value, null)) に関してはnull判定のときから ldc.i4.0 と ceq で結果を反転してるだけなので省きます 値型・null判定 // Value == null // !(Value.HasValue) // Value is null // !(Value is { }) // !(Value is not null) ldarg . 0 call instance valuetype [ System .Runtime ] System .Nullable ` 1 < int32 > NullCheck .Benchmark.ValueNullBench :: get_Value () stloc . 2 ldloca .s 2 call instance bool valuetype [ System .Runtime ] System .Nullable ` 1 < int32 >:: get_HasValue () ldc .i4 . 0 ceq さて値型の場合ですが、null許容値型は内部的にはNullable<T>構造体で表現されています。プロパティから値を取ってきたあとは stloc.2 でローカル変数に保存し、 ldloca.s.2 でそのローカル変数のアドレスを取得しています。そしてそのアドレスに対しNullable<T>構造体のHasValueプロパティのgetter実装である get_HasValue メソッドを呼び出しています。そのあとは値を反転するために ldc.i3.0 と ceq を使っていますね // !(Value is int) ldarg . 0 call instance valuetype [ System .Runtime ] System .Nullable ` 1 < int32 > NullCheck .Benchmark.ValueNullBench :: get_Value () box valuetype [ System .Runtime ] System .Nullable ` 1 < int32 > isinst [ System .Runtime ] System .Int32 ldnull cgt .un ldc .i4 . 0 ceq IsOperatorのケースが値型で極端に遅いというベンチマーク結果が出ていましたが、原因は box でボックス化しているからですね。この遅さはC# 7.3の頃に 調べてみた結果 と同様なままのようです。コンパイラーの最適化次第な領域ではありますが、現時点でボックス化される形にコンパイルされることを鑑みると値型においては value is int みたいな形式は避けておいたほうが無難でしょう !(Value is int notNullValue) IL_0006 : ldarg . 0 IL_0007 : call instance valuetype [ System .Runtime ] System .Nullable ` 1 < int32 > NullCheck .Benchmark.ValueNullBench :: get_Value () IL_000c : stloc . 2 IL_000d : ldloca .s 2 IL_000f : call instance bool valuetype [ System .Runtime ] System .Nullable ` 1 < int32 >:: get_HasValue () IL_0014 : brfalse .s IL_0021 IL_0016 : ldloca .s 2 IL_0018 : call instance !0 valuetype [System.Runtime]System.Nullable`1<int32>::GetValueOrDefault() IL_001d : pop IL_001e : ldc .i4 . 1 IL_001f : br .s IL_0022 IL_0021 : ldc .i4 . 0 IL_0022 : ldc .i4 . 0 IL_0023 : ceq 今まで行数は省略して紹介してきましたが、今度のはジャンプする命令があるため行数も書いています。 brfalse.s ではstackの値(ここではget_HasValueの結果)がfalse(つまり0の値)の場合に引数の行数であるIL_0021にジャンプさせています、つまり早期リターンのようなものですね。trueだった場合はその直後の命令が実行されていき、GetValueOrDefaultを呼び出しています。しかし、C#コード上では宣言した notNullValue 変数をしていない箇所が直訳されてるようで、 pop によってGetValueOrDefaultの結果を捨てています。このような無駄な命令があるため、他の早いケースと比べるとちょっと遅くなってしまっています 参照型の場合では跡形もなく消えていた未使用変数部分が値型の場合では直訳されるようなのでまだ少し最適化の余地があるという感じのようです // object.Equals(Value, null) ldarg . 0 call instance valuetype [ System .Runtime ] System .Nullable ` 1 < int32 > NullCheck .Benchmark.ValueNullBench :: get_Value () box valuetype [ System .Runtime ] System .Nullable ` 1 < int32 > ldnull call bool [ System .Runtime ] System .Object :: Equals ( object , object ) object.Equalsの場合もボックス化が走っているようですね。これが遅い原因だとは思いますがあとでobject.Equalsの実装を覗けたらなと思います // EqualityComparer<int?>.Default.Equals(Value, null) call class [ System .Collections ] System .Collections.Generic.EqualityComparer ` 1 < !0> class [System.Collections]System.Collections.Generic.EqualityComparer`1<valuetype [System.Runtime]System.Nullable`1<int32>>::get_Default() ldarg . 0 call instance valuetype [ System .Runtime ] System .Nullable ` 1 < int32 > NullCheck .Benchmark.ValueNullBench :: get_Value () ldloca .s 2 initobj valuetype [ System .Runtime ] System .Nullable ` 1 < int32 > ldloc . 2 callvirt instance bool class [ System .Collections ] System .Collections.Generic.EqualityComparer ` 1 < valuetype [ System .Runtime ] System .Nullable ` 1 < int32 >>:: Equals ( !0, !0) EqualityComparerのケースは少しややこしいですね *9 1スタック目 *10 に ldarg.0 と call によってValueプロパティの値をpushし、2スタック目に ldloca.s 2 でローカル変数のNullable構造体のアドレスをpushし、 initobj でNullable構造体の初期化を行っています(ここでスタックは消費している)。そして2スタック目に ldloc.2 で初期化したNullable構造体のローカル変数の値をpushし、 callvirt でそれらの値を使ってEqualityComparerのメソッドを呼んでいます EqualityComparerのケースがボックス化してるケースよりは早いけど時間がかかっているのはcallvirtしてるからという可能性もありますが、Devirtualizationされてると思われる箇所なのでEqualityComparerの実装体が少し遅い処理ということなんじゃないかなと想像できますね 値型・not null判定 // Value != null // Value.HasValue // Value is { } // Value is not null ldarg . 0 call instance valuetype [ System .Runtime ] System .Nullable ` 1 < int32 > NullCheck .Benchmark.ValueNotNullBench :: get_Value () stloc . 2 ldloca .s 2 call instance bool valuetype [ System .Runtime ] System .Nullable ` 1 < int32 >:: get_HasValue () こちらは値型・null判定での Value is null などのケースから ldc.i4.0 と ceq をしなくなったバージョンです。単純にbool値の反転がなくなったということですね Value is int や Value is int notNullValue でも同様に ldc.i4.0 と ceq の命令がなくなっていました // !(Value is null) ldarg . 0 call instance valuetype [ System .Runtime ] System .Nullable ` 1 < int32 > NullCheck .Benchmark.ValueNotNullBench :: get_Value () stloc . 2 ldloca .s 2 call instance bool valuetype [ System .Runtime ] System .Nullable ` 1 < int32 >:: get_HasValue () ldc .i4 . 0 ceq ldc .i4 . 0 ceq !(Value is null) のケースでは Value is null のケースからさらに ldc.i4.0 と ceq で値の反転をしています これと同様に !(object.Equals(Value, null)) と !(EqualityComparer<int?>.Default.Equals(Value, null)) もC#コードの通りに ldc.i4.0 と ceq による値の反転がされていました Object.Equalsの実装 さて、Object.Equalsの実装が気になるので調べてみましょう。Objectは.NETの基礎となる型です。そのためソースコードを見るならば.NET5や.NET Coreのランタイム側を見るとよさそうです .NET5や.NET Coreのランタイムは dotnet/runtime に公開されています *11 GitHubの左上にある検索ボックスで filename:Object と検索してみます。すると大量のファイルがマッチするのでその中からObject.csを探し出します src/libraries/System.Private.CoreLib/src/System/Object.cs が検索結果の3ページ目ぐらいのところにあるのでそこからたどっていくことにします public virtual bool Equals( object ? obj) { return RuntimeHelpers.Equals( this , obj); } public static bool Equals( object ? objA, object ? objB) { if (objA == objB) { return true ; } if (objA == null || objB == null ) { return false ; } return objA.Equals(objB); } コードを読む static bool Equals のほうで早期リターンを行っている部分があるものの、早期リターンできなかった場合は RuntimeHelpers.Equals を呼び出していることがわかります。そのままだと闇雲に探すことになってしまうのでヘッダー部分のusingされている名前空間を見ておきましょう using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; 名前的に System.Runtime.CompilerServices か System.Runtime.InteropServices にありそうですね 今度は filename:RuntimeHelpers で検索してみます。すると8件ほどヒットするので目星をつけた名前空間に着目すると src/coreclr/System.Private.CoreLib/src/System/Runtime/CompilerServices/RuntimeHelpers.CoreCLR.cs src/mono/netcore/System.Private.CoreLib/src/System/Runtime/CompilerServices/RuntimeHelpers.Mono.cs src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/RuntimeHelpers.cs の3つが該当しました。 src/mono/netcore というMonoなのか.NET Coreなのかよくわからないディレクトリーがありますが、まずはObject.csと同様な場所にある src/libraries から読もうとなりましたがこのファイルでは定義されていません partial class となっているのでどうやらプラットフォームごとの別のソースコードを参照する形で実装されている様子です [MethodImpl(MethodImplOptions.InternalCall)] public static extern new bool Equals( object ? o1, object ? o2); src/coreclr のほうを見てみるとこのようになっているため、どうやらCの世界まで潜らないといけないようです 検索ボックスで RuntimeHelpers をCやC++に絞って検索してみるとそれっぽいものが2つありました src/coreclr/vm/corelib.h src/coreclr/vm/ecalllist.h src/coreclr/vm/corelib.h では DEFINE_CLASS(RUNTIME_HELPERS, CompilerServices, RuntimeHelpers) と記述されているだけなのでどうやらクラスを宣言してるだけのようです(CやC++詳しくないので間違ってるかもしれません) src/coreclr/vm/ecalllist.h のほうを見てみます FCFuncStart(gRuntimeHelpers) /* 略 */ FCFuncElement( "Equals" , ObjectNative::Equals) /* 略 */ FCFuncEnd() するとなにやら関数を登録してそうな処理が入っています。ObjectNativeに答えがありそうです ObjectNative で検索すると src/coreclr/classlibnative/bcltype/objectnative.cpp と src/coreclr/classlibnative/bcltype/objectnative.h が引っ掛かりますが、 .h はヘッダーファイルなので .cpp に実装がありそうです .cpp のほうで Equals と検索するとこの処理がヒットしました FCIMPL2(FC_BOOL_RET, ObjectNative::Equals, Object *pThisRef, Object *pCompareRef) { CONTRACTL { FCALL_CHECK; INJECT_FAULT(FCThrow(kOutOfMemoryException);); } CONTRACTL_END; if (pThisRef == pCompareRef) FC_RETURN_BOOL(TRUE); // Since we are in FCALL, we must handle NULL specially. if (pThisRef == NULL || pCompareRef == NULL ) FC_RETURN_BOOL(FALSE); MethodTable *pThisMT = pThisRef->GetMethodTable(); // If it's not a value class, don't compare by value if (!pThisMT->IsValueType()) FC_RETURN_BOOL(FALSE); // Make sure they are the same type. if (pThisMT != pCompareRef->GetMethodTable()) FC_RETURN_BOOL(FALSE); // Compare the contents (size - vtable - sync block index). DWORD dwBaseSize = pThisRef->GetMethodTable()->GetBaseSize(); if (pThisRef->GetMethodTable() == g_pStringClass) dwBaseSize -= sizeof (WCHAR); BOOL ret = memcmp( ( void *) (pThisRef+ 1 ), ( void *) (pCompareRef+ 1 ), dwBaseSize - sizeof (Object) - sizeof ( int )) == 0 ; FC_GC_POLL_RET(); FC_RETURN_BOOL(ret); } FCIMPLEND C#erには少し厳しいC++です…頑張って読み解いていきましょう 最初の CONTRACTL のところはおそらく防衛をしてるだけなのでスキップ。 if (pThisRef == pCompareRef) で true を返してるので真っ先に参照を比較していますね 次に if (pThisRef == NULL || pCompareRef == NULL) でnullチェックをしています そのあとの if (!pThisMT->IsValueType()) ではコメントに 値クラスではない場合は値で比較しないでください 的なコメントが書かれています。参照型の場合は最初の参照比較で等値比較を終わらせていて、ここで値型以外は false として返すようにしてるようです そのあとの if (pThisMT != pCompareRef->GetMethodTable()) では同じ型でなければ false にするという処理が入っています // Compare the contents (size - vtable - sync block index). DWORD dwBaseSize = pThisRef->GetMethodTable()->GetBaseSize(); if (pThisRef->GetMethodTable() == g_pStringClass) dwBaseSize -= sizeof (WCHAR); BOOL ret = memcmp( ( void *) (pThisRef+ 1 ), ( void *) (pCompareRef+ 1 ), dwBaseSize - sizeof (Object) - sizeof ( int )) == 0 ; さて、最後のこの比較が難関な匂いがします まず真っ先に出てくる DWORD とはなんぞやというところからなので、グーグル大先生で clr dword とググってみます。検索にヒットした VB.NET/VB6.0/CLR/C/C++/Win32API 型一覧表 - 山崎はるかのメモ によると DWORD はC#でいうところの uint のようです、またそのあとに出てくる WCHAR はC#でいうところの char のようです dwBaseSize はMethodTableから取得しているようなのでおそらくその型が使用するメモリー量かなと思います。そのあとMethodTableが g_pStringClass だったら WCHAR のサイズ分小さくしてるのはString末尾のヌル文字分減らしてるんじゃないかなと思いますが、値型でなかったらここまで到達しないはずでは…?というのもあるので謎ですね そのあとはコメントの通りに memcmp で参照先のメモリーブロックを比較しているという感じだと思います。( dwBaseSize から sizeof(int) を減らしてる理由はわからないです) Object.Equalsが遅かった理由 さて、本題に戻りObject.Equalsが参照型の場合はだいたい10ns、値型の場合はだいたい250nsかかっていたことについてですが、値型の場合はBenchmarkDotNetの結果をみるとAllocatedされているので明らかなのですが、Object.Equalsを呼び出すときにボックス化が行われていることがコストになっているようです。それを抜きにしても参照型・値型双方で通常の比較よりも多少の時間がかかっています nullの場合は static bool Equals のほうで早期リターンされるので早く終わってもいいはずですが、ベンチマーク結果的にはnullと空文字の場合であまり時間差がないため、メソッドの呼び出しコストに7nsぐらいかかってるんじゃないかなという匂いがします。一方で値型の場合は躊躇に差が表れているのでnullの場合は早期リターンされ、1の場合 *12 はObjectNativeの処理のあたりまで行ってるんじゃないかなと創造できます 真相は不明です、コード上からだとここが限界どころですね ちなみにsrc/mono/netcoreのほうは public static new bool Equals( object ? o1, object ? o2) { if (o1 == o2) return true ; if (o1 == null || o2 == null ) return false ; if (o1 is ValueType) return ValueType.DefaultEquals(o1, o2); return false ; } 値型以外の場合は単純な処理のようです。値型の場合だと ValueType.DefautEquals で比較するようです internal static bool DefaultEquals( object o1, object o2) { RuntimeType o1_type = (RuntimeType)o1.GetType(); RuntimeType o2_type = (RuntimeType)o2.GetType(); if (o1_type != o2_type) return false ; object [] fields; bool res = InternalEquals(o1, o2, out fields); if (fields == null ) return res; for ( int i = 0 ; i < fields.Length; i += 2 ) { object meVal = fields[i]; object youVal = fields[i + 1 ]; if (meVal == null ) { if (youVal == null ) continue ; return false ; } if (!meVal.Equals(youVal)) return false ; } return true ; } DefautEquals に関してはこのメソッド名で検索すると このファイル しか候補に上がらないため比較的楽に見つかりましたが InternalEquals が嫌な予感しますね [MethodImplAttribute(MethodImplOptions.InternalCall)] private static extern bool InternalEquals( object o1, object o2, out object [] fields); 同じファイルに宣言されてましたが、どうやらまたCの世界に行くようです InternalEquals で調べるとsrc/coreclrのファイルも引っ掛かりますが、今回はsrc/monoコンテキストなのでその配下にあるそれっぽい結果の src/mono/mono/metadata/icall-def.h に探りをいれると HANDLES(VALUET_1, "InternalEquals", ves_icall_System_ValueType_Equals, MonoBoolean, 3, (MonoObject, MonoObject, MonoArrayOut)) とあるので ves_icall_System_ValueType_Equals が本命のようです 同じように検索すると src/mono/mono/metadata/icall.c が出てきました。 目的のメソッド は170行ぐらいあるのでここでは割愛しますが、リフレクションのような処理を行っているように見えます さてC#erのみなさん、ここで思い出しましょう、値型のEqualsメソッドでは規定でリフレクションが使われるので遅いということを。このことは公式リファレンスの 型の値の等価性を定義する方法 (C# プログラミング ガイド) でも書かれてることなので知ってる方も多いことでしょう *13 。リファレンスに書かれてる通りのような実装をされているのでsrc/monoのほうは納得できるでしょう、しかしここまで読んでいただけた方はsrc/coreclrのほうではリフレクションではなくメモリブロックの比較を行っていたことにお気づきだと思います。自分のソースコード探索が間違っていなければいつの間にかにより高速だと思われる比較に変わっているということになりますね *14 Runtimeによる差を確認 前述のとおりsrc/monoはどうやら値型でEqualsメソッドを使うとリフレクションで遅いようだということがわかりました。本当にそうなのか比較したいところですがdotnet/runtimeのsrc/monoは謎の存在です(たぶんXamarin.Androidあたりのために mono/mono からクローンしてるんじゃないかなと想像) mono/monoでdotnet/runtimeのsrc/monoにあった DefaultEquals を検索すると ほぼ同様なコード がありましたので前述のとおりにmonoでは値型のEqualsメソッドでリフレクションが使われるという前提のもとその違いが出ないかの調査をします *15 調査と言ってもRuntimeによってEqualsの速度差が出るはずなのでBenchmarkDotNetによって差を計測していくことにします 今のcsprojファイルではそのまま計測することができないので少し手を加えます <TargetFrameworks> net5.0;net48 </TargetFrameworks> <PlatformTarget> AnyCPU </PlatformTarget> <LangVersion> 9.0 </LangVersion> csprojの PropertyGroup にほとんどの場合では TargetFramework が記述されてるかと思いますが、それは TargetFrameworks に変え.NET Framework 4.8である net48 を記載します。それと同時に PlatformTarget と LangVersion を指定します。ここでは.NET 5に合わせて9.0にしていますが.NET Framework 4.8やMonoだと対応するC#バージョンが異なりますので一部C# 9.0の機能が使えなくなります(検証では関係ありませんが) 次に.NET Framework 4.8とMonoを準備します。.NET FrameworkはDeveloper Packを入れ、Monoは公式サイトからインストーラーを入れて環境変数にPathを通せばいいです [SimpleJob(RuntimeMoniker.Net48)] [SimpleJob(RuntimeMoniker.NetCoreApp50)] [SimpleJob(RuntimeMoniker.Mono)] [MeanColumn, MinColumn, MaxColumn] [MemoryDiagnoser] public class ValueTypeEqualsBench { private object expect = 1 ; [Params( null , 1 )] public object ? Value { get; set; } [Benchmark] public bool ValueTypeEquals() { return object .Equals(Value, expect); } } まず検証するのはこちらのベンチマークケースです。単純にメソッドの実行速度の差を計測したいのであらかじめボックス化をさせておき object.Equals を呼び出すだけのコードにしています 計測に関してですがMonoが.NET FrameworkのBCLを参照する必要があるらしくHost Processを.NET Framework 4.8にするためにdotnetコマンド( dotnet run -c Release -f net48 )でHost Processを指定することで実施しています そして結果がこう: BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19042, VM=Hyper-V Intel Core i9-10900K CPU 3.70GHz, 1 CPU, 16 logical and 8 physical cores [Host] : .NET Framework 4.8 (4.8.4250.0), X64 RyuJIT .NET 4.8 : .NET Framework 4.8 (4.8.4250.0), X64 RyuJIT .NET Core 5.0 : .NET Core 5.0.0 (CoreCLR 5.0.20.51904, CoreFX 5.0.20.51904), X64 RyuJIT Mono : Mono 6.12.0 (Visual Studio), X64 .NET Framework 4.8が.NET 5より少し早いという結果になりましたがMonoが予想通り遅そうな結果が出ていますね。リフレクションで比較しているということは比較対象のフィールドが多くなればなるほど差が躊躇に現れるはずです [SimpleJob(RuntimeMoniker.Net48)] [SimpleJob(RuntimeMoniker.NetCoreApp50)] [SimpleJob(RuntimeMoniker.Mono)] [MeanColumn, MinColumn, MaxColumn] [MemoryDiagnoser] public class ValueTypeLongStructEqualsBench { public struct BigStruct { public long Value1; public long Value2; public long Value3; public long Value4; public long Value5; public long Value6; public long Value7; public long Value8; } private object expect = new BigStruct(); public IEnumerable< object > Source() { yield return new BigStruct(); } [Benchmark] [ArgumentsSource(nameof(Source))] public bool ValueTypeEquals( object value ) { return object .Equals( value , expect); } } 今度は無理やり肥大化させた構造体でベンチマークをしてみます 結果としては予想通りMonoが躊躇に遅くなりました。どうやらいつかわからないタイミングで値型のEqualsのパフォーマンスチューニングが施されていたようです EqualityComparer<T>.Defaultの実装 さてEqualityComparer<T>.Defaultの実装を深掘っていこうと思いますが、すでに EqualityComparer .Defaultの実装を追ってみる。 - ねののお庭。 で先駆者の方が実装を追っているようです。どうやら正攻法でコードを読んでいくと沼になるようなので趣向を変えてDevirtualizationが実装されたPullRequestを見ていこうと思います EqualityComparer<T>.DefaultのDevirtualizationが実装されたのは.NET Core 2.1の頃なのでdotnet/runtimeリポジトリーではなく dotnet/coreclr リポジトリー *16 を探すことになります PullRequestを検索すると JIT: devirtualization support for EqualityComparer .Default #14125 というそれっぽいPullRequestが見つかります 中身を見てみるとDevirtualizationは IntrinsicAttribute をC#コードに付けそれをJITが見つけると特殊対応をする構造になっているようです EqualityComparer<T>.Defaultの場合は元々は public static EqualityComparer<T> Default { get; } = (EqualityComparer<T>)ComparerHelpers.CreateDefaultEqualityComparer( typeof (T)); というコードだったものが public static EqualityComparer<T> Default { [Intrinsic] get; } = (EqualityComparer<T>)ComparerHelpers.CreateDefaultEqualityComparer( typeof (T)); というコードに変更されています それと同時にsrc/mscorlib/src/System/Collections/Generic/ComparerHelpers.csの CreateDefaultComparer メソッドに and in vm/jitinterface.cpp so the jit can model the behavior of this method. というドキュメントコメントが追記されているためJIT側でDefaultのEqualityComparerを作成してるようです JIT側のコードはC++でよくわからないので割愛するとして、Devirtualizationの特殊対応をする対象メソッドはsrc/jit/namedintrinsiclist.hの NamedIntrinsic 列挙型で管理されているようです enum NamedIntrinsic { NI_Illegal = 0 , NI_System_Enum_HasFlag = 1 , NI_MathF_Round = 2 , NI_Math_Round = 3 , NI_System_Collections_Generic_EqualityComparer_get_Default = 4 }; このPullRequestの時点では対象となるメソッドは少ないようですが気になるので現在の.NET 5の実装を見ましょう dotnet/coreclrのコードはdotnet/runtimeだとsrc/coreclrの中に移ってるはずなのでそのディレクトリーからそれっぽいところを探すと namedintrinsiclist.h が見つかりました enum NamedIntrinsic : unsigned short { NI_Illegal = 0 , NI_System_Enum_HasFlag, NI_System_Math_FusedMultiplyAdd, NI_System_Math_Sin, NI_System_Math_Cos, NI_System_Math_Cbrt, NI_System_Math_Sqrt, NI_System_Math_Abs, NI_System_Math_Round, NI_System_Math_Cosh, NI_System_Math_Sinh, NI_System_Math_Tan, NI_System_Math_Tanh, NI_System_Math_Asin, NI_System_Math_Asinh, NI_System_Math_Acos, NI_System_Math_Acosh, NI_System_Math_Atan, NI_System_Math_Atan2, NI_System_Math_Atanh, NI_System_Math_Log10, NI_System_Math_Pow, NI_System_Math_Exp, NI_System_Math_Ceiling, NI_System_Math_Floor, NI_System_Collections_Generic_EqualityComparer_get_Default, NI_System_Buffers_Binary_BinaryPrimitives_ReverseEndianness, NI_System_Numerics_BitOperations_PopCount, NI_System_GC_KeepAlive, NI_System_Threading_Thread_get_CurrentThread, NI_System_Threading_Thread_get_ManagedThreadId, NI_System_Type_get_IsValueType, NI_System_Type_IsAssignableFrom, NI_System_Type_IsAssignableTo, /* 略 */ 現在だとかなりのメソッドが特殊対応の対象のようですがほとんどがMathクラスのものですね EqualityComparer<T>.Defaultの実装の調査に関しては沼なのでここまでにしておきます 結局のところなにがいいのよ 総合評価(安全性・可読性・速度)をするとnull判定は value is null 、not null判定は value is not null が無難という感じでしょうか。もちろん他の表現方法でも気にする必要がないケースがほとんどだろうのでなんでもいいっちゃいいという感じではありますが ちなみに ldarg . 0 call instance string NullCheck .Benchmark.ReferenceNullBench :: get_Value () ldnull cgt .un ldc .i4 . 0 ceq 最初のほうで !(Value is not null) の場合は上記のようなCILになってたよと紹介しましたが public class ReferenceNull { public bool PatternMatchNotNull9( string ? value ) { return !( value is not null ); } } というコードのCILを見ると .method public hidebysig instance bool PatternMatchNotNull9 ( string ' value ' ) cil managed { // Method begins at RVA 0x2102 // Code size 5 (0x5) .maxstack 8 IL_0000 : ldarg . 1 IL_0001 : ldnull IL_0002 : ceq IL_0004 : ret } // end of method ReferenceNull::PatternMatchNotNull9 というコードになっていました。式も文脈によっては異なるCILに変換されるようなので今回のベンチマークケースでこうなったからといってif文やreturn文で同様になるとは限らなさそうです。ボックス化している箇所についてはほぼ確実にどのような場所でもおきそうですがCILの命令数的な誤差は目をつぶるしかなさそうです おまけ public class Evil { public static bool operator ==(Evil left, Evil right) => true ; public static bool operator !=(Evil left, Evil right) => false ; } public class Program { public void Example() { var evil1 = new Evil(); var evil2 = new Evil(); bool isEquality = evil1 == evil2; } } C#的には合法(コンパイル可能)で邪悪なコードですが Example メソッドのCILに変換されたコードを見るとオーバーロードした == 演算子が呼ばれていることがわかります .method public hidebysig instance void Example () cil managed { // Method begins at RVA 0x2060 // Code size 22 (0x16) .maxstack 2 .locals init ( [0] class Evil evil1 , [ 1 ] class Evil evil2 , [ 2 ] bool isEquality ) IL_0000 : nop IL_0001 : newobj instance void Evil :: .ctor () IL_0006 : stloc . 0 IL_0007 : newobj instance void Evil :: .ctor () IL_000c : stloc . 1 IL_000d : ldloc . 0 IL_000e : ldloc . 1 IL_000f : call bool Evil :: op_Equality ( class Evil , class Evil ) IL_0014 : stloc . 2 IL_0015 : ret } // end of method Program::Example Evilの変数を object 型として受け取れば object の規定の動作通りに == 演算子で比較したようになったりしますが、たいていの場合はそういうわけにもいかないので == 演算子のオーバーロードは注意が必要だったりします おわりに この記事はアドベントカレンダーだしC#の記事書いとくか〜と書き始めたら止まらなくなり肥大化してしまったものです(スコープの管理ができてない)。最後の方はダレてしまって手抜き感がありますがご了承ください、気になればいつの日か調査するかもしれません 昨日の「dely #2 Advent Calendar 2020」はnancyさんの「 iOSのサブスクリプション機能 プロモーションオファーを触ってみた 」でした 明日は永井さんの「Merged Manifest を使って uses-permission を調査した話」ですお楽しみに! join-us.dely.jp bethesun.connpass.com *1 : サンプルコードはすべてC# 9.0ベースです *2 : ポインターとか参照をUnsafeに比較する方法とかあるかもしれません *3 : 普通はそんなオーバーロードをしないので普通のプラットフォーム向けのコードの場合は気にしなくていいですが、気にしないといけないプラットフォームがあるので闇です *4 : こういう場合のスマートな解決策があれば教えてください *5 : i9-10900Kは10core20threadなCPUですが、計測はHyper-V上のWindowsで行ったため8core16threadです。フルパフォーマンスとは言えませんが同環境での比較となるためベンチマーク結果としては有効かと思います *6 : ガチで判断するならnullの頻度分布によって調整をかけないといけませんがここでは手抜きということで平均値です *7 : たとえばIEqualityComparerのインスタンスが用意できてるという前提でcomparere.Equals(Value, null)を計測するのかとか *8 : Common Intermediate Language、共通中間言語、一部からはMSILとも呼ばれる *9 : ここのCILを理解するのに5分考えこみました *10 : EqualityComparerは0スタック目という数え方 *11 : ちょっと前まではdotnet/coreclrで公開されていましたね *12 : nullと対比するための値として設定した1のことです *13 : 自分はすっかり忘れてました *14 : それでもボックス化で遅いのは変わらず *15 : すべてのコードを確かめたわけではありませんが雰囲気的にはdotnet/runtimeのsrc/monoはmono/monoから手書きクローンしてそうな感じがしました *16 : 今はdotnet/runtimeに移行されてコードがほとんどない状態ですがCommitやPullRequestは残っています
アバター
こんにちは! dely開発部の高松です。 この記事は「dely #1 Advent Calendar 2020」の16日目の記事です。 昨日はクラシルのUIデザイナーをされているymdskoさんの「UIデザイナーとして働く私が就活生に戻ったら絶対やること5つ」でした。 是非こちらもご覧ください。 note.com 「dely #1 Advent Calendar 2020」 adventar.org  「dely #2 Advent Calendar 2020」もありますので、是非そちらもご覧ください。 adventar.org さて、いきなりですが質問です。 目の前にそれぞれ決められた一定の確率で報酬を得ることができるボタンが4つあります。 合計で1000回ボタンを押して、4つの内どれが一番当たる確率が高いボタンかを検証してみてください。 なお、1000回ボタンを押したことで得た報酬は全て差し上げます。 さあ、どうしましょう。 4つ全てのボタンを250回ずつ押して、一番当たりの多いボタンを探しますか? きっと一番当たる確率が高いボタンは見つかりますが、その分確率の低いボタンもたくさん押しているので、実はもっと多くの報酬が得られたかもしれません。 では、それぞれ100回ずつ押してみて、その時点で一番当たりが多かったボタンを残りの600回押しますか? 100回押した時点で一番当たる確率が高いボタンが本当に一番当たる確率が高いボタンなのでしょうか。 このように、最適な選択肢を探しつつ、その間に得られる報酬を最大にする決定問題をバンディット問題と呼び、この問題に対するアルゴリズムが今回紹介するバンディットアルゴリズムです。 実際どのように選択をするのか 今回はバンディットアルゴリズムのUCB(Upper Confidence Bound)という方策をご紹介します。 UCB方策は、期待値の高い選択肢を選ぶ一方で、それまで施行数が少ない選択肢を優先的に選択されるようにする方策です。 具体的には下記の数式にて算出される値が一番大きい選択肢を逐次選んでいきます。 記号 意味 選択肢の期待値 全選択肢の選択回数の合計 その選択肢の選択回数 期待値というのはその選択肢を1回選ぶことでどれくらいの報酬が得られるかを表した値です。 今回の例で言えば、その時点でより当たりが出ているボタンほど期待値が高いということになります。 各選択肢の期待値に下記の補正項が上乗せされています。 上の式は、総選択数 N に対して、選択数 n が少ない選択肢ほど値が高くなるようになっています。 この補正項のおかげで、他の選択肢に対して検証ができていない選択肢が優先的に選ばれることになります。 検証してみる 今回は、全ての選択肢を同じ回数施行するいわゆるA/Bテストを行った場合とUCB方策のバンディットアルゴリズムを用いて施行を行った場合を比較します。 class Arm attr_accessor :name # 選択肢名 attr_accessor :num_of_run # 施行回数 attr_accessor :num_of_conversion # 報酬獲得回数 attr_accessor :unit_reward # 1回分の報酬(今回は固定で1) attr_accessor :probability # 報酬が得られる確率 def initialize ( name, unit_reward : 1 , probability : 1.0 , num_of_run : 1 , num_of_conversion : 0 ) @name = name @num_of_run = num_of_run @num_of_conversion = num_of_conversion @unit_reward = unit_reward @probability = probability end def run! self .num_of_run += 1 return false unless ( 1 .. 100 ).to_a.sample(probability * 100 ).include?( 1 ) self .num_of_conversion += 1 return true end def total_reward unit_reward * num_of_conversion end def expectation total_reward / num_of_run.to_f end def ucb_weight (total_num_of_run) Math .sqrt( ( 2 * Math .log(total_num_of_run)) / num_of_run.to_f ) end def upper_confidence_bounce (total_num_of_run) expectation + ucb_weight(total_num_of_run) end end class ArmSelector attr_accessor :arms # 選択肢Armの配列 attr_accessor :select_type # 選択方法 def initialize (arms, select_type : ' ab ' ) @arms = arms @select_type = select_type end def select case select_type when ' ab ' return arms.sort_by { | arm | arm.num_of_run }.first when ' ucb ' unverified_arm = arms.find { | arm | arm.num_of_run == 0 } return unverified_arm if unverified_arm total_num_of_run = arms.map(& :num_of_run ).sum sorted_arms = arms.sort_by do | arm | arm.upper_confidence_bounce(total_num_of_run) end sorted_arms.last end end end 上記のコードを基に逐次選択される過程を表したのが下記です。 今回は説明を簡単にするために、a, b, c, dの選択肢はそれぞれ一定の確率で当たり(当たりなら1、ハズレなら0)が出るような形とし、 a < b < c < d の順で確率が大きいとします。 左が全ての選択肢を同じ回数施行した際の検証です。 右が上でご紹介したUCB方策のバンディットアルゴリズムを用いた検証です。 グラフの紫のバーが施行回数、緑のバーが報酬を得た回数、黄色の点がその時点の選択肢の期待値を表しています。 左は全ての選択肢を同じ回数施行するので、確率に基づいて選択肢「d」の報酬を得た回数が一番大きくなっているのがわかります。 一方バンディットアルゴリズムの方は選択肢毎に施行回数が違っています。 施行を始めたばかりは、期待値にばらつきが出るので全ての選択肢を満遍なく施行しますが、ある程度施行を重ねると選択肢「d」が多く選択されていくのがわかります。 また、一番期待値の低いと思われる選択肢「a」に対しても、完全に施行されなくなるわけではなく、頻度は少なくなるものの施行が続いていることもわかります。 そして、同じ試行回数で報酬を得られた回数を比較するとバンディットアルゴリズムの方がより多く報酬を得られました。 このように、バンディットアルゴリズムを用いることでより多くの報酬を得ることを確認できました。 まとめ バンディットアルゴリズムは、選択を続け報酬を得るプロセスに於いてその報酬の合計を最大にするためのアルゴリズムです。 今回比較にも利用したA/Bテストと呼ばれる「最適椀識別」のように特定の選択肢を見つけることが主の目的ではありません。 今回は時間が足りず紹介出来ませんでしたが、バンディットアルゴリズムにて最適な選択肢を誤識別する様なシチュエーションも存在します。 また、今回はUCB方策を例に取りご紹介しましたが、他にも様々な選択方法が存在します。 こちらもまた時間があれば紹介できればと思います。 今回のこの記事が少しでもバンディットアルゴリズムの理解の助けになると嬉しいです。 さいごに dely ではエンジニアを絶賛募集中です! ご興味ある方はこちらのリンクからお気軽にエントリーください! join-us.dely.jp さらに、定期的に TechTalk というイベントを通じて、クラシルで利用している技術や開発手法、組織に関する情報も発信しております。 ノウハウの共有だけでなく、クラシルで働くエンジニアがどんな想いを持って働いているのかや、働く人の雰囲気を感じていただけるイベントになっていますので、ぜひお気軽にご参加ください! bethesun.connpass.com
アバター
TRILL開発部の石田です。 2020年9月にXcode12がリリースされ、Scroll Hitch Rateという機能が追加されました。 今回はこの機能について紹介します。 Xcode Organizerとは Xcode Organizerについて、 Appleのドキュメント では以下のように説明されています。 Appのクラッシュログ、エネルギーレポート、パフォーマンスに関する指標(お客様が使用した際のバッテリー消費量や起動時間など)を簡単に確認できます。 ユーザの端末からバッテリーライフやパフォーマンスデータ等の情報がAppleのサーバに送られ、それがXcodeのOrganizerに表示されます。 ただし情報を送信するのはプライバシー設定の「Appデベロッパと共有」をOnにしている端末に限られるようです。 ちなみに、Organizerでユーザの統計情報を確認するために追加の実装は不要です。 Scroll Hitch Rateとは Scroll Hitch RateはXcode12からOrganizerに追加された機能で、アプリ内のスクロールのスムーズさを表現しています。 Scroll Hitchとはレンダリングされたフレームがスクロール中に画面に表示されないことで、これによってフレーム落ちし、スクロールが不安定な挙動となります。 iPhoneはフレッシュレートが60Hzなので、1フレームは16.67msであり、それ以上の時間がかかるとフレーム落ちし、ユーザ体験が下がります。 表示される指標 Hitch time: フレームが画面に表示されるのに必要な追加の時間の合計 Scroll duration: スクロール時間 Hitch rate = Hitch time / Scroll duration Hitch rateが高ければ高いほどHitchが多く、ユーザにとって体験の悪いスクロールとなります。 逆にHitch rateが低いほどユーザ体験の良いスクロールとなります。 目指すべき数値 5ms/s以下: 良いユーザ体験 5ms〜10ms/s: ユーザがHitchに気づき始めるので調査すべき 10ms〜: かなり使いづらいので早急に解決すべき 基本的に5ms/sを下回っていれば問題ないようです。 Xcode Organizerはアプリバージョン毎の結果が表示されるので、アプリをアップデートした際に改善しているか・悪化していないかチェックするのが良さそうです。 まとめ Xcode12からScroll Hitch Rateという機能が追加され、スクロールのスムーズさ(ユーザの手元で起こっているもの)が定量的に判断できるようになりました。 TRILLでも定期的な確認と改善をしていき、ユーザ体験をより良いものにしたいと思います。 delyでは全方面でエンジニアを積極採用中です。 興味のある方は是非お声がけください。 join-us.dely.jp 参考 https://developer.apple.com/videos/play/wwdc2020/10076/ https://developer.apple.com/videos/play/wwdc2020/10077/
アバター
こんにちは! dely で iOS エンジニアをしている nancy です。 はじめに この記事は「dely #2 Advent Calendar 2020」の15日目の記事です。 adventar.org adventar.org 昨日はクラシルのフロントエンドを担当されている しらりん さんの「ウェブの未来を描く Project Fugu」という記事でした。 tech.dely.jp ウェブとネイティブアプリとの操作性のギャップを埋めるプロジェクト、 Project Fugu について書かれています! ウェブの開発をされている方だけでなく、ネイティブアプリの開発をされている方にもオススメの記事なので、興味のある方は是非ご覧ください! 本日は WWDC 2019 で発表された iOS の自動更新サブスクリプション機能の一つ、 「 プロモーションオファー 」について書きたいと思います。 はじめに プロモーションオファーとは お試しオファーとプロモーションオファーの違い 実装のはなし プロモーションオファーの作成 秘密鍵の生成 プロモーションオファーの設定 プラン/プロモーションオファーの詳細を取得 署名を生成 購入処理を実行 レシート検証 && トランザクションを完了 実装していてハマったところ 過去課金経験がないとエラーになる ~identifier が多く混乱してくる 署名生成時のデバッグがやりづらい おわりに プロモーションオファーとは プロモーションオファーとは、WWDC 2019 で発表された iOS の自動更新サブスクリプション機能の一つで 過去にサブスクリプションに登録していた、もしくは現在サブスクリプションに登録しているユーザに対し、 「1ヶ月無料」や「1ヶ月100円引き」といった値引きしたプランを提供できるような機能です。 お試しオファーとプロモーションオファーの違い プロモーションオファーが実装されるより以前は「お試しオファー」という初めてサブスクリプションに登録するユーザ向けの機能を使用してユーザへサブスクリプションへの登録を行っていました。 どのサブスクリプションでもよく見る「初めて登録される方なら nヶ月無料!」みたいなやつですね。 このお試しオファーは新規サブスクリプション登録者の獲得には有用ですが、一度しか利用できないため、解約ユーザへの訴求、現在サブスクリプションに登録してくれているユーザへの訴求には使用できませんでした。 これに対し「プロモーションオファー」では、解約したユーザや現在サブスクリプションに登録してくれているユーザに対して再度お得なプランを訴求することができるため、一度解約してしまったユーザに再登録を促すことができたり、現在サブスクリプションに登録してくれているユーザの長期継続につながったりすることが期待できます。 下の画像にもありますが、iOS 12.2 以上のユーザのみ利用可能な機能であるため、導入する際は注意が必要です。 https://developer.apple.com/jp/app-store/subscriptions/#providing-subscription-offers ※オファーコードも記載されていますが、今回の記事ではこの部分には触れません。 実装のはなし プロモーションオファーは以下のような流れで実装していきます プロモーションオファーの作成 プラン/プロモーションオファーの詳細を取得 署名を生成 購入処理を実行 レシート検証 トランザクションを完了 プロモーションオファーの作成 実装に入っていく前にプロモーションオファーの準備を App Store Connect 上で行います。 秘密鍵の生成 まずはプロモーションオファーの課金に使用する秘密鍵の生成を行います。 こちら に記載の流れのように進めていきます 「ユーザとアクセス」→「キー」をクリックし、「サブスクリプション」を選択 +ボタンをクリック 任意の名前を設定し、サブスクリプションキーを生成 生成した秘密鍵をダウンロード ※秘密鍵のダウンロードは1回しかできないため、ご注意ください。 プロモーションオファーの設定 秘密鍵が生成できたら、次にプロモーションオファーのプランを作成していきます。 プロモーションオファーは既存のプランに紐づく形で作成するため、元となるプランがまだ存在しない場合はそちらから作成するようにしてください。 プロモーションオファーの設定も Apple のドキュメント 通りに進めていきます 「マイ App」から、設定する App を選択 サイドバーの「App 内課金」で、「管理」をクリック 自動更新登録タイプのプロダクトをクリックし、「登録価格」セクションに移動して、「追加」ボタン(+)をクリック 「プロモーションオファーの作成」を選択 内部参照名とオファーコードを入力 「都度払い」、「前払い」、「無料」のいずれかを選択した後、適切な期間、通貨、価格を選択 ちなみに、都度払いを選択した場合は「適用期間」「価格」を選択できるようになり、特定の期間だけ○円みたいな設定ができます。 以上でプロモーションオファーの作成は完了です。 以降は実装に入っていきます。 プラン/プロモーションオファーの詳細を取得 まずは以下のような処理でプランの詳細( SKProduct )のリクエストを行います。 var purchaseProductIdentifier : String ? func fetchProduct (id : String ) { purchaseProductIdentifier = id let productIdentifiers = Set < [purchaseProductIdentifier ! ] > request = SKProductsRequest(productIdentifiers : productIdentifiers ) request.delegate = self request.start() } 次に SKPaymentQueueDelegate にて SKProduct の取得完了通知を受け取ります。この辺りは既に自動更新サブスクリプションを導入されている場合は同じ処理になるかと思います。 ただ、プロモーションオファーが紐づいているプランを取得した場合、 SKProduct の discounts: [SKProductDiscount] に紐づいているプロモーションオファーが全て入った状態で取得できます。 SKProductDiscount にはプランの料金や適用期間などが入っているので、 SKProduct の情報を使用して View を更新する場合は discounts を参照するようにしてください。 public func productsRequest (_ request : SKProductsRequest , didReceive response : SKProductsResponse ) { guard let purchaseProductIdentifier = purchaseProductIdentifier, let product = response.products.first( where : { $0 .productIdentifier == purchaseProductIdentifier }) else { // エラー処理 return } // .discounts にプロモーションオファーの情報が入った状態で取得できる print(product.discounts) // 課金処理へ } 署名を生成 参考: Generating a Signature for Promotional Offers プロモーションオファーを導入するには既存の自動更新サブスクリプションとは異なり「署名の生成」を行い、得られた文字列を購入リクエストに含める必要があります。 署名の生成に必要な情報以下の通りです。 名称 説明 appBundleID アプリの Bundle Identifier keyIdentifier App Store Connect で生成した秘密鍵を識別する ID productIdentifier プランの ID offerIdentifier プロモーションオファーの ID applicationUsername サービス内でユーザを一意に識別する文字列(任意) nonce サーバーサイドで生成する UUID(小文字) timestamp サーバーサイドで生成する UNIX タイムスタンプ(ミリ秒) node.js を用いて署名生成を行うサンプルコードを Apple が公開しているので、こちらを例に出しておきます。 一部変更を加えている部分もあるので、元コードを参照したい方は下記のリンクからご参照ください。 Generating a Subscription Offer Signature on the Server router.get( '/offer' , function (req, res) { // App Bundle Identifier. ここではパラメータから取得しているが、環境変数として保持しても良さそう. const appBundleID = req.body.appBundleID; // プランの ID const productIdentifier = req.body.productIdentifier; // プロモーションオファーの ID const subscriptionOfferID = req.body.offerID; // ユーザを識別する文字列 const applicationUsername = req.body.applicationUsername; // 環境変数等で保持している、秘密鍵の ID const keyID = 'xxxxx' ; // 秘密鍵の中身(こちらも本来は環境変数として持つべき) const keyString = '-----BEGIN PRIVATE KEY-----xxxxx-----END PRIVATE KEY-----' ; // UUID を生成. const nonce = uuidv4(); // タイムスタンプを生成. const currentDate = new Date (); const timestamp = currentDate.getTime(); // 全ての文字列を不可視の分離文字列('\u2063')で結合 const payload = appBundleID + ' \u 2063' + keyID + ' \u 2063' + productIdentifier + ' \u 2063' + subscriptionOfferID + ' \u 2063' + applicationUsername + ' \u 2063' + nonce + ' \u 2063' + timestamp; // 秘密鍵を使用して楕円曲線デジタルアルゴリズム(ECDSA)オブジェクトを生成 const key = new ECKey(keyString, 'pem' ); // SHA-256 署名アルゴリズムを使用するよう設定 const cryptoSign = key.createSign( 'SHA256' ); // 結合した文字列を追加 cryptoSign.update(payload); // 署名を生成し、base64 でエンコード. const signature = cryptoSign.sign( 'base64' ); // 生成した署名が正しいものなのか検証. // 署名の処理が正しく完了しているかを検証するもので、生成した署名で課金処理が正しく行えるかを検証するものではないので注意. // ex)timestamp を "ミリ秒" ではなく "秒" で作成していると課金時にエラーになるが、この部分での検証には成功する const verificationResult = key.createVerify( 'SHA256' ).update(payload).verify(signature, 'base64' ); console.log( "Verification result: " + verificationResult) // アプリにレスポンスを返却. res.setHeader( 'Content-Type' , 'application/json' ); res.json( { 'keyID' : keyID, 'nonce' : nonce, 'timestamp' : timestamp, 'signature' : signature } ); } ); 購入処理を実行 通常の自動更新サブスクリプションでは SKPayment を使用して購入リクエストを作成しますが、プロモーションオファーで課金する場合は SKMutablePayment を使用します。 また、課金するプロモーションオファーの ID などの情報は SKPaymentDiscout という型が用意されているので、こちらに必要な情報を入れ、 SKMutablePayment の paymentDiscount プロパティにセットします func purchase (product : SKProduct , username : String , offerIdentifier : String ) { // サーバーに署名の生成をリクエスト YourServer.createSignature(username : username , productIdentifier : product.productIdentifier , offerIdentifier : offerIdentifier , completion : { (nonce : UUID , timestamp : NSNumber , keyIdentifier : String , signature : String ) in // プロモーションオファーでは SKMutablePayment を使用 let payment = SKMutablePayment(product : product ) // プロモーションオファーの情報 let discountOffer = SKPaymentDiscount(identifier : offerIdentifier , // プロモーションオファーの ID keyIdentifier : keyIdentifier , // 秘密鍵を識別する ID nonce : nonce , // サーバー側で生成する UUID signature : signature , // サーバー側で生成した署名文字列 timestamp : timestamp ) // サーバー側で生成したタイムスタンプ // SKMutablePayment に プロモーションオファーの情報を追加 payment.paymentDiscount = discount // (任意)ユーザを識別する文字列を追加 payment.applicationUsername = username SKPaymentQueue. default ().add(payment) }) } レシート検証 && トランザクションを完了 こちらは既存の自動更新サブスクリプションと変わらないため割愛します 実装していてハマったところ 過去課金経験がないとエラーになる プロモーションオファーは過去に課金経験があること前提の機能なので、 新たに作成したテストアカウントで課金しようとすると当然エラーになります。 Apple からその旨のエラーが表示されるのでこの点で詰まったと言う訳ではないんですが、お試しされる際は過去に課金経験のあるアカウントで実施するようご注意ください。 ちなみに、iOS 14 から Sandbox アカウントで「利用資格のリセット」というものができるようになりましたが、過去課金経験のあるアカウントでこれを実行しても上記のエラーメッセージが表示されませんでした。 ~identifier が多く混乱してくる 実装を進めている中で、~identifier と言う名称のものが多く、混乱してくる時がありました。 また、デバッグの際、プランの ID とプロモーションオファーの ID を似たものにしてしまっていたため、正しい値がセットされているのかを判断し難くなってしまっていたので、プロモーションオファーの ID には promotion_offer_~ のように接頭辞などを付けるようにすると分かりやすくなりそうでした。 署名生成時のデバッグがやりづらい ここが最も詰まったポイントなんですが、署名生成のデバッグが辛かったです。 当然と言えば当然なんですが、署名生成時に改行文字列等の不要なものが含まれていたりすると決済に失敗してしまいます。 また、この際、ID/Password を入力する決済画面は問題なく表示され、ID/Password 入力後の決済時にエラーになると言う挙動になります。 そのため、当初は署名の生成自体は問題なく、アプリ側のロジックの不備を疑っていたので回り道をする結果となりました。署名生成時に検証を行っていましたが、あくまで正常に署名処理が完了できているかを確認するもので、Apple が定める条件通りに署名が行われているかを判定するものでは無かったのも落とし穴でした。 その他、気をつけたほうが良さそうなポイントを記載したので、参考にしていただければと思います。 項目 気をつけるところ nonce の生成 アルファベットは必ず小文字である必要があるので注意 timestamp の生成 単位は "秒" ではなく "ミリ秒" なので注意 署名後の文字列 使用する言語やライブラリの仕様によっては勝手に "\n" を挿入したりすることがあるので注意 おわりに いかがでしたでしょう? プロモーションオファーを導入することで 今までアプローチできなかったユーザ層にアプローチすることが可能になるので、 既に自動更新サブスクリプションを実装されている場合は導入を検討してみると良いかもしれません! 明日、16日は弊社の Android エンジニアの meil さんによる「C# 9.0時代のnull判定解剖」です! お楽しみに! また、dely では一緒にサービスを成長させていく仲間を募集中です! www.wantedly.com www.wantedly.com 定期的にイベントも開催しているので、dely のことを知りたいという方は是非是非ご参加お待ちしています! bethesun.connpass.com
アバター
はじめに こんにちは! クラシルWebのフロントエンドを担当している all-user です。 今回は、とあるプロジェクトをVue 2からVue 3に書き換えてみたので、その過程と所感についてまとめたいと思います。 この記事は dely #1 Advent Calendar 2020 14日目の記事です。 adventar.org adventar.org 昨日は funzin さんの Carthageで生成したframeworkの管理でRomeを導入してみた でした。 元々使用していたcarthage_cacheをRomeに置き換える過程が分かりやすく解説されています。ぜひこちらも覗いてみてください🙌 さて、今回題材に選んだプロジェクトは小規模なVue 2で書かれたアプリケーションですが、そのスタック構成はかなりクラシルWebに近いものとなっており、今後クラシルWebへの導入を検討する上での良い足がかりにできればと考えております。 目次 はじめに 目次 Vue 3について知る Vue 3の目玉。Composition APIとは Composition APIで何が変わる? Options API(従来のコンポーネント定義)の課題 Composition APIによる関心事の分離 TypeScriptサポートの改善 個人プロジェクトについて 書き換え作業ログ nodeとyarnを最新にアップデート ビルド設定をVue CLIで一気に最新に書き換える スタックを選択 いったんビルドしてみる .babelrcを削除 vue-property-decoratorを外す vuex-smart-moduleを外す その他のライブラリ 関心事を整理してhooksディレクトリに切り出す 実際に書き換えてみての所感 コードの見通しはとても良くなる Vuexの使い方はまだ模索中 VuexのTSサポートはこれから プロダクションに投入できるタイミング まだ試せていないこと さいごに Vue 3について知る なにはともあれ、書き換えるにあたりまずはVue 3のキャッチアップから始めなければなりません。 マイグレーションガイド を読んで解釈した内容を残しておきます。 Vue 3の目玉。Composition APIとは Vue 3で導入された新しいコンポーネント実装のためのAPIです。 Reactの hooks にインスパイアされた機能で、コンセプトもとても似ています。 Composition APIで何が変わる? これまでのViewModelインスタンスを起点にしたロジックの記述( this.xx や vm.xx な記述)ではなく、関数の組み合わせによる記述が可能になります。 たとえばコンポーネントのローカルステートを定義する時、Vue 2(Options API)では data を使いました。 // DisplayCount.vue import { defineComponent } from "vue" ; export default defineComponent ( { // Vue.extendは廃止されたためdefineComponentを使用します data () { return { count: 0 } ; } , methods: { increment () { this .count += 1 ; } , decrement () { this .count -= 1 ; } } } ); テンプレート部分は以下のようになります。 < template > < span v- text = "count" /> <!-- Vue 3ではFragmentがサポートされルート要素が単一の要素である必要がなくなりました --> < button @click= "increment" > + </ button > < button @click= "decrement" > - </ button > </ template > レンダリング結果はこんな感じ。 Vue 3(Composition API)では ref 関数を使います。 // DisplayCount.vue import { defineComponent , ref } from "vue" ; export default defineComponent ( { setup () { const count = ref ( 0 ); const increment = () => ( count.value += 1 ); // thisが消えた const decrement = () => ( count.value -= 1 ); // thisが消えた return { count , increment , decrement } ; }} ); テンプレートの内容は同じです。 setup 関数では count や increment が生えたオブジェクトを返していますが、これらのプロパティをテンプレート内で参照できるようになります。 ref 関数が返すRefオブジェクトには value というプロパティが生えており、このプロパティを通じて現在の値を取得することができます。 このように値をRefオブジェクトでラップすることで、ローカルステートの読み書きを捕捉できるようになり、リアクティブにDOMの更新を行えるようになります。 従来はこのラッパーの役割をViewModelインスタンス( this )が担っていました。 そして、 increment , decrement から this が消えています。 これは、 count というローカルステートおよび+1、-1するメソッドの定義が、特定のコンポーネントに属さなくなったと考えることができます。 そのため、以下のように書き換えることができます。 // useCount.ts import { ref } from "vue" ; export const useCount = () => { const count = ref ( 0 ); const increment = () => ( count.value += 1 ); const decrement = () => ( count.value -= 1 ); return { count , increment , decrement } ; } ; // DisplayCount.vue import { defineComponent } from "vue" ; import { useCount } from "../hooks/useCount" ; export default defineComponent ( { setup () { return { ...useCount () } ; } } ); 「countというローカルステートを持ち、+1、-1することができる」機能を useCount という関数に切り出し、さらに useCount.ts という別ファイルに切り出すことができました。 次に、これが出来るようになることで、どんなうれしいことがあるのかを考えてみます。 Options API(従来のコンポーネント定義)の課題 ViewModelを起点にした記述では、 data , computed , methods などの制約により、関心事の異なるロジック同士が一箇所に束ねられ、逆に関心事を同じくするコードが分散してしまうという問題がありました。 次のコードは先ほどのサンプルに、 display というローカルステートと toggleDisplayText という算術プロパティを加えたものです。 関心事を次の2つとし、 数をカウントしたい 表示を切り替えたい コード上の対応する部分にコメントを入れると次のようになります。 // DisplayCount.vue import { defineComponent } from "vue" ; export default defineComponent ( { data () { return { count: 0 , // a. 数をカウントしたい display: true // b. 表示を切り替えたい } ; } , computed: { toggleDisplayText () : string { // b. 表示を切り替えたい return this .display ? "hide" : "show" ; } } , methods: { increment () { // a. 数をカウントしたい this .count += 1 ; } , decrement () { // a. 数をカウントしたい this .count -= 1 ; } , toggleDisplay () { // b. 表示を切り替えたい this .display = ! this .display ; } } } ); アプリーケーションが大きくなりコンポーネントが複雑化すると、このように分散した関心事を頭の中でマッピングしながら読み解いていくコストが大きくなってきます。 テンプレートも更新します。 < template > < template v-if= "display" > < span v- text = "count" /> < button @click= "increment" > + </ button > < button @click= "decrement" > - </ button > </ template > < button @click= "toggleDisplay" v- text = "toggleDisplayText" /> </ template > レンダリング結果はこんな感じ。 hide をクリックすると以下のようになります。 次にこれをComposition APIに置き換えてみます。 Composition APIによる関心事の分離 Composition APIではロジックの記述がViewModelに依存しなくなり、 data , computed , methods などの制約から開放されます。 computed 関数が登場しましたが、コンセプトは ref の時と同じです。 ViewModelへの参照を無くしたバージョンの算術プロパティと考えればOKです。 数をカウントしたい 表示を切り替えたい コード上の対応する部分にコメントを入れると次のようになります。 // DisplayCount.vue import { defineComponent } from "vue" ; export default defineComponent ( { setup () { const count = ref ( 0 ); // a. 数をカウントしたい const increment = () => ( count.value += 1 ); // a. 数をカウントしたい const decrement = () => ( count.value -= 1 ); // a. 数をカウントしたい const display = ref ( true ); // b. 表示を切り替えたい const toggleDisplay = () => ( display.value = !display.value ); // b. 表示を切り替えたい const toggleDisplayText = computed (() => ( display.value ? "hide" : "show" )); // b. 表示を切り替えたい return { count , // a. 数をカウントしたい increment , // a. 数をカウントしたい decrement , // a. 数をカウントしたい display , // b. 表示を切り替えたい toggleDisplay , // b. 表示を切り替えたい toggleDisplayText // b. 表示を切り替えたい } ; } } ); 関心事ベースでコードをまとめることができていることが分かります。 最初のサンプル同様に、 setup 関数の外にロジックを切り出し、 useCount.ts 、 useToggleDisplay.ts という別ファイルに切り出してみます。 // useCount.ts // a. 数をカウントしたい import { ref } from "vue" ; export const useCount = () => { const count = ref ( 0 ); const increment = () => ( count.value += 1 ); const decrement = () => ( count.value -= 1 ); return { count , increment , decrement } ; } ; // useToggleDisplay.ts // b. 表示を切り替えたい import { computed , ref } from "vue" ; export const useToggleDisplay = () => { const display = ref ( true ); const toggleDisplay = () => ( display.value = !display.value ); const toggleDisplayText = computed (() => ( display.value ? "hide" : "show" )); return { display , toggleDisplay , toggleDisplayText } ; } ; // DisplayCount.vue import { defineComponent } from "vue" ; import { useCount } from "../hooks/useCount" ; // a. 数をカウントしたい import { useToggleDisplay } from "../hooks/useToggleDisplay" ; // b. 表示を切り替えたい export default defineComponent ( { setup () { return { ...useCount (), // a. 数をカウントしたい ...useToggleDisplay () // b. 表示を切り替えたい } ; } } ); それぞれの関心事が DisplayCount コンポーネントから完全に分離されています。 1 これは、これらのロジックが特定のコンポーネントに依存していないことを示しています。 ロジックが特定のコンポーネントに依存していないため、移植性・再利用性を高めることができます。 複数コンポーネント間でロジックを共通化しようとして、extendやmixinsを使って無茶をしたことがあるのは僕だけではないはずです。 Composition APIを使えば、より自然にロジックの共通化を表現できます。 TypeScriptサポートの改善 Composition APIによりTypeScriptの型定義もかなり改善しました。 従来のOptions APIの型定義は、 this に data , computed , methods などの定義を生やすためのコードがとても複雑で、型定義を見に行っては迷子になることもしょっちゅうでした。 ViewModel( this )への参照を無くし、関数の合成によってロジックを表現出来るようになったことで無理なく型を表現できているため、TypeScriptのコードが理解しやすくなりました。 個人プロジェクトについて 今回書き換えるのは rxjs-stream-editor というRxJSの非同期処理を可視化するツールです。 Vue 2, Vuex, TypeScriptを使用しており、コンポーネントの数もルートコンポーネントを入れて8つ、Vuex moduleの数も3つと検証にはもってこいの大きさです。 クラス記法コンポーネント、 vue-property-decorator 、 vuex-smart-module を使用しており、現時点ではVue 3未対応なライブラリなため、今回の検証ではいったん外しつつなるべくVueの素のAPIを使う方針でいきます。 memowomome.hatenablog.com 書き換え作業ログ github.com nodeとyarnを最新にアップデート node 👉 15.3.0にアップデート yarn 👉 1.22.10にアップデート ビルド設定をVue CLIで一気に最新に書き換える rxjs-stream-editorはVue CLIを使用したプロジェクトなので、今回もVue CLIを使用してアップデートします。 vue upgrade というコマンドも用意されていますが、規模も小さいので今回は vue create で上書く方法でやってみました🙌 # プロジェクトのひとつ上のディレクトリに移動 cd .. # 同名のディレクトリを指定して上書き # オプションでGitコミット無し、既存ファイルとマージするように指定 vue create -n --merge rxjs-stream-editor スタックを選択 スタックをマニュアルで選択 TS, Vuex, Stylus, ESLint, Prettierを使用 Vue 3を使用 クラス記法のコンポーネント定義ではなく素のAPIを使用 TypeScriptと一緒にBabelを使用 保存時にLintを実行 ESLint等のコンフィグファイルはpackage.jsonにまとめず、専用のファイルを使用 Vue CLI v4.5.9 ? Please pick a preset: Manually select features ? Check the features needed for your project: Choose Vue version, Babel, TS, Vuex, CSS Pre-processors, Linter ? Choose a version of Vue.js that you want to start the project with 3.x (Preview) ? Use class-style component syntax? No ? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? Yes ? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): Stylus ? Pick a linter / formatter config: Prettier ? Pick additional lint features: Lint on save ? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files ? Save this as a preset for future projects? No いったんビルドしてみる エラーがたくさん出ます。 このまま一旦コミットしておきます。 エラーログ全文 ERROR in src/App.vue:21:1 TS1238: Unable to resolve signature of class decorator when called as an expression. Type 'VueClass<any>' is missing the following properties from type 'typeof App': extend, set, delete, directive, and 6 more. 19 | import { ColorDefinition } from './core/ColorDefinition'; 20 | > 21 | @Component({ | ^^^^^^^^^^^^ > 22 | components: { | ^^^^^^^^^^^^^^^ > 23 | AppHeader, | ^^^^^^^^^^^^^^^ > 24 | StreamEditor, | ^^^^^^^^^^^^^^^ > 25 | BottomNav, | ^^^^^^^^^^^^^^^ > 26 | }, | ^^^^^^^^^^^^^^^ > 27 | }) | ^^^ 28 | export default class App extends Vue.extend({ 29 | computed: { 30 | ...domainStreamColorizerModule.mapState(['colorMatcherSourceCode']), ERROR in src/App.vue:21:2 TS2345: Argument of type 'typeof App' is not assignable to parameter of type 'VueClass<any>'. Type 'typeof App' is missing the following properties from type 'typeof import("/Users/okamoto.k/_ghqroot/github.com/all-user/rxjs-stream-editor/node_modules/vue/dist/vue")': useCssModule, useCssVars, createApp, createSSRApp, and 108 more. 19 | import { ColorDefinition } from './core/ColorDefinition'; 20 | > 21 | @Component({ | ^^^^^^^^^^^ > 22 | components: { | ^^^^^^^^^^^^^^^ > 23 | AppHeader, | ^^^^^^^^^^^^^^^ > 24 | StreamEditor, | ^^^^^^^^^^^^^^^ > 25 | BottomNav, | ^^^^^^^^^^^^^^^ > 26 | }, | ^^^^^^^^^^^^^^^ > 27 | }) | ^^^ 28 | export default class App extends Vue.extend({ 29 | computed: { 30 | ...domainStreamColorizerModule.mapState(['colorMatcherSourceCode']), ERROR in src/App.vue:28:22 TS1219: Experimental support for decorators is a feature that is subject to change in a future release. Set the 'experimentalDecorators' option in your 'tsconfig' or 'jsconfig' to remove this warning. 26 | }, 27 | }) > 28 | export default class App extends Vue.extend({ | ^^^ 29 | computed: { 30 | ...domainStreamColorizerModule.mapState(['colorMatcherSourceCode']), 31 | ...domainStreamColorizerModule.mapGetters(['colorDefinitions']), ERROR in src/components/AppHeader/AppHeader.ts:3:1 TS1238: Unable to resolve signature of class decorator when called as an expression. Type '<VC extends VueClass<any>>(target: VC) => VC' is missing the following properties from type 'typeof AppHeader': extend, nextTick, set, delete, and 9 more. 1 | import { Component, Vue } from 'vue-property-decorator'; 2 | > 3 | @Component | ^^^^^^^^^^ 4 | export default class AppHeader extends Vue {} 5 | ERROR in src/components/AppHeader/AppHeader.ts:3:2 TS2769: No overload matches this call. Overload 1 of 2, '(options: ComponentOptionsBase<any, any, any, any, any, any, any, any, string, {}> & ThisType<any> & ThisType<any>): <VC extends VueClass<any>>(target: VC) => VC', gave the following error. Argument of type 'typeof AppHeader' is not assignable to parameter of type 'ComponentOptionsBase<any, any, any, any, any, any, any, any, string, {}> & ThisType<any> & ThisType<any>'. Type 'typeof AppHeader' is not assignable to type 'ComponentOptionsBase<any, any, any, any, any, any, any, any, string, {}>'. Types of property 'call' are incompatible. Type '<T, A extends any[]>(this: new (...args: A) => T, thisArg: T, ...args: A) => void' is not assignable to type '(this: unknown, ...args: unknown[]) => never'. The 'this' types of each signature are incompatible. Type 'unknown' is not assignable to type 'new (...args: unknown[]) => unknown'. Overload 2 of 2, '(target: VueClass<any>): VueClass<any>', gave the following error. Argument of type 'typeof AppHeader' is not assignable to parameter of type 'VueClass<any>'. Type 'typeof AppHeader' is missing the following properties from type 'typeof import("/Users/okamoto.k/_ghqroot/github.com/all-user/rxjs-stream-editor/node_modules/vue/dist/vue")': useCssModule, useCssVars, createApp, createSSRApp, and 108 more. 1 | import { Component, Vue } from 'vue-property-decorator'; 2 | > 3 | @Component | ^^^^^^^^^ 4 | export default class AppHeader extends Vue {} 5 | ERROR in src/components/AppHeader/AppHeader.ts:4:22 TS1219: Experimental support for decorators is a feature that is subject to change in a future release. Set the 'experimentalDecorators' option in your 'tsconfig' or 'jsconfig' to remove this warning. 2 | 3 | @Component > 4 | export default class AppHeader extends Vue {} | ^^^^^^^^^ 5 | ERROR in src/components/BottomNav/BottomNav.ts:9:1 TS1238: Unable to resolve signature of class decorator when called as an expression. Type 'VueClass<any>' is missing the following properties from type 'typeof BottomNav': extend, set, delete, directive, and 6 more. 7 | import StreamColorizer from '../StreamColorizer/StreamColorizer.vue'; 8 | > 9 | @Component({ | ^^^^^^^^^^^^ > 10 | components: { | ^^^^^^^^^^^^^^^ > 11 | MessageOutput, | ^^^^^^^^^^^^^^^ > 12 | StreamColorizer, | ^^^^^^^^^^^^^^^ > 13 | }, | ^^^^^^^^^^^^^^^ > 14 | }) | ^^^ 15 | export default class BottomNav extends Vue.extend({ 16 | computed: { 17 | ...domainStreamEditorModule.mapState(['errorMessage', 'message']), ERROR in src/components/BottomNav/BottomNav.ts:9:2 TS2345: Argument of type 'typeof BottomNav' is not assignable to parameter of type 'VueClass<any>'. Type 'typeof BottomNav' is missing the following properties from type 'typeof import("/Users/okamoto.k/_ghqroot/github.com/all-user/rxjs-stream-editor/node_modules/vue/dist/vue")': useCssModule, useCssVars, createApp, createSSRApp, and 108 more. 7 | import StreamColorizer from '../StreamColorizer/StreamColorizer.vue'; 8 | > 9 | @Component({ | ^^^^^^^^^^^ > 10 | components: { | ^^^^^^^^^^^^^^^ > 11 | MessageOutput, | ^^^^^^^^^^^^^^^ > 12 | StreamColorizer, | ^^^^^^^^^^^^^^^ > 13 | }, | ^^^^^^^^^^^^^^^ > 14 | }) | ^^^ 15 | export default class BottomNav extends Vue.extend({ 16 | computed: { 17 | ...domainStreamEditorModule.mapState(['errorMessage', 'message']), ERROR in src/components/BottomNav/BottomNav.ts:15:22 TS1219: Experimental support for decorators is a feature that is subject to change in a future release. Set the 'experimentalDecorators' option in your 'tsconfig' or 'jsconfig' to remove this warning. 13 | }, 14 | }) > 15 | export default class BottomNav extends Vue.extend({ | ^^^^^^^^^ 16 | computed: { 17 | ...domainStreamEditorModule.mapState(['errorMessage', 'message']), 18 | ...uiBottomNavModule.mapState(['enabled']), ERROR in src/components/MessageOutput/MessageOutput.ts:4:1 TS1238: Unable to resolve signature of class decorator when called as an expression. Type '<VC extends VueClass<any>>(target: VC) => VC' is missing the following properties from type 'typeof MessageOutput': extend, nextTick, set, delete, and 9 more. 2 | import { domainStreamEditorModule } from '../../store/modules/internal'; 3 | > 4 | @Component | ^^^^^^^^^^ 5 | export default class MessageOutput extends Vue.extend({ 6 | computed: { 7 | ...domainStreamEditorModule.mapState(['errorMessage', 'message']), ERROR in src/components/MessageOutput/MessageOutput.ts:4:2 TS2769: No overload matches this call. Overload 1 of 2, '(options: ComponentOptionsBase<any, any, any, any, any, any, any, any, string, {}> & ThisType<any> & ThisType<any>): <VC extends VueClass<any>>(target: VC) => VC', gave the following error. Argument of type 'typeof MessageOutput' is not assignable to parameter of type 'ComponentOptionsBase<any, any, any, any, any, any, any, any, string, {}> & ThisType<any> & ThisType<any>'. Type 'typeof MessageOutput' is not assignable to type 'ComponentOptionsBase<any, any, any, any, any, any, any, any, string, {}>'. Types of property 'call' are incompatible. Type '<T, A extends any[]>(this: new (...args: A) => T, thisArg: T, ...args: A) => void' is not assignable to type '(this: unknown, ...args: unknown[]) => never'. Overload 2 of 2, '(target: VueClass<any>): VueClass<any>', gave the following error. Argument of type 'typeof MessageOutput' is not assignable to parameter of type 'VueClass<any>'. Type 'typeof MessageOutput' is missing the following properties from type 'typeof import("/Users/okamoto.k/_ghqroot/github.com/all-user/rxjs-stream-editor/node_modules/vue/dist/vue")': useCssModule, useCssVars, createApp, createSSRApp, and 108 more. 2 | import { domainStreamEditorModule } from '../../store/modules/internal'; 3 | > 4 | @Component | ^^^^^^^^^ 5 | export default class MessageOutput extends Vue.extend({ 6 | computed: { 7 | ...domainStreamEditorModule.mapState(['errorMessage', 'message']), ERROR in src/components/MessageOutput/MessageOutput.ts:5:22 TS1219: Experimental support for decorators is a feature that is subject to change in a future release. Set the 'experimentalDecorators' option in your 'tsconfig' or 'jsconfig' to remove this warning. 3 | 4 | @Component > 5 | export default class MessageOutput extends Vue.extend({ | ^^^^^^^^^^^^^ 6 | computed: { 7 | ...domainStreamEditorModule.mapState(['errorMessage', 'message']), 8 | }, ERROR in src/components/StreamColorizer/StreamColorizer.ts:2:10 TS2305: Module '"../../../node_modules/vue/dist/vue"' has no exported member 'VueConstructor'. 1 | import { Component, Vue } from 'vue-property-decorator'; > 2 | import { VueConstructor } from 'vue'; | ^^^^^^^^^^^^^^ 3 | import { domainStreamColorizerModule } from '../../store/modules/internal'; 4 | import { Photoshop } from 'vue-color'; 5 | import { ColorDefinition } from '../../core/ColorDefinition'; ERROR in src/components/StreamColorizer/StreamColorizer.ts:7:1 TS1238: Unable to resolve signature of class decorator when called as an expression. Type 'VueClass<any>' is missing the following properties from type 'typeof StreamColorizer': extend, set, delete, directive, and 6 more. 5 | import { ColorDefinition } from '../../core/ColorDefinition'; 6 | > 7 | @Component({ | ^^^^^^^^^^^^ > 8 | components: { | ^^^^^^^^^^^^^^^ > 9 | PhotoshopPicker: Photoshop as VueConstructor, | ^^^^^^^^^^^^^^^ > 10 | }, | ^^^^^^^^^^^^^^^ > 11 | }) | ^^^ 12 | export default class StreamColorizer extends Vue.extend({ 13 | computed: { 14 | ...domainStreamColorizerModule.mapState([ ERROR in src/components/StreamColorizer/StreamColorizer.ts:7:2 TS2345: Argument of type 'typeof StreamColorizer' is not assignable to parameter of type 'VueClass<any>'. Type 'typeof StreamColorizer' is missing the following properties from type 'typeof import("/Users/okamoto.k/_ghqroot/github.com/all-user/rxjs-stream-editor/node_modules/vue/dist/vue")': useCssModule, useCssVars, createApp, createSSRApp, and 108 more. 5 | import { ColorDefinition } from '../../core/ColorDefinition'; 6 | > 7 | @Component({ | ^^^^^^^^^^^ > 8 | components: { | ^^^^^^^^^^^^^^^ > 9 | PhotoshopPicker: Photoshop as VueConstructor, | ^^^^^^^^^^^^^^^ > 10 | }, | ^^^^^^^^^^^^^^^ > 11 | }) | ^^^ 12 | export default class StreamColorizer extends Vue.extend({ 13 | computed: { 14 | ...domainStreamColorizerModule.mapState([ ERROR in src/components/StreamColorizer/StreamColorizer.ts:12:22 TS1219: Experimental support for decorators is a feature that is subject to change in a future release. Set the 'experimentalDecorators' option in your 'tsconfig' or 'jsconfig' to remove this warning. 10 | }, 11 | }) > 12 | export default class StreamColorizer extends Vue.extend({ | ^^^^^^^^^^^^^^^ 13 | computed: { 14 | ...domainStreamColorizerModule.mapState([ 15 | 'colorMatcherSourceCode', ERROR in src/components/StreamEditor/StreamEditor.ts:7:1 TS1238: Unable to resolve signature of class decorator when called as an expression. Type 'VueClass<any>' is missing the following properties from type 'typeof StreamEditor': extend, set, delete, directive, and 6 more. 5 | import debounce from 'lodash-es/debounce'; 6 | > 7 | @Component({ | ^^^^^^^^^^^^ > 8 | components: { | ^^^^^^^^^^^^^^^ > 9 | StreamEditorItem, | ^^^^^^^^^^^^^^^ > 10 | }, | ^^^^^^^^^^^^^^^ > 11 | }) | ^^^ 12 | export default class StreamEditor extends Vue.extend({ 13 | computed: { 14 | ...domainStreamEditorModule.mapGetters(['streamDatasets', 'sourceCode']), ERROR in src/components/StreamEditor/StreamEditor.ts:7:2 TS2345: Argument of type 'typeof StreamEditor' is not assignable to parameter of type 'VueClass<any>'. Type 'typeof StreamEditor' is missing the following properties from type 'typeof import("/Users/okamoto.k/_ghqroot/github.com/all-user/rxjs-stream-editor/node_modules/vue/dist/vue")': useCssModule, useCssVars, createApp, createSSRApp, and 108 more. 5 | import debounce from 'lodash-es/debounce'; 6 | > 7 | @Component({ | ^^^^^^^^^^^ > 8 | components: { | ^^^^^^^^^^^^^^^ > 9 | StreamEditorItem, | ^^^^^^^^^^^^^^^ > 10 | }, | ^^^^^^^^^^^^^^^ > 11 | }) | ^^^ 12 | export default class StreamEditor extends Vue.extend({ 13 | computed: { 14 | ...domainStreamEditorModule.mapGetters(['streamDatasets', 'sourceCode']), ERROR in src/components/StreamEditor/StreamEditor.ts:12:22 TS1219: Experimental support for decorators is a feature that is subject to change in a future release. Set the 'experimentalDecorators' option in your 'tsconfig' or 'jsconfig' to remove this warning. 10 | }, 11 | }) > 12 | export default class StreamEditor extends Vue.extend({ | ^^^^^^^^^^^^ 13 | computed: { 14 | ...domainStreamEditorModule.mapGetters(['streamDatasets', 'sourceCode']), 15 | }, ERROR in src/components/StreamEditor/StreamEditor.ts:25:10 TS1219: Experimental support for decorators is a feature that is subject to change in a future release. Set the 'experimentalDecorators' option in your 'tsconfig' or 'jsconfig' to remove this warning. 23 | }) { 24 | @Watch('sourceCode') > 25 | public watchSourceCode() { | ^^^^^^^^^^^^^^^ 26 | this.evaluateSourceCodeDebounced(); 27 | } 28 | ERROR in src/components/StreamEditorItem/StreamEditorItem.ts:23:1 TS1238: Unable to resolve signature of class decorator when called as an expression. Type 'VueClass<any>' is missing the following properties from type 'typeof StreamEditorItem': extend, set, delete, directive, and 6 more. 21 | }; 22 | > 23 | @Component({ | ^^^^^^^^^^^^ > 24 | components: { | ^^^^^^^^^^^^^^^ > 25 | StreamEditorTextarea, | ^^^^^^^^^^^^^^^ > 26 | }, | ^^^^^^^^^^^^^^^ > 27 | }) | ^^^ 28 | export default class StreamEditorItem extends Vue.extend({ 29 | computed: { 30 | ...domainStreamColorizerModule.mapGetters([ ERROR in src/components/StreamEditorItem/StreamEditorItem.ts:23:2 TS2345: Argument of type 'typeof StreamEditorItem' is not assignable to parameter of type 'VueClass<any>'. Type 'typeof StreamEditorItem' is missing the following properties from type 'typeof import("/Users/okamoto.k/_ghqroot/github.com/all-user/rxjs-stream-editor/node_modules/vue/dist/vue")': useCssModule, useCssVars, createApp, createSSRApp, and 108 more. 21 | }; 22 | > 23 | @Component({ | ^^^^^^^^^^^ > 24 | components: { | ^^^^^^^^^^^^^^^ > 25 | StreamEditorTextarea, | ^^^^^^^^^^^^^^^ > 26 | }, | ^^^^^^^^^^^^^^^ > 27 | }) | ^^^ 28 | export default class StreamEditorItem extends Vue.extend({ 29 | computed: { 30 | ...domainStreamColorizerModule.mapGetters([ ERROR in src/components/StreamEditorItem/StreamEditorItem.ts:28:22 TS1219: Experimental support for decorators is a feature that is subject to change in a future release. Set the 'experimentalDecorators' option in your 'tsconfig' or 'jsconfig' to remove this warning. 26 | }, 27 | }) > 28 | export default class StreamEditorItem extends Vue.extend({ | ^^^^^^^^^^^^^^^^ 29 | computed: { 30 | ...domainStreamColorizerModule.mapGetters([ 31 | 'colorCodeGetter', ERROR in src/components/StreamEditorItem/StreamEditorItem.ts:39:18 TS1219: Experimental support for decorators is a feature that is subject to change in a future release. Set the 'experimentalDecorators' option in your 'tsconfig' or 'jsconfig' to remove this warning. 37 | }, 38 | }) { > 39 | @Prop() public dataset: StreamDataset | undefined; | ^^^^^^^ 40 | @Prop({ required: true }) public index!: boolean; 41 | @Prop({ default: false }) public disabled!: boolean; 42 | ERROR in src/components/StreamEditorItem/StreamEditorItem.ts:40:36 TS1219: Experimental support for decorators is a feature that is subject to change in a future release. Set the 'experimentalDecorators' option in your 'tsconfig' or 'jsconfig' to remove this warning. 38 | }) { 39 | @Prop() public dataset: StreamDataset | undefined; > 40 | @Prop({ required: true }) public index!: boolean; | ^^^^^ 41 | @Prop({ default: false }) public disabled!: boolean; 42 | 43 | get events() { ERROR in src/components/StreamEditorItem/StreamEditorItem.ts:41:36 TS1219: Experimental support for decorators is a feature that is subject to change in a future release. Set the 'experimentalDecorators' option in your 'tsconfig' or 'jsconfig' to remove this warning. 39 | @Prop() public dataset: StreamDataset | undefined; 40 | @Prop({ required: true }) public index!: boolean; > 41 | @Prop({ default: false }) public disabled!: boolean; | ^^^^^^^^ 42 | 43 | get events() { 44 | return this.dataset ? this.dataset.events : []; ERROR in src/components/StreamEditorTextarea/StreamEditorTextarea.ts:5:1 TS1238: Unable to resolve signature of class decorator when called as an expression. Type '<VC extends VueClass<any>>(target: VC) => VC' is missing the following properties from type 'typeof StreamEditorTextarea': extend, nextTick, set, delete, and 9 more. 3 | import { domainStreamEditorModule } from '../../store/modules/internal'; 4 | > 5 | @Component | ^^^^^^^^^^ 6 | export default class StreamEditorTextarea extends Vue.extend({ 7 | methods: { 8 | ...domainStreamEditorModule.mapMutations(['setSourceCode']), ERROR in src/components/StreamEditorTextarea/StreamEditorTextarea.ts:5:2 TS2769: No overload matches this call. Overload 1 of 2, '(options: ComponentOptionsBase<any, any, any, any, any, any, any, any, string, {}> & ThisType<any> & ThisType<any>): <VC extends VueClass<any>>(target: VC) => VC', gave the following error. Argument of type 'typeof StreamEditorTextarea' is not assignable to parameter of type 'ComponentOptionsBase<any, any, any, any, any, any, any, any, string, {}> & ThisType<any> & ThisType<any>'. Type 'typeof StreamEditorTextarea' is not assignable to type 'ComponentOptionsBase<any, any, any, any, any, any, any, any, string, {}>'. Types of property 'call' are incompatible. Type '<T, A extends any[]>(this: new (...args: A) => T, thisArg: T, ...args: A) => void' is not assignable to type '(this: unknown, ...args: unknown[]) => never'. Overload 2 of 2, '(target: VueClass<any>): VueClass<any>', gave the following error. Argument of type 'typeof StreamEditorTextarea' is not assignable to parameter of type 'VueClass<any>'. Type 'typeof StreamEditorTextarea' is missing the following properties from type 'typeof import("/Users/okamoto.k/_ghqroot/github.com/all-user/rxjs-stream-editor/node_modules/vue/dist/vue")': useCssModule, useCssVars, createApp, createSSRApp, and 108 more. 3 | import { domainStreamEditorModule } from '../../store/modules/internal'; 4 | > 5 | @Component | ^^^^^^^^^ 6 | export default class StreamEditorTextarea extends Vue.extend({ 7 | methods: { 8 | ...domainStreamEditorModule.mapMutations(['setSourceCode']), ERROR in src/components/StreamEditorTextarea/StreamEditorTextarea.ts:6:22 TS1219: Experimental support for decorators is a feature that is subject to change in a future release. Set the 'experimentalDecorators' option in your 'tsconfig' or 'jsconfig' to remove this warning. 4 | 5 | @Component > 6 | export default class StreamEditorTextarea extends Vue.extend({ | ^^^^^^^^^^^^^^^^^^^^ 7 | methods: { 8 | ...domainStreamEditorModule.mapMutations(['setSourceCode']), 9 | }, ERROR in src/components/StreamEditorTextarea/StreamEditorTextarea.ts:11:18 TS1219: Experimental support for decorators is a feature that is subject to change in a future release. Set the 'experimentalDecorators' option in your 'tsconfig' or 'jsconfig' to remove this warning. 9 | }, 10 | }) { > 11 | @Prop() public dataset: StreamDataset | undefined; | ^^^^^^^ 12 | @Prop({ default: false }) public disabled!: boolean; 13 | 14 | get sourceCode() { ERROR in src/components/StreamEditorTextarea/StreamEditorTextarea.ts:12:36 TS1219: Experimental support for decorators is a feature that is subject to change in a future release. Set the 'experimentalDecorators' option in your 'tsconfig' or 'jsconfig' to remove this warning. 10 | }) { 11 | @Prop() public dataset: StreamDataset | undefined; > 12 | @Prop({ default: false }) public disabled!: boolean; | ^^^^^^^^ 13 | 14 | get sourceCode() { 15 | return this.dataset ? this.dataset.sourceCode : ''; ERROR in src/main.ts:6:5 TS2339: Property 'use' does not exist on type 'typeof import("/Users/okamoto.k/_ghqroot/github.com/all-user/rxjs-stream-editor/node_modules/vue/dist/vue")'. 4 | import VueTextareaAutosize from 'vue-textarea-autosize'; 5 | > 6 | Vue.use(VueTextareaAutosize); | ^^^ 7 | Vue.config.productionTip = false; 8 | 9 | new Vue({ ERROR in src/main.ts:7:5 TS2339: Property 'config' does not exist on type 'typeof import("/Users/okamoto.k/_ghqroot/github.com/all-user/rxjs-stream-editor/node_modules/vue/dist/vue")'. 5 | 6 | Vue.use(VueTextareaAutosize); > 7 | Vue.config.productionTip = false; | ^^^^^^ 8 | 9 | new Vue({ 10 | store, ERROR in src/main.ts:9:5 TS2351: This expression is not constructable. Type 'typeof import("/Users/okamoto.k/_ghqroot/github.com/all-user/rxjs-stream-editor/node_modules/vue/dist/vue")' has no construct signatures. 7 | Vue.config.productionTip = false; 8 | > 9 | new Vue({ | ^^^ 10 | store, 11 | render: h => h(App), 12 | }).$mount('#app'); ERROR in src/main.ts:11:11 TS7006: Parameter 'h' implicitly has an 'any' type. 9 | new Vue({ 10 | store, > 11 | render: h => h(App), | ^ 12 | }).$mount('#app'); 13 | ERROR in src/store/index.ts:6:5 TS2339: Property 'use' does not exist on type 'typeof import("/Users/okamoto.k/_ghqroot/github.com/all-user/rxjs-stream-editor/node_modules/vue/dist/vue")'. 4 | import { rootModule } from './modules'; 5 | > 6 | Vue.use(Vuex); | ^^^ 7 | 8 | export default createStore(rootModule); 9 | .babelrcを削除 Babel最新ではbabel.config.jsを使用するように変わったようなのですが、Vue CLIでディレクトリをマージした際に.babelrcが残っており、そちらの設定を見に行ってしまいエラーが出ていました。 これを削除します。 vue-property-decoratorを外す デコレータを使ったコンポーネント定義を defineComponent に置き換えていきます。 vuex-smart-moduleを外す Vue 3対応のために素のVuexに書き換えます。 vuex-smart-moduleを通して store にアクセスしていた部分は、 useStore 関数を使用して置き換えます。 この時点で commit , dispatch , getters に付いていたメソッドの型は、 string であればなんでも受け入れるようになってしまいます。 やはりまだvuex-smart-moduleは外せないという所感。 Composition API対応に関するIssueでは対応予定とのコメントもあり今後の動きに期待です。 ktsn/vuex-smart-module | Composition API? #106 テンプレートから参照するものは従来通り mapState や mapGetters を使用し、コンポーネント定義内で参照するものは useStore を使用しています。 この辺りの書き方はどうするのが良いだろう、という感じでまだ模索中です。 その他のライブラリ vue-textarea-autosize テキストの入力に合わせて自動的に高さを調整してくれる Vue 3未対応 とりあえず普通のtextareaに置き換えてしのぐ vue-color カラーピッカー Vue 3未対応 いったんあきらめる 関心事を整理して hooks ディレクトリに切り出す setup にまとめて書かれていた初期化処理を3つのファイルに分割し、各関数内でそれぞれ useStore を使用するように変更します。 カラーパレットの初期化処理。 カラーパレットとイベントをマッピングするためのソースコードの初期化処理。 可視化するRxJS Observableを生成するソースコードの初期化処理。 実際に書き換えてみての所感 コードの見通しはとても良くなる 関心事の分離がうまく表現できるようになり、コードの見通しが良くなりました。 規模の大きいプロジェクトであればより効果を発揮できるのではと感じました。 Vuexの使い方はまだ模索中 今回はテンプレートから参照する値を従来のOptions APIで記述しましたが、Composition APIに最適化されたヘルパーについての議論が行われていていました。 近いうちにベストプラクティスが発明されそうです。 github.com VuexのTSサポートはこれから Vuex 4でTypeScriptのサポートは強化されたものの、state以外の型周りはまだサポートされていません。 現時点ではvuex-smart-moduleなどのTypeScriptサポートのライブラリは必要だと感じました。 こちらのIssueを見ると、本格的なTSサポートの強化はVuex 5を予定しているようです。 github.com また、それに伴うBreaking Changeを検討している模様。 一方で、TS 4.1というゲームチェンジャーの登場によりVuex 4もサポートされる可能性が出てきたようです。 github.com TS 4.1で導入されたTemplate Literal Typesにより文字列の柔軟な型検査が可能になり、これまで難しかったnamespaceをスラッシュで繋いだ文字列に対しての静的な型検査が実装可能になりました。 近い将来TS完全対応が実現するかもしれません。 プロダクションに投入できるタイミング 上記を踏まえると今すぐのプロダクション投入は難しいものの、確実にメリットを感じたので、少しづつ移行に向けて準備を進めていきたいと思います。 まだ試せていないこと ローカルステートを定義しているコンポーネントが無かったため、 ref , reactive を使った複雑な実装はまだ試せていないです vue-routerも使用していないためこちらもまだ未検証です 引き続き検証していきたいと思います さいごに 以上Vue 3への書き換えを通しての所感をまとめてみました。 この記事では紹介できなかった細かいAPIの変更などもありますが、暗黙的な挙動の削除やパフォーマンス改善のための変更など確実にパワーアップしています。 個人的にはFragmentが使えるようになったのが最高です。 これからのVue 3を楽しんでいきましょう! そして明日は sako さんの「UIデザイナーとして働く私が就活生に戻ったら絶対やること5つ」です! めちゃくちゃ知りたい! delyではエンジニアを全方位絶賛募集中です🚀 こちらのリンクからお気軽にエントリーください🙌 join-us.dely.jp また、delyでは普段表に出ない開発チームの裏側をお伝えするイベントをたくさん開催しております! こちらもぜひ覗いてみてください! bethesun.connpass.com ではまた! toggleDisplayText についてはUI都合な部分が大きいため、 useToggleDisplay には含めず DisplayCount コンポーネント側に寄せたいところですが、今回は分かりやすさのためにこのようにしています。 ↩
アバター
目次 目次 はじめに ウェブのこれからを追いかける Project Fugu とは 検討されているエコシステム User Idle Detection API macOS Touch Bar API その他 おわりに はじめに こんにちは、dely株式会社でエンジニアをしているしらりんです。4月に立ち上げられたリテール事業部という部署で、主にウェブフロントエンド・サーバーサイド領域の開発を担当しています。 先日初めてのぎっくり腰を経験し、それ以降日々腰を曲げるのに恐怖を感じています。 そんなことはさておき、この記事は「dely #2 Advent Calendar 2020」の14日目の記事です。 adventar.org adventar.org 昨日は開発部GM 井上さん( @gomesuit )の「技術だけではもう足りない?エンジニアとしての成長のために避けては通れない4つの領域とは!」という記事でした。 tech.dely.jp ウェブのこれからを追いかける 記事のテーマに「ウェブの未来」とスケール大きめな表現が含まれていますが大した内容ではありません。 Project Fuguで考えられているこれからのウェブでどんなことができるようになっていくのか、今どんなことが進んでいるのかをfugu-tracker-apiを少し眺めてみるといった内容になります。 Project Fugu とは ウェブは汎用的なプラットフォームとしてとても優秀ですが、汎用的であることもあり少し凝ったこと(特化したこと)をやろうとすると、ネイティブアプリではできるがウェブでは難しいということが多く存在します。アプリではよくみるシェア機能をウェブで提供するWeb Share APIが使えるようになったのもつい最近のことになります。 Web Share APIの対応状況 caniuse.com そういったネイティブアプリとウェブのギャップを埋めるための取り組みがProject Fugu(Web Capabilities Project)です。 www.chromium.org そんなProject Fuguで考えられている機能がいつ提供されるのかが把握しやすくまとまっているのがfugu-api-trackerです。 docs.google.com ここでは現在どのようなエコシステムが試験・検討されているのか、いくつかみてみることにします。 検討されているエコシステム User Idle Detection API 現在origin trialとして提供されている機能。 マウスやキーボード、タッチスクリーンに対する操作が行われていないことや、別のタブやウィンドウを開いているなどユーザーのアイドル状態を検知することができます。 例えばフィードバックのタイミングをユーザーがアクティブな状態に戻ったときに行うなどといった活用が考えられます。 bugs.chromium.org macOS Touch Bar API 中にはこんなものもあります。 bugs.chromium.org 名前の通り、最近のmacbook(pro)に付いているタッチバーを活用するAPIです。 例えばこれを使ってタッチバーでプログレスバーを表現したり、簡単なゲームを作ったりとすることができます。 ドキュメント内から参照できるelectronのAPIでは、electronと以下のコードを利用してタッチバーのついているmacbookで動かせるスロットゲームを作るサンプルがのっています。 将来的にこれがweb apiとしても利用可能になるかもしれません。 // touchbar.js const { app, BrowserWindow, TouchBar } = require( 'electron' ) const { TouchBarLabel, TouchBarButton, TouchBarSpacer } = TouchBar let spinning = false const reel1 = new TouchBarLabel() const reel2 = new TouchBarLabel() const reel3 = new TouchBarLabel() const result = new TouchBarLabel() const spin = new TouchBarButton( { label: '🎰 Spin' , backgroundColor: '#7851A9' , click: () => { if (spinning) { return } spinning = true result.label = '' let timeout = 10 const spinLength = 4 * 1000 const startTime = Date .now() const spinReels = () => { updateReels() if (( Date .now() - startTime) >= spinLength) { finishSpin() } else { timeout *= 1.1 setTimeout(spinReels, timeout) } } spinReels() } } ) const getRandomValue = () => { const values = [ '🍒' , '💎' , '7️⃣' , '🍊' , '🔔' , '⭐' , '🍇' , '🍀' ] return values [ Math.floor(Math.random() * values.length) ] } const updateReels = () => { reel1.label = getRandomValue() reel2.label = getRandomValue() reel3.label = getRandomValue() } const finishSpin = () => { const uniqueValues = new Set( [ reel1.label, reel2.label, reel3.label ] ).size if (uniqueValues === 1) { result.label = '💰 Jackpot!' result.textColor = '#FDFF00' } else if (uniqueValues === 2) { result.label = '😍 Winner!' result.textColor = '#FDFF00' } else { result.label = '🙁 Spin Again' result.textColor = null } spinning = false } const touchBar = new TouchBar( { items: [ spin, new TouchBarSpacer( { size: 'large' } ), reel1, new TouchBarSpacer( { size: 'small' } ), reel2, new TouchBarSpacer( { size: 'small' } ), reel3, new TouchBarSpacer( { size: 'large' } ), result ] } ) let window app.whenReady().then(() => { window = new BrowserWindow( { frame: false , titleBarStyle: 'hiddenInset' , width: 200, height: 200, backgroundColor: '#000' } ) window .loadURL( 'about:blank' ) window .setTouchBar(touchBar) } ) ./node_modules/.bin/electron touchbar.js (一瞬ラッキーセブンが揃った🤑 ) アドベントカレンダー用 pic.twitter.com/Y0cbPp9ZYF — しらりん (@Srrn97) 2020年12月14日 その他 その他にも上の方で少し触れたWeb Share APIのデスクトップ対応だったり、 input type="file" などから選択されたファイルのリサイズ機能(将来的には動画のサイズ変更にも対応)といった様々な機能の提供が検討されています。 bugs.chromium.org bugs.chromium.org おわりに いかがでしたでしょうか。 ブラウザやOSの種類、バージョンなど多くの壁が存在するウェブですが、他のどんなネイティブアプリケーションにもなれる可能性を秘めていることがウェブの魅力の1つだと僕は思っています。 fugu-api-trackerを見れば今後どのようなエコシステムが試験され、提供されていくのかを把握しやすく眺めているだけでも楽しいです。ここで紹介したものは極一部のものでしかないので、是非一度目を通してみてください。 明日はnancyさんの「iOSのサブスクリプション機能 プロモーションオファーを触ってみた」です。お楽しみに! 最後になりますが、delyではエンジニア・デザイナーを積極的に採用しています。 興味がある方は是非エントリーしてください! join-us.dely.jp また、定期的にテックトークイベントを開催しています。 delyについてちょっと興味ある・もっと知ってみたいという方は、お気軽にご参加ください。 bethesun.connpass.com
アバター
こんにちは! dely開発部GMの井上( @gomesuit )です。 この記事は「 dely #2 Advent Calendar 2020 」の13日目の記事です。 昨日はサーバサイドエンジニアのyamanoiさんの「 Cloud Runで手軽にサーバーレス・SSR 」という記事でした。 adventar.org adventar.org 目次 目次 はじめに プロダクト開発における技術選定の捉え方 プロダクト開発における意思決定って何 意思決定はどのように行われるか 意思決定において必要な情報とは プロダクト開発における情報のマネジメント テクノロジー領域の知識だけでは精度の高い技術選定はできない例 例1:マイクロサービス化 例2:プログラミング言語・フレームワークの採用 まとめ さいごに はじめに delyに来てマネジメントに関わるようになってから2年が経ちました。エンジニアの成長について色々考えさせられることがあるのですが、 プロダクト開発においてエンジニアがやるべきことはシステムの設計と実装だけではなくなってきた なと、最近になってより強く感じるようになりました。環境や携わっているプロダクトの特性やフェーズによってももちろん変わると思うのですが、この流れは今後加速していくだろうと感じるので、この機会に整理しておこうと思います。 インターネット技術の進化によって、世の中の色々なものがIT技術に置き換えられていっています。プロダクトやサービスの数も急速に増えていますが、WEBサービス開発における技術的な実現方法はどのプロダクトもそこまで大きな違いはないと思います。というのも、どのプロダクトでも共通に課題だと認識しているコアな技術的要素はクラウドやSaaSが生まれ、それらに置き換えられていってるからです。数年前までは技術的な実現手段についてエンジニアが頭を悩ませていましたが、今ではサービスを組み合わせることで、比較的簡単に実現できてしまう世の中になってきました。この流れは今後落ち着いていくことはなく、より一層加速してくと考えています。 技術的な実現方法がコモディティ化していくこの時代において、プロダクト開発に携わるエンジニアは今後何をしていくべきなのか を考えてみました。 考えを整理するにあたって下記の記事を参考にさせて頂きました。 エンジニアリングマネージャ/プロダクトマネージャのための知識体系と読書ガイド プロダクト開発における技術選定の捉え方 エンジニアなら誰しも課題に対して適切な手段を選定して、ビシバシ解消していきたいですよね。ただこの「技術選定」という言葉、"技術"という言葉が入っているが故に若干ミスリードしているのではないかなと最近思っています。「技術選定」という言葉を使うと何か特別感が出てしまうのですが、プロダクト開発においては「 意思決定の一種 」として捉えた方が自然ではないかなと思っています。 プロダクト開発における意思決定って何 プロダクト開発においては大小様々な意思決定が行われます。 例えば、「何の機能を作るか」、「どの機能から作るか」、「いつまでに作るか」、「誰が作るか」、「どうやって作るか」、などなど上げればキリがないですが、これらはプロダクト開発において決めないと何も進まない要素であり、意思決定すべき項目になります。例に上げた抽象度の高いものだけではなく、実装において何のライブラリを使うのかやどういったデータ構造にするのかといった具体性の高いものも意思決定の一つになります。 ちなみにdelyではスクアッドという組織構成を作っていて、スピード感を持った意思決定ができるようにしています。 https://speakerdeck.com/tsubotax/dely?slide=29 意思決定はどのように行われるか 意思決定を行うためには、決定を下すために必要な情報を整理する必要があります。意思決定の不確実性が高ければ高いほど、広範囲に渡る情報を集める必要があり、それぞれの分野の有識者を巻き込む必要が出てきます。情報を整理することで選択肢とメリデメを洗い出し、各ステークホルダーの合意を形成を行った後、責任者が決定を下すことで意思決定は行われます。 意思決定において必要な情報とは 意思決定に必要な情報は、意思決定しようとしている範囲によっても変わりますが、例えば「何の機能を作るか」でいくと、その機能が会社のビジョンや事業計画に沿っているのかやどうかといった情報が必要になります。「どの機能から作るか」であれば、要件定義や予算が必要になり、「いつまでに作るか」であれば見積もりやスケジュールが、「誰が作るか」であれば開発リソース、「どうやって作るか」であれば機能要件や非機能要件、現状のシステムのアーキテクチャが必要になります。その他のところではマーケティングやセールスとの関連がないかなど、上げればキリがないですが、 精度の高い決定を下そうとすればするほど、多くの情報が必要に なります。 プロダクト開発における情報のマネジメント 前項では、意思決定に必要な情報は何かという話をしましたが、それらの情報を得るための知識は4分類できます。そして、 エンジニアとしての成長のために避けては通れない4つの領域 もこちらになります。 https://qiita.com/hirokidaichi/items/95678bb1cef32629c317#各種スキルと読書ガイド を元に画像を作成しました。 不確実性の高い意思決定を下す際は、基本的に複数の分野の情報が必要になります。1人が全ての知識を持っていることは稀なので、基本的には複数人で情報を集める必要があります。ただし、必要な人数が増えれば増えるほどコミュニケーションコストが増えるとともに、それ相応の対話スキルが求められるようになります。そのため 複数領域の知識を持つ人は必然的に意思決定に巻き込まれやすく なります。 https://qiita.com/hirokidaichi/items/95678bb1cef32629c317#弱めのem定義と強めのem定義 を元に画像を作成しました。 意思決定の一種である技術選定においても、 不確実性の高いものはテクノロジー領域の知識だけでは決定を下すための情報が足りないということが言える と思います。 テクノロジー領域の知識だけでは精度の高い技術選定はできない例 2つほど具体例を考えてみました。 例1:マイクロサービス化 最近話題に上がりやすい「マイクロサービス」を題材にしてみます。プロダクトが成長し組織が大きくなったタイミングで、システムの構造をマイクロサービス化しようという話が上がったとします。当然マイクロサービス化する「目的」にもよると思いますが、一旦そこは無視した上でそういった状況になった場合、技術的な観点以外で必要になりそうな情報は下記の通りです。 今後の事業計画や組織体制 サービスの粒度の決め方やサービス間の循環依存を防止する仕組み 仮にテクノロジー領域の知識だけで意思決定を行った場合、アーキテクチャと組織構造のズレが発生したり、ルールやガイドラインなしでマイクロサービス化を行った結果、横断的な視点が失われてより一層課題が大きくなるといったことが考えられそうです。テクノロジー領域だけの情報で決めるのではなく、将来的な事業方針や組織体制を踏まえた上で決めていく事柄ではないかなと思います。 例2:プログラミング言語・フレームワークの採用 クラウドやコンテナ技術の浸透によって、プログラミング言語やフレームワークの流行は大きく影響を受けました。周りの技術が進歩・浸透することによって、その環境前提のプログラミング言語やフレームワークが新しく生まれたり、流行ったりします。現在のプロダクトがレガシーなプログラミング言語やフレームワークで開発されていた場合、新しい機能は別のプログラミング言語・フレームワークで開発しようという話が上がることもあると思います。そういった状況になった場合、技術的な観点以外で必要になりそうな情報は下記の通りです。 採用計画 既存メンバーの学習コスト 既存システムの移管プロジェクト エンジニアのモチベーション 仮にテクノロジー領域の知識だけで意思決定を行った場合、採用計画への影響や既存システムをどうするのかのプロジェクト観点が抜け落ち、別の課題が新たに生まれることは間違いないでしょう。テクノロジー領域の情報だけで決めるのではなく、採用計画を含めた長期の視点も含めて決めていく必要がある領域であると思います。 まとめ この記事を通して自分が伝えたかったことは下記になります。 プロダクト開発において、より不確実性の大きい技術選定をするためには、テクノロジー領域の知識だけでは足りない テクノロジー領域の知識をつけることだけがエンジニアの成長ではない。プロダクト、プロジェクト、ピープル領域のマネジメントスキルを身につければ、より不確実性の大きい領域に対して技術選定ができるようになる エンジニアが各領域のマネジメントを行う経験は、エンジニアとしての成長にとっても大きな要素になる エンジニアでありながら、プロダクトマネジメント、プロジェクトマネジメント、ピープルマネジメントの役割を担っている方がいると思いますが、 コードを書かないことを不安に思う必要は全くない と思います。プロダクト開発において、エンジニアがやるべきことの中で「コードを書く」ということの割合は少しずつ減っていきます。今自分がやるべきことをやり、物事を成し遂げて行くことが重要であり、その結果プロダクトが成長していけば、自ずと自身もエンジニアとして成長するはずだと思います。 さいごに また、dely ではエンジニアを絶賛募集中です! ご興味ある方はこちらのリンクからお気軽にエントリーください! join-us.dely.jp さらに、定期的に TechTalk というイベントを通じて、クラシルで利用している技術や開発手法、組織に関する情報も発信しております。 ノウハウの共有だけでなく、クラシルで働くエンジニアがどんな想いを持って働いているのかや、働く人の雰囲気を感じていただけるイベントになっていますので、ぜひお気軽にご参加ください! bethesun.connpass.com
アバター
はじめまして、dely開発部の funzin です。普段はクラシルのiOSアプリ開発を担当しています。 この記事は「dely #1 Advent Calendar 2020」の13日目の記事です。 adventar.org adventar.org 昨日はbababachiさんの コンテナサポートされたLambdaで湯婆婆実装してみた という記事でした。 Lambdaによる湯婆婆実装が丁寧に説明されているので、気になる方はぜひみてみてください! さっそく本題ですが、この記事ではCarthageで生成したframeworkの管理でRomeを導入したことについてまとめていきます。 Romeとは Rome は、Carthageで生成したframeworkを様々なストレージで管理することを可能にしてくれるツールです。保存先はローカル、AWSのS3などが指定可能です。 なぜ導入したか 元々クラシルでは、 carthage_cache を使ってCarthageで生成したframeworkをS3に保存していました。また、自前のfastlane actionでライブラリのアップデートも自動化をしていました。 しかし、入社時のキャッチアップとしてライブラリ周りについて確認してみると以下のような課題がでてきました。 carthage_cache自体が3年ほど前からメンテナンスがされていない carthage_cacheを利用した自前のライブラリアップデートも1~2年ほどメンテナンスがされていない状態、ライブラリの自動アップデートも毎週CIでfailしていた 自前の自動アップデートが動いていなかったため、必要なタイミングでライブラリを手動アップデートして、carthage_cacheを通してアップロードしていた 利用しているcarthage_cacheが長年メンテナンスされていない、かつ社内でも自動化まわりのメンテナンスをする人がいないという状態の中で、このまま利用するべきなのかという議題がチーム内であがりました。このタイミングでcocoapods-binaryやSwiftPackageMangerに移行をすることも考えましたが、現状の開発フローで低コストに置き換えが可能であったのが今回紹介する Rome でした。 置き換え時の決め手となったのは以下の観点です。 carthage_cacheで元々利用していたS3を保存先として利用可能 carthage_cacheの利用箇所をRomeに置き換えるのみで対応可能(元々動かなくなっていた自動アップデートActionも含め) 導入手順 Romeの導入手順は README にまとまっているため、こちらを参照してください。 その中でも特徴的な箇所を抜粋して説明していきます。 Romefile Romeを利用する上でconfigファイルとして、Romefileを定義します。 以下のようなCartfileがある場合、Romefileは次のようになります。 Cartfile github "ReactiveX/RxSwift" github "ashleymills/Reachability.swift" github "realm/realm-cocoa" Romefile cache: s3Bucket: your-bucket-name repositoryMap: - RxSwift: - name: RxSwift - name: RxTest - name: RxAtomic - name: RxBlocking - name: RxCocoa - name: RxRelay - Reachability.swift: - name: Reachability - realm-cocoa: - name: Realm - name: RealmSwift 注意しなければいけないこととして、RxSwiftのように複数のframeworkが生成される場合やrealm-cocoaのようにCartfileに記述した名前とframework名が異なる場合は、明示的に名前を指定してあげる必要があります。 Upload ストレージにframeworkをアップロードする場合、Carthageでframeworkを生成後に rome upload を実行することで指定のストレージにアップロードされます。 $ carthage update && rome upload Download ストレージからframeworkをダウンロードする場合、Cartfile.resolvedに定義してあるバージョンで既にアップロード済みの場合、以下のコマンドからダウンロードができます。 $ rome download static/dynamic frameworkの指定が可能 Romefileにstatic/dynamicを指定することで、static/dynamic frameworkの管理することが可能です。 例えばAlamofireがstatic frameworkの場合、以下のように記述します。 repositoryMap: - Alamofire: - name: Alamofire type: static 注意点として、typeのdefault値は dynamic なため、static frameworkの場合必ずrepositoryMapに記述する必要があるためRomefileの記述量は増えます。 Swiftのバージョンごとに管理可能 Romeではオプションとして --cache-prefix が用意されています。このオプションを利用することでprefixごとにframeworkを管理することが可能になります。 以下のようにprefixを指定することでSwiftのバージョンごとに管理するが可能になります。 --cache-prefix `xcrun swift --version | head -1 | sed 's/.*\((.*)\).*/\1/' | tr -d "()" | tr " " "-"` クラシルではAWSのS3を利用しているため、実際には以下のように格納されています。 運用例 実際の運用では、Bitriseで最新のXcodeを利用するstackを指定して、 carthage update を実行後に rome upload を実行するワークフローを平日の毎朝7時に実行しています。これによってSwiftのバージョンごとにframeworkがS3に格納されているため、Xcodeのバージョンを切り替え時にスムーズに移行することができるようになります。 実際にRome運用してみて 当初検討していたcarthage_cacheからRomeへの移行は、機能開発と並行して1~2週間で終えることができました。 (9月に移行完了) 移行してすぐに、Xcode12で Carthageが動かない問題 に直面しましたが、workaroundなscript対応で現状も問題なく動いています。 また、ライブラリの更新時やXcodeのバージョンを変更時に、 rome download を実行することでバージョンに対応したframeworkがダウンロードできるため切り替えが楽になりました。 まとめ Carthageで生成したframeworkをRomeで管理するようにしました。今後もRomeでiOSのライブラリ管理を進めていくというよりは、cocoapods-binary、SwiftPackageManagerに移行するなどの検討も引き続きしていきたいと思います。 明日はall-userさんの「Vue 2で書かれた個人プロジェクトをVue 3に書き換えてみた」です。Vue 3にしてどのような気づきがあるのか楽しみですね! また、dely ではエンジニアを絶賛募集中です! ご興味あればこちらのリンクからお気軽にエントリーください! join-us.dely.jp さらに TechTalk というイベントも行っているので、dely について詳しく知りたい方は是非参加してみてください! bethesun.connpass.com
アバター
こんにちはdelyでサーバーサイドエンジニアをしているyamanoiです この記事は「dely #2 Advent Calendar 2020」の12日目の記事です。 adventar.org adventar.org 昨日は @yochidros さんの「KMMでiOS・Android
を共通化しよう」でした。 みなさんwebサイトを作成する時にSPAを利用していますか? SPAはユーザーに対してメリットが大きいですが、SEO観点やOGPタグのレンダリング等で SSRが避けられない場面に出くわすことがあると思います。 SSRが不要であればビルドして生成された成果物をs3等でホスティングするだけなのでデプロイや、運用が楽なのですが、 SSRをするとなるとNode jsの実行環境必要になります。 ある程度大きなプロジェクトであればECSやGKE, GAEに載せてガッチリと運用すべきだと思いますが、 個人開発のや検証段階のプロダクトのような規模の小さいプロジェクトに対して、自前でサーバーを用意したりECSやGKEに載せるとなると運用コストが増すので、できればサーバーレスで実行したいですよね SPAをサーバーレスで実現する方法はいくつかあり、AWS LambdaやCloud Functionsを使用するのがよくある方法かなと思います。 ここからは筆者が触ったことのあるNuxt.jsについて話していきたいと思います。 Nuxt.jsでAWS LambdaやCloud Functionsを利用したSSRを行うには以下のことを考慮する必要があります Node.jsのバージョンが対応しているものからしか選択することができない サーバー用のコードを追加で書く必要がある 追加のpackageを読み込む必要がある(aws-serverless-express) そこで Cloud Run の出番です。 Cloud Runは簡単に言うとコンテナをサーバーレスで動かすことのできるサービスになります。 Cloud Runはプラットフォームがいくつかあるのですが、普通に使用する場合はフルマネージド版を選択すれば大丈夫です。 フルマネージドでは、デプロイしたいDockerfileを用意するだけアプリケーションを構築することができます。 独自ドメインの設定もでき、httpsも証明書の管理不要で使用することができます。 またコンテナなので、Node.jsのバージョンもプラットフォームに制限されることなく柔軟に扱うことができます。 構築手順 簡単に構築手順を解説します。 *gcpプロジェクトの作成、各apiの有効化・認証、gcloudコマンドのセットアップは省略します。 1 Nuxt.jsプロジェクトの作成 Universalモードを選択してNuxtプロジェクトを作成します yarn create nuxt-app nuxt-cloud-run-sample 2 Dockerifleの作成 Nuxt.jsを実行するためのDockerfileを作成します FROM node:10 ARG asset_path ENV ASSET_PATH=$asset_path WORKDIR /src ENV PORT 8080 ENV HOST 0.0.0.0 COPY ./package.json . COPY ./yarn.lock . RUN yarn install --only=production COPY . . RUN yarn build CMD ["yarn", "start"] 3 docker imageをGCR(Google Container Registry)にpush 1で作成したDockerfileをビルドし、GCRにpushします。 docker build -t nuxt-cloud-run-sample . docker tag nuxt-cloud-run-sample gcr.io/project_id/nuxt-cloud-run-sample docker push gcr.io/project_id/nuxt-cloud-run-sample 4 デプロイ実行 gcloudコマンドでCloud Runにデプロイします gcloud run deploy nuxt-cloud-run-sample --image gcr.io/project_id/nuxt-cloud-run-sample --platform managed --region asia-northeast1 --allow-unauthenticated デプロイが完了するとターミナルにURLが出力されるので、そのURLにアクセスするとNuxt.jsのデフォルトのビューが表示されると思います。 たった4ステップ(所要時間約10分)でSSRを実現することができます。 その他 ドメイン周り 独自ドメインを設定したい場合はコンソールから設定することができます。 カスタムドメインを管理 マッピングを追加 設定するドメインは予め所有権の検証を済ませておく必要があります。 assets周り そのままでは画像や分割されたjsのようなassets類もCloud Run経由で配信されてしまいます。 assets類はCloud Runを経由する必要がないですし、余計なリソースも必要となってしまうため、Cloud Storageから取得するように変更します Cloud Storageにassets-nuxt-cloud-run-sampleという名前でバケットを作成 バケットの中身が外部からアクセスできるように公開設定をしておきます Cloud Storageにビルドしたassetsをアップロード id=$(docker create gcr.io/project_id/nuxt-cloud-run-sample:latest) docker cp $id:/src/.nuxt/dist/client ./.assets gsutil cp -r ./.assets gs://assets-nuxt-cloud-run-sample/ nuxt.config.jsを変更 ASSET_PATHという環境変数でCloud StorageのURLを受け取れるようにしておきます。 ... build: { /* ** You can extend webpack config here */ publicPath: process.env.ASSET_PATH, extend(_, __) {} } , ... 構築手順2のDockerfile内でasset_pathを受け取れるようにしてあるため、dockerrビルド時に指定することで、publicPathを切り替えます。 docker build -t nuxt-cloud-run-sample --build-arg asset_path =https://storage.googleapis.com/assets-nuxt-cloud-run-sample/.assets . CI, CD GCPではCI, CDにCloud Buildが使えます。 以下は上に書いてある構築手順を各ステップに定義したymlのサンプルになります。参考にしてみてください steps : - name : 'gcr.io/cloud-builders/docker' id : Pull Cache entrypoint : 'bash' args : - '-c' - | docker pull gcr.io/project_id/nuxt-cloud-run-sample:latest || exit 0 - name : 'gcr.io/cloud-builders/docker' id : Build App args : - 'build' - '-t' - 'gcr.io/project_id/nuxt-cloud-run-sample:$SHORT_SHA' - '-t' - 'gcr.io/project_id/nuxt-cloud-run-sample:latest' - '--cache-from' - 'gcr.io/project_id/nuxt-cloud-run-sample:latest' - '--build-arg' - 'asset_path=https://storage.googleapis.com/assets-nuxt-cloud-run-sample/.assets' - '.' - name : 'gcr.io/cloud-builders/docker' id : Push Image entrypoint : 'bash' args : - '-c' - | docker push gcr.io/project_id/nuxt-cloud-run-sample:$SHORT_SHA docker push gcr.io/project_id/nuxt-cloud-run-sample:latest - name : 'gcr.io/cloud-builders/docker' id : Copy assets entrypoint : 'bash' args : - '-c' - | id=$(docker create gcr.io/project_id/nuxt-cloud-run-sample:$SHORT_SHA) docker cp $id:/src/.nuxt/dist/client ./.assets - name : 'gcr.io/cloud-builders/gsutil' id : Upload assets args : - 'cp' - '-r' - './.assets' - 'gs://assets-nuxt-cloud-run-sample/' - name : 'gcr.io/cloud-builders/gcloud' args : - 'beta' - 'run' - 'deploy' - 'nuxt-cloud-run-sample' - '--image' - 'gcr.io/project_id/nuxt-cloud-run-sample:$SHORT_SHA' - '--region' - 'asia-northeast1' - '--platform' - 'managed' - '--allow-unauthenticated' 最後に Cloud Runは小さなプロジェクトをサクッと立ち上げたい時には重宝するサービスだと思います。 他にもPub/Subから起動することができたり、IAMでアクセス制御できたりと便利な機能が沢山あります。 これを書いている途中でAWS Lambdaでもdocker imageを用いたデプロイが可能になったため、そちらも今度試してみたいと思います。 aws.amazon.com 明日は @gomesuit さんの「技術だけではもう足りない?エンジニアとしての成長のために避けては通れない4つの領域とは!」です、お楽しみに! また、delyではエンジニア・デザイナーを絶賛募集中です。 ご興味がある方はこちらのリンクからお気軽にエントリーください! join-us.dely.jp また定期的にTechTalkというイベントも開催しているので、delyについて詳しく知りたい方は是非参加してみてください! bethesun.connpass.com
アバター
こんにちは!初めまして!delySREの中鉢です。 今年の10月にjoinしたばかりで、今は主にクラシルのインフラ基盤拡充を行っています。 本記事はdely #1 Advent Calendarの12日目の記事です。熱量が伝わる素晴らしい記事ばかりで戦々恐々ですが、がんばって書いていこうと思います。 昨日はサーバサイドエンジニアのYuji Takahashiさんの"DynamoDBでサポートされたPartiQLをRubySDKで利用する"でした。 tech.dely.jp PartiQLでSQLライクにいじれるようになって、より手軽にDynamoのデータを取れるようになりましたね。アナウンスされたばかりの機能なので、今後も注目です! delyの他の記事は以下リンクから!是非見て行って下さい。 adventar.org adventar.org さて、kurashiruのバックボーンではAWSを利用しているのですが、AWSでは現在一年で最大のカンファレンス re:Invent が絶賛開催中です。 aws.amazon.com 今年はオンライン開催、しかも無料ということでどなたでも参加できますので是非チェックしてみてください。なんと1月まで続きます! 毎年ワクワクさせられるこの"お祭り"ですが、今年もたくさんのリリースやアナウンスが行われていますね! その中でLambdaのコンテナイメージランタイムサポートがアナウンスされました。 aws.amazon.com ECRにアップロードしたコンテナイメージをそのままランタイムとして指定でき、その最大サイズはなんと10GB! 従来のzipパッケージのアップロードが最大250MBだったことを考えると、より使いやすく、できることも格段に増えた注目のアップデートだと思います! 今回はこれを使って、最近流行の湯婆婆ネタで FaaU(Function as a 湯婆婆) を実装します。語感が良いのでYじゃなくてUにしました。 記事書き終わってから気付きました 映画:千と千尋の神隠しのネタバレを含みますのでご注意を。 準備 実装 コード テスト ECRへpush Lambda関数の作成 APIGatewayと接続 動作確認(※今は止めてます) 所感 おわりに 準備 今回作るのはAWSで、Lambda, ECR, API Gatewayを利用します。dockerはローカルで使えるようにしておいてください。 コンソールからawsコマンドが叩けて、credential設定ができていれば基本的に大丈夫です。 実装 それではいざ、尋常に。 コード 今回のファイル構成はこんな感じです $ tree . ├── Dockerfile └── app.rb シンプルイズベスト。 keep it simple. それぞれのファイルを見ていきます app.rb module LambdaFunction class Handler def self . yubaba ( event :, context :) name = event[ ' name ' ] na_index = rand(name.size) na = name[na_index] message = " フン。 #{ name } というのかい。贅沢な名だねぇ。 \n 今からお前の名前は #{ na } だ。いいかい、 #{ na } だよ。分かったら返事をするんだ、 #{ na } !! " return { statusCode : 200 , body : message.to_json } end end end Dockerfile FROM public.ecr.aws/lambda/ruby:2.7 COPY app.rb ./ CMD [ "app.LambdaFunction::Handler.yubaba" ] 受け取ったnameの値からランダムに1文字摘出してメッセージを返すだけ。 ベースイメージは ECR Public gallery (こちらも今回発表されました! 詳細は こちら )から、現在提供されている AWS公式イメージ ruby2.7 を利用します。 なお、自分でビルドしたコンテナイメージを使うことも可能ですが、 RIC(AWS Lambda Runtime Interface Clients) を適用しLambdaのランタイムAPIと連携する必要があります。 公式イメージはRICが適用済みです。 テスト 今回のアナウンスでもう一つ気になっていたのが" RIE(Lambda Runtime Interface Emulator) "。 また、Lambda Runtime Interface Emulator をオープンソースとしてリリースします。これにより、コンテナイメージのローカルテストを実行して、Lambda にデプロイした際に実行されることを確認することができます。Lambda Runtime Interface Emulator は、AWS が提供するすべてのベースイメージに含まれており、任意のイメージでも使用できます。 つまりはwebサーバとして機能し、ローカルでテストができるようです。 ECR public garrayのイメージUsageにサンプルがあったのでやってみます まずはイメージのbuild $ docker build -t yubaba . [+] Building 1.5s (7/7) FINISHED => [internal] load .dockerignore 0.0s => => transferring context: 2B 0.0s => [internal] load build definition from Dockerfile 0.0s => => transferring dockerfile: 36B 0.0s => [internal] load metadata for public.ecr.aws/lambda/ruby:2.7 1.5s => [internal] load build context 0.0s => => transferring context: 481B 0.0s => CACHED [1/2] FROM public.ecr.aws/lambda/ruby:2.7@sha256:cb8c6f95a9464b07c8320985b292f4d95d14641c710f556146de6e9cc33ccc04 0.0s => [2/2] COPY app.rb ./ 0.0s => exporting to image 0.0s => => exporting layers 0.0s => => writing image sha256:740ba6c06851e1e917f1590819ae61e56c88161da5ea6377790df36476a81116 0.0s => => naming to docker.io/library/yubaba そしてrun $ docker run -p 9000:8080 yubaba:latest time="2020-12-09T11:14:20.716" level=info msg="exec '/var/runtime/bootstrap' (cwd=/var/task, handler=)" curlしてみます。参照するパスをRIEが提供してくれているようです。 $ curl -X POST "http://localhost:9000/2015-03-31/functions/function/invocations" -H "Content-Type: application/json" -d '{"name":"BETHESUN"}' {"statusCode":200,"body":"\"フン。BETHESUNというのかい。贅沢な名だねぇ。\\n今からお前の名前はBだ。いいかい、Bだよ。分かったら返事をするんだ、B!!\""}% めっちゃ簡単 これだけ手軽にテストできるのは良いですね。 なお、"BE THE SUN"はdelyのビジョンになります。下記コーポレートサイトにその想いが書かれていますので、合わせて見ていただけると嬉しいです。 ECRへpush Lambdaからコンテナを利用するにはECRにイメージをpushしておく必要があります。 リポジトリから作成していきます。リポジトリ名はfaauで。 $ REPOSITORY_NAME= faau $ aws ecr create - repository -- repository - name $ { REPOSITORY_NAME } -- region = ap - northeast -1 { " repository ": { " repositoryArn ": " arn:aws:ecr:ap-northeast-1:xxxxxxxx:repository/faau ", " registryId ": " xxxxxxxx ", " repositoryName ": " faau ", " repositoryUri ": " xxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/faau ", " createdAt ": " 2020-12-10T12:52:10+09:00 ", " imageTagMutability ": " MUTABLE ", " imageScanningConfiguration ": { " scanOnPush ": false } , " encryptionConfiguration ": { " encryptionType ": " AES256 " } } docker login $ ACCOUNT_ID=xxxxxxxx $ aws ecr get-login-password | docker login --username AWS --password-stdin https://${ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com Login Succeeded イメージのビルド $ docker build -t ${ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/${REPOSITORY_NAME}:v1.0 . [+] Building 1.3s (7/7) FINISHED => [internal] load .dockerignore 0.0s => => transferring context: 2B 0.0s => [internal] load build definition from Dockerfile 0.0s => => transferring dockerfile: 36B 0.0s => [internal] load metadata for public.ecr.aws/lambda/ruby:2.7 1.2s => [internal] load build context 0.0s => => transferring context: 28B 0.0s => [1/2] FROM public.ecr.aws/lambda/ruby:2.7@sha256:38efb961e9fab0de46a5086b03b8217e5cd955fcbf917 0.0s => CACHED [2/2] COPY app.rb ./ 0.0s => exporting to image 0.0s => => exporting layers 0.0s => => writing image sha256:0fbaa46b2e8efb3fad4ec0fa77324743cd9d0eb78b90ef9bd0fb82dee22d64ef 0.0s => => naming to 266255920091.dkr.ecr.ap-northeast-1.amazonaws.com/faau:v1.0 0.0s そして、push $ docker push ${ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/${REPOSITORY_NAME}:v1.0 The push refers to repository [xxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/faau] ff87162bdcf5: Pushed 40bab667bbe5: Pushed a76348bfadb0: Pushed d6fa53d6caa6: Pushed 0360f591b796: Pushed 6b4100115ba9: Pushed af6d16f2417e: Pushed v1.0: digest: sha256:xxxxxxxxxxxx size: 1788 確認してみます! マネジメントコンソールにログインし、サービスからECRを検索 リポジトリができています イメージも無事確認できました! Lambda関数の作成 それでは実際にLambdaから利用してみましょう。 画像を参照 -> (コンテナ)イメージを参照 ですね すんなり作成できました。早速テスト実行してみます。 テストケース 実行 問題ないですね! APIGatewayと接続 せっかくなのでAPI化します。 API Gatewayから、RESTを選択。 名前を入力して作成。 POSTメソッドを作成し、Lambda関数を選択。 テストして デプロイします。 ステージはcontract(契約書)で。申し訳程度に説明にセリフを入れておきます。 これで完了! 動作確認(※今は止めてます) では、ローカルからcurlしてみます。 $ curl -X POST "https://atb4o0qjf3.execute-api.ap-northeast-1.amazonaws.com/contract" -H "Content-Type: application/json" -d '{"name":"びーざさん"}' {"statusCode":200,"body":"\"フン。びーざさんというのかい。贅沢な名だねぇ。\\n今からお前の名前はんだ。いいかい、んだよ。分かったら返事をするんだ、ん!!\""}% ん。。。 最後とか、湯婆婆って言うよりトトロのカンタになっちゃってますね。。。 無事、APIの実装まで完了しました!今回の検証はここまでになります! 本当に手軽にできました! 所感 今回はすごく簡易なアプリケーションとはいえ、とてもスムーズに実装することができました。 従来であれば、gemやnpmなど依存パッケージはローカルインストールして、固めてs3にアップして、、と手間かかっていましたが、検証した環境をそのまま上げることができるので、余計なことを考える必要がなくて良いですね! イメージサイズが10GBまで許可されている点も大きな魅力ですね。ジョブ系アプリケーションの実行場所としては有力な選択肢になるのではないでしょうか。 もちろん、外部リソースへの接続は権限を別で用意する必要があったり、ひとつのコンテナだけなので載せる物によっては全部入り設計になったりしますが、そのような多くの場合そもそもLambdaという技術選定が違うんじゃないかとも思います。 AWSは他にも様々なコンテナプラットフォームがありますし、このような大きなアップデートがあったタイミングで見直すのもいいかもと思いました。 選択肢が増え続ける中で、用途に合ったソリューションを適切に選定していくためにもどんどん触って使い心地を確かめていきたいと思います! おわりに 明日はNakazawaさんの「Carthageで生成したframeworkの管理でRomeを導入してみた」です!楽しみにしています! さいごに、delyではエンジニアを絶賛大募集です! BE THE SUNをビジョンに掲げ、プロダクトに情熱を注いでみませんか? 採用ページはこちら! join-us.dely.jp こちらはコーポレートサイトです。 www.dely.jp また、「クラシル Tech Talk」などのイベントも多数行っています。 エントリー前に開発部の様子を知りたいという方はぜひ覗いてみてください。ご応募をお待ちしております! bethesun.connpass.com それでは!
アバター
こんにちは。開発部の高橋です。 本記事はdely #1 Advent Calendarの11日目の記事です。 adventar.org dely #2もあるのでこちらもどうぞ。 adventar.org 昨日はうっくんさんの「UIデザイナーがSwiftを学んでUIを実装したら生産性が爆上がりした」でした。 note.com 先月末、DynamoDBがSQL互換言語であるPartiQLに対応しました。 aws.amazon.com PartiQLとはSQL互換のクエリ言語で、PartiQLから出力される中間表現を各サービスが対応することによって様々なサービスがSQLライクに操作できるようになります。 aws.amazon.com 今回の対応で、DynamoDBのGetItemやPutItemといった操作をSQLライクに実行できるようになりました。 また、それに合わせてRubySDKの方でも早速APIが追加されています。(2020/12/07時点でリリースはまだされてなさそうです) github.com 今回はRubySDKを利用しながら、DynamoDBのPartiQL対応を眺めていこうと思います。 準備 必要な機能一覧 スキーマ テーブル Partition Key, Sort Keyに関して 使ってみる 特定ユーザーの情報を取得 特定レシピの情報を取得 複数のレシピ情報を取得 ユーザーがレシピをお気に入りに追加する ユーザーがレシピのお気に入りを解除する 複数レシピを一度にお気に入り追加・解除する ユーザーのお気に入りレシピ一覧を取得 レシピのお気に入り数の取得 所感 最後に 準備 ただただサンプルを実行するだけだとつまらないので、(無理矢理ではありますが)今回はそれっぽい機能を実現するための手段としてDynamoDBをPartiQLで操作する形にしました。 今回はレシピサービスを題材にして、ユーザー、レシピの取得、お気に入りといった実装をPartiQLを通して行おうと思います。 必要な機能一覧 今回は以下のような機能をPartiQLで実装してみようと思います。 特定ユーザーの情報を取得 特定レシピの情報を取得 複数レシピの情報を取得 ユーザーがレシピをお気に入りに追加する ユーザーがレシピのお気に入りを解除する 複数レシピを一度にお気に入り追加・解除する ユーザーのお気に入りレシピ一覧を取得 レシピのお気に入り数の取得 スキーマ 今回は単一のDymamoDBテーブルで全てのデータを入れる形にします。 設計するにあたっては以下のドキュメントを参考にし、1テーブルで多対多になるよう設計してみました。 多対多の関係を管理するためのベストプラクティス - Amazon DynamoDB テーブル 今回は以下のようなPartition Key, Sort Keyの構成にします。 名前 説明 pk String(Partition Key) sk String(Sort Key) 今回は1つの属性が様々な役割を持ちうるため、名前もあえて役割を特定しないような名前にしてます。 また、逆引きも行いたいためGSIも設定しておきます。 名前 説明 sk String(Partition Key) pk String(Sort Key) Partition Key, Sort Keyに関して 今回は一つのDynamoDBテーブルで多対多構造を実現するため、項目毎に接頭辞を定義して付与します。 項目 接頭辞 ユーザー users# レシピ recipes# ユーザーのレシピに対するお気に入り favorites#recipes# ここまでの設計を元にデータとして落とし込むと、例えば以下のようになります。 pk sk created_at user_name recipe_title users#1 users#1 1600000000 No.1 null users#2 users#2 1600000000 No.2 null recipes#1 recipes#1 1600000000 null 小松菜と豚肉の卵炒め recipes#2 recipes#2 1600000000 null 照り焼きチキン users#1 favorites#recipes#1 1600000000 null null users#2 favorites#recipes#2 1600000000 null null 使ってみる DynamoDBでPartiQLが使えるようになる修正は2020/12/07時点ではリリースされてため、パス指定で直接使うことにします。 Rubyのバージョンは2.7.1を使ってます。 # Gemfile source " https://rubygems.org " gem " aws-sdk-dynamodb " , path : " aws-sdk-ruby/gems/aws-sdk-dynamodb 以下を実行して依存解決し準備完了です。 $ git clone https://github.com/aws/aws-sdk-ruby.git $ bundle install これから実行するコードは以下を読み込んだ上での実行結果になります。 require ' aws-sdk-dynamodb ' CLIENT = Aws :: DynamoDB :: Client .new TABLE_NAME = " partiql_sample " .freeze クエリを書くにあたっては以下の公式ドキュメントを参考にしました。 PartiQL - A SQL-Compatible Query Language for Amazon DynamoDB - Amazon DynamoDB 特定ユーザーの情報を取得 まずは単一リソースの取得です。 単一のSELECT, INSERTなどの操作は ExecuteStatement で行えます。 RubyのDynamoDBクライアントからは execute_statement メソッドからAPIを呼び出せるためこちらからSELECT文を実行します。 ただ、LIMIT句は使えないためRuby側で1つに絞ります。 pp CLIENT .execute_statement( statement : " SELECT * FROM #{ TABLE_NAME } WHERE pk='users#5' AND sk='users#5' " ).items.first { " sk " => " users#5 " , " created_at " => 0.160429738e10 , " pk " => " users#5 " , " user_name " => " No.4 " } 特定レシピの情報を取得 こちらもユーザーと同様にSELECTで取得できることが確認できました。 pp CLIENT .execute_statement( statement : " SELECT * FROM #{ TABLE_NAME } WHERE pk='recipes#10' AND sk='recipes#10' " ).items.first { " sk " => " recipes#10 " , " recipe_title " => " 小松菜と豚肉の卵炒め " , " created_at " => " 1601273380 " , " pk " => " recipes#10 " } 複数のレシピ情報を取得 今度はPartition Keyを元に複数のレシピを取得してみましょう。 BatchExecuteStatement で複数のクエリを一度に取得できるため今回はこちらを利用します。 元々DynamodBには BatchGetItem と BatchWriteItem というAPIが提供されており、ReadとWriteで使い分ける必要がありました。 一方でPartiQLから利用する場合はどちらも BatchExecuteStatement から行います。 ※ただし、一回のリクエストでReadとWriteを混ぜて実行することはできません。 CLIENT .batch_execute_statement( statements : [ { statement : " SELECT * FROM #{ TABLE_NAME } WHERE pk = 'recipes#1' AND sk = 'recipes#1' " }, { statement : " SELECT * FROM #{ TABLE_NAME } WHERE pk = 'recipes#2' AND sk = 'recipes#2' " }, { statement : " SELECT * FROM #{ TABLE_NAME } WHERE pk = 'recipes#3' AND sk = 'recipes#3' " }, ]) #<struct Aws::DynamoDB::Types::BatchExecuteStatementOutput responses= [ #<struct Aws::DynamoDB::Types::BatchStatementResponse error= nil , table_name= " partiql_sample " , item= { " sk " => " recipes#1 " , " recipe_title " => " お酒にピッタリ しいたけと玉ねぎの中華風ポン酢和え " , " created_at " => 0.160205098e10 , " pk " => " recipes#1 " , " user_name " => nil }>, #<struct Aws::DynamoDB::Types::BatchStatementResponse error= nil , table_name= " partiql_sample " , item= { " sk " => " recipes#2 " , " recipe_title " => " ジェノバソースの冷製パスタ " , " created_at " => 0.160196458e10 , " pk " => " recipes#2 " , " user_name " => nil }>, #<struct Aws::DynamoDB::Types::BatchStatementResponse error= nil , table_name= " partiql_sample " , item= { " sk " => " recipes#3 " , " recipe_title " => " たっぷり胡麻風味の無限キャベツ " , " created_at " => 0.160187818e10 , " pk " => " recipes#3 " , " user_name " => nil }>]> なお、 BatchGetItem , BatchWriteItem では処理が失敗すると失敗した要素がレスポンスの UnprocessedKeys や UnprocessedItems 属性として返却され、それを元に使う側が適宜リトライをすることができました。 ただ、 BatchExecuteStatement に関しては、実装を見る限りはそのようなレスポンスは現状生えてなさそうなので、エラーがあるかどうかを元にリトライするなど工夫する必要がありそうです。 BatchGetItemのレスポンス github.com BatchExecuteStatementのレスポンス github.com ユーザーがレシピをお気に入りに追加する 今度はINSERT文を実行してレシピをお気に入りに追加してみます。 一点注意したいのが、MySQLといったRDBだと INSERT INTO tbl (column_a, columb_b) VALUES ( xxx , yyy) のような形式ですが、PartiQLの場合は INSERT INTO tbl VALUE { ' column_a ' : xxx , ' column_b ' : yyy } のような形式になります。 ではお気に入りに追加してみます。 CLIENT .execute_statement( statement : " INSERT INTO #{ TABLE_NAME } VALUE {'pk' : 'users#1', 'sk' : 'favorites#recipes#10', 'created_at' : #{ Time .now.to_i } } " ) => #<struct Aws::DynamoDB::Types::ExecuteStatementOutput items=[], next_token=nil> 念の為正しく追加されてることをSELECT文を発行して確認しておきます。 CLIENT .execute_statement( statement : " SELECT * FROM #{ TABLE_NAME } WHERE pk = 'users#1' AND sk = 'favorites#recipes#10' " ) => #<struct Aws::DynamoDB::Types::ExecuteStatementOutput items=[{"pk"=>"users#1", "sk"=>"favorites#recipes#10", "created_at"=>0.160739201e10}], next_token=nil> ユーザーがレシピのお気に入りを解除する DELETE文もサポートされているためこちらで物理削除してみます。 CLIENT .execute_statement( statement : " DELETE FROM #{ TABLE_NAME } WHERE pk = 'users#1' AND sk = 'favorites#recipes#10' " ) => #<struct Aws::DynamoDB::Types::ExecuteStatementOutput items=[], next_token=nil> 今度は消えてることがSELECTで確認できました。 CLIENT .execute_statement( statement : " SELECT * FROM #{ TABLE_NAME } WHERE pk = 'users#1' AND sk = 'favorites#recipes#10' " ) => #<struct Aws::DynamoDB::Types::ExecuteStatementOutput items=[], next_token=nil> 複数レシピを一度にお気に入り追加・解除する 現状INSERTは複数のレコード書き込みには対応してないので、複数レコードに対する追加・更新は BatchExecuteStatement か ExecuteTransaction を利用する必要があります。 今回は ExecuteTransaction を利用して、複数レシピに対するお気に入り追加・お気に入り解除を行ってみます。 まずは複数レシピのお気に入り追加です。 ExecuteTransaction の引数に複数のINSERTを含めます。 CLIENT .execute_transaction( transact_statements : [ { statement : " INSERT INTO #{ TABLE_NAME } VALUE { 'pk' : 'users#1', 'sk' : 'favorites#recipes#1' } " }, { statement : " INSERT INTO #{ TABLE_NAME } VALUE { 'pk' : 'users#1', 'sk' : 'favorites#recipes#2' } " } ]) => #<struct Aws::DynamoDB::Types::ExecuteTransactionOutput responses=[]> SELECTでIN句を指定して取得することで、両方書き込まれていることが確認できました。 CLIENT .execute_statement( statement : " SELECT * FROM #{ TABLE_NAME } WHERE pk = 'users#1' AND sk IN ['favorites#recipes#1', 'favorites#recipes#2'] " ) => #<struct Aws::DynamoDB::Types::ExecuteStatementOutput items=[{"pk"=>"users#1", "sk"=>"favorites#recipes#1"}, {"pk"=>"users#1", "sk"=>"favorites#recipes#2"}], next_token=nil> 今度は複数お気に入り解除です。引数に複数のDELETEを含めます。 CLIENT .execute_transaction( transact_statements : [ { statement : " DELETE FROM #{ TABLE_NAME } WHERE pk = 'users#1' AND sk = 'favorites#recipes#1' " }, { statement : " DELETE FROM #{ TABLE_NAME } WHERE pk = 'users#1' AND sk = 'favorites#recipes#2' " }, ]) こちらも解除できていることが確認できました。 CLIENT .execute_statement( statement : " SELECT * FROM #{ TABLE_NAME } WHERE pk = 'users#1' AND sk IN ['favorites#recipes#1', 'favorites#recipes#2'] " ) => #<struct Aws::DynamoDB::Types::ExecuteStatementOutput items=[], next_token=nil> ユーザーのお気に入りレシピ一覧を取得 組み込み関数に BEGINS_WITH があるのでそれを元にソートキーでfavoritesが含まれるものを絞り込んで見ます。 pp res = CLIENT .execute_statement( statement : " SELECT * FROM #{ TABLE_NAME } WHERE pk = 'users#1' AND BEGINS_WITH(sk, 'favorites') " ) #<struct Aws::DynamoDB::Types::ExecuteStatementOutput items= [{ " sk " => " favorites#recipes#16 " , " recipe_title " => nil , " created_at " => 0.160723498e10 , " pk " => " users#1 " , " user_name " => nil }, { " sk " => " favorites#recipes#17 " , " recipe_title " => nil , " created_at " => 0.160706218e10 , " pk " => " users#1 " , " user_name " => nil }, ... 上記で取得したレスポンスを元にレシピIDを取得し、レシピ情報を取得します。 recipe_ids = res.items.map { _1[ ' sk ' ].delete_prefix( " favorites# " ) } pp CLIENT .execute_statement( statement : " SELECT * FROM #{ TABLE_NAME } WHERE pk IN #{' [ ' + recipe_ids.map { " ' #{ _1 } ' " }.join( ' , ' ) + ' ] '}" ).items.map { _1[ ' recipe_title ' ] } [ " 葉にんにくと牡蠣のバターソテー " , " しめじとスナップえんどうの簡単和風パスタ " , " ピンク色!桜の花の塩漬けで混ぜご飯 " , " 大根の中華風浅漬け " , " めんつゆで簡単かつ煮 " , " 油揚げの明太玉ねぎ包み " , " ちくわと半熟卵の天ぷらの節約天丼 " , " ゆず香る 豚バラとカブのレンジ蒸し " , " フレッシュトマトソースのガーリックバターチキンソテー " , " 皮から作る 豚こまおやき " ] IN句の中身に関しては [\"a\", \"b\"] のようなエスケープを含む形だと上手くいかないため、不格好ですが自前で加工してます。 レシピのお気に入り数の取得 MySQLなどのRDBであれば条件に当てはまるレコード数は COUNT で取得できますが、現状そのような関数はないため、今回はRuby側で数えます。 CLIENT .execute_statement( statement : " SELECT * FROM #{ TABLE_NAME } WHERE sk = 'favorites#recipes#25' AND BEGINS_WITH(pk, 'users') " ).items.size => 2 なお、今回GSIを貼ってますが、この場合正しくインデックスを使ってくれるかはよくわかってません。 (Query APIで同様の処理を行う場合は利用するインデックスを自分で指定する必要があるため、インデックスを指定しない今回の場合はフルスキャンになってる...?) ちなみに今回の処理をQuery APIで行う場合は以下のようになります。 # Queryの場合 res = CLIENT .query( table_name : TABLE_NAME , index_name : ' inverse ' , select : " COUNT " , expression_attribute_names : { ' #pk ' : ' pk ' , ' #sk ' : ' sk ' }, expression_attribute_values : { ' :sk ' : ' favorites#recipes#25 ' , ' :pk ' : ' users ' }, key_condition_expression : " #sk = :sk AND begins_with(#pk, :pk) " ) res.count => 2 所感 今回は無理やりではありますがそれっぽいユースケースに沿う形で試してみました。 自分で試してみた感想としては、DynamoDBのAPIのパラメータは色々あって中々覚えるのが大変なのでSQLで簡潔に書けるというのはよいと思いました。 AWS CLIでGetItemなどのクエリを組み立てるのは面倒なので、サクッとデータを確認する用途として使うには非常に便利そうです。 一方で、現段階においてはまだ機能が足りなかったりドキュメントが少なかったりするなど詰まりポイントが多い印象も受けましたが、まだまだ出たばかりなので今後どうなっていくか楽しみです。 これからDynamoDBのPartiQL周りがどう進化していくのか引き続きウォッチしていきたいと思います。 最後に 明日はbababachiさんの「コンテナイメージ対応したLambdaで湯婆婆してみる」です。ぜひ御覧ください! またdelyではエンジニアを絶賛大募集中です! 興味があればぜひ以下からエントリーください! join-us.dely.jp エントリー前に開発部の様子を知りたいという方は「クラシル Tech Talk」などのイベントを定期的に行っているのでこちらを覗いてみるのがおすすめです! bethesun.connpass.com
アバター
こんにちは! dely開発部でiOSエンジニアをしている @yochidros です。 この記事は「dely #2 Advent Calendar 2020」の11日目の記事です。 adventar.org adventar.org 昨日は @_kobuuukata さんの 開発者向けのオンラインイベントを開催してわかった7つのポイント でした 普段,業務上はiOSアプリをゴリゴリ開発していますが、過去にAndroid・iOS両方を開発してたこともあり 一度に両OSを開発できたら良いと思っていました。 Flutter や React Native などマルチプラットフォーム開発できるツールがありますがそれぞれで メリット・デメリットがありなかなか導入に至るまでにはいかないケースがあると思います。 そこで今回は2020年の9月についにalpha版になったKMMについて調べてみました。 KMMとは? Kotlin Multiplatform Mobileの略でiOS・Androidで共通のビジネスロジックを一つのコードで実現できるSDKです。 Kotlinで記述したものが各プラットフォームにネイティブなバイナリコードに変換されるので他のライブラリとも併用できます。 詳しくは公式サイトにも紹介があります。 blog.jetbrains.com KMPとKMMの違いは? Kotlin Multi PlatformとKotlin Multiplatform Mobileの違いは簡単に言えばモバイルに特化しているという点です。 KMPはかくデスクトップのOSにコンパイルされるのに対してKMMはモバイルのOSにコンパイルすることができます。 KMPでもiOS/Androidに変換することができますがモバイルだけならKMMを利用すれば良いということになります。   使ってみよう 説明はさておき、実際に触ってみましょう。 実際に動かしたコードはこちらにあります。 github.com ※ 以下、こちらの環境で動作を確認してます Android Studio 4.2 Preview Kotlin 1.4.20-release-Studio4.2-1 Xcode 12.2 新規プロジェクトを作成する前にプラグインを導入します。 Android Studioで Preference -> Plugins で kotlin multiplatform と調べるとKMMのプラグインがでてきますのでインストールします。 インストールしたらAndroid Studioを再起動しましょう。 再起動が完了したら新規プロジェクトのテンプレートに KMM Application が増えているので選択します。 KMMプラグインを追加する プロジェクトの名前を決めます。 今回はサンプルなので共通コードは Shared としています。 プロジェクトの名前を決める FINISH を押すと共通コードのmoduleと各OSのプロジェクトが作成されます。 iOS・Android・共通ロジックが生成される 早速各OSで動かしてみましょう。 動かしてみた ビルドが通って Hello, #{OSのバージョン} が表示されました! ※注意 エミュレーター・シュミレーターでの動作は確認できましたがiOSの実機での動作確認では注意が必要です。 iOSの場合はDeviceの選択がAndroidと同様に設定ができないです。 Configuration で Edit Configuration を選択します。 iOSのconfigを選択して Execution Target を確認したい実機を選択します。 これで動くかと思いきやiOSだと開発者の証明書がないと実機に入れることができません。 証明書を設定せず動かそうとするとエラーが起きます。 error: Signing for "KmmiOSApp" requires a development team. Select a development team in the Signing & Capabilities editor. (in target 'KmmiOSApp' from project 'KmmiOSApp') 証明書の設定はXcodeでiOSのプロジェクトを開いて設定します。(サンプルでは KmmiOSApp.xcodeproj ) 証明書の設定には Apple Developerアカウントが必要なのでない人は作っておきましょう。 developer.apple.com [iOS]証明書を設定する これで再度Android Studioでビルド&Runすると実機のiOSで動作が確認できます。 もし以下のようなエラーが起きた時は Clean Project をしてからビルドしてみましょう。 error: Building for iOS, but the linked and embedded framework 'Shared.framework' was built for iOS Simulator. (in target 'KmmiOSApp' from project 'KmmiOSApp') 通信処理を共通化してみる 両OSとも動作確認ができたので次に共通の処理を書いていきます。 共通化できる部分は - 通信処理 - ログ送信 - etc.. が挙げられると思います。 その中で通信部分のところを共通化してみましょう。 今回は例として GithubのREST API を使って 自分のレポジトリを取得するAPIクライアントを書いてみます。 まず、通信やJSONパーサーのライブラリを導入します。 Shared/build.gradle.kts val commonMain by getting { dependencies { implementation( "io.ktor:ktor-client-core:1.4.1" ) implementation( "org.jetbrains.kotlinx:kotlinx-serialization-json:1.0.1" ) implementation( "io.ktor:ktor-client-json:1.4.1" ) implementation( "io.ktor:ktor-client-serialization:1.4.1" ) implementation( "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.1-native-mt" ) { version { strictly( "1.4.1-native-mt" ) } } } } ... val androidMain by getting { dependencies { implementation( "io.ktor:ktor-client-android:1.4.1" ) implementation( "com.google.android.material:material:1.2.1" ) } } ... val iosMain by getting { dependencies { implementation( "io.ktor:ktor-client-ios:1.4.1" ) } } 導入が完了したらコードを書いていきます。 最初に共通化したい処理を宣言します。 Shared/../commonMain/../GithubAPIClient.kt expect class GithubAPIClient() { val client: HttpClient val dispatcher: CoroutineDispatcher } あくまでこれは抽象的に宣言しているので実際に各プラットフォームに適したコードを定義します。 Shared/../androidMain/../GithubAPIClient.kt actual class GithubAPIClient actual constructor () { actual val client: HttpClient = HttpClient(Android) { install(JsonFeature) { serializer = KotlinxSerializer(json = kotlinx.serialization.json.Json { isLenient = false ignoreUnknownKeys = true allowSpecialFloatingPointValues = true useArrayPolymorphism = false }) } } actual val dispatcher: CoroutineDispatcher = Dispatchers.Default } Shared/../iosMain/../GithubAPIClient.kt actual class GithubAPIClient actual constructor () { actual val client: HttpClient = HttpClient(Ios) { install(JsonFeature) { serializer = KotlinxSerializer(json = kotlinx.serialization.json.Json { isLenient = false ignoreUnknownKeys = true allowSpecialFloatingPointValues = true useArrayPolymorphism = false }) } } actual val dispatcher: CoroutineDispatcher = Dispacher(dispatch_get_main_queue()) } iOSの場合はそのままではコルーチンが使えないので以下のように dispatch_queue を使って対応します。 class Dispacher( private val dispatchQueue: dispatch_queue_t) : CoroutineDispatcher() { override fun dispatch(context: CoroutineContext, block: Runnable) { dispatch_async(dispatchQueue) { block.run() } } } あとは実際に通信する処理を書いていきます。 レスポンスはレポジトリ名とそのリンクだけを定義してます。 Shared/../commonMain/../GithubAPI.kt @Serializable data class Repository( val name: String, @SerialName ( "html_url" ) val url: String, ) class GithubAPI { val apiClient: GithubAPIClient = GithubAPIClient() companion object { val BASEURL = "https://api.github.com/users/ ${ 自分のgithubのユーザーID } /repos" } fun fetchRepos(callback: (List<Repository>) -> Unit ) { GlobalScope.apply { launch(apiClient.dispatcher) { val result = apiClient.client. get <List<Repository>>(BASEURL) callback(result) } } } } これで共通部分の実装が完了しました。さくっとできましたね! 各OS毎でビルドをするとアプリから利用できるようになります。 iOS import UIKit import Shared class ViewController : UIViewController { @IBOutlet weak var textView : UITextView ! override func viewDidLoad () { super .viewDidLoad() GithubAPI().fetchRepos { (repos) in DispatchQueue.main.async { self .textView.text = repos.map { $0 .name }.joined(separator : "\n" ) } } } } Android import com.yochidros.kmmsample.Shared.GithubAPI class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super .onCreate(savedInstanceState) setContentView(R.layout.activity_main) GithubAPI().fetchRepos { runOnUiThread { val tv: TextView = findViewById(R.id.text_view) tv.text = it.map { it.name }.joinToString( " \n " ) } } } } 動かしてみると実際に画面に自分のレポジトリの名前がでていますね! 実際に自分のgithubのレポジトリを取得した時の画像 最後に いかがでしたでしょうか? KMMを使って共通のロジックとなりうる部分を一つのコードで処理することができたと思います。 iOSの場合は CocoaPods にも対応しているので 共通ロジックとアプリケーションでレポジトリを分けて開発することもできると思います。 まだ、Alpha版なのでいきなり導入することはできないかと思いますが、少しでも共通できるものは共通化しておけば 相互の仕様の齟齬が少なくなると思います。 明日は @yyamanoi1222 の Cloud Runで手軽にサーバーレス・SSR(サーバーサイドレンダリング) です!お楽しみに! また、delyではエンジニアを絶賛募集中です!ご興味があればこちらのリンクからお気軽にエントリーください! join-us.dely.jp さらに定期的にTechTalkというイベントも開催しているので、delyについて詳しく知りたい方は是非参加してみてください! bethesun.connpass.com
アバター
こんにちは!dely 開発部でクラシルのサーバーサイドエンジニアをやっています @_kobuuukata です!👩🏻‍💻 この記事は「dely #2 Advent Calendar 2020」の10日目の記事です。 adventar.org adventar.org 昨日は @tsubotax さんの 開発体制をSquad化してきてわかってきたコツと課題 という記事でした! dely では10月からオンラインイベントをやるようになったのですが、それまではオフラインイベントを何度かやったことがあるという程度で、オンラインイベントは全くの未経験でした。 まだまだ改善点はたくさんありますが、約3ヶ月間オンラインイベントを運営してわかった 「事前準備」「イベント当日」「イベント後」 それぞれで大事なポイントについて本記事では書いていきます。 そもそも、なぜオンラインイベントを始めたのか? dely では以前より次のような課題を抱えていました・・ エンジニアが足りない クラシルは認知されているけど、dely はあまり認知されてない dely をもっと多くのエンジニアに認知してもらい、ファンになってもらうべく、情報発信をしていこう!ということで、10月よりオンラインイベントを開催することになりました。 ▲採用チームのロードマップ ちなみにオンラインイベント開始前の connpass メンバーは 64人 でした・・震えるw 事前準備 1)配信方法を決めよう! オンラインイベントなので、配信をどうやって行うかを検討する必要があります。Zoom や Youtube Live など色々な配信方法があるかと思いますが、まずはお手軽に!ということで、 Zoom のウェビナー機能 を利用した配信で行うことにしました。 ▼ Zoom のウェビナー機能を利用するメリット ・ 参加者が視聴専用モードで参加できる(音声やビデオがオンにならないので心理的障壁が低い、事故らない) ・ 参加者リストをホストとパネリストのみが閲覧可能(匿名性) ・ Q&Aとチャットの利用、管理ができる ・ Q&Aは匿名でも投稿できるため心理安全性が高い マイクに関しては、当初普段会議で使用している会議用マイクを使用していましたが、パネルディスカッションのような複数人が同時にしゃべる可能性があるイベントではあまり向きませんでした。 やはり、オンラインイベントでは音声が命になるので、ピンマイクやミキサーの購入をおすすめします! ちなみに弊社で利用しているピンマイクとミキサーはこちらです↓ www.amazon.co.jp www.amazon.co.jp 2)イベントを作成しよう! 配信方法が決まったら、イベント内容を決めていきます。いくつかイベントを開催してみて、気をつけることがあったので紹介します! ・ テーマは、参加者が「自分ごと化しやすい」ものを選ぶ テーマを設定する際は 「誰に向けたものなのか」「何を得られるか」 を明確にすることが大事です!参加者に自分ごと化しやすい内容でないと興味を持ってもらえません。これまでのイベントでもしっかりテーマを決め、参加者が「自分ごと化」しやすいテーマにはたくさん申し込みが来ました! 3)事前にリハーサルしておこう! イベントにトラブルは付きものです。オンラインイベントに慣れていない場合は特に本番を想定して、以下の点は事前にチェックしておきましょう! 実際に事前準備不足により、当日「音声が聞き取りづらい・・」「話の抽象度が高すぎでわかりずらかった・・」などの声をいただいてしまいました、、 ▼ オンラインイベントで事前にチェックするポイント ・音声は問題ないか ・スライドは画面共有できるか ・コメントなどを確認できるモニターなどは確保できているか ・イベント当日の流れを関係者に共有できているか ・運営メンバーの当日の役割は決まっているか 4)connpass のグループメッセージを活用しよう! イベント公開後、なかなか人が集まらないな・・というときには connpass のグループメッセージを活用しましょう。これを使うと、connpass のグループメンバーにメッセージ送信を行うことができます。 実際、イベント集客に伸び悩みグループメッセージを送信したところ、10 名増えたというイベントもありました! グループメッセージはこちらから送信可能です↓ ▲グループメッセージの送信 イベント当日 5)コメントやQ&Aを盛り上げよう! Zoom のウェビナー機能は、気軽に参加してもらうことができるものの、参加者の顔が映らないので運営側からは参加者の反応がわかりずらいというデメリットもあります。参加者の反応を得るためには、コメントや Q&A にたくさん書き込んでもらう雰囲気が大切です! ・イベント開始時に参加者にコメントしてもらう まず、イベントを開始したら、冒頭で参加者にコメントしてもらうように促します。例えば、朝ごはん何食べた?とか内容は何でもOK!参加者に気軽にコメントしてもらえる質問にしましょう。 ・社内メンバーにも参加してもらう 「気軽にコメントしてね!」と言ってもなかなかしてもらえないのも現状・・。そんなときは、社内メンバーにも協力してもらってコメント欄を盛り上げてもらいましょう!ただし、そのとき気をつけなければいけないのが、 身内感を出しすぎないこと です。 身内感を感じてしまうと参加者は蚊帳の外のような印象を持ってしまいかねませんので、注意が必要です。 6)イベントアンケートに答えてもらおう! イベントの PDCA を回していくためには、参加者の意見がとても大事ですよね!オンラインイベントだと、任意のタイミングで退出可能なため、アンケートの存在を知らぬまま・・なんてことも、、 これまで何回かイベントを行ってきましたが、アンケートを集めるのにまだまだ苦労しているのが現状です。 ・ アンケートに答えやすいよう、コメント欄に URL を貼ったり、QR コードも用意する イベント終了前にアンケートに答えてもらえるよう、コメント欄にアンケート URL を貼ったり、スライドに QR コードを映しておきます。こうすることで、PC でもスマホでもアンケートに答えてもらえるようにしておきます。 ▲アンケート回答スライド ・ イベント終了直後にも、connpass のメッセージ機能でアンケートURLを送信する 万が一、最後まで参加してもらえなくてもアンケートに答えてもらうきっかけになるので、イベント終了後すぐにアンケート URL を送信しましょう! イベント後 7)イベントの振り返りをしよう! アンケートで参加者からの声を聞くことも大事ですが、自分たちでやっていて良かったこと・気になったことなど振り返って次回のイベントに活かしていきましょう! 弊社ではいつもイベント終了後に少し時間を取って振り返りを行うようにしています。また、登壇者として参加してくれた社内メンバーからも気づいた点を共有してもらっています。 ▲実際のイベント振り返りKPT 最後に・・ オンラインイベントを実施するにあたって大事な7つのポイントについて紹介しました。これからオンラインイベントを初めようと思っている方や企業の方、オンラインイベントの運営に困っているという方の参考になれば幸いです! そして・・おかげさまで現在 connpass メンバーは 300人 を超えました!!👏👏(KPIの目標値にはまだ程遠いですが・・) dely のオンラインイベントに参加してみたいぞ!と思ってくださった方は是非こちらからお申し込みください🙌 bethesun.connpass.com また、エンジニア・デザイナーの採用積極的に行っています!ご興味ある方はこちらのリンクからお気軽にエントリーしてくださいね! join-us.dely.jp 明日は @ych_dp さんの記事です!お楽しみに〜〜🎅🎄
アバター
こんにちは、dely開発部のnozaです。 今年の7月に入社しました、よろしくお願いします🙋‍♂️ クラシルのエンジニアを担当しつつ、レシピ検索の機能改善を活動内容とするチーム(以降、「検索チーム」と書きます)でPdMをしています。 この記事は「dely #1 Advent Calendar 2020」の9日目の記事です。 「dely #1 Advent Calendar 2020」はこちら↓ 昨日は @ysk_en さんの「 ITベンチャーで働く、新卒デザイナーの立ち回り方 」という記事でした。 「職種が溶ける」っていい表現ですね、好きです👴   今回はdelyならではな、 "エンジニアだけどディレクター的な仕事もするよ" 的なところを紹介できたらいいなと思い、レシピ検索における良い体験について考え、新しく "選ばれた" という考え方をあみ出した話を書いていきたいと思います。 目次 目次 いきさつ "選ばれた" 何において"選ばれた"なのか レシピが"選ばれた"を何で判断するか 考えがまとまるまでの道のり レシピ検索において大切な事を考える "選ばれた"についてブレストする 行動ログの分析 分析したデータを集計 社内に共有してみる ひとまず考えはまとまった 感想 さいごに いきさつ 冒頭での自己紹介にもありましたが、私はクラシルのレシピ検索の機能改善を目的としたチームでPdMをしています。 このチームが立ち上がったのは自分の入社と同時期の2020年7月でした。 ぶっちゃけクラシルのレシピ検索には課題がもりもりです。 そんなもりもりの課題を解決していくにあたり、それをすることによって 何が良くなったのか? を明確にし、ユーザーへの価値提供が成せたのかを示す必要があります。 例えば、ECの場合は商品を検索して購入につながれば良い検索体験を提供できたという一つの判断ができると思います。 レシピ検索ではこの「購入」にあたる行動はなんなのかを明確に考えられていませんでした。 ECでの商品検索との比較 そこで、私たちが目指す 良い検索体験とは何か を考えることにしました。 "選ばれた"   まずは、今回考えた"選ばれた"について解説していきます。 何において"選ばれた"なのか 冒頭で、 ECの場合は商品を検索して購入につながれば良い検索体験を提供できたという一つの判断ができると思います。 と述べましたが、レシピ検索においてこの「購入」にあたる行動を そのレシピの料理が作られた 事だと考えました。 何か作ろうと思ってクラシルで検索行動をとるとします。 ある1日の検索行動例 上記のようなイメージで、1日の中で複数回検索行動をとる場合もあれば、1回だけの場合も考えられます。 この検索行動の中で重要なことは、 ユーザーが作りたいと思えるレシピに出会えているか どうかです。 この 検索行動の中で作りたいと思ったレシピに出会ったこと を、 レシピが"選ばれた" と呼ぶことにしました。 レシピが"選ばれた"を何で判断するか この場では詳しい説明は割愛しますが、検索行動の中でユーザーが特定の行動をした場合に"選んだ"と判定するようにしました。 あとで紹介する 考えがまとまるまでの道のり でわかったことですが、ユーザーがそのレシピで作ろうと思った時にとる行動には多様性がありました。 そんないろんな行動の中で割合が多いもの、明らかにパターンが異なるものを採用していきました。 検索行動の中でこれらのうちいずれかを実行した場合は"選ばれた"と判定されます。 検索行動の中で"選ばれた"のイメージ 検索行動の中で"選ばれなかった"のイメージ 考えがまとまるまでの道のり まずは考えた内容を紹介しましたが、どういう道のりを辿ってきたのかも興味深い(ですよね?)内容だと思うので紹介したいと思います。 レシピ検索において大切な事を考える レシピ検索においては、作るためのレシピに出会う体験(※)を重要視していました。 (※ザッピング的に、無目的な状態でレシピをみている行動ではない) つまり、検索体験の中で大事なことは、 ユーザーが検索行動の中でこのレシピで料理しようと思えること = レシピが料理されるものとして"選ばれる"こと だと考えます。 クラシルのレシピ検索においては課題がもりもりあり、この"選ばれる"体験をうまく提供できていない面が多々ありました。 なので、まずはこの"選ばれる"体験を増やしていこうじゃないかと決めました。 そして、最終的には下記のように活動していこうと考えました。 ・検索においてレシピが"選ばれた"体験を増やす ・レシピが"選ばれる"ための適切なUXの提供 ・レシピが"選ばれなかった"体験の検知と改修   "選ばれた"についてブレストする まずは"選ばれた"とは、ユーザーがどんな具体的な行動をすると成立するのかをチーム内で考えました。 ユーザーが何か行動したログはいろいろと仕込んであるので、その中で「これは"選ばれた"と言えるのではないか?」と思うものを、ブレストでばんばん出し合いました。 (判断材料候補として確か20個くらい出た記憶があります...) よーし、じゃあこれらを実行したら"選ばれた"としよう!とはなりません。 あくまでサービスの作り手が 予想しただけ なので、 実際のところどうなのか を確かめる必要があります。 行動ログの分析 前述した判断材料の候補たちが、"選ばれた"とすることができるのかどうかを確かめるために、実際にレシピを作ったユーザーがそのレシピに出会う際どんな行動をしていたのかを調査するために 行動ログの分析 をしました。 残念ながら、ユーザーがこのレシピで料理したという便利なログは仕込まれていない(アプリの外の行動なので仕込むことができませんね)ので、下記の2パターンで分析を進めました。 1. ユーザーインタビューで得られた情報から集計 2. たべれぽ投稿したログをさかのぼって、その人がそのレシピを検索結果で見つけた時のログを分析 それぞれ細かく解説していきます。 【ユーザーインタビューで得られた情報から分析】 クラシルでは、どんなふうにサービスを利用しているのかなどを実際にユーザーに対してインタビューしています。 内容はその時々で異なりますが、その中でも「どんなふうにレシピを探しますか?」という質問をすることがあったため、過去のインタビュー記録からその行動内容を集計していきました。 【たべれぽ投稿ログから分析】 たべれぽ投稿は、ユーザーが実際にそのレシピを作った上で画像を添付して投稿するものなので、ほぼそのレシピを作ったと言えます。 なので、そのレシピを見つけたと思われる検索行動ログ抽出し、内容を集計していきました。 この時、ユーザー属性によって特徴が出るかもしれないと思い、いろいろな属性ごとに集計をしました 分析したデータを集計 ユーザーインタビューやたべれぽ投稿から分析したデータから、ユーザーが検索行動の中で作る対象のレシピを発見した時の行動パターンを、それぞれ何%のユーザーが実行していたのかを集計していきます。 この時、たべれぽ投稿の場合はユーザー属性を分けて見ていたので、属性ごとにどんな特徴が出るのかも見てみました。 この結果から"選んだ"という行動の考え方を決めていきました。 考えはひとまずまとまりましたが、チーム内に閉じ込めておくのはもったいないので、他チームや他部署にも共有することにしました。 社内に共有してみる delyにはUX改善MTGという、部署やチームを横断して新たにやろうとしている施策や実装した施策の効果を共有するなどの議論をする機会があります。( 社長も参加します! ) その場で"選ばれた"率について考えた内容を共有することにしました。 質問があったり、改善案ももらったりしつつひとまず了解を得られました。 ひとまず考えはまとまった  以上の道のりを経て、良いレシピ検索体験について考えをまとめることができました! といいつつ、一生この考え方でいくつもりはないです。 もっとブラッシュアップして信憑性を高めたりできる可能性があったり、その時々で求められる計測の仕方は異なりますので、ひとまずv0.1という感じで考え方を利用していこうと思います。 感想 締めに言うのもなんですが、検索チームには データエンジニアや統計学の知識を持つ有識者的なメンバーはいませんでした 。(全員素人) そんな中で"選ばれた"を考えていくのは困難であると思っていました それでもメンバーでやり切れると信じ、他チームのデータエンジニアに何度も相談しながら進んで行けました。(超絶感謝…!!) データ抽出や分析をたくさん行ってきましたが、ユーザーがどういう検索行動をしているのかを深く知る良いきっかけになったと感じています。 (一体いくつのRedashクエリを作成したことか…) 何より、"選ばれた"を考えたことよって以前よりも 私たちが目指す検索体験の向上を測りやすくなった と思います。 今後、この"選ばれた"体験を観測しながら、検索体験の向上を目指していきます! さいごに 「 dely #1 Advent Calendar 2020 」の明日は うっくん さんの「 UIデザイナーがSwiftを学んでUIを実装したら生産性が爆上がりした 」です、お楽しみに〜。 また、delyではエンジニア・デザイナーを絶賛募集中です。 ご興味がある方はこちらのリンクからお気軽にエントリーくださませ! さらに TechTalk というイベントも行っているので、dely について詳しく知りたい方は是非参加してみてください!    
アバター
こんにちは。dely株式会社でAndroidチームのマネージャーをやっているうめもり(Twitter: @kr9ly )です。 この記事は「dely #2 Advent Calendar 2020」の7日目の記事です。 6日目の記事は、 knchst さんによる「エンジニアの僕が初めてプロダクトマネージャーをする上で特に意識したこと」でした。僕も人に依頼するときは菓子折り持って行ってその場で食べてもらってから依頼することにします。 www.notion.so 「dely #1 Advent Calendar 2020」もありますので、是非そちらもご覧ください。 早速ですが、皆さん、AWS CodeBuild使ってますか? Amazon Elastic Container Registryと組み合わせて使うと、ビルドイメージのProvisioningがとても高速に終わるのでdelyのAndroidチームでもアプリのビルド用に使っています。 今回の記事は、「Androidのビルド用Dockerイメージダイエット計画」ということで、 AndroidチームでAndroidのビルド用Dockerイメージのサイズを小さくした際のTipsをご紹介します。 はじめに 最近Androidチームではアプリの高速化プロジェクトを進めていて、並列ビルドをしっかり効かせられるプロジェクト構造に変えたおかげでビルド時間が大分短縮できました。元々1回のCI用のビルドに 10分以上 かかっていたものが 2~3分 で終わるようになってそれ自体は非常によいことなのですが、相対的にDockerイメージのProvisioning速度が気になるようになってきました。 AndroidチームではなるべくCIを高速にやるために、ライブラリーのダウンロードなども済ませた状態でカスタムのDockerイメージを作成しているのですが、その反面Dockerイメージ自体のサイズが肥大化しがちで、 2.5GB 程度のサイズになっていました。 この状態ではいくらCodeBuildとAmazon Elastic Container Registryの組み合わせでも、DockerイメージのProvisioning時間が 1分半~2分程度 かかるようになっていました。イメージのサイズを考えるとこれでも十分速いとは思うのですが、これをもっと短縮できないかと考えました。 Dockerイメージが肥大化しがちな原因 Androidのビルド用Dockerイメージは割と肥大化しがちだと思うのですが、それには次のような原因があります。 Javaを使うのであまり考えずにイメージを構築するとそもそも素のイメージサイズが大きくなる Android SDKがそもそも大きい これらの問題に対してどのように対処したかについてご説明して、最後に出来上がったDockerfileを紹介します。 今回行った工夫 alpineをベースにする ダイエット前のDockerイメージは、debianのopenjdk-slimイメージをベースに構築していました。手間なく構築する際にはこういったあらかじめ環境が構築されたイメージは便利ですが、今回はなるべくイメージサイズを小さくするためにalpineをベースにイメージを構築することにしました。ただし、alpineはmusl-libcを使っておりglibcを使っていないので、Android SDKを使う場合には問題があるのですが、その問題をどのように解決したかについては後述します。 Java9から導入されたjlinkを使ってJavaランタイムのサイズを小さくする Java9からはJavaランタイムがモジュール化され、jlinkというツールを使うことで使いたいモジュールだけが入ったJavaランタイムが構築できるようになっています。これを使うことでJavaランタイムのサイズを小さくすることを試みました。なお、今回はJava11を使用しました。 手順としては結果的にはとても泥臭くなってしまったのですが、まず最小のJavaランタイムを作成し、そのJavaランタイムを使ってGradleでビルドを繰り返すことで、一つずつ必要なモジュールを足していくという手段で最小のJavaランタイムを構築しました。 一回の実行にも時間がかかる上、どのモジュールが足りないか調べるのがなかなかしんどい試行錯誤だったので、もしスマートなやり方を知っている方がいたらぜひ教えてください…。 alpine上でglibcがリンクされたバイナリが動くようにする これらの工夫で大分イメージが小さくなりましたが、そもそもalpineにはglibcが入っていないので、Android SDKが動きません。Android SDKが動かないのであればまったく意味がないので、alpine上でglibcが動くように環境を構築しました。なお、こちらのQiitaの記事を参考にしました。(ありがとうございます) Android SDKのインストールを済ませた後、emulatorを削除する Javaランタイムを構築した後、sdkmanagerでAndroid SDKをインストールするのですが、実はデフォルトのインストールではemulatorもインストールされてしまいます。ビルドするだけであればemulatorは不要なので、インストールを済ませた後、emulatorだけ削除しておきます。 Docker multi-stage buildを使い極力不要なファイルがイメージに残らないようにする AndroidチームではRuby Dangerを利用しているので、Rubyのビルド用に本来実行には不要なファイルをイメージにインストールする必要があります。これをマニュアルで削除することもできますが、Docker multi-stage buildの機能を使い、必要最低限のファイルだけを最終イメージに移すことでDockerイメージを小さくしました。 Dockerfile そして出来上がったのが以下のDockerfileです。 FROM alpine:3.12 AS alpine-glibc ENV LANG=C.UTF-8 # Here we install GNU libc (aka glibc) and set C.UTF-8 locale as default. RUN ALPINE_GLIBC_BASE_URL="https://github.com/sgerrand/alpine-pkg-glibc/releases/download" && \ ALPINE_GLIBC_PACKAGE_VERSION="2.32-r0" && \ ALPINE_GLIBC_BASE_PACKAGE_FILENAME="glibc-$ALPINE_GLIBC_PACKAGE_VERSION.apk" && \ ALPINE_GLIBC_BIN_PACKAGE_FILENAME="glibc-bin-$ALPINE_GLIBC_PACKAGE_VERSION.apk" && \ ALPINE_GLIBC_I18N_PACKAGE_FILENAME="glibc-i18n-$ALPINE_GLIBC_PACKAGE_VERSION.apk" && \ apk add --no-cache --virtual=.build-dependencies wget ca-certificates && \ echo \ "-----BEGIN PUBLIC KEY-----\ MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApZ2u1KJKUu/fW4A25y9m\ y70AGEa/J3Wi5ibNVGNn1gT1r0VfgeWd0pUybS4UmcHdiNzxJPgoWQhV2SSW1JYu\ tOqKZF5QSN6X937PTUpNBjUvLtTQ1ve1fp39uf/lEXPpFpOPL88LKnDBgbh7wkCp\ m2KzLVGChf83MS0ShL6G9EQIAUxLm99VpgRjwqTQ/KfzGtpke1wqws4au0Ab4qPY\ KXvMLSPLUp7cfulWvhmZSegr5AdhNw5KNizPqCJT8ZrGvgHypXyiFvvAH5YRtSsc\ Zvo9GI2e2MaZyo9/lvb+LbLEJZKEQckqRj4P26gmASrZEPStwc+yqy1ShHLA0j6m\ 1QIDAQAB\ -----END PUBLIC KEY-----" | sed 's/ */\n/g' > "/etc/apk/keys/sgerrand.rsa.pub" && \ wget "$ALPINE_GLIBC_BASE_URL/$ALPINE_GLIBC_PACKAGE_VERSION/$ALPINE_GLIBC_BASE_PACKAGE_FILENAME" && \ wget "$ALPINE_GLIBC_BASE_URL/$ALPINE_GLIBC_PACKAGE_VERSION/$ALPINE_GLIBC_BIN_PACKAGE_FILENAME" && \ wget "$ALPINE_GLIBC_BASE_URL/$ALPINE_GLIBC_PACKAGE_VERSION/$ALPINE_GLIBC_I18N_PACKAGE_FILENAME" && \ apk add --no-cache \ "$ALPINE_GLIBC_BASE_PACKAGE_FILENAME" \ "$ALPINE_GLIBC_BIN_PACKAGE_FILENAME" \ "$ALPINE_GLIBC_I18N_PACKAGE_FILENAME" && \ \ rm "/etc/apk/keys/sgerrand.rsa.pub" && \ /usr/glibc-compat/bin/localedef --force --inputfile POSIX --charmap UTF-8 "$LANG" || true && \ echo "export LANG=$LANG" > /etc/profile.d/locale.sh && \ \ apk del glibc-i18n && \ \ rm "/root/.wget-hsts" && \ apk del .build-dependencies && \ rm \ "$ALPINE_GLIBC_BASE_PACKAGE_FILENAME" \ "$ALPINE_GLIBC_BIN_PACKAGE_FILENAME" \ "$ALPINE_GLIBC_I18N_PACKAGE_FILENAME" FROM alpine-glibc AS intermediate ENV ANDROID_SDK_FILENAME=commandlinetools-linux-6514223_latest.zip ENV ANDROID_SDK_ROOT=/opt/android-sdk-linux ENV ANDROID_SDK_CMDLINE_TOOLS=${ANDROID_SDK_ROOT}/cmdline-tools ENV ANDROID_SDK_URL="http://dl.google.com/android/repository/${ANDROID_SDK_FILENAME}" ENV ANDROID_API_LEVELS=android-30 ENV ANDROID_BUILD_TOOLS_VERSIONS=29.0.3 ENV GRADLE_USER_HOME=/usr/local/gradle ENV JAVA_HOME=/opt/jdk-11-mini-runtime ENV BUNDLER_PATH=/opt/bundle ENV PATH=${PATH}:${ANDROID_SDK_CMDLINE_TOOLS}/tools/bin:${JQ_PATH}:${JAVA_HOME}/bin RUN apk update && \ apk --update --no-cache add openjdk11 \ fontconfig ttf-dejavu \ ruby ruby-dev alpine-sdk zlib-dev && \ rm -rf /var/cache/apk/* RUN /usr/lib/jvm/java-11-openjdk/bin/jlink \ --module-path /usr/lib/jvm/java-11-openjdk/jmods \ --compress=2 \ --add-modules java.base,java.compiler,jdk.compiler,java.logging,java.xml,jdk.unsupported,java.naming,java.desktop,java.management,jdk.crypto.ec,java.sql,java.rmi,jdk.zipfs,java.instrument,jdk.attach \ --no-header-files \ --no-man-pages \ --output ${JAVA_HOME} COPY Gemfile ./ RUN gem install bundler && \ bundle config set path ${BUNDLER_PATH} && \ bundle config set without 'development test' && \ bundle install RUN mkdir ${ANDROID_SDK_ROOT} && \ mkdir ${ANDROID_SDK_CMDLINE_TOOLS} && cd ${ANDROID_SDK_CMDLINE_TOOLS} && \ curl -O ${ANDROID_SDK_URL} && \ unzip ${ANDROID_SDK_FILENAME} && \ rm ${ANDROID_SDK_FILENAME} && \ mkdir ${GRADLE_USER_HOME} && \ echo y | sdkmanager $(echo ${ANDROID_BUILD_TOOLS_VERSIONS} | sed 's/,/\n/g' | sed -E 's/(.+)/build-tools;\1/g' | tr '\n' ' ') "platform-tools" $(echo ${ANDROID_API_LEVELS} | sed 's/,/\n/g' | sed -E 's/(.+)/platforms;\1/g' | tr '\n' ' ') && \ sdkmanager --uninstall emulator WORKDIR /workspace FROM intermediate AS dependencies COPY . /workspace RUN ./gradlew --full-stacktrace --project-cache-dir=${GRADLE_USER_HOME} checkCi bundleRelease FROM alpine-glibc ENV JQ_PATH=/usr/local/jq ENV ANDROID_SDK_ROOT=/opt/android-sdk-linux ENV ANDROID_SDK_CMDLINE_TOOLS=${ANDROID_SDK_ROOT}/cmdline-tools ENV GRADLE_USER_HOME=/usr/local/gradle ENV JAVA_HOME=/opt/jdk-11-mini-runtime ENV BUNDLER_PATH=/opt/bundle ENV PATH=${PATH}:${ANDROID_SDK_CMDLINE_TOOLS}/tools/bin:${JQ_PATH}:${JAVA_HOME}/bin COPY --from=intermediate /opt/jdk-11-mini-runtime /opt/jdk-11-mini-runtime COPY --from=intermediate /opt/bundle /opt/bundle COPY --from=intermediate /opt/android-sdk-linux /opt/android-sdk-linux COPY --from=dependencies /usr/local/gradle /usr/local/gradle RUN apk update && \ apk --no-cache add bash ruby ruby-json ruby-bigdecimal wget git curl fontconfig ttf-dejavu && \ rm -rf /var/cache/apk/* RUN gem install bundler && \ bundle config set path ${BUNDLER_PATH} RUN mkdir $JQ_PATH && cd $JQ_PATH && \ curl -o jq https://github.com/stedolan/jq/releases/download/jq-1.5/jq-linux64 && chmod +x $JQ_PATH/jq && \ cd ~ && \ curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" && \ unzip awscliv2.zip && rm awscliv2.zip && \ ./aws/install && rm -rf ./aws WORKDIR /workspace 結果 そうしてDockerイメージをダイエットした結果、 2.5GB -> 1.4GB 程度までDockerイメージがダイエットできました。Provisioning時間も約半分程度まで短くできたので満足です。 オチ ちなみに、最初想定したよりはイメージが小さくならなかったのですが、最低限Gradleが走るように必要なファイルに絞るのであれば、イメージサイズは0.7GB程度まで落とせることを確認しました。あらかじめビルドに必要なライブラリなどを組み込んでおり、それがかなりのイメージサイズを消費していたということが分かったというのが今回のオチですね…。
アバター
はじめまして、ソフトウェア・エンジニアの松岡です。 私はコマース事業部で先日に発表した ネットスーパー機能 のインフラ、バックエンド、たまにiOSなどわりといろいろなことを担当しています。 また今年の7月にサービスを終了したクラシルストアでは開発だけでなく、販売する商品の管理などストアの運営や、カスタマーサポートなどもやってました。 いろいろなことをやることは大変ですが、視点が増えることで新たに気づくことや考えが深まることがあり、そこには大変さ以上の恩恵があるので好きでやっています。 これは「dely #1 Advent Calendar 2020」の6日目のエントリーです。 「dely #2 Advent Calendar 2020」もあるのでぜひご覧ください。こちらの今日のエントリーはfukuさんの エンジニアの僕が初めてプロダクトマネージャーをする上で特に意識したこと です。大作でした、最高です。 昨日は#1が安尾友佑さんの エンジニアがゼロから始めるプロダクトマネジメント で、#2は伊ヶ崎(@_ikki02)さんの delyクラシル、最近のデータ基盤の話 でした。こちらも大作でした。 今回の私のエントリーでは「VS Codeで作るAWS Vaultの一時認証つきのポータブルなTerraform環境の作り方」を紹介します。 *紹介する作り方はMacの場合を想定しています。WindowsやLinuxの場合は差異を適宜読み替えてください。 紹介するTerraform環境の特徴 特徴は次のとおりです。 hashicorp/terraform というDockerイメージで作るのでTerraformの実行環境がポータブルです。 VS Codeのリモートコンテナー機能 ( Visual Studio Code Remote - Containers )を使うのでVS Codeの設定や拡張機能もポータブルです。 このように実行環境もエディターの設定もポータブルのため、いつでもどこでもそして誰でも同じ環境で開発することができます。また、一からTerraform環境を作るときも簡単にその環境を準備することができます。 作るまえにご準備するもの Terraform環境を作る前にご準備していただくものは次のとおりです。 AWS アカウント スイッチロールで切り替えるプロファイル AWS Vault VS Code Remote - Containers (VS Codeの拡張機能) Docker (Docker for Macなど) 作り方 ではさっそくはじめましょう。やることは1つです。 次の内容の .devcontaier.json というファイルを作り、任意の場所に保存してください。 * aws-vault exec に渡すプロファイルの sample-profile は適宜変えてください。 { " image ": " hashicorp/terraform:0.13.5 ", " runArgs ": [ " --env-file ", " sample-profile.env " ] , " extensions ": [ " HashiCorp.terraform " ] , " settings ": { " [terraform] ": { " editor.formatOnSave ": true , " editor.tabSize ": 2 , } } , " initializeCommand ": " aws-vault exec sample-profile -- env | grep AWS > sample-profile.env " } .devcontaier.json は Remote - Containers の設定ファイルです。設定の内容は次のとおりです。 hashicorp/terraform:0.13.5 というDockerイメージを使います。 0.13.5 の部分はTerraformのバージョンですので適宜、バージョンを選んでください。 Dockerイメージのビルドのときにenvファイルを渡します。 HashiCorp.terraform というVS Codeの拡張機能をプリインストールします。またこの拡張機能向けの設定も追加します。 コンテナーを作るときに aws-vault exec sample-profile -- env | grep AWS > sample-profile.env というコマンドを実行して、上記のDockerイメージに渡すenvファイルを作ります。 .devcontaier.json の詳しい解説は devcontainer.json reference をご覧ください。 それではTerraform環境を作りましょう。 .devcontaier.json を保存したディレクトリーをVS Codeで開いてください。 開いたらコマンドパレットから Remote-Containers: Reopen in Container を実行してください。 AWS VaultがOSのセキュア情報 ( Keychain Access など)へアクセスするためのパスワードをたずねてきたら、パスワードを入力してください。 これで出来上がりです!!! VS Codeが完全に立ち上がったらターミナルで terraform version でTerraformの実行環境を、 env | grep AWS でAWSの一時的な認証情報を確認してください。 ご利用の注意 1つ注意です。 aws-vault で作る IAM の一時的なセキュリティ認証情報 はVS Codeを開くときに作ります。 この認証情報は1時間くらいで有効期間が切れてしまいます。切れたらコマンドパレットから Remote-Containers: Rebuild Container を実行してください。これを行うと再び認証情報が作られます。 認証情報が切れるなら --server オプションを使えばいいじゃないと思った方もいらっしゃるかと思います。 ごつっこみありがとうございます!!! 適用できるかどうかを調査中です。適用できたときは開発ブログで報告させていただきます。 それでは快適なTerraform開発を!!! 最後に お知らせです。 dely ではエンジニアやデザイナーを絶賛募集中です!ご興味があれば次のリンクからお気軽にご応募くださいませ! www.wantedly.com join-us.dely.jp またエンジニア、デザイナー向けのイベントも開催しています。こちらもご興味があればどうぞ! bethesun.connpass.com 明日のエントリーは#1が奥原さんの「新規事業で闘い続けるためのプロダクトマネージメント」、#2が梅森さんの「Androidのビルド用Dockerイメージダイエット計画」です、きっと大作ですね、どうぞお楽しみに!!!
アバター
こんにちは! dely開発本部でクラシルのサーバーサイドエンジニア兼PdMを担当している yasuo です。 この記事は「dely #1 Advent Calendar 2020」の5日目の記事です。 adventar.org adventar.org 昨日は funzin さんの RenovateをiOSアプリ開発に導入してみた という記事でした。 ライブラリの自動アップデートに興味がある方はぜひご覧ください。 本日は 「エンジニアがゼロから始めるプロダクトマネジメント 」 というテーマで、僕自身が2020年7月にプロダクトマネージャー(以下、PdM)となってから約5ヶ月間で得た学びを、 よかったこと3選 という形でご紹介したいと思います。   PdMになった背景 6月以前のクラシル開発本部は、全員同じスクラムに所属して開発を行なっておりましたが、規模の拡大に伴ってSquad体制化し、分解したKPI毎に小規模チームを作り、各チームに権限移譲をすることになりました。 それに伴って、各Squadに1人PdMを立てることになり、1つのチームのPdMを僕がやっていくことに決まりました。 詳しくは以下のTweetをご覧ください。 delyのエンジニア・デザイナー採用ページリニューアルに伴い開発カルチャースライドをアップデートしました!全てが実現できているわけでは無くまだ成長過程ですが方針が伝わると嬉しいです。共感いただける方ぜひお茶でも行きましょう! https://t.co/vQmng5x0Iz pic.twitter.com/0ZzUqDWBwy — 坪田 朋 / クラシル (@tsubotax) 2020年8月28日   それでは、いよいよ よかったこと3選 を紹介していきます。 よかったこと3選 PdMをやるにあたって、本や記事で学んでやったこと、実際に取り組む中での失敗から学んでやってみたこと色々ありますが、中でも特にやってよかったと思っている取り組みを3つ紹介したいと思います。 1. ステークホルダー間で登りたい山だけでなく、登り方まで合意する こちらは僕が失敗から学んで改善したことの1つですが、この取組みをやる前の課題として次のようなことがありました。 関連する他チームに自分たちのチームがやろうとしていること、やったことがきちんと伝わっていない 関連する他チームからの差し込みによって、計画通りに開発を進められない 多くの会社において、部署毎やチーム毎の目標を決めてその達成のための計画を立てて仕事をするというのはやられていることだと思いますが、一方で他部署・他チームの目標や達成計画まで頭に入れながら仕事をしている人は少ないのではないでしょうか? 僕も最初はチーム内のことだけで精一杯で他チームがやっていることの把握や、自チームでやっていることを他チームに伝えるということがほとんどできていませんでした。 その結果、チームの信頼や評価が思うように得られず、他チームを巻き込まないと実現できない施策を思ったように進められなかったり、他チームの目標達成のために必要な開発が緊急で入ってきて、自分たちの目標達成のための施策が後回しになるというようなことが発生していました。  そこで、関連するチーム間で、お互いの目標・ロードマップを共有し合う場を設けることにしました。 結果、チーム間で協力し合う必要がある施策が可視化され、事前に余裕を持って懸念事項について議論することができるようになり、上述したような課題が解決することができました。 2. ユーザーの定性、定量的な理解  多くのプロダクトマネージャーに関する書籍や記事に書いてあることですが、プロダクトマネージャーの重要な役割の1つに ユーザーに関する深い理解 というものありますが、クラシルにおいてもこれはとても大切だと思います。 僕がPdMになって最初にやったことは、徹底した定量理解で、クエリを書きまくって重要な数値をとにかく可視化し、どこに課題があるのか大枠を理解するということでした。 ここまでは良かったのですが、僕の場合は定性理解を最初サボってしまったのは失敗でした。 定量理解だけでも既存機能の改善程度ならうまくいくこともあるのですが、新しい価値をつくったり、より多くユーザーに満足してもらうためには、ユーザーの行動・心理を深く理解しないと的外れな施策となり、ユーザーに刺さらないということをこの後痛いほど痛感することになりました。 定量分析よりもかなり手間はかかるのですが、ユーザーインタビューをしっかりやることで、「この機能をリリースしたらあの人が喜んでくれそう」と具体的な1ユーザーをイメージしながら企画をすることができるようになったのがとても良かったと思っています。 3. 経験値の共有 これは僕たちのチームオリジナルの取り組みというよりは開発部のカルチャーの1つですが、意思決定のプロセス・理由をチーム全員で共有するということを徹底して、良いプロダクトをつくる上でとても大切なことだなと思っています。 施策実施前に目的、期待する効果、どうやって効果検証するか、成功の定義など、PdMが施策に関して考えていることをドキュメント化した上でチームメンバーで、それを更にブラッシュアップしてから開発することで、PdM1人で考えるより明らかに良い施策になりますし、開発も言われたものをただ作るよりもより高いモチベーションで行うことができています。 また、リリース後の効果検証についても数値を可視化した上で全員で行い、今回のリリースでどういう学びがあったか、ネクストアクションをどうするかの意思決定にも全員が関わるようにしています。 最後に 本記事では、僕がエンジニア兼PdMにチャレンジする中で得た学びをご紹介させていただきました。 PdMというキャリアに興味を持っている方や、現在PdMをしている方にとって少しでお役に立てれば嬉しいです! 明日はMatsuokaさんの記事です。お楽しみに! また、dely ではエンジニアを絶賛募集中です! ご興味ある方はこちらのリンクからお気軽にエントリーください! https://join-us.dely.jp/ join-us.dely.jp さらに、定期的に TechTalk というイベントを通じて、クラシルで利用している技術や開発手法、組織に関する情報も発信しております。 ノウハウの共有だけでなく、クラシルで働くエンジニアがどんな想いを持って働いているのかや、働く人の雰囲気を感じていただけるイベントになっていますので、ぜひお気軽にご参加ください! bethesun.connpass.com  
アバター
はじめに こんにちは。dely開発部でデータエンジニアしてる伊ヶ崎( @_ikki02 )です。 本記事はdely Advent Calendar 2020の5日目の記事です。 adventar.org adventar.org (delyでは今年から2レーンでアドベントカレンダーやってます。) 昨日は当社デザイナーの @ysk_en が「 マジで助かった、新卒1年目デザイナーの教科書的noteや便利なサービス8選 」という記事を書きました。 タイトルの付け方がうまいですね。僕も見習いたいです。 ぜひこちらも一読いただけると嬉しいです! さて、本日僕が書こうと思う内容はズバリこれです。 あまり最近のデータ基盤について外部発信できていなかったのと、 ちょうど社内でデータマネジメントの機運を高めようと動いていることもあり、 この機会に情報発信しちゃおうと思います。 (ちなみに過去の記事はこちら) tech.dely.jp 目次 データ基盤の課題 データマネジメント 取組事例:データアーキテクチャ 最後に データ基盤の課題 まず、なぜデータ基盤をやるか、その目的を振り返りたいと思います。 dely(クラシル)では日々様々な施策をみんなで話し合って企画・開発・効果測定に取り組んでいます。 データ基盤はこのサイクルの信憑性を高め、加速化していく役割を担っていると考えます。 しかし、現実としてデータ基盤は完全ではなく様々な課題があります。 特にデータが取れない、異常がある、といった状況では正しい意思決定の妨げとなり、最悪の場合ミスリードしていることにさえ気づかない、という状況を誘導しかねません。 また、基盤の保守運用に際して、その構造を把握していないと、誰もが同じように開発・保守運用ができません。 このような課題感に対処する上で、その管理手法の考え方や方法論のよりどころを求めていました。 そんな時、DAMA Internationalという米国機関によってDMBOKとしてデータマネジメントの体系が整理されていることを知りました。 delyではこのデータマネジメントという考え方を自社の実態に合った形で解釈し直し、課題に取り組む活動にこの半年間取り組んでいます。 データマネジメント まず、データマネジメントの紹介に際して、 @yuzutas0 さんの『データマネジメントが30分でわかる本』から抜粋させて頂きます。 データマネジメントとは、「データを資産と捉え、体系的に価値を引き出すための手法」です。 - 資産なので置き場所を決めます。 - 資産なのでどこからきて、どこへ行くのか把握します。 - 資産の価値が減らないように気をつけます。 - 資産を監督する人やそのルールを決めます。 やっていることは、他の資産、例えば預金や不動産の管理と大差ありません。資産管理のデータ版がデータマネジメントです。 僕はこの文章を見た時、発想の転換を迫られました。 様々な異常を問題と捉え正常に捉えること、アーキテクチャの理解の重要性は「資産」として捉えれば当然のことのように思えました。 なお、本家では11の項目に分かれて整理されていますが、 最初に適用を試みるには項目が多く少しハードルが高かったため、 まずは「保守運用」「セキュリティ」「品質」の3つに大別した上で、 自社のデータ基盤について管理していくことにしました。 また、こちらの考え方を四半期初めのロードマップ共有会で マーケター、エンジニア、デザイナーなど様々な参加者に共有し、 少しずつデータマネジメントの考え方を広げて行こうという活動も開始しています。 取組み事例:データアーキテクチャ これまで取り組んできた内容について、1つこの場で紹介したいと思います。 さきほど紹介した「保守運用」の項目の中に、「データアーキテクチャ」の項目があります。 具体的には、基盤の構成を可視化し、ビジネスの指標やKPIと紐づけることで、何の基盤がどんな役割を果たしているのか書き出しています。 理論的なメリットとしては、構成を可視化することで、継足しアーキテクチャを避け、同じ指標に対して基盤が違うから値が違う、といった問題が起こるのを避けるのに役立ちます。 実際に書き下ろしたクラシルのデータ基盤は今こんな感じになっています。 アプリの「ユーザー行動ログ」をBigQueryとAthenaの2つのラインに流しています。 メインとしてはAthenaを利用しており、行動ログ以外の外部データ(プッシュ通知、ダウンロード数、課金レポートなど)を、データレイクであるS3に転送、ETL処理してAthenaで集計できるようにしています。 BigQueryについては、Firebaseで取得しているアプリの「アンインストール数」や「地理情報等」を取得し、必要に応じてBIツールであるRedashから参照できるようにしています。 上記図の各パイプラインは別途細部まで可視化しており、 その結果、感じているメリットとして以下のような点を感じています。 何の役に立っているか明確になった。 採用活動時、現状を説明できる絵ができた。 各種調査の時間が減った気がする。 課題の特定がしやすくなった。 Redashで実施したクエリ結果の異常について、原因がRedashなのかAthenaなのかもっと上流なのか、など 各リソースの依存関係が明確になり、消していいリソースを判断できるようになった。 コスト節約に繋がった。可視化後不要なリソースを削除したことで、データ基盤・機械学習基盤について数十%もの金額的コストを削減できています(結構でかい)。 「データアーキテクチャ」の項目を意識して情報を整理したところ、「保守性」が上がったと感じています。 現在は「品質改善」を目指すべく、データを利用するステークホルダーへ品質要求のヒアリングや、異常検知の取組みも進め、「攻めのデータマネジメント」に転じ始めています。 こちらの内容はまた別の機会にこのブログで紹介できればと思っていますので、興味ある方は是非ウォッチしていただければ嬉しいです。 最後に いかがでしたでしょうか? delyではユーザーへの価値提供のために自分たちで企画・開発・効果測定のサイクルを日々グルグル回しています。 データマネジメントはこのサイクルを影で支えている縁の下の力持ち的役割に捉えられがちですが、ユーザーもデータも「資産」として大切にする心を忘れずに、価値創造に貢献していきたい想いです。 また、delyでは上記サイクルをさらに加速化させるために、データエンジニアの採用を募集しています。もしこれを読んだデータエンジニアの中で、興味を持っていただける方がいたら、カジュアルな会話からも可能なので是非以下のリンク先からご応募お待ちしてます。笑 www.wantedly.com https://join-us.dely.jp/ join-us.dely.jp bethesun.connpass.com 明日は knchst さんの「エンジニアの僕が初めてプロダクトマネージャーをする上で特に意識したこと」です。お楽しみに!
アバター