Kotlin Fest 2022の登壇内容のスライドです。
1©2022 Loglass Inc.型だけでバグを減らそう!Kotlinの型パワーを使った実践タイプセーフエンジニアリング2022.12.10 佐藤有斗(Yuiiitoto) 株式会社ログラス
View Slide
2©2022 Loglass Inc.自己紹介佐藤有斗(Yuiiitoto)ログラス エンジニア# React# 組織・アジャイル# Kotlin
3©2022 Loglass Inc.ログラスについて(5秒)企業価値を向上する 経営管理クラウド
4©2022 Loglass Inc.目次● なぜ型を使いこなすことでバグが減るのか?● 型によってバグが減らせるパターン1. 標準の型をラップする2. 認可処理などの特定の処理をパスしたことを型で示す3. 型でデータの不整合をなくす● まとめ● 最後に: タイプセーフなポストモーテム
5©2022 Loglass Inc.目次● なぜ型を使いこなすことでバグが減るのか?● 型によってバグが減らせるパターン1. 標準の型をラップする2. 認可処理などの特定の処理をパスしたことを型で示す3. 型でデータの不整合をなくす● まとめ● 最後に: タイプセーフなポストモーテム
6©2022 Loglass Inc.なぜ型を使いこなすことでバグが減るのかコンパイル時に実装ミスに気付けるから
7©2022 Loglass Inc.コンパイル時に実装ミスに気づくとは?
8©2022 Loglass Inc.ミドルネームにnullを渡す
9©2022 Loglass Inc.ぬるぽ、ガッ! Λ_Λ \\ ( ・∀・) | | ガッ と ) | | Y /ノ 人 / ) < >_Λ∩ _/し' //. V`Д´)/ (_フ彡 / ←>>1NullPointerExceptionが起きる
10©2022 Loglass Inc.と思いきやKotlinはコンパイルで落ちる
11©2022 Loglass Inc.Kotlinではデフォルトで引数や変数にnullを渡すことができないKotlinのNull Safety
12©2022 Loglass Inc.Kotlinではデフォルトで引数や変数にnullを渡すことができないKotlinのNull Safety
13©2022 Loglass Inc.実装ミスをコンパイル時に気づけると何がいいのか?● 実行時に気付くバグと比較して ...○ 全ての分岐を実行しなくてもよく 必ず実装ミスに気づくことができる○ テストカバレッジ100%は現実的ではない
14©2022 Loglass Inc.では、なぜKotlin?● Kotlinは型に関する機能が豊富○ Generics○ 型推論○ Smart Cast○ 代数的データ型○ 高階関数やラムダ○ Delegation などなど
15©2022 Loglass Inc.要するに、、Kotlinを駆使して、実装ミスをコンパイルで気付けるようにしよう!!!
16©2022 Loglass Inc.型によってバグが減らせるパターン● なぜ型を使いこなすことでバグが減るのか?● 型によってバグが減らせるパターン1. 標準の型をラップする2. 認可処理などの特定の処理をパスしたことを型で示す3. 型でデータの不整合をなくす● まとめ● 最後に: タイプセーフなポストモーテム
17©2022 Loglass Inc.標準の型をラップする● なぜ型を使いこなすことでバグが減るのか?● 型によってバグが減らせるパターン1. 標準の型をラップする2. 認可処理などの特定の処理をパスしたことを型で示す3. 型でデータの不整合をなくす● まとめ● 最後に: タイプセーフなポストモーテム
18©2022 Loglass Inc.標準の型をラップする● よくあるミス: 引数のIDを取り間違えてしまった
19©2022 Loglass Inc.標準の型をラップする● よくあるミス: 引数のIDを取り間違えてしまった
20©2022 Loglass Inc.Stringをラップする
21©2022 Loglass Inc.TaskIdとUserIdを区別することでコンパイル時に実装ミスに気付ける
22©2022 Loglass Inc.TaskIdとUserIdを区別することでコンパイル時に実装ミスに気付ける
23©2022 Loglass Inc.ちなみに: ラップすることによるオーバーヘッドを避ける● Stringをクラスでラップするとヒープ領域のオーバーヘッドが増える● inline classesという機能を使うことで String型として扱えてスタック領域だけの割り当てで済む
24©2022 Loglass Inc.ここで一つ疑問が全てのIDに別々の型をつけるの面倒ではないか?
25©2022 Loglass Inc.タイプセーフにしたいが、型の増加を抑制したい=> Genericsを使おう!Genericsでさらに汎用的にする
26©2022 Loglass Inc.Genericsでさらに汎用的にする
27©2022 Loglass Inc.Genericsでさらに汎用的にする
28©2022 Loglass Inc.Genericsでさらに汎用的にする● 型を増やさずに、タスク IDとユーザーIDを区別できた!
29©2022 Loglass Inc.Genericsでさらに汎用的にする● 型を増やさずに、タスク IDとユーザーIDを区別できた!
30©2022 Loglass Inc.Genericsでさらに汎用的にする● 型を増やさずに、タスク IDとユーザーIDを区別できた!
31©2022 Loglass Inc.Genericsでさらに汎用的にする● 型を増やさずに、タスク IDとユーザーIDを区別できた!
32©2022 Loglass Inc.Genericsでさらに汎用的にする● 型を増やさずに、タスク IDとユーザーIDを区別できた!
33©2022 Loglass Inc.標準の型をラップする : まとめ標準の型をラップして取り間違いを防ぐ型の増加をGenericsで抑制する
34©2022 Loglass Inc.認可処理などの特定の処理をパスしたことを型で示す● なぜ型を使いこなすことでバグが減るのか?● 型によってバグが減らせるパターン1. 標準の型をラップする2. 認可処理などの特定の処理をパスしたことを型で示す3. 型でデータの不整合をなくす● まとめ● 最後に: タイプセーフなポストモーテム
35©2022 Loglass Inc.認可処理などの特定の処理をパスしたことを型で示す● よくあるミス: 認可されていないIDで参照をしてしまった
36©2022 Loglass Inc.認可処理などの特定の処理をパスしたことを型で示す● よくあるミス: 認可されていないIDで参照をしてしまった
37©2022 Loglass Inc.認可処理などの特定の処理をパスしたことを型で示す● よくあるミス: 認可されていないIDで参照をしてしまった
38©2022 Loglass Inc.「認可された」ID型を導入する● 認可されたID型として AuthorizedTaskId を定義する
39©2022 Loglass Inc.「認可された」ID型を導入する● TaskAuthChecker だけが返す特別な型とする
40©2022 Loglass Inc.「認可された」ID型を導入する● TaskAuthChecker だけが返す特別な型とする
41©2022 Loglass Inc.「認可された」ID型を導入する● findTaskByIdはAuthorizedTaskIdしか受け付けないように修正
42©2022 Loglass Inc.「認可された」ID型を導入する● findTaskByIdはAuthorizedTaskIdしか受け付けないように修正
43©2022 Loglass Inc.「認可された」ID型を導入する● 認可処理を通過していない IDはコンパイル時に落ちるようになる
44©2022 Loglass Inc.「認可された」ID型を導入する● 認可処理を通過していない IDはコンパイル時に落ちるようになる
45©2022 Loglass Inc.「認可された」ID型を導入する● 認可処理を通過していない IDはコンパイル時に落ちるようになる
46©2022 Loglass Inc.「認可された」ID型を導入する● 認可処理を通過していない IDはコンパイル時に落ちるようになる
47©2022 Loglass Inc.しかし油断してはいけない● AuthorizedTaskIdのコンストラクタが公開されている!ここに public が潜んでいる
48©2022 Loglass Inc.しかし油断してはいけない● AuthorizedTaskIdのコンストラクタが公開されている!
49©2022 Loglass Inc.しかし油断してはいけない● 不正に認可されていない AuthorizedTaskIdが作成されてしまう
50©2022 Loglass Inc.しかし油断してはいけない● 不正に認可されていない AuthorizedTaskIdが作成されてしまう
51©2022 Loglass Inc.不正にインスタンスを作成されないためには?data classを使ってコンストラクタの可視性をコントロールする
52©2022 Loglass Inc.コンストラクタの可視性を data classで制御する
53©2022 Loglass Inc.コンストラクタの可視性を data classで制御する● インスタンスを作成できない interface AuthorizedTaskIdを定義するsealed を使えば同ファイルからしか継承できない
54©2022 Loglass Inc.コンストラクタの可視性を data classで制御する● コンストラクタ機能だけの data classをprivateで定義するprivate classは同ファイルからしか参照できない
55©2022 Loglass Inc.コンストラクタの可視性を data classで制御する● TaskAuthCheckerからのみprivate data classにアクセスする
56©2022 Loglass Inc.コンストラクタの可視性を data classで制御する● 別ファイルから AuthorizedTaskId インスタンスが生成できなくなった!
57©2022 Loglass Inc.コンストラクタの可視性を data classで制御する● 別ファイルから AuthorizedTaskId インスタンスが生成できなくなった!
58©2022 Loglass Inc.コンストラクタの可視性を data classで制御する● 別ファイルから AuthorizedTaskId インスタンスが生成できなくなった!
59©2022 Loglass Inc.コンストラクタをprivateにするのではダメ?● この場合のprivateは同じクラス ファイル のみアクセス可能という意味なので NG
60©2022 Loglass Inc.認可処理などの特定の処理をパスしたことを型で示す : まとめ処理が通過したことを型で示すコンストラクタを非公開にして不正にインスタンスを作らせない
61©2022 Loglass Inc.型でデータの不整合をなくす● なぜ型を使いこなすことでバグが減るのか?● 型によってバグが減らせるパターン1. 標準の型をラップする2. 認可処理などの特定の処理をパスしたことを型で示す3. 型でデータの不整合をなくす● まとめ● 最後に: タイプセーフなポストモーテム
62©2022 Loglass Inc.型でデータの不整合をなくす● よくあるミス: あり得ないデータを作成してしまった
63©2022 Loglass Inc.型でデータの不整合をなくす● タスクには以下のステータスがある● 完了ステータスのときのみ完了時刻を持つ
64©2022 Loglass Inc.型でデータの不整合をなくす● しかし間違えて着手時に完了時刻を入れてしまった● →着手中なのに完了時刻を持つという不整合データが誕生
65©2022 Loglass Inc.型でデータの不整合をなくす● しかし間違えて着手時に完了時刻を入れてしまった● →着手中なのに完了時刻を持つという不整合データが誕生
66©2022 Loglass Inc.データ不整合をなくすには?sealed classを使ってデータの不整合が起こりえない型を定義する
67©2022 Loglass Inc.sealed class を導入する● 完了の時のみ完了時刻をもつというデータ構造を sealed classで定義
68©2022 Loglass Inc.sealed class を導入する● sealed class + object, data classのパターンはEnumのように扱える
69©2022 Loglass Inc.sealed class を導入する● Enumだとこうなる● objectはEnumとほぼ同じ扱いで、data classはEnum + 構造体というような振る舞い
70©2022 Loglass Inc.sealed class を導入する● Task型からnullableなcompletedAtプロパティが消える
71©2022 Loglass Inc.sealed class を導入する● 着手中時に完了時刻を渡せなくなった
72©2022 Loglass Inc.sealed class を導入する● 着手中時に完了時刻を渡せなくなった
73©2022 Loglass Inc.sealed class を導入する● 完了時には完了時刻の入力を強制できる
74©2022 Loglass Inc.sealed class を導入する● 完了時には完了時刻の入力を強制できる
75©2022 Loglass Inc.OSS: kotlin-resultの事例● 成功値かエラー値かどちらかの値をとる Result型を提供するOSSkotlin-result : https://github.com/michaelbull/kotlin-result
76©2022 Loglass Inc.OSS: kotlin-resultの事例● 成功値かエラー値かどちらかの値をとる Result型を提供するOSSkotlin-result : https://github.com/michaelbull/kotlin-result
77©2022 Loglass Inc.OSS: kotlin-resultの事例● NGパターン: nullableな value と errorを持っているわけではないことに注意
78©2022 Loglass Inc.型でデータ不整合をなくす : まとめsealed classを使ってデータの不整合が起こりえない型を定義する
79©2022 Loglass Inc.まとめ● なぜ型を使いこなすことでバグが減るのか?● 型によってバグが減らせるパターン1. 標準の型をラップする2. 認可処理などの特定の処理をパスしたことを型で示す3. 型でデータの不整合をなくす● まとめ● 最後に: タイプセーフなポストモーテム
80©2022 Loglass Inc.まとめ● なぜ型を使いこなすことでバグが減るのか?○ →コンパイル時に実装ミスを検出できるから○ →Kotlinの型パワーを使ってコンパイル時により多くのミスを検出する
81©2022 Loglass Inc.まとめ● 型によってバグが減らせるパターン1. 標準の型をラップする→ genericsで型の増加を抑制しよう2. 認可処理などの特定の処理をパスしたことを型で示す→ コンストラクタの可視性を data classを使ってコントロールしよう3. 型でデータの不整合をなくす→ sealed classでデータの不整合をなくそう
82©2022 Loglass Inc.最後に: タイプセーフなポストモーテム● なぜ型を使いこなすことでバグが減るのか?● 型によってバグが減らせるパターン1. 標準の型をラップする2. 認可処理などの特定の処理をパスしたことを型で示す3. 型でデータの不整合をなくす● まとめ● 最後に: タイプセーフなポストモーテム
83©2022 Loglass Inc.最後に: タイプセーフなポストモーテム情報漏洩が起きました!再発防止策は?
84©2022 Loglass Inc.最後に: タイプセーフなポストモーテム- レビュワーを2人に増やす- 偉い人のチェックを増やす- GitHubのPRテンプレにチェック項目を足す- テスト工数を2倍にする etc…
85©2022 Loglass Inc.最後に: タイプセーフなポストモーテムそれ、型で解決できませんか?
86©2022 Loglass Inc.最後に: タイプセーフなポストモーテム● 障害のたびにテスト工数を増やしたり、「儀式」を追加するとスピードが落ちる● 最優先で考えるべき再発防止策はコンパイル時に障害のタネを気付けるようにすること● Kotlinはそのための機能をたくさん有している
87