SlideShare a Scribd company logo
1 of 141
Download to read offline
Kotlinアプリの
リファクタリングポイント
Naoto Nakazato @ Kotlin Fest 2018
自己紹介
● Naoto Nakazato
○ Android開発歴 8年くらい
● Recruit Lifestyle
○ 2017年6月〜
○ HOT PEPPER Beauty
● アカウント
○ Twitter: @oxsoft
○ Facebook: naoto.nakazato
○ GitHub: oxsoft
○ Qiita: oxsoft
KotlinとAndroid
普段Androidアプリ開発してるって人はどれくらいいますか?
プロダクトの紹介
今日お話しすること
ホットペッパービューティー
ホットペッパービューティーは、
国内最大級のサロン検索・予約サービス
ヘアサロン以外にも、
ネイル、まつげ、リラクゼーション、エステがあります
ホットペッパービューティー
100% Kotlin化したAndroidアプリ
新機能開発とリファクタリングを繰り返す毎日
Kotlinの新しい書き方や機能の使いどころを日々発見
今日お話しすること
機能はそのままに「より良い」Kotlinコードにリファクタリングする
Tipsを8個紹介します
「より良い」の定義は好みがあるので、
チームで話し合うきっかけになれば嬉しいです
#01 Gradle Kotlin DSL
Gradle Kotlin DSLとは?
GradleのビルドスクリプトをGroovyではなくKotlinで書ける
build.gradleではなくbuild.gradle.ktsというファイル名にする
何が良いのか?
俺たちは雰囲気でGroovyをやっている
↓
補完やコードジャンプも効いてよく理解できる
個人的なオススメ
 新規プロジェクトならbuild.gradle.ktsで
 既存プロジェクトの書き換えは「時間があれば」
 補助的なタスクを別ファイルで追加するのがオススメ
例:未使用のdrawableを削除
未使用のdrawableを削除するタスクを考える
● Gradleコマンドで実行可能なタスク
● git grep して使用しているかどうかチェック
● build.gradleとは別のファイル(kts)として定義
タスクの定義
cleaner.gradle.ktsというファイルを作成
● this は Project で、task という拡張関数がある
● Task の doLast でタスク実行時に行う処理を記述する
task("タスク名") {
doLast {
// タスク実行時に行う処理
}
}
関数の定義
/** [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
}
再帰的にチェック&削除
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()
}
}
}
}
build.gradleでimport
build.gradleに以下のように書けばimportできる
Gradleタスクとして実行可能に
apply from: rootProject.file('cleaner.gradle.kts')
./gradlew removeUnusedDrawables
まとめ
● Gradle Kotlin DSLを使うと補完も効いてメンテナンス性UP
● シェルスクリプトなどでやっていたようなタスクを
Gradle Kotlin DSLで別ファイルに記述すると導入しやすい
#02 処理の共通化
重複した処理
重複した処理は生まれやすいのでリファクタリングポイント
処理が重複していると、
● 修正箇所が増える
● 一部を修正し忘れる
処理を共通化する
「処理が重複していたら必ず共通化せよ」というわけでないが、
処理を共通化する時にどういう選択肢があるか整理してみる
処理を共通化する
1. abstract class
2. interfaceのデフォルト実装
3. class delegation
4. property delegation
5. 拡張関数
6. トップレベル関数
abstract class vs. interface
abstract class interface
状態 持てる 持てない
継承元 classとinterface interfaceのみ
多重継承 できない できる
class method default method
final できる できない
protected できる できない
abstract class vs. interface
abstract classが良いケース
● onCreateなどの継承部分で処理を共通化したい
● クラス外から呼ばれたくない
● 子クラスでoverrideされたくない
abstract class vs. interface
interfaceのデフォルト実装が良いケース
● ベースクラスの肥大化を防ぎたい
● 多重継承したい
interfaceのデフォルト実装であっても、
class delegationを使えば状態を持つことができる
class delegation
interfaceの実装を別クラスに委譲することができる
これによりinterfaceの特性を活かしつつ状態を持つことができる
class UserModel() : DefaultImpl by State() { ... }
class UserModel(s: State) : DefaultImpl by s { ... }
状態を持ちつつも、一部の処理は子クラスで実装させたい場合
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
状態をインタフェースとして切り出し、
状態を実装したクラスを用意する
property delegation
プロパティのgetter(とsetter)を別のクラスに委譲できる
var userId: String by MyClass()
property delegation
値のように扱うことができるものはproperty delegationが向いている
● Intentのextra
● Fragmentのarguments
● savedInstanceState
● SharedPreferences
● FirebaseRemoteConfig
拡張関数
クラスに関数を追加する(ように見せる)ことができる
そうすると、どこからでも呼び出せるようになる
fun Any?.log() {
Log.d("DEBUG", this.toString())
}
fun foo() {
123.log()
"hello".log()
}
拡張関数
公式でありそうな名前なのに雑に実装しないように注意
fun String.isInteger() =
this.all { it in '0'..'9' }
↑空文字や負の数が考慮されていない
拡張関数
一般性がない・局所的にしか使わないメソッドも注意
fun String.decorate() =
"_人人人人人_n" +
"> $this <n" +
" ̄Y^Y^Y^Y^Y ̄"
↑”decorate”の意味が広く、
 文字幅も考慮されていない
拡張関数
interfaceのデフォルト実装でスコープを限定すると乱用が防げる
interface BookmarkableActivity {
fun bookmark() {
// 共通のブックマーク処理
}
fun String.toLabel() = "★ $this"
}
トップレベル関数
ファイルに直接関数を書くことができる
こちらも、どこからでも呼び出すことができる
fun throwOrLog(t: Throwable) {
// 開発はクラッシュ、本番はログ送信
}
fun foo() {
throwOrLog(e)
}
トップレベル関数
本当にどこからでも呼べるので、拡張関数以上に乱用に注意する
拡張関数と同様にinterfaceに書くとスコープを制限できるが、
それはすなわちinterfaceのデフォルト実装のこと
まとめ
● 重複する処理をまとめる方法はいくつかの選択肢がある
● 状態を持つかどうか、スコープは適切かなどによって使い分ける
● 拡張関数やトップレベル関数は強力だが乱用に注意する
#03 Mutableを避ける
val と var
val はgetterのみで再代入できない(read-only)
var はgetterとsetterがあり再代入できる(mutable)
val a = 0
var b = 0
a = 1 // NG
b = 1 // OK
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
なぜMutableを避けるのか
● 今どんな値が入っているかを常に考える必要がある
● 「この処理に入る時はこの値が入っているはず」
という暗黙の前提を生みやすくバグの温床となる
● (メンバ変数の var の場合)smart castが効かない
どうやってMutableを避けるか
1. その var は本当に var である必要があるか?
2. その MutableList は本当に MutableList である必要があるか?
まずは1について考えます
プロパティの優先順位
コンパイル時からずっと不変だ const val
インスタンス生成時に値が決まり不変だ
ある時点を過ぎると値が確定し、不変だ
ある時点を過ぎると値が確定し、可変だ
しかしNullableにはしたくない
val
lazy
lateinit var
var
YES
NO
※companion objectSTART
プロパティの優先順位
ある時点を過ぎると値が確定し、不変だ
ある時点を過ぎると値が確定し、可変だ
しかしNullableにはしたくない
↓この辺について考えてみます
ある時点を過ぎると値が確定し、不変だ
ある時点を過ぎると値が確定し、不変だ
↓インスタンス生成時は intent が取れないのでクラッシュ
class MainActivity : AppCompatActivity() {
val articleId: String = intent.getStringExtra(ARTICLE_ID)
}
ある時点を過ぎると値が確定し、不変だ
値は変わらないのに var で、しかもNullable
class MainActivity : AppCompatActivity() {
var articleId: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
articleId = intent.getStringExtra(ARTICLE_ID)
}
}
ある時点を過ぎると値が確定し、不変だ
lazy で書くと val かつNonNullにできる
class MainActivity : AppCompatActivity() {
val articleId: String by lazy {
intent.getStringExtra(ARTICLE_ID)
}
}
ある時点を過ぎると値が確定し、可変だ
しかしNullableにはしたくない
ある時点を過ぎると値が確定し、可変だ
しかしNullableにはしたくない
実質的にはNonNullなのに、Nullableになってしまう
class MainActivity : AppCompatActivity() {
var defaultQuery: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
defaultQuery = intent.getStringExtra(DEFAULT_QUERY)
}
}
ある時点を過ぎると値が確定し、可変だ
しかしNullableにはしたくない
lateinitを用いるとNonNullとして扱うことができる
class MainActivity : AppCompatActivity() {
lateinit var defaultQuery: String
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
defaultQuery = intent.getStringExtra(DEFAULT_QUERY)
}
}
lateinit ではなくNullableにすべきケース
onCreate や onCreateView よりも後で値が決まる場合は、
lateinit ではなく素直にNullableにするべき
lateinit var foo: String
var foo: String? = null
OR
lateinit ではなくNullableにすべきケース
通信後に代入する値を lateinit にした場合、
通信中や通信エラー時にタップするとクラッシュする
lateinit var article: Article
fun init() {
fetchArticle().subscribe { article ->
this.article = article
}
button.setOnClickListener {
textView.text = article.title
}
}
(おまけ)
var と MutableList
var と MutableList
var と MutableList の併用は基本的には避けられるはず
少なくとも以下のいずれかに
MutableList を回避する方法はこの後のCollectionのセクションで
var foo: MutableList<Int> = ……
val foo: MutableList<Int> = ……
var foo: List<Int> = ……
まとめ
● メンテナンス性向上のために、避けられるMutableは極力避ける
● どの時点で値が初期化できるか、その後可変かどうかに着目する
#04 Collection
Collectionについて
簡単におさらい
KotlinにおけるCollection
Iterable
Collection
Set
List
Map
Mutable
Iterable
Mutable
Collection
Mutable
Set
Mutable
List
Mutable
Map
その他のIterableまわりクラス
Iterable Sequence
IntProgression ClosedRange
IntRange
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
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]
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") // ここのミスマッチを事前に阻止している
生成方法
基本的にはlistOf、setOf、mapOf
取り出し順序やパフォーマンスを気にする場合は適切なクラスを使う
setOf(1, 2, 3) // LinkedHashSet
linkedSetOf(1, 2, 3) // LinkedHashSet
hashSetOf(1, 2, 3) // HashSet
sortedSetOf(1, 2, 3) // TreeSet
Collectionを活かした書き方に
リファクタリング
例1:2つのリストを同時に扱う
2つのリストを同時に扱う
(例)対応関係のある名前と年齢のリストをそれぞれ出力
val nameList = listOf("Alice", "Bob", "Charlie")
val ageList = listOf(20, 25, 30)
Alice(20), Bob(25), Charlie(30)
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)")
}
2つのリストを同時に扱う:zip
val nameList = listOf("Alice", "Bob", "Charlie")
val ageList = listOf(20, 25, 30)
nameList.zip(ageList) { name, age ->
println("$name($age)")
}
例2:変換と除外
変換と除外
(例)IDのリストをenumに変換(enumに存在しない場合は捨てる)
val ids = listOf("apple", "orange", "carrot", "banana")
[Fruit.APPLE, Fruit.ORANGE, Fruit.BANANA]
変換と除外
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
変換と除外:map,filter
変換と除外をしているので、map と filter を使う
return ids.filter { id ->
Fruit.values().any { it.id == id }
}.map { id ->
Fruit.values().find { it.id == id }!!
}
変換と除外:mapNotNull
変換と除外を同時に行うには、mapNotNull も有効
return ids.mapNotNull { id ->
Fruit.values().find { it.id == id }
}
例3:リストをN個ずつに分割
リストをN個ずつに分割
(例)A~Zのリストを10個ずつに分割する
val idList = 'A'..'Z'
// ……何らかの処理……
return // [[A,B,……,J],[K,L,……,T],[U,V,……,Z]]
リストを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]]
リストを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, …], …]
その他:all と any と none
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 }
all と any と none
否定「!」を使わずには書けない場合もある
例:「どれかが 0 じゃなければ true」
!list.all { it == 0 }
list.any { it != 0 }
!list.none { it != 0 }
まとめ
● index でアクセスしたり MutableList を使ったりしていたら、
Collectionのメソッドで簡潔に書けるかも
● map と filter を同時に行う場合は mapNotNull も有効
● all と any と none は積極的に使い分ける
#05 DSLの拡張
DSLとは
Domain Specific Languageの略
KotlinのDSLは内部DSLとも呼ばれ、Kotlinの言語機能で実現できる
構造的なデータの宣言や設定値の記述などが簡潔にできる
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
クラスの拡張
クラスの場合は継承によって既存クラスを拡張できる
open class Animal {
fun foo() {}
}
class Dog : Animal() {
fun bar() {}
}
fun zoo() {
val dog = Dog()
dog.foo() // 元の機能も使えて
dog.bar() // 機能を拡張できる
}
DSLの拡張
通常のクラスと同じようにDSLの機能を拡張したい場合
animal {
foo()
}
dog {
foo() // 元の機能も使えて
bar() // 機能を拡張するには?
}
DSLの拡張
状態を持たないならレシーバに拡張関数を追加する
状態を持つならレシーバを委譲で拡張したクラスを作成する
fun animal(block: Animal.() -> Unit) {
// ...
}
レシーバ
レシーバに拡張関数を追加
特に論点なし fun Animal.custom() {
// ...
}
animal {
custom()
}
レシーバを委譲で拡張
レシーバを受け取って委譲し、関数を追加する
class Dog(private val animal: Animal) {
fun foo() = animal.foo()
fun bar() {}
}
fun dog(block: Dog.() -> Unit) = animal {
Dog(this).block()
}
レシーバがinterfaceの場合
レシーバがinterfaceの場合はclass delegationを使うと楽
class Dog(animal: Animal) : Animal by animal {
fun bar() {}
}
レシーバをそのまま継承する場合
インスタンスの状態に不整合が起きる可能性がある
fun animal(block: Animal.() -> Unit) {
val animal = Animal()
animal.init()
animal.block()
}
fun dog(block: Dog.() -> Unit) = animal {
Dog().block() // Dog is not initialized!
}
レシーバをそのまま継承する場合
インスタンスを差し替えて同様の処理が書けるようなら大丈夫
fun animal(block: Animal.() -> Unit) {
val animal = Animal()
animal.init()
animal.block()
}
fun dog(block: Dog.() -> Unit) {
val dog = Dog()
dog.init()
dog.block()
}
まとめ
● DSLは委譲によって拡張した方が良さそう
● レシーバがinterfaceの場合はclass delegationを使うと良い
#06 Nullability
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
Nullableをどこで変換するか
APIがNullableで返してくる場合、
UIに渡す途中のどこまでNullableにするか
Infra Domain UI
Nullable Nullable
or
NonNull
or
Exception
Nullable
or
NonNull
or
Exception
Nullableのまま渡すと
至る所でnullチェックやsafe callするハメになる
→ 実質的には条件分岐が増え、挙動把握が困難になる
→ 「 null って何だっけ?」を毎回考えることになる
return team?.user?.name?.length
NonNullにするため無効なデータを入れると
無効なデータかチェックするハメになる
→ チェックを忘れて表示崩れが起きる
if (user.name.isNotEmpty()) {
// ↑しんどい or 忘れる
}
nullは何を表している?
「nullが何を表しているか」を考える
1. 滅多に起きないエラーケース
2. 任意項目であり、「存在しない」を表していることが自明
3. 自明じゃない何か
滅多に起きないエラーケース
InfraでExceptionに変換する
return name ?: throw NameNotFoundException
任意項目であり、
「存在しない」を表していることが自明
UI層までNullableで渡す
「N/A」などと表示する場合 → UI層でその文字列に変換する
見た目が大きく変わる場合 → UI層でnullチェックして分岐する
class ViewModel(user: User) {
val name = user.name ?: "N/A"
}
自明じゃない何か
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
まとめ
● Nullableばっかり、NonNullばっかりだと混乱が生まれやすい
● 「nullが何を表しているか」を考えて適切に変換する
#07 スコープ関数
スコープ関数
let/run/also/apply/withの5つがあるが、
withを除くとザックリ以下のように分類できる
it this
結果 let run
自身 also apply
戻り値
レシーバ戻り値 = 自身.スコープ関数 {
レシーバ.method()
結果
}
キャプチャする
キャプチャする
null判定時の値をそのまま使えるため、smart castが効かない時に有効
if (nullable != null) {
foo(nullable!!)
}
nullable?.let {
foo(it)
}
キャプチャする
単にif文の代わりとして使っても便利
str?.let {
// strがnullじゃない場合
}
val r = str ?: run {
// strがnullの場合
}
キャプチャする
ただし、if文と全く同じというわけではない
str?.let {
// A
} ?: run {
// B
}
if (str != null) {
// A
} else {
// B
}
キャプチャする
良くない例
data だけでなく listener が null の場合も
showNoDataMessage() が呼ばれる
data?.let {
listener?.onClickItem(it)
} ?: showNoDataMessage()
処理をまとめる
処理をまとめる
初期化処理などをまとめる
val intent = Intent().apply {
putExtra("key1", "value1")
putExtra("key2", "value2")
putExtra("key3", "value3")
}
処理をまとめる
ローカル変数の方が優先なので注意が必要
(特にプロパティアクセス形式)
※我々のチームでは let と also だけ使うようにしています
var text = "" // この行があると
val button = Button(context).apply {
text = "hello"
}
button.text // "hello"にならない
処理をまとめる:補足
DSLでもローカル変数に関しては同じ問題はある
ただし通常のメンバーは内側のブロックから順番に解決される
また親のメンバーへのアクセスは @DslMarker を使うと禁止できる
div {
a("http://kotlinlang.org") {
target = ATarget.blank
+"Main site"
}
}
div {
var target = ""
a("http://kotlinlang.org") {
target = ATarget.blank
+"Main site"
}
}
まとめ
● スコープ関数で値をキャプチャしたり処理をまとめたりできる
● let/elvis は if/else と等価ではない
● スコープ外の呼び出しに注意する
#08 データ構造と状態数
肥大化しがちなdata class
プロダクトの核となるdata classは肥大化していきがち
ホットペッパービューティーの場合だと、
検索条件を表すクラスや、予約情報を表すクラス
【検索条件】エリア、駅、緯度経度、メニュー、料金、検索クエリ、特集、日付、時刻、 etc……
【予約情報】サロン情報、来店日時、施術時間、利用クーポン、指名スタイリスト、金額、
      利用ポイント、加算予定ポイント、要望、 etc……
そのままdata classにすると
検索条件はこんな感じに → data class SearchCondition(
val area: Area?,
val station: Station?,
val location: Location?,
val query: String?,
val menu: Menu?,
val price: Price?
// ...
)
「ありえない状態」ができてしまう
【ビジネスロジック】
● エリア/駅/緯度経度は排他
○ でもいずれか1つは必須
● 料金はメニュー指定時のみ
● etc……
data class SearchCondition(
val area: Area?,
val station: Station?,
val location: Location?,
val query: String?,
val menu: Menu?,
val price: Price?
// ...
)
「ありえない状態」を避ける
使う時に毎回「ありえない状態」の考慮をするのは不毛
「ありえないはず」という暗黙の前提が増えるとバグの温床
「ありえない状態」を避ける方法は何があるか?
クラス化するほど関連がある場合
(例)sealed class で「エリアか駅か緯度経度」を表現する
sealed class Place {
class Area
class Station
class Location
}
data class SearchCondition(
// ...
val place: Place
// ...
)
クラス化するほど関連がある場合
(例)data class で「料金はメニュー指定時のみ」を表現する
data class MenuPrice(
val menu: Menu,
val price: Price?
)
data class SearchCondition(
// ...
val menuPrice: MenuPrice?
// ...
)
クラス化するほど関連がない場合
EitherやPairなどの一般的なクラスで表現する
sealed class → Either
data class → Pair
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>()
}
Λrrow
関数型っぽい書き方がKotlinでもできるようにするためのライブラリ
https://arrow-kt.io
Eitherも定義されている
もう少し一般化
毎回色々と考えるのは大変
↓
状態数(そのクラスがとりうる状態の合計数)
を考えてから実装に落とす
状態数
Kotlin 状態数
Nothing 0
Unit 1
Boolean 2
enum 要素数
状態数
Kotlin 状態数
A? A + 1
Either<A, B> A + B
Pair<A, B> A * B
Kotlin 状態数
Set<A> 2A
Map<A, B> (B+1)A
List<A> (An+1
-1)/(A-1)
状態数
※厳密には違う部分もありますがそこはご容赦
状態数が等しいものの例
Either<Boolean, Boolean>
Pair<Boolean, Boolean>
Set<Boolean>
Boolean
Unit?
Either<Unit, Unit>
Set<A>
Map<A, Unit>
状態数の分析
1. Kotlinを数式に変換する
2. 数式を展開する
3. ありえない項を削除する
4. 残った項をまとめる
5. 数式をKotlinに戻す
状態数の分析
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)
状態数の分析
1. Kotlinを数式に変換する
2. 数式を展開する
3. ありえない項を削除する
4. 残った項をまとめる
5. 数式をKotlinに戻す
ABC + AB + BC + CA + A + B + C + 1
(A + 1)(B + 1)(C + 1)
状態数の分析
1. Kotlinを数式に変換する
2. 数式を展開する
3. ありえない項を削除する
4. 残った項をまとめる
5. 数式をKotlinに戻す
AB + A + C + 1
ABC + AB + BC + CA + A + B + C + 1
【ビジネスロジック】
状態数の分析
1. Kotlinを数式に変換する
2. 数式を展開する
3. ありえない項を削除する
4. 残った項をまとめる
5. 数式をKotlinに戻す
A(B + 1) + C + 1
AB + A + C + 1
状態数の分析
1. Kotlinを数式に変換する
2. 数式を展開する
3. ありえない項を削除する
4. 残った項をまとめる
5. 数式をKotlinに戻す
A(B + 1) + C + 1
data class Target(
val abc: Either<Pair<A, B?>, C>?
)
data class Target(
val abc: Either<Pair<A, B?>, C>?
)
状態数の分析
1. Kotlinを数式に変換する
2. 数式を展開する
3. ありえない項を削除する
4. 残った項をまとめる
5. 数式をKotlinに戻す
6. 適宜 sealed class や data class を使って意味付けをする
data class Target(
val abc: Either<Pair<A, B?>, C>?
)
状態数の分析
1. Kotlinを数式に変換する
2. 数式を展開する
3. ありえない項を削除する
4. 残った項をまとめる
5. 数式をKotlinに戻す
6. 適宜 sealed class や data class を使って意味付けをする
まとめ
● クラスの肥大化で「ありえない状態」が生まれやすい
● 「ありえない状態」はバグの温床なので避ける
● 状態数を分析すると厳密に精査できる
● Either や Pair を使って状態を整理しつつ、
sealed class や data class で適宜意味付けをする
おわりに
まとめ
日々の開発で考えていることをまとめてみました
リファクタリングする際の1つの指針になったら幸いです
ご意見・ご感想お待ちしてます

More Related Content

What's hot

Goのサーバサイド実装におけるレイヤ設計とレイヤ内実装について考える
Goのサーバサイド実装におけるレイヤ設計とレイヤ内実装について考えるGoのサーバサイド実装におけるレイヤ設計とレイヤ内実装について考える
Goのサーバサイド実装におけるレイヤ設計とレイヤ内実装について考える
pospome
 

What's hot (20)

SQLアンチパターン 幻の第26章「とりあえず削除フラグ」
SQLアンチパターン 幻の第26章「とりあえず削除フラグ」SQLアンチパターン 幻の第26章「とりあえず削除フラグ」
SQLアンチパターン 幻の第26章「とりあえず削除フラグ」
 
ユーザーストーリー駆動開発で行こう。
ユーザーストーリー駆動開発で行こう。ユーザーストーリー駆動開発で行こう。
ユーザーストーリー駆動開発で行こう。
 
開発速度が速い #とは(LayerX社内資料)
開発速度が速い #とは(LayerX社内資料)開発速度が速い #とは(LayerX社内資料)
開発速度が速い #とは(LayerX社内資料)
 
振り返り(アジャイルレトロスペクティブズ)
振り返り(アジャイルレトロスペクティブズ)振り返り(アジャイルレトロスペクティブズ)
振り返り(アジャイルレトロスペクティブズ)
 
正しいものを正しくつくる
正しいものを正しくつくる正しいものを正しくつくる
正しいものを正しくつくる
 
Goのサーバサイド実装におけるレイヤ設計とレイヤ内実装について考える
Goのサーバサイド実装におけるレイヤ設計とレイヤ内実装について考えるGoのサーバサイド実装におけるレイヤ設計とレイヤ内実装について考える
Goのサーバサイド実装におけるレイヤ設計とレイヤ内実装について考える
 
2015-10-31 クラウドネイティヴ時代の運用を考える 〜 ドキュメント駆動運用へ
2015-10-31 クラウドネイティヴ時代の運用を考える  〜 ドキュメント駆動運用へ2015-10-31 クラウドネイティヴ時代の運用を考える  〜 ドキュメント駆動運用へ
2015-10-31 クラウドネイティヴ時代の運用を考える 〜 ドキュメント駆動運用へ
 
そのRails Engine、 本当に必要ですか?
そのRails Engine、 本当に必要ですか?そのRails Engine、 本当に必要ですか?
そのRails Engine、 本当に必要ですか?
 
まじめに!できる!LT
まじめに!できる!LT まじめに!できる!LT
まじめに!できる!LT
 
怖くないSpring Bootのオートコンフィグレーション
怖くないSpring Bootのオートコンフィグレーション怖くないSpring Bootのオートコンフィグレーション
怖くないSpring Bootのオートコンフィグレーション
 
例外設計における大罪
例外設計における大罪例外設計における大罪
例外設計における大罪
 
テストコードの DRY と DAMP
テストコードの DRY と DAMPテストコードの DRY と DAMP
テストコードの DRY と DAMP
 
テスト文字列に「うんこ」と入れるな
テスト文字列に「うんこ」と入れるなテスト文字列に「うんこ」と入れるな
テスト文字列に「うんこ」と入れるな
 
メタバースのビジネスモデルと技術限界
メタバースのビジネスモデルと技術限界メタバースのビジネスモデルと技術限界
メタバースのビジネスモデルと技術限界
 
ホットペッパービューティーにおけるモバイルアプリ向けAPIのBFF/Backend分割
ホットペッパービューティーにおけるモバイルアプリ向けAPIのBFF/Backend分割ホットペッパービューティーにおけるモバイルアプリ向けAPIのBFF/Backend分割
ホットペッパービューティーにおけるモバイルアプリ向けAPIのBFF/Backend分割
 
リクルートのWebサービスを支える共通インフラ「RAFTEL」
リクルートのWebサービスを支える共通インフラ「RAFTEL」リクルートのWebサービスを支える共通インフラ「RAFTEL」
リクルートのWebサービスを支える共通インフラ「RAFTEL」
 
会社でClojure使ってみて分かったこと
会社でClojure使ってみて分かったこと会社でClojure使ってみて分かったこと
会社でClojure使ってみて分かったこと
 
カネとAgile(大企業新規事業編) #rsgt2021
カネとAgile(大企業新規事業編) #rsgt2021カネとAgile(大企業新規事業編) #rsgt2021
カネとAgile(大企業新規事業編) #rsgt2021
 
「再代入なんて、あるわけない」 ~ふつうのプログラマが関数型言語を知るべき理由~ (Gunma.web #5 2011/05/14)
「再代入なんて、あるわけない」 ~ふつうのプログラマが関数型言語を知るべき理由~ (Gunma.web #5 2011/05/14)「再代入なんて、あるわけない」 ~ふつうのプログラマが関数型言語を知るべき理由~ (Gunma.web #5 2011/05/14)
「再代入なんて、あるわけない」 ~ふつうのプログラマが関数型言語を知るべき理由~ (Gunma.web #5 2011/05/14)
 
オンプレML基盤on Kubernetes パネルディスカッション
オンプレML基盤on Kubernetes パネルディスカッションオンプレML基盤on Kubernetes パネルディスカッション
オンプレML基盤on Kubernetes パネルディスカッション
 

Similar to Refactoring point of Kotlin application

G*workshop sendai 20100424(v2)
G*workshop sendai 20100424(v2)G*workshop sendai 20100424(v2)
G*workshop sendai 20100424(v2)
Nobuhiro Sue
 
Replace Output Iterator and Extend Range JP
Replace Output Iterator and Extend Range JPReplace Output Iterator and Extend Range JP
Replace Output Iterator and Extend Range JP
Akira Takahashi
 
初心者講習会資料(Osaka.r#6)
初心者講習会資料(Osaka.r#6)初心者講習会資料(Osaka.r#6)
初心者講習会資料(Osaka.r#6)
Masahiro Hayashi
 

Similar to Refactoring point of Kotlin application (20)

たのしい関数型
たのしい関数型たのしい関数型
たのしい関数型
 
Lisp Tutorial for Pythonista : Day 3
Lisp Tutorial for Pythonista : Day 3Lisp Tutorial for Pythonista : Day 3
Lisp Tutorial for Pythonista : Day 3
 
G*workshop sendai 20100424(v2)
G*workshop sendai 20100424(v2)G*workshop sendai 20100424(v2)
G*workshop sendai 20100424(v2)
 
Essential Scala 第5章 シーケンス処理
Essential Scala 第5章 シーケンス処理Essential Scala 第5章 シーケンス処理
Essential Scala 第5章 シーケンス処理
 
初心者講習会資料(Osaka.R#7)
初心者講習会資料(Osaka.R#7)初心者講習会資料(Osaka.R#7)
初心者講習会資料(Osaka.R#7)
 
Project lambda
Project lambdaProject lambda
Project lambda
 
VS勉強会 .NET Framework 入門
VS勉強会 .NET Framework 入門VS勉強会 .NET Framework 入門
VS勉強会 .NET Framework 入門
 
Replace Output Iterator and Extend Range JP
Replace Output Iterator and Extend Range JPReplace Output Iterator and Extend Range JP
Replace Output Iterator and Extend Range JP
 
F#によるFunctional Programming入門
F#によるFunctional Programming入門F#によるFunctional Programming入門
F#によるFunctional Programming入門
 
JavaScript経験者のためのGo言語入門
JavaScript経験者のためのGo言語入門JavaScript経験者のためのGo言語入門
JavaScript経験者のためのGo言語入門
 
PHP AST 徹底解説
PHP AST 徹底解説PHP AST 徹底解説
PHP AST 徹底解説
 
プログラムの処方箋~健康なコードと病んだコード
プログラムの処方箋~健康なコードと病んだコードプログラムの処方箋~健康なコードと病んだコード
プログラムの処方箋~健康なコードと病んだコード
 
関数型都市忘年会『はじめての函数型プログラミング』
関数型都市忘年会『はじめての函数型プログラミング』関数型都市忘年会『はじめての函数型プログラミング』
関数型都市忘年会『はじめての函数型プログラミング』
 
Tokyor23 doradora09
Tokyor23 doradora09Tokyor23 doradora09
Tokyor23 doradora09
 
genuine-highlighter: マクロを認識するClojure向けのシンタックスハイライター
genuine-highlighter: マクロを認識するClojure向けのシンタックスハイライターgenuine-highlighter: マクロを認識するClojure向けのシンタックスハイライター
genuine-highlighter: マクロを認識するClojure向けのシンタックスハイライター
 
Ruby on Rails 入門
Ruby on Rails 入門Ruby on Rails 入門
Ruby on Rails 入門
 
Flutterを体験してみませんか
Flutterを体験してみませんかFlutterを体験してみませんか
Flutterを体験してみませんか
 
Java x Groovy: improve your java development life
Java x Groovy: improve your java development lifeJava x Groovy: improve your java development life
Java x Groovy: improve your java development life
 
初心者講習会資料(Osaka.r#6)
初心者講習会資料(Osaka.r#6)初心者講習会資料(Osaka.r#6)
初心者講習会資料(Osaka.r#6)
 
(Ruby使いのための)Scalaで学ぶ関数型プログラミング
(Ruby使いのための)Scalaで学ぶ関数型プログラミング(Ruby使いのための)Scalaで学ぶ関数型プログラミング
(Ruby使いのための)Scalaで学ぶ関数型プログラミング
 

More from Recruit Lifestyle Co., Ltd.

More from Recruit Lifestyle Co., Ltd. (20)

業務と消費者の体験を同時にデザインするリクルートの価値検証のリアル ー 「Airレジ ハンディ」セルフオーダーのブレない「価値」の確かめ方 ー
業務と消費者の体験を同時にデザインするリクルートの価値検証のリアル ー 「Airレジ ハンディ」セルフオーダーのブレない「価値」の確かめ方 ー業務と消費者の体験を同時にデザインするリクルートの価値検証のリアル ー 「Airレジ ハンディ」セルフオーダーのブレない「価値」の確かめ方 ー
業務と消費者の体験を同時にデザインするリクルートの価値検証のリアル ー 「Airレジ ハンディ」セルフオーダーのブレない「価値」の確かめ方 ー
 
分散トレーシングAWS:X-Rayとの上手い付き合い方
分散トレーシングAWS:X-Rayとの上手い付き合い方分散トレーシングAWS:X-Rayとの上手い付き合い方
分散トレーシングAWS:X-Rayとの上手い付き合い方
 
OOUIを実践してわかった、9つの大切なこと
OOUIを実践してわかった、9つの大切なことOOUIを実践してわかった、9つの大切なこと
OOUIを実践してわかった、9つの大切なこと
 
Flutter移行の苦労と、乗り越えた先に得られたもの
Flutter移行の苦労と、乗り越えた先に得られたものFlutter移行の苦労と、乗り越えた先に得られたもの
Flutter移行の苦労と、乗り越えた先に得られたもの
 
CTIサービスを支える裏側 〜物理デバイスとの戦い〜 | iOSDC Japan 2020
CTIサービスを支える裏側 〜物理デバイスとの戦い〜 | iOSDC Japan 2020CTIサービスを支える裏側 〜物理デバイスとの戦い〜 | iOSDC Japan 2020
CTIサービスを支える裏側 〜物理デバイスとの戦い〜 | iOSDC Japan 2020
 
「進化し続けるインフラ」のためのマルチアカウント管理
「進化し続けるインフラ」のためのマルチアカウント管理「進化し続けるインフラ」のためのマルチアカウント管理
「進化し続けるインフラ」のためのマルチアカウント管理
 
Air事業のデザイン組織とデザイナー
Air事業のデザイン組織とデザイナーAir事業のデザイン組織とデザイナー
Air事業のデザイン組織とデザイナー
 
リクルートライフスタイル AirシリーズでのUXリサーチ
リクルートライフスタイル AirシリーズでのUXリサーチリクルートライフスタイル AirシリーズでのUXリサーチ
リクルートライフスタイル AirシリーズでのUXリサーチ
 
データサイエンティストが力を発揮できるアジャイルデータ活用基盤
データサイエンティストが力を発揮できるアジャイルデータ活用基盤データサイエンティストが力を発揮できるアジャイルデータ活用基盤
データサイエンティストが力を発揮できるアジャイルデータ活用基盤
 
Real-time personalized recommendation using embedding
Real-time personalized recommendation using embeddingReal-time personalized recommendation using embedding
Real-time personalized recommendation using embedding
 
データから価値を生み続けるには
データから価値を生み続けるにはデータから価値を生み続けるには
データから価値を生み続けるには
 
データプロダクト開発を成功に導くには
データプロダクト開発を成功に導くにはデータプロダクト開発を成功に導くには
データプロダクト開発を成功に導くには
 
Jupyter だけで機械学習を実サービス展開できる基盤
Jupyter だけで機械学習を実サービス展開できる基盤Jupyter だけで機械学習を実サービス展開できる基盤
Jupyter だけで機械学習を実サービス展開できる基盤
 
SQLを書くだけでAPIが作れる基盤
SQLを書くだけでAPIが作れる基盤SQLを書くだけでAPIが作れる基盤
SQLを書くだけでAPIが作れる基盤
 
BtoBサービスならではの顧客目線の取り入れ方
BtoBサービスならではの顧客目線の取り入れ方BtoBサービスならではの顧客目線の取り入れ方
BtoBサービスならではの顧客目線の取り入れ方
 
The Design for Serverless ETL Pipeline データ分析基盤のレガシーなデータロードをサーバレスでフルリプレースするまで道のり
The Design for Serverless ETL Pipeline データ分析基盤のレガシーなデータロードをサーバレスでフルリプレースするまで道のりThe Design for Serverless ETL Pipeline データ分析基盤のレガシーなデータロードをサーバレスでフルリプレースするまで道のり
The Design for Serverless ETL Pipeline データ分析基盤のレガシーなデータロードをサーバレスでフルリプレースするまで道のり
 
リクルートライフスタイルにおける深層学習の活用とGCPでの実現方法
リクルートライフスタイルにおける深層学習の活用とGCPでの実現方法リクルートライフスタイルにおける深層学習の活用とGCPでの実現方法
リクルートライフスタイルにおける深層学習の活用とGCPでの実現方法
 
ビックデータ分析基盤の成⻑の軌跡
ビックデータ分析基盤の成⻑の軌跡ビックデータ分析基盤の成⻑の軌跡
ビックデータ分析基盤の成⻑の軌跡
 
データサイエンティストとエンジニア 両者が幸せになれる機械学習基盤を求めて
データサイエンティストとエンジニア 両者が幸せになれる機械学習基盤を求めてデータサイエンティストとエンジニア 両者が幸せになれる機械学習基盤を求めて
データサイエンティストとエンジニア 両者が幸せになれる機械学習基盤を求めて
 
データ分析基盤運⽤チームの 運⽤業務を改善してみた話
データ分析基盤運⽤チームの 運⽤業務を改善してみた話データ分析基盤運⽤チームの 運⽤業務を改善してみた話
データ分析基盤運⽤チームの 運⽤業務を改善してみた話
 

Refactoring point of Kotlin application