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.

Kotlinアンチパターン

20,037 views

Published on

DroidKaigi2018の発表資料です。
https://droidkaigi.jp/2018/

Published in: Engineering
  • Be the first to comment

Kotlinアンチパターン

  1. 1. Kotlinアンチパターン Naoto Nakazato @DroidKaigi2018
  2. 2. 自己紹介 ● Naoto Nakazato ○ Android開発歴 7年くらい ● Recruit Lifestyle ○ 2017年6月〜 ○ HOT PEPPER Beauty ● アカウント ○ Twitter: @oxsoft ○ Facebook: naoto.nakazato ○ GitHub: oxsoft ○ Qiita: oxsoft
  3. 3. KotlinとAndroid 普段のアプリ開発でKotlin使ってますか?
  4. 4. KotlinとAndroid 2017年5月にGoogle I/Oで公式サポートが発表されたKotlin Javaよりもシンプルで安全なのは間違いない!けど…… ● 色々な書き方があって悩む ● 使い方次第ではバグが発生するかも
  5. 5. KotlinとAndroid 公式ドキュメントに「文法」は書いてあるが、 それ以外に「アンチパターン」のようなものがありそう このセッションでは、30分で10個紹介します!
  6. 6. 今日お話しすること
  7. 7. その前に……
  8. 8. ホットペッパービューティー ホットペッパービューティーは、 国内最大級のサロン検索・予約サービス ヘアサロン以外にも、 ネイル、まつげ、リラクゼーション、エステがあります
  9. 9. Androidアプリ 2010年 Androidアプリリリース 徐々に秘伝のタレ化 2016年05月 フルリニューアルを決意 2017年02月 フルKotlinに方針転換 2017年12月 リニューアル版リリース
  10. 10. 今日お話しすること KotlinでAndroidアプリを開発して感じたアンチパターン ● リリース済みの巨大なアプリのフルリニューアル ● メンバーのAndroid開発経験が様々 ● 最大11人という大人数のチーム開発 プロダクトや開発メンバーによって賛否両論ありそう →ブースでは実際のソースコードを展示しているので、  開発メンバーとディスカッションしに来てください!
  11. 11. 今日お話しすること 今回はアンチパターンを10個話しますが、 それぞれ以下の4つの構成で話していこうと思います ● 言語機能の説明 ● アンチパターン ● 何が良くないか ● 解決策の例 Kotlinを普段使っている人は、 「言語機能の説明」は聞かなくても良いかもしれないです
  12. 12. #01 lateinitとnull初期化
  13. 13. lateinitとは Javaでは、初期値がない変数宣言は null などが入る Kotlinでは、基本的に初期値を書かないといけない TextView message; // nullが入る var message: TextView // errorになる var message: TextView? = null // OK 言語機能の説明
  14. 14. lateinitとは Androidでは、onCreate以降で初期化するものが多い 実質NonNullなものが、全部Nullableになってしまう 言語機能の説明 var message: TextView? = null fun clear() { message!!.text = "" // 怪しい message?.text = "" // 面倒 }
  15. 15. lateinitとは そこでlateinitを使うと、宣言時に初期値を入れなくて良い ただし、値を代入していない状態で値を参照すると UninitializedPropertyAccessException が投げられる lateinit var message: TextView // OK 言語機能の説明
  16. 16. lateinitの使いどころ インスタンス作成時には値が定まらないが、 onCreateやonCreateViewで代入されるもの 例えば、 ● findViewByIdしたView ● DataBindingのbinding ● DaggerなどのDIによってinjectされるもの ただし、プリミティブ型やNullableには使えない 言語機能の説明
  17. 17. アンチパターン 通信後に得られる情報をlateinitにする lateinit var profile: Profile fun init() { fetchProfile().subscribe { profile -> this.profile = profile } }
  18. 18. 何が良くないか あらかじめリスナーをセットしている場合に、 通信中や通信エラー時にアクセスして、 UninitializedPropertyAccessException button.setOnClickListener { textView.text = profile.name }
  19. 19. 解決策の例 Nullableにして、 常に「情報未取得時どうするか」を考えさせる var profile: Profile? = null fun init() { fetchProfile().subscribe { profile -> this.profile = profile } }
  20. 20. 解決策の例 onCreate / onCreateView で初期化可能  →  lateinitで良さそう それ以降で値が決まる  → Nullableにする      or    メンバ変数にするのを避ける
  21. 21. isInitialized Kotlin 1.2から、isInitializedが追加された 値が代入されたかどうかを確認することができる lateinit var str: String fun foo() { val before = ::str.isInitialized // false str = "hello" val after = ::str.isInitialized // true } 補足
  22. 22. isInitialized 元々はテストコードでの利用を想定して追加された これを日常的に使うともはやnull安全じゃなくなるので、 プロダクションコードでの使用は避けたほうが良さそう 補足 参考:https://youtrack.jetbrains.com/issue/KT-9327 private lateinit var file: File @After fun tearDown() { // ファイル作成前にfailするとエラーになる file.delete() }
  23. 23. #02 スコープ関数
  24. 24. スコープ関数 let/run/also/apply/withの5つがあるが、 withを除くとザックリ以下のように分類できる it this 結果 let run 自身 also apply 戻り値 レシーバ 言語機能の説明 戻り値 = 自身.スコープ関数 { レシーバ.method() 結果 }
  25. 25. 使いどころ1 null関連の制御に便利 str?.let { // strがnullじゃない場合 } 言語機能の説明 val r = str ?: run { // strがnullの場合 }
  26. 26. 使いどころ2 初期化処理をまとめる 言語機能の説明 val intent = Intent().apply { putExtra("key", "value") putExtra("key", "value") putExtra("key", "value") }
  27. 27. アンチパターン apply内でプロパティアクセス形式の処理を書く val button = Button(context) button.text = "hello" // JavaのsetText(...)が呼ばれる // ... buttonの設定が続く ... val button = Button(context).apply { text = "hello" // ... buttonの設定が続く ... } ↓ apply を使って処理をまとめよう!
  28. 28. 何が良くないか ローカル変数を定義すると、アクセス先が変わる fun init() { var text = "" // 後々この行が追加されると…… val button = Button(context).apply { text = "hello" } button.text // "hello"にならない! }
  29. 29. 解決策の例1 apply 内ではプロパティアクセス形式を使わず、 通常の関数呼び出しにする ただし、ローカル関数が定義されると同じ問題が起きる val button = Button(context).apply { setText("hello") }
  30. 30. 解決策の例2 this 必須というルールにする alsoと似たような感じになってしまう val button = Button(context).apply { this.text = "hello" }
  31. 31. 解決策の例3 apply 禁止( also を使う) 我々のチームでは、let と also のみに限定 val button = Button(context).also { it.text = "hello" }
  32. 32. #03 Nullable と NonNull
  33. 33. Nullable と NonNull Nullable / NonNull はKotlinの大きな魅力の1つ 言語機能の説明 val nullable: String? = null // OK val nonNull: String = null // NG nullable.length // NG nullable!!.length // OK nullable?.length // OK nonNull.length // OK
  34. 34. アンチパターン1 Nullableのままデータを引き回す data class User( val id: Long? = null, val name: String? = null, val age: Int? = null ) API Domain UI Nullable Nullable
  35. 35. 何が良くないか1 Nullableのデータを引き回すと、 至る所でnullチェックやsafe callするハメになる → 実質的には条件分岐が増え、挙動把握が困難になる → 「 null って何だっけ?」を毎回考えることになる return team?.user?.name?.length
  36. 36. アンチパターン2 全てNonNullにするために無効なデータを入れる val response = ... return User( id = response.id ?: 0L, name = response.name.orEmpty(), age = response.age ?: 0 )
  37. 37. 何が良くないか2 全てNonNullにするために無効なデータを入れると…… → 無効なデータかどうかチェックするハメになる → チェックを忘れて表示崩れが起きる if (user.name.isNotEmpty()) { // ↑しんどい or 忘れる }
  38. 38. 解決策の例 「nullが何を表すか」で処理するレイヤを決める ● APIが返してくれないからnull → APIのレイヤで処理 ● ユーザーが設定してないからnull → UIのレイヤで処理 ● 自明じゃなくてレイヤをまたぐ → クラスで明確化 データ表現に困った時、安易にnullに頼らない (例外を投げる、別のクラスにする、など)
  39. 39. #04 data class
  40. 40. data classとは ● equals/hashCodeやtoStringをよしなにoverride ● componentNやcopyなどのメソッドを生成 言語機能の説明 data class User(val name: String, val age: Int) val alice = User("Alice", 27) alice == User("Alice", 27) // true alice.toString() // "User(name=Alice, age=27)" val (name, age) = alice val nextYear = alice.copy(age = 28)
  41. 41. アンチパターン インスタンス生成用のメソッドで制約を保証したい data class Range(val min: Int, val max: Int) { companion object { fun getRange(a: Int, b: Int): Range { return Range(minOf(a, b), maxOf(a, b)) } } }
  42. 42. 何が良くないか data classにはcopyメソッドが生成される → 任意のデータを持つインスタンスが生成可能 val range = Range.getRange(3, 5) val illegal = range.copy(max = 0) ↑ Range(min=3, max=0) となり、制約が壊れる
  43. 43. 解決策の例 値がまとまっているからといってdata classにしない 「値の内部表現」=「クラスに期待される振る舞い」 → 値のまとまりに名前を付けているだけ → data class 「値の内部表現」!=「クラスに期待される振る舞い」 → 通常のクラスが良さそう
  44. 44. #05 interfaceとabstract class
  45. 45. interfaceのデフォルト実装 Kotlinではinterfaceにデフォルト実装ができる Java8にもある機能だが、Androidで使うためには、 minSdkVersionを24以上にする必要がある 言語機能の説明 interface Downloadable { fun download() { // 共通の処理など } }
  46. 46. abstract classと何が違う? interface abstract class 状態 持てない 持てる 継承元 Interfaceのみ classとInterface 多重継承 できる できない default method class method final できない できる protected できない できる 言語機能の説明
  47. 47. アンチパターン Javaの頃から言われていたアンチパターンで、 処理を共通化するためだけにBaseクラスを肥大化させる abstract class BaseActivity : AppCompatActivity() { protected fun showNetworkError() { // エラー表示(エラーがないページもあるのに……) } }
  48. 48. 何が良くないか 何千行レベルの親クラスになり、手に負えなくなる
  49. 49. 解決策の例 Javaでは「継承よりも委譲」などと言われてきた Kotlinではインタフェースのデフォルト実装も良さそう interface NetworkErrorView { // エラー用のViewとリロード処理は子クラスで用意 val networkErrorView: View fun onClickReload() fun showNetworkError() { // リスナーのセット、エラーの表示などの共通処理 } }
  50. 50. 解決策の例 もちろん「Baseクラス=悪」ではない abstractクラスが有効な例 ● onCreateなどの継承部分で処理を共通化したい ● クラス外から呼ばれたくない ● 子クラスでoverrideされたくない
  51. 51. 解決策の例 デフォルト実装が有効な例 ● 前述以外で、処理を共通化させたい場合 ● 多重継承したい場合 状態を持ちたい場合も、class delegationを使うとできる class UserModel() : DefaultImpl by State() { ... } class UserModel(s: State) : DefaultImpl by s { ... }
  52. 52. 状態を持ちつつも、一部の処理は子クラスで実装させたい場合 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 状態をインタフェースとして切り出し、 状態を実装したクラスを用意する
  53. 53. #06 トップレベル関数と拡張関数
  54. 54. トップレベル関数 ファイルに直接関数を書くことができる そうすると、どこからでも呼び出すことができる fun throwOrLog(t: Throwable) { // 開発はクラッシュ、本番はログ送信 } fun foo() { throwOrLog(e) } 言語機能の説明
  55. 55. 拡張関数 クラスに関数を追加する(ように見せる)ことができる こちらも、どこからでも呼び出せるようになる fun Any?.log() { Log.d("DEBUG", this.toString()) } fun foo() { 123.log() "hello".log() } 言語機能の説明
  56. 56. アンチパターン1 一般性がない・局所的にしか使わないメソッドを追加する fun String.decorate() = "_人人人人人_n" + "> $this <n" + " ̄Y^Y^Y^Y^Y ̄" ↑”decorate”の意味が広く、  文字幅も考慮されていない
  57. 57. アンチパターン2 公式でありそうな名前なのに雑に実装する fun String.isInteger() = this.all { it in '0'..'9' } ↑空文字や負の数が考慮されていない
  58. 58. 何が良くないか 拡張関数が便利なので乱発しがち ライブラリよりもバグが混入している可能性が高い JavaでUtilクラスを作りまくるのと同種の問題だが、 Kotlinだと通常の関数っぽく見えるので被害が大きい 開発メンバーが多いと特に問題になる
  59. 59. 解決策の例 チーム内で拡張関数を作る基準・感覚を揃える Interfaceのデフォルト実装でスコープを限定する interface BookmarkableActivity { fun bookmark() { // 共通のブックマーク処理 } fun String.toLabel() = "★ $this" }
  60. 60. #07 lazyとcustom getter
  61. 61. lazyとは 最初にアクセスがあった時に値が計算され、 以降はその値を返す、delegated propertyの1つ 言語機能の説明 private val userId by lazy { intent.getStringExtra("USER_ID") }
  62. 62. 通常の代入とcustom getter ● 通常の代入は、インスタンス生成時に計算される ● lazyは、最初にアクセスがあった時に計算される ● custom getterは、アクセスがあるたびに計算される 言語機能の説明 val isAdmin = userId == ADMIN_ID val isAdmin by lazy { userId == ADMIN_ID } val isAdmin get() = userId == ADMIN_ID
  63. 63. アンチパターン 通常の代入、lazy、custom getterを、曖昧に使い分ける
  64. 64. 何が良くないか クラッシュしたり、 古い値が返ってきたり、 無駄な計算が何度も走ったりする private val button = findViewById<Button>(R.id.button) val area by lazy { width * height } val user: User get() = User(userId)
  65. 65. 解決策の例 値の性質応じて、適切に使い分ける クラス初期化時に値が確定し、不変 → 通常の代入 ある時点を過ぎるまで値が取れないが、不変 → lazy 状態が変わると値が変わる → custom getter
  66. 66. delegated property プロパティのget(とset)を別のクラスに委譲できる → 普通の値のように見えて分かりやすい ● Intentのextra ● Fragmentのarguments ● savedInstanceState ● SharedPreferences ● FirebaseRemoteConfig   などに使える ついでに紹介 var userId: String by MyClass()
  67. 67. delegated propertyの使用例 ついでに紹介 class Extra<out T> : ReadOnlyProperty<Activity, T> { override fun getValue( thisRef: Activity, property: KProperty<*> ): T = thisRef.intent.extras.get(property.name) as T } fun Intent.put( prop: KProperty1<*, String>, value: String ): Intent = this.putExtra(prop.name, value)
  68. 68. delegated propertyの使用例 ついでに紹介 class ProfileActivity : AppCompatActivity() { val userId: String by Extra() companion object { fun createIntent(context: Context, userId: String): Intent { return Intent(context, ProfileActivity::class.java) .put(ProfileActivity::userId, userId) } } } かなりの部分を隠蔽することができる 参考:https://speakerdeck.com/sakuna63/kotlins-delegated-properties-x-android
  69. 69. #08 Fragmentとlazy
  70. 70. lazyとは(再掲) 最初にアクセスがあった時に値が計算され、 以降はその値をキャッシュして返す private val userId by lazy { intent.getStringExtra("USER_ID") } 言語機能の説明
  71. 71. アンチパターン (アンチパターンとして有名ですが) FragmentのViewをlazyにする val button by lazy { view!!.findViewById<Button>(R.id.button) }
  72. 72. 何が良くないか Fragmentでは同じインスタンスに対して onCreateViewが再び呼ばれる           ↓ lazyが初回のviewをずっと保持するため、 再びonCreateViewされても値が更新されない 出典:https://developer.android.com/guide/components/fragments.html
  73. 73. 解決策の例 lateinit を使い、onCreateView で代入する Data Bindingの場合も、binding自体はlateinitが良い lateinit var button: Button override fun onCreateView(...): View? { val view = ... button = view.findViewById(R.id.button) return view }
  74. 74. #09 custom getter
  75. 75. custom getterとは valやvarの返り値は、カスタマイズすることができる 文法上は、引数がない関数は全てcustom getterにできる var userId: String = "" val isAdmin get() = userId == ADMIN_ID 言語機能の説明
  76. 76. アンチパターン1 値を取得する過程で副作用がある private val itemCount: Int get() { if (recyclerView.adapter == null) { initXXX() // 副作用 } return recyclerView.adapter.itemCount }
  77. 77. アンチパターン2 計算量が多い private val total: Int get() = countView(root) fun countView(view: View): Int = if (view is ViewGroup) { (0 until view.childCount).map { countView(view.getChildAt(it)) }.sum() } else { 1 }
  78. 78. 何が良くないか 呼び出す側からは通常の変数と同じように見える そのため、状態が変化したり、計算が重いことが予期できず、 予想外のバグやパフォーマンス低下を引き起こす if (this.itemCount > 20) { // ↑のように無邪気にアクセスしてしまう }
  79. 79. 解決策の例 副作用がなくて、計算量が少ない → custom getter 副作用があるか、計算量が多い  → 関数 上記の分類で実装していけば、関数名よりも明確に、 呼び出し側が副作用の有無や計算量を予想することができる 公式のリファレンスにも同様の記述がある https://kotlinlang.org/docs/reference/coding-conventions.html#functions-vs-properties
  80. 80. #10 custom setter
  81. 81. custom setterとは Kotlinでは通常のvarをカスタマイズすることができる var text: String = "" set(value) { field = value notifyDataSetChanged() } 言語機能の説明
  82. 82. custom setterの使いどころ ● 値をセットするついでに更新処理などを行う ● 変数の委譲 言語機能の説明
  83. 83. 変数の委譲 メンバー変数への委譲を簡単に書くことができる (この場合はバッキングフィールドも生成されない) private val owner = Person() var ownerName: String get() = owner.name set(value) { owner.name = value } 言語機能の説明 ※バッキングフィールド:Javaに変換された時に生成されるメンバ変数
  84. 84. 変数の委譲 もちろん以下のように書くこともできるが、やや冗長 private val owner = Person() var ownerName: String by object : ReadWriteProperty<Any, String> { override fun getValue(thisRef: Any, property: KProperty<*>): String = owner.name override fun setValue(thisRef: Any, property: KProperty<*>, value: String) { owner.name = value } } 言語機能の説明
  85. 85. アンチパターン 常に必要とは限らない処理をしている 例:値が更新されたら、XXXを更新して、YYYに通知して、ZZZ の値も更新して…… var person: Person = Person() set(value) { field = value updateXXX() notifyYYY() ZZZ = value.name }
  86. 86. 何が良くないか 値だけ変えることができない(たとえクラス内からでも) 反映や計算を後でまとめてやるとかができない 値を代入しただけだと思ったら予想外の挙動になる
  87. 87. 解決策の例 当たり前だけど、varの責務は値の保持にとどめておくべき varは普通にprivateで持っておいて、更新用の関数を公開する というJavaのパターンの方が混乱が少ないケースも多い
  88. 88. 最後にもう1つ
  89. 89. 最後にもう1つだけ…… Kotlinの機能を無理やり使わないように注意が必要かも ● operator overload ● infix ● tailrec etc... キレイに書けると超気持ちいいが、 実際のプロダクトで有効な例はそんなに多くないはず 特にチーム開発では、分かりやすさも大事
  90. 90. まとめ Kotlinは最高!だけど書き方の自由度が高い →チーム開発の場合は、基準・感覚をこまめに話し合う まずは今回話した10個のテーマで ブースにて実際のソースコードを展示しているので、 開発メンバーとディスカッションしに来てください!

×