Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Refactoring point of Kotlin application

3,864 views

Published on

Kotlin Fest 2018の発表資料です

Published in: Engineering
  • Be the first to comment

Refactoring point of Kotlin application

  1. 1. Kotlinアプリの リファクタリングポイント Naoto Nakazato @ Kotlin Fest 2018
  2. 2. 自己紹介 ● Naoto Nakazato ○ Android開発歴 8年くらい ● Recruit Lifestyle ○ 2017年6月〜 ○ HOT PEPPER Beauty ● アカウント ○ Twitter: @oxsoft ○ Facebook: naoto.nakazato ○ GitHub: oxsoft ○ Qiita: oxsoft
  3. 3. KotlinとAndroid 普段Androidアプリ開発してるって人はどれくらいいますか?
  4. 4. プロダクトの紹介 今日お話しすること
  5. 5. ホットペッパービューティー ホットペッパービューティーは、 国内最大級のサロン検索・予約サービス ヘアサロン以外にも、 ネイル、まつげ、リラクゼーション、エステがあります
  6. 6. ホットペッパービューティー 100% Kotlin化したAndroidアプリ 新機能開発とリファクタリングを繰り返す毎日 Kotlinの新しい書き方や機能の使いどころを日々発見
  7. 7. 今日お話しすること 機能はそのままに「より良い」Kotlinコードにリファクタリングする Tipsを8個紹介します 「より良い」の定義は好みがあるので、 チームで話し合うきっかけになれば嬉しいです
  8. 8. #01 Gradle Kotlin DSL
  9. 9. Gradle Kotlin DSLとは? GradleのビルドスクリプトをGroovyではなくKotlinで書ける build.gradleではなくbuild.gradle.ktsというファイル名にする
  10. 10. 何が良いのか? 俺たちは雰囲気でGroovyをやっている ↓ 補完やコードジャンプも効いてよく理解できる
  11. 11. 個人的なオススメ  新規プロジェクトならbuild.gradle.ktsで  既存プロジェクトの書き換えは「時間があれば」  補助的なタスクを別ファイルで追加するのがオススメ
  12. 12. 例:未使用のdrawableを削除 未使用のdrawableを削除するタスクを考える ● Gradleコマンドで実行可能なタスク ● git grep して使用しているかどうかチェック ● build.gradleとは別のファイル(kts)として定義
  13. 13. タスクの定義 cleaner.gradle.ktsというファイルを作成 ● this は Project で、task という拡張関数がある ● Task の doLast でタスク実行時に行う処理を記述する task("タスク名") { doLast { // タスク実行時に行う処理 } }
  14. 14. 関数の定義 /** [target]をgit grepして何ファイルに使われているか返します */ fun countUsage(target: String): Int { val byteArrayOutputStream = ByteArrayOutputStream() exec { executable = "sh" args("-c", "git grep -l '$target' | wc -l") standardOutput = byteArrayOutputStream } return byteArrayOutputStream.toString().trim().toIntOrNull() ?: 0 }
  15. 15. 再帰的にチェック&削除 task("removeUnusedDrawables") { doLast { File(rootDir.absolutePath + "/app/src/main/res/").walkTopDown().filter { it.isFile }.filter { it.parent.contains("drawable") }.filter { it.name.endsWith(".png") || it.name.endsWith(".xml") }.forEach { val name = it.name.substringBefore(".") val count1 = countUsage("@drawable/$name") val count2 = countUsage("R.drawable.$name") if (count1 == 0 && count2 == 0) { it.delete() } } } }
  16. 16. build.gradleでimport build.gradleに以下のように書けばimportできる Gradleタスクとして実行可能に apply from: rootProject.file('cleaner.gradle.kts') ./gradlew removeUnusedDrawables
  17. 17. まとめ ● Gradle Kotlin DSLを使うと補完も効いてメンテナンス性UP ● シェルスクリプトなどでやっていたようなタスクを Gradle Kotlin DSLで別ファイルに記述すると導入しやすい
  18. 18. #02 処理の共通化
  19. 19. 重複した処理 重複した処理は生まれやすいのでリファクタリングポイント 処理が重複していると、 ● 修正箇所が増える ● 一部を修正し忘れる
  20. 20. 処理を共通化する 「処理が重複していたら必ず共通化せよ」というわけでないが、 処理を共通化する時にどういう選択肢があるか整理してみる
  21. 21. 処理を共通化する 1. abstract class 2. interfaceのデフォルト実装 3. class delegation 4. property delegation 5. 拡張関数 6. トップレベル関数
  22. 22. abstract class vs. interface abstract class interface 状態 持てる 持てない 継承元 classとinterface interfaceのみ 多重継承 できない できる class method default method final できる できない protected できる できない
  23. 23. abstract class vs. interface abstract classが良いケース ● onCreateなどの継承部分で処理を共通化したい ● クラス外から呼ばれたくない ● 子クラスでoverrideされたくない
  24. 24. abstract class vs. interface interfaceのデフォルト実装が良いケース ● ベースクラスの肥大化を防ぎたい ● 多重継承したい interfaceのデフォルト実装であっても、 class delegationを使えば状態を持つことができる
  25. 25. class delegation interfaceの実装を別クラスに委譲することができる これによりinterfaceの特性を活かしつつ状態を持つことができる class UserModel() : DefaultImpl by State() { ... } class UserModel(s: State) : DefaultImpl by s { ... }
  26. 26. 状態を持ちつつも、一部の処理は子クラスで実装させたい場合 interface IState { var state: String } interface DefaultImpl : IState { fun abstract(): String fun default() { ... } } class State : IState { override var state: String = "" } class UserModel : DefaultImpl, IState by State() { override fun abstract(): String { return "concrete" } } IState State DefaultImpl UserModel 状態をインタフェースとして切り出し、 状態を実装したクラスを用意する
  27. 27. property delegation プロパティのgetter(とsetter)を別のクラスに委譲できる var userId: String by MyClass()
  28. 28. property delegation 値のように扱うことができるものはproperty delegationが向いている ● Intentのextra ● Fragmentのarguments ● savedInstanceState ● SharedPreferences ● FirebaseRemoteConfig
  29. 29. 拡張関数 クラスに関数を追加する(ように見せる)ことができる そうすると、どこからでも呼び出せるようになる fun Any?.log() { Log.d("DEBUG", this.toString()) } fun foo() { 123.log() "hello".log() }
  30. 30. 拡張関数 公式でありそうな名前なのに雑に実装しないように注意 fun String.isInteger() = this.all { it in '0'..'9' } ↑空文字や負の数が考慮されていない
  31. 31. 拡張関数 一般性がない・局所的にしか使わないメソッドも注意 fun String.decorate() = "_人人人人人_n" + "> $this <n" + " ̄Y^Y^Y^Y^Y ̄" ↑”decorate”の意味が広く、  文字幅も考慮されていない
  32. 32. 拡張関数 interfaceのデフォルト実装でスコープを限定すると乱用が防げる interface BookmarkableActivity { fun bookmark() { // 共通のブックマーク処理 } fun String.toLabel() = "★ $this" }
  33. 33. トップレベル関数 ファイルに直接関数を書くことができる こちらも、どこからでも呼び出すことができる fun throwOrLog(t: Throwable) { // 開発はクラッシュ、本番はログ送信 } fun foo() { throwOrLog(e) }
  34. 34. トップレベル関数 本当にどこからでも呼べるので、拡張関数以上に乱用に注意する 拡張関数と同様にinterfaceに書くとスコープを制限できるが、 それはすなわちinterfaceのデフォルト実装のこと
  35. 35. まとめ ● 重複する処理をまとめる方法はいくつかの選択肢がある ● 状態を持つかどうか、スコープは適切かなどによって使い分ける ● 拡張関数やトップレベル関数は強力だが乱用に注意する
  36. 36. #03 Mutableを避ける
  37. 37. val と var val はgetterのみで再代入できない(read-only) var はgetterとsetterがあり再代入できる(mutable) val a = 0 var b = 0 a = 1 // NG b = 1 // OK
  38. 38. List と MutableList List は要素を変更するメソッドがない(read-only) MutableList は要素を変更するメソッドがある(mutable) val a = listOf(1, 2, 3) val b = mutableListOf(1, 2, 3) a.add(4) // NG b.add(4) // OK
  39. 39. なぜMutableを避けるのか ● 今どんな値が入っているかを常に考える必要がある ● 「この処理に入る時はこの値が入っているはず」 という暗黙の前提を生みやすくバグの温床となる ● (メンバ変数の var の場合)smart castが効かない
  40. 40. どうやってMutableを避けるか 1. その var は本当に var である必要があるか? 2. その MutableList は本当に MutableList である必要があるか? まずは1について考えます
  41. 41. プロパティの優先順位 コンパイル時からずっと不変だ const val インスタンス生成時に値が決まり不変だ ある時点を過ぎると値が確定し、不変だ ある時点を過ぎると値が確定し、可変だ しかしNullableにはしたくない val lazy lateinit var var YES NO ※companion objectSTART
  42. 42. プロパティの優先順位 ある時点を過ぎると値が確定し、不変だ ある時点を過ぎると値が確定し、可変だ しかしNullableにはしたくない ↓この辺について考えてみます
  43. 43. ある時点を過ぎると値が確定し、不変だ
  44. 44. ある時点を過ぎると値が確定し、不変だ ↓インスタンス生成時は intent が取れないのでクラッシュ class MainActivity : AppCompatActivity() { val articleId: String = intent.getStringExtra(ARTICLE_ID) }
  45. 45. ある時点を過ぎると値が確定し、不変だ 値は変わらないのに var で、しかもNullable class MainActivity : AppCompatActivity() { var articleId: String? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) articleId = intent.getStringExtra(ARTICLE_ID) } }
  46. 46. ある時点を過ぎると値が確定し、不変だ lazy で書くと val かつNonNullにできる class MainActivity : AppCompatActivity() { val articleId: String by lazy { intent.getStringExtra(ARTICLE_ID) } }
  47. 47. ある時点を過ぎると値が確定し、可変だ しかしNullableにはしたくない
  48. 48. ある時点を過ぎると値が確定し、可変だ しかしNullableにはしたくない 実質的にはNonNullなのに、Nullableになってしまう class MainActivity : AppCompatActivity() { var defaultQuery: String? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) defaultQuery = intent.getStringExtra(DEFAULT_QUERY) } }
  49. 49. ある時点を過ぎると値が確定し、可変だ しかしNullableにはしたくない lateinitを用いるとNonNullとして扱うことができる class MainActivity : AppCompatActivity() { lateinit var defaultQuery: String override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) defaultQuery = intent.getStringExtra(DEFAULT_QUERY) } }
  50. 50. lateinit ではなくNullableにすべきケース onCreate や onCreateView よりも後で値が決まる場合は、 lateinit ではなく素直にNullableにするべき lateinit var foo: String var foo: String? = null OR
  51. 51. lateinit ではなくNullableにすべきケース 通信後に代入する値を lateinit にした場合、 通信中や通信エラー時にタップするとクラッシュする lateinit var article: Article fun init() { fetchArticle().subscribe { article -> this.article = article } button.setOnClickListener { textView.text = article.title } }
  52. 52. (おまけ) var と MutableList
  53. 53. var と MutableList var と MutableList の併用は基本的には避けられるはず 少なくとも以下のいずれかに MutableList を回避する方法はこの後のCollectionのセクションで var foo: MutableList<Int> = …… val foo: MutableList<Int> = …… var foo: List<Int> = ……
  54. 54. まとめ ● メンテナンス性向上のために、避けられるMutableは極力避ける ● どの時点で値が初期化できるか、その後可変かどうかに着目する
  55. 55. #04 Collection
  56. 56. Collectionについて 簡単におさらい
  57. 57. KotlinにおけるCollection Iterable Collection Set List Map Mutable Iterable Mutable Collection Mutable Set Mutable List Mutable Map
  58. 58. その他のIterableまわりクラス Iterable Sequence IntProgression ClosedRange IntRange
  59. 59. ListとMutableList 実体はどちらもArrayList val list1 = listOf(1, 2, 3) val list2 = mutableListOf(1, 2, 3) println(list1::class.java.simpleName) // ArrayList println(list2::class.java.simpleName) // ArrayList
  60. 60. ImmutableとRead Only ListはRead OnlyであってImmutableではない val mutableList = mutableListOf(1, 2, 3) val list: List<Int> = mutableList println(list) // [1, 2, 3] mutableList.add(4) println(list) // [1, 2, 3, 4]
  61. 61. ListとMutableListの共変性 List<out E> は共変だが、MutableList<E> は不変 val intList: List<Int> = listOf(1, 2, 3) val anyList: List<Any> = intList // OK // anyListは読み取り専用なのでStringなどを追加できない val intMutableList: MutableList<Int> = mutableListOf(1, 2, 3) val anyMutableList: MutableList<Any> = intMutableList // Type mismatch! anyMutableList.add("String") // ここのミスマッチを事前に阻止している
  62. 62. 生成方法 基本的にはlistOf、setOf、mapOf 取り出し順序やパフォーマンスを気にする場合は適切なクラスを使う setOf(1, 2, 3) // LinkedHashSet linkedSetOf(1, 2, 3) // LinkedHashSet hashSetOf(1, 2, 3) // HashSet sortedSetOf(1, 2, 3) // TreeSet
  63. 63. Collectionを活かした書き方に リファクタリング
  64. 64. 例1:2つのリストを同時に扱う
  65. 65. 2つのリストを同時に扱う (例)対応関係のある名前と年齢のリストをそれぞれ出力 val nameList = listOf("Alice", "Bob", "Charlie") val ageList = listOf(20, 25, 30) Alice(20), Bob(25), Charlie(30)
  66. 66. 2つのリストを同時に扱う val nameList = listOf("Alice", "Bob", "Charlie") val ageList = listOf(20, 25, 30) val size = minOf(nameList.size, ageList.size) for (i in 0 until size) { val name = nameList[i] val age = ageList[i] println("$name($age)") }
  67. 67. 2つのリストを同時に扱う:zip val nameList = listOf("Alice", "Bob", "Charlie") val ageList = listOf(20, 25, 30) nameList.zip(ageList) { name, age -> println("$name($age)") }
  68. 68. 例2:変換と除外
  69. 69. 変換と除外 (例)IDのリストをenumに変換(enumに存在しない場合は捨てる) val ids = listOf("apple", "orange", "carrot", "banana") [Fruit.APPLE, Fruit.ORANGE, Fruit.BANANA]
  70. 70. 変換と除外 val ids = listOf(...) val fruits = mutableListOf<Fruit>() for (id in ids) { for (fruit in Fruit.values()) { if (fruit.id == id) { fruits.add(fruit) } } } return fruits
  71. 71. 変換と除外:map,filter 変換と除外をしているので、map と filter を使う return ids.filter { id -> Fruit.values().any { it.id == id } }.map { id -> Fruit.values().find { it.id == id }!! }
  72. 72. 変換と除外:mapNotNull 変換と除外を同時に行うには、mapNotNull も有効 return ids.mapNotNull { id -> Fruit.values().find { it.id == id } }
  73. 73. 例3:リストをN個ずつに分割
  74. 74. リストをN個ずつに分割 (例)A~Zのリストを10個ずつに分割する val idList = 'A'..'Z' // ……何らかの処理…… return // [[A,B,……,J],[K,L,……,T],[U,V,……,Z]]
  75. 75. リストをN個ずつに分割 val idList = 'A'..'Z' val charList = mutableListOf<MutableList<Char>>() val count = idList.count() for (i in 0 until count) { if (i % 10 == 0) { charList.add(mutableListOf()) } charList.last().add(idList.elementAt(i)) } return charList // [[A,B,……,J],[K,L,……,T],[U,V,……,Z]]
  76. 76. リストをN個ずつに分割:groupBy など インデックスを付けてグループ分けする val idList = 'A'..'Z' return idList .withIndex() .groupBy({ it.index / 10 }, { it.value }) .values A, B, C, … (0, A), (1, B), (2, C), … {0:[A, B, C, …], 1:[K, …], …} [[A, B, …, J], [K, L, …], …]
  77. 77. その他:all と any と none
  78. 78. all と any と none 否定「!」を使わずに書く方法を考える !list.any { it != 0 } list.none { it != 0 } list.all { it == 0 } !list.all { it != 0 } !list.none { it == 0 } list.any { it == 0 } list.all { it != 0 } !list.any { it == 0 } list.none { it == 0 }
  79. 79. all と any と none 否定「!」を使わずには書けない場合もある 例:「どれかが 0 じゃなければ true」 !list.all { it == 0 } list.any { it != 0 } !list.none { it != 0 }
  80. 80. まとめ ● index でアクセスしたり MutableList を使ったりしていたら、 Collectionのメソッドで簡潔に書けるかも ● map と filter を同時に行う場合は mapNotNull も有効 ● all と any と none は積極的に使い分ける
  81. 81. #05 DSLの拡張
  82. 82. DSLとは Domain Specific Languageの略 KotlinのDSLは内部DSLとも呼ばれ、Kotlinの言語機能で実現できる 構造的なデータの宣言や設定値の記述などが簡潔にできる
  83. 83. DSLの例 ● Gradle Kotlin DSL ○ https://github.com/gradle/kotlin-dsl ● kotlinx.html ○ https://github.com/Kotlin/kotlinx.html ● Anko ○ https://github.com/Kotlin/anko ● Spek ○ https://github.com/spekframework/spek
  84. 84. クラスの拡張 クラスの場合は継承によって既存クラスを拡張できる open class Animal { fun foo() {} } class Dog : Animal() { fun bar() {} } fun zoo() { val dog = Dog() dog.foo() // 元の機能も使えて dog.bar() // 機能を拡張できる }
  85. 85. DSLの拡張 通常のクラスと同じようにDSLの機能を拡張したい場合 animal { foo() } dog { foo() // 元の機能も使えて bar() // 機能を拡張するには? }
  86. 86. DSLの拡張 状態を持たないならレシーバに拡張関数を追加する 状態を持つならレシーバを委譲で拡張したクラスを作成する fun animal(block: Animal.() -> Unit) { // ... } レシーバ
  87. 87. レシーバに拡張関数を追加 特に論点なし fun Animal.custom() { // ... } animal { custom() }
  88. 88. レシーバを委譲で拡張 レシーバを受け取って委譲し、関数を追加する class Dog(private val animal: Animal) { fun foo() = animal.foo() fun bar() {} } fun dog(block: Dog.() -> Unit) = animal { Dog(this).block() }
  89. 89. レシーバがinterfaceの場合 レシーバがinterfaceの場合はclass delegationを使うと楽 class Dog(animal: Animal) : Animal by animal { fun bar() {} }
  90. 90. レシーバをそのまま継承する場合 インスタンスの状態に不整合が起きる可能性がある fun animal(block: Animal.() -> Unit) { val animal = Animal() animal.init() animal.block() } fun dog(block: Dog.() -> Unit) = animal { Dog().block() // Dog is not initialized! }
  91. 91. レシーバをそのまま継承する場合 インスタンスを差し替えて同様の処理が書けるようなら大丈夫 fun animal(block: Animal.() -> Unit) { val animal = Animal() animal.init() animal.block() } fun dog(block: Dog.() -> Unit) { val dog = Dog() dog.init() dog.block() }
  92. 92. まとめ ● DSLは委譲によって拡張した方が良さそう ● レシーバがinterfaceの場合はclass delegationを使うと良い
  93. 93. #06 Nullability
  94. 94. Nullable と NonNull KotlinといえばNullabilityが嬉しい val nullable: String? = null // OK val nonNull: String = null // NG nullable.length // NG nullable!!.length // OK nullable?.length // OK nonNull.length // OK
  95. 95. Nullableをどこで変換するか APIがNullableで返してくる場合、 UIに渡す途中のどこまでNullableにするか Infra Domain UI Nullable Nullable or NonNull or Exception Nullable or NonNull or Exception
  96. 96. Nullableのまま渡すと 至る所でnullチェックやsafe callするハメになる → 実質的には条件分岐が増え、挙動把握が困難になる → 「 null って何だっけ?」を毎回考えることになる return team?.user?.name?.length
  97. 97. NonNullにするため無効なデータを入れると 無効なデータかチェックするハメになる → チェックを忘れて表示崩れが起きる if (user.name.isNotEmpty()) { // ↑しんどい or 忘れる }
  98. 98. nullは何を表している? 「nullが何を表しているか」を考える 1. 滅多に起きないエラーケース 2. 任意項目であり、「存在しない」を表していることが自明 3. 自明じゃない何か
  99. 99. 滅多に起きないエラーケース InfraでExceptionに変換する return name ?: throw NameNotFoundException
  100. 100. 任意項目であり、 「存在しない」を表していることが自明 UI層までNullableで渡す 「N/A」などと表示する場合 → UI層でその文字列に変換する 見た目が大きく変わる場合 → UI層でnullチェックして分岐する class ViewModel(user: User) { val name = user.name ?: "N/A" }
  101. 101. 自明じゃない何か sealed classを作成し、その状態に意味付けを行う sealed class User { data class LoginUser(val name: String) : User() object AnonymousUser : User() } return if (name != null) User.LoginUser(name) else User.AnonymousUser
  102. 102. まとめ ● Nullableばっかり、NonNullばっかりだと混乱が生まれやすい ● 「nullが何を表しているか」を考えて適切に変換する
  103. 103. #07 スコープ関数
  104. 104. スコープ関数 let/run/also/apply/withの5つがあるが、 withを除くとザックリ以下のように分類できる it this 結果 let run 自身 also apply 戻り値 レシーバ戻り値 = 自身.スコープ関数 { レシーバ.method() 結果 }
  105. 105. キャプチャする
  106. 106. キャプチャする null判定時の値をそのまま使えるため、smart castが効かない時に有効 if (nullable != null) { foo(nullable!!) } nullable?.let { foo(it) }
  107. 107. キャプチャする 単にif文の代わりとして使っても便利 str?.let { // strがnullじゃない場合 } val r = str ?: run { // strがnullの場合 }
  108. 108. キャプチャする ただし、if文と全く同じというわけではない str?.let { // A } ?: run { // B } if (str != null) { // A } else { // B }
  109. 109. キャプチャする 良くない例 data だけでなく listener が null の場合も showNoDataMessage() が呼ばれる data?.let { listener?.onClickItem(it) } ?: showNoDataMessage()
  110. 110. 処理をまとめる
  111. 111. 処理をまとめる 初期化処理などをまとめる val intent = Intent().apply { putExtra("key1", "value1") putExtra("key2", "value2") putExtra("key3", "value3") }
  112. 112. 処理をまとめる ローカル変数の方が優先なので注意が必要 (特にプロパティアクセス形式) ※我々のチームでは let と also だけ使うようにしています var text = "" // この行があると val button = Button(context).apply { text = "hello" } button.text // "hello"にならない
  113. 113. 処理をまとめる:補足 DSLでもローカル変数に関しては同じ問題はある ただし通常のメンバーは内側のブロックから順番に解決される また親のメンバーへのアクセスは @DslMarker を使うと禁止できる div { a("http://kotlinlang.org") { target = ATarget.blank +"Main site" } } div { var target = "" a("http://kotlinlang.org") { target = ATarget.blank +"Main site" } }
  114. 114. まとめ ● スコープ関数で値をキャプチャしたり処理をまとめたりできる ● let/elvis は if/else と等価ではない ● スコープ外の呼び出しに注意する
  115. 115. #08 データ構造と状態数
  116. 116. 肥大化しがちなdata class プロダクトの核となるdata classは肥大化していきがち ホットペッパービューティーの場合だと、 検索条件を表すクラスや、予約情報を表すクラス 【検索条件】エリア、駅、緯度経度、メニュー、料金、検索クエリ、特集、日付、時刻、 etc…… 【予約情報】サロン情報、来店日時、施術時間、利用クーポン、指名スタイリスト、金額、       利用ポイント、加算予定ポイント、要望、 etc……
  117. 117. そのままdata classにすると 検索条件はこんな感じに → data class SearchCondition( val area: Area?, val station: Station?, val location: Location?, val query: String?, val menu: Menu?, val price: Price? // ... )
  118. 118. 「ありえない状態」ができてしまう 【ビジネスロジック】 ● エリア/駅/緯度経度は排他 ○ でもいずれか1つは必須 ● 料金はメニュー指定時のみ ● etc…… data class SearchCondition( val area: Area?, val station: Station?, val location: Location?, val query: String?, val menu: Menu?, val price: Price? // ... )
  119. 119. 「ありえない状態」を避ける 使う時に毎回「ありえない状態」の考慮をするのは不毛 「ありえないはず」という暗黙の前提が増えるとバグの温床 「ありえない状態」を避ける方法は何があるか?
  120. 120. クラス化するほど関連がある場合 (例)sealed class で「エリアか駅か緯度経度」を表現する sealed class Place { class Area class Station class Location } data class SearchCondition( // ... val place: Place // ... )
  121. 121. クラス化するほど関連がある場合 (例)data class で「料金はメニュー指定時のみ」を表現する data class MenuPrice( val menu: Menu, val price: Price? ) data class SearchCondition( // ... val menuPrice: MenuPrice? // ... )
  122. 122. クラス化するほど関連がない場合 EitherやPairなどの一般的なクラスで表現する sealed class → Either data class → Pair
  123. 123. Eitherを定義する Eitherとは、LeftかRightの値を保持するクラス sealed class Either<out L, out R> { data class Left<out L>(val value: L) : Either<L, Nothing>() data class Right<out R>(val value: R) : Either<Nothing, R>() }
  124. 124. Λrrow 関数型っぽい書き方がKotlinでもできるようにするためのライブラリ https://arrow-kt.io Eitherも定義されている
  125. 125. もう少し一般化 毎回色々と考えるのは大変 ↓ 状態数(そのクラスがとりうる状態の合計数) を考えてから実装に落とす
  126. 126. 状態数 Kotlin 状態数 Nothing 0 Unit 1 Boolean 2 enum 要素数
  127. 127. 状態数 Kotlin 状態数 A? A + 1 Either<A, B> A + B Pair<A, B> A * B
  128. 128. Kotlin 状態数 Set<A> 2A Map<A, B> (B+1)A List<A> (An+1 -1)/(A-1) 状態数 ※厳密には違う部分もありますがそこはご容赦
  129. 129. 状態数が等しいものの例 Either<Boolean, Boolean> Pair<Boolean, Boolean> Set<Boolean> Boolean Unit? Either<Unit, Unit> Set<A> Map<A, Unit>
  130. 130. 状態数の分析 1. Kotlinを数式に変換する 2. 数式を展開する 3. ありえない項を削除する 4. 残った項をまとめる 5. 数式をKotlinに戻す
  131. 131. 状態数の分析 1. Kotlinを数式に変換する 2. 数式を展開する 3. ありえない項を削除する 4. 残った項をまとめる 5. 数式をKotlinに戻す data class Target( val a: A?, val b: B?, val c: C? ) (A + 1)(B + 1)(C + 1)
  132. 132. 状態数の分析 1. Kotlinを数式に変換する 2. 数式を展開する 3. ありえない項を削除する 4. 残った項をまとめる 5. 数式をKotlinに戻す ABC + AB + BC + CA + A + B + C + 1 (A + 1)(B + 1)(C + 1)
  133. 133. 状態数の分析 1. Kotlinを数式に変換する 2. 数式を展開する 3. ありえない項を削除する 4. 残った項をまとめる 5. 数式をKotlinに戻す AB + A + C + 1 ABC + AB + BC + CA + A + B + C + 1 【ビジネスロジック】
  134. 134. 状態数の分析 1. Kotlinを数式に変換する 2. 数式を展開する 3. ありえない項を削除する 4. 残った項をまとめる 5. 数式をKotlinに戻す A(B + 1) + C + 1 AB + A + C + 1
  135. 135. 状態数の分析 1. Kotlinを数式に変換する 2. 数式を展開する 3. ありえない項を削除する 4. 残った項をまとめる 5. 数式をKotlinに戻す A(B + 1) + C + 1 data class Target( val abc: Either<Pair<A, B?>, C>? )
  136. 136. data class Target( val abc: Either<Pair<A, B?>, C>? )
  137. 137. 状態数の分析 1. Kotlinを数式に変換する 2. 数式を展開する 3. ありえない項を削除する 4. 残った項をまとめる 5. 数式をKotlinに戻す 6. 適宜 sealed class や data class を使って意味付けをする data class Target( val abc: Either<Pair<A, B?>, C>? )
  138. 138. 状態数の分析 1. Kotlinを数式に変換する 2. 数式を展開する 3. ありえない項を削除する 4. 残った項をまとめる 5. 数式をKotlinに戻す 6. 適宜 sealed class や data class を使って意味付けをする
  139. 139. まとめ ● クラスの肥大化で「ありえない状態」が生まれやすい ● 「ありえない状態」はバグの温床なので避ける ● 状態数を分析すると厳密に精査できる ● Either や Pair を使って状態を整理しつつ、 sealed class や data class で適宜意味付けをする
  140. 140. おわりに
  141. 141. まとめ 日々の開発で考えていることをまとめてみました リファクタリングする際の1つの指針になったら幸いです ご意見・ご感想お待ちしてます

×