Slide 1

Slide 1 text

Kotlin で改善する Androidアプリの品質 あんざいゆき(@yanzm)

Slide 2

Slide 2 text

なぜ品質の話をするのか • 動いているアプリを Java から Kotlin に移⾏するのは難 しい • 書き直しのコスト • デグレのリスク • コストやリスクを上回る効果があるのか?

Slide 3

Slide 3 text

Android アプリの品質? • 速い • 落ちない • 使いやすい •

Slide 4

Slide 4 text

Android アプリの品質? • 変更しやすい • 読みやすい •

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

ソフトウェアの品質 = 要因の組み合わせ • 外的品質要因 • ソフトウェア製品にその性質があるかないかをユーザーが認 識できる性質 • スピード、使いやすさ、… • 内的品質要因 • ソフトウェア製品について形容されるそれ以外の性質 • モジュール性、読みやすさ、…

Slide 7

Slide 7 text

外的品質要因の特に重要な4要因 • 正確さ(correctness) • 頑丈さ(robustness) • 信頼性 = 正確さ + 頑丈さ • 拡張性(extendibility) • 再利⽤性(reusability) • モジュール性 = 拡張性 + 再利⽤性

Slide 8

Slide 8 text

Java と品質 • ⻑年第⼀線で使われている • 品質要因に関連する様々なプラクティス、イデオム、規 則、原則、デザインパターンなど

Slide 9

Slide 9 text

Javaで広く実践されている 規則はKotlinだとどうなる?

Slide 10

Slide 10 text

No content

Slide 11

Slide 11 text

項⽬2 数多くのコンストラクタパラメータに直⾯した時には ビルダーを検討する new NutritionFacts(240, 8, 100, 0, 35, 27); new NutritionFacts.Builder(240, 8) .calories(100) .sodium(35) .carbohydrate(27) .build(); NG OK

Slide 12

Slide 12 text

項⽬2 数多くのコンストラクタパラメータに直⾯した時には ビルダーを検討する Kotlin class NutritionFacts( val servingSize: Int, val servings: Int, val calories: Int = 0, val fat: Int = 0, val sodium: Int = 0, val carbohydrate: Int = 0 ) NutritionFacts( 240, 8, calories = 100, sodium = 35, carbohydrate = 27 )

Slide 13

Slide 13 text

AlertDialog( message = "削除しますか?", negativeButtonLabel = "はい", positiveButtonListener = { delete() } ) class AlertDialog( private val title: String? = null, private val message: String? = null, private val positiveButtonLabel: String? = null, private val positiveButtonListener: (() -> Unit)? = null, private val negativeButtonLabel: String? = null, private val negativeButtonListener: (() -> Unit)? = null ) NG Kotlin

Slide 14

Slide 14 text

AlertDialog.Builder() .message("削除しますか?") .positiveButton("はい") { delete() } .build() class AlertDialog private constructor(…) { class Builder { … private var positiveButtonLabel: String? = null private var positiveButtonListener: (() -> Unit)? = null … fun positiveButton(label: String, listener: (() -> Unit)?): Builder { positiveButtonLabel = label positiveButtonListener = listener return this } … } } OK 不変式を構成するパラメータ全体をセッターで受け取る Kotlin

Slide 15

Slide 15 text

項⽬3 private のコンストラクタか enum 型でシングルトン 特性を強制する オブジェクト宣⾔をつかう object Elvis { fun leaveTheBuilding() { … } } Kotlin

Slide 16

Slide 16 text

項⽬4 private のコンストラクタでインスタンス化不可能を強 制する オブジェクト宣⾔ or トップレベル関数 fun util1(a: Int, b: Int): Int { … } public class UtilityClass { private UtilityClass() { } public static int util1(int a, int b) { … } } Kotlin

Slide 17

Slide 17 text

項⽬5 不必要なオブジェクトの⽣成を避ける Java では書けてしまう new String("hello"); new Boolean(true); Kotlin ではコンパイルエラー String("hello") Boolean(true)

Slide 18

Slide 18 text

項⽬8 equals をオーバーライドするときは⼀般契約にしたが う public class MyClass { public boolean equals(MyClass o) { … } } class MyClass { override fun equals(o: MyClass): Boolean { … } } Java では書けてしまう Kotlin ではコンパイルエラー

Slide 19

Slide 19 text

項⽬9 equals をオーバーライドする時は、常に hashCode をオーバーライドする IDE による⽣成を利⽤する or data class を利⽤する equals(), hashCode() の実装は⼿で書くのは難しい Kotlin

Slide 20

Slide 20 text

項⽬11 clone を注意してオーバーライドする public class Object { … protected native Object clone() throws CloneNotSupportedException; } 「オブジェクトのコピーを⾏う何らかの代替⼿段を提供するか、 オブジェクトの複製を単に提供しない⽅がおそらく賢明です。」 Kotlin の Any には clone() がない Kotlin

Slide 21

Slide 21 text

項⽬11 clone を注意してオーバーライドする 「オブジェクトのコピーに対する上⼿い⽅法は、コピーコンスト ラクタかコピーファクトリを提供することです。」 data class の copy() val donutsBook = Book("donuts", "Android") val eclairBook = donutsBook.copy(title = "eclair") Iterable の拡張関数 toList() val list = listOf("donuts", "eclair") val list2 = list.toList() Kotlin

Slide 22

Slide 22 text

data class Person( val name: String, val age: Int ) : Comparable { override fun compareTo(other: Person): Int { age.compareTo(other.age).let { if (it != 0) { return it } } return name.compareTo(other.name) } } 項⽬12 Comparable の実装を検討する Kotlin の数値型,Boolean,String,Charは compareTo() が ⽤意されている 年齢昇順 → 名前ABC順 Kotlin

Slide 23

Slide 23 text

項⽬12 Comparable の実装を検討する 標準関数を活⽤する data class Person( val name: String, val age: Int ) : Comparable { override fun compareTo(other: Person): Int { return compareValuesBy(this, other, { it.age }, { it.name }) } } 年齢昇順 → 名前ABC順 Kotlin

Slide 24

Slide 24 text

項⽬13 クラスとメンバーへのアクセス可能性を最⼩限にする 「各クラスやメンバーをできる限りアクセスできないように すべきです。」 Java Kotlin private, package private (default), protected, public private, internal, protected, public (default)

Slide 25

Slide 25 text

項⽬14 public のクラスでは、public のフィールドではな く、アクセサーメソッドを使う 「まとめると、publicのクラスは決して可変のフィールドを公開 すべきではありません。」 public class Person { String name; int age; public Person(String name, int age) { this.name = name; this.age = age; } } NG

Slide 26

Slide 26 text

項⽬14 public のクラスでは、public のフィールドではな く、アクセサーメソッドを使う public class Person { private String name; private int age; … public String getName() { return name; } public void setName(String name) { this.name = name; } … } OK

Slide 27

Slide 27 text

項⽬14 public のクラスでは、public のフィールドではな く、アクセサーメソッドを使う 外部からのプロパティアクセスは常に getter setter 経由 なので、⾔語としてアクセサーメソッドが強制されている Kotlin class Person(name: String, age: Int) { var name: String = name set(value) { println("set : name = $value") field = value } … }

Slide 28

Slide 28 text

項⽬15 可変性を最⼩限にする • 1. オブジェクトの状態を変更するためにいかなるメソッド も提供しない • 2. クラスが拡張できないことを保証する。 • 3. すべてのフィールドを final にする • 4. すべてのフィールドを private にする • 5. 可変コンポーネントに対する独占的アクセスを保証す る。

Slide 29

Slide 29 text

項⽬15 可変性を最⼩限にする • 1. オブジェクトの状態を変更するためにいかなるメソッド も提供しない • 2. クラスが拡張できないことを保証する。 • 3. すべてのプロパティを val にする • 4. 可変コンポーネントのプロパティを private にする • 5. 可変コンポーネントに対する独占的アクセスを保証す る。 Kotlin

Slide 30

Slide 30 text

項⽬16 継承よりコンポジションを選ぶ 「継承は強⼒ですが、カプセル化を破ってしまうので問題があ ります。」 public class InstrumentedSet extends HashSet { private int addCount = 0; @Override public boolean add(E e) { addCount++; return super.add(e); } @Override public boolean addAll(@NotNull Collection extends E> c) { addCount += c.size(); return super.addAll(c); } } NG

Slide 31

Slide 31 text

public class InstrumentedSet implements Set { private final Set s; private int addCount = 0; public InstrumentedSet(Set s) { this.s = s; } @Override public boolean add(E e) { addCount++; return s.add(e); } @Override public boolean addAll(@NotNull Collection extends E> c) { addCount += c.size(); return s.addAll(c); } @Override public int size() { return s.size(); } … } OK

Slide 32

Slide 32 text

public class InstrumentedSet implements Set { private final Set s; private int addCount = 0; public InstrumentedSet(Set s) { this.s = s; } @Override public boolean add(E e) { addCount++; return s.add(e); } @Override public boolean addAll(@NotNull Collection extends E> c) { addCount += c.size(); return s.addAll(c); } @Override public int size() { return s.size(); } @Override public boolean isEmpty() { return s.isEmpty(); } @Override public boolean contains(Object o) { return s.contains(o); } @NotNull @Override public Iterator iterator() { return s.iterator(); } @NotNull @Override public Object[] toArray() { return s.toArray(); } @NotNull @Override public T[] toArray(@NotNull T[] a) { return s.toArray(a); } @Override public boolean remove(Object o) { return s.remove(o); } @Override public boolean containsAll(@NotNull Collection> c) { return s.contains(c); } @Override public boolean retainAll(@NotNull Collection> c) { return s.retainAll(c); } @Override public boolean removeAll(@NotNull Collection> c) { return s.removeAll(c); } @Override public void clear() { s.clear(); } } 転送してるだけ

Slide 33

Slide 33 text

class InstrumentedSet2( private val s: MutableSet ) : MutableSet by s { private var addCount = 0 override fun add(element: E): Boolean { addCount++ return s.add(element) } override fun addAll(elements: Collection): Boolean { addCount += elements.size return s.addAll(elements) } } Kotlin

Slide 34

Slide 34 text

項⽬17 継承のために設計および⽂書化する、でなければ継承 を禁⽌する public final class Point { public class Point { private Point(int x, int y) { … } public static Point of(int x, int y) { return new Point(x, y); } クラスを final と宣⾔する コンストラクタを private かパッケージプライベートにして、代 わりに static ファクトリーメソッドを追加する

Slide 35

Slide 35 text

項⽬17 継承のために設計および⽂書化する、でなければ継承 を禁⽌する 継承のために設計および⽂書化されたクラスだけ open にする Kotlin

Slide 36

Slide 36 text

項⽬18 抽象クラスよりインタフェースを選ぶ • 既存のクラスを、新たなインタフェースを実装するように 変更することは容易にできる • インタフェースは、ミックスインを定義するには理想的 • インタフェースは、階層を持たない型フレームワークを構 築することができる • 抽象⾻格実装を提供することでインタフェースを抽象クラ スの⻑所を組み合わせることができる

Slide 37

Slide 37 text

項⽬18 抽象クラスよりインタフェースを選ぶ Kotlin • インタフェースにプロパティを宣⾔できる • インタフェースにデフォルト実装を与えることができる

Slide 38

Slide 38 text

項⽬19 型を定義するためだけにインタフェースを使⽤する 定数の修飾を回避するために定数をインタフェースに定義し ている public interface PhysicalConstants { double AVOGADROS_NUMBER = 6.02214199e23; } public class Atoms implements PhysicalConstants { public double atoms() { return AVOGADROS_NUMBER * mols; } … } NG

Slide 39

Slide 39 text

項⽬19 型を定義するためだけにインタフェースを使⽤する 定数ユーティリティクラス + static インポート public class PhysicalConstants { private PhysicalConstants() {} // インスタンス化を防⽌止 public static final double AVOGADROS_NUMBER = 6.02214199e23; } import static net.yanzm.sample.PhysicalConstants.AVOGADROS_NUMBER; public class Atoms { public double atoms() { return AVOGADROS_NUMBER * mols; } … } OK

Slide 40

Slide 40 text

項⽬19 型を定義するためだけにインタフェースを使⽤する Interface に定数を定義できない interface PhysicalConstants { const val AVOGADROS_NUMBER = 6.02214199e23 } object PhysicalConstants { const val AVOGADROS_NUMBER = 6.02214199e23 } コンパイルエラー OK トップレベルに定数を定義できる const val AVOGADROS_NUMBER = 6.02214199e23 定数ユーティリティとして object を使えば不要なインスタンス化 を防げる OK Kotlin

Slide 41

Slide 41 text

項⽬20 タグ付クラスよりクラス階層を選ぶ タグ付きクラスは、冗⻑で、誤りやすく、⾮効率

Slide 42

Slide 42 text

public class Figure { enum Shape {RECTANGLE, CIRCLE} final Shape shape; double length, width; // RECTANGLE の場合だけ使われる double radius; // CIRLE の場合だけ使われる Figure(double length, double width) { shape = Shape.RECTANGLE; this.length = length; this.width = width; } Figure(double radius) { shape = Shape.CIRCLE; this.radius = radius; } double area() { switch (shape) { case RECTANGLE: return length * width; case CIRCLE: return Math.PI * radius * radius; default: throw new AssertionError(); } } } NG

Slide 43

Slide 43 text

abstract class Figure { abstract double area(); } public class Rectangle extends Figure { final double length, width; … @Override double area() { return length * width; } } public class Circle extends Figure { final double radius; … @Override double area() { return Math.PI * radius * radius; } } OK

Slide 44

Slide 44 text

sealed class を使ったクラス階層で置き換える sealed class Figure { abstract fun area(): Double } class Rectangle(val length: Double, val width: Double) : Figure() { override fun area(): Double { return length * width } } class Circle(val radius: Double) : Figure() { override fun area(): Double { return Math.PI * radius * radius } } Kotlin

Slide 45

Slide 45 text

項⽬21 戦略を表現するために関数オブジェクトを使⽤する public class Person { private final String name; private final int age; public Person(String name, int age) { this.name = name; this.age = age; } public static final Comparator NAME_LENGTH_ORDER = new Comparator() { @Override public int compare(Person o1, Person o2) { return o1.name.length() - o2.name.length(); } }; } OK

Slide 46

Slide 46 text

data class Person(val name: String, val age: Int) { companion object { val NAME_LENGTH_ORDER: Comparator = Comparator { o1, o2 -> o1.name.length - o2.name.length } } } list.sortedWith(Person.NAME_LENGTH_ORDER) data class Person(val name: String, val age: Int) { companion object { val NAME_LENGTH_ORDER: Comparator = compareBy { it.name.length } } } 標準関数を使⽤用 Kotlin

Slide 47

Slide 47 text

項⽬22 ⾮ static のメンバークラスより static のメンバーク ラスを選ぶ 「エンクロージングインスタンスへアクセスする必要がないメン バークラスを宣⾔言するのであれば、その宣⾔言に static 修飾⼦子を常 に付ける」 Java メンバークラスの宣⾔言はデフォルトが⾮非 static → 明示的に static を付けないといけない メンバークラスの宣⾔言はデフォルトが not inner Kotlin

Slide 48

Slide 48 text

項⽬23 原型を使⽤しない List や Map ではなく、List, Map のよ うに常に型パラメータを指定する 移⾏互換性のために原型(List や Map)が残されている 原型は使⽤できず、コンパイルエラーになる Java Kotlin List list = new ArrayList(); NG val list :List = ArrayList() コンパイルエラー

Slide 49

Slide 49 text

項⽬25 配列よりリストを選ぶ 配列は共変 コンパイルエラーにならない Array は不変 コンパイルエラーになる Object[] objectArray = new Long[1]; val objectArray : Array = Array(1) { _ -> 0L } Java Kotlin

Slide 50

Slide 50 text

項⽬36 常に Override アノテーションを使⽤する オーバーライドするときは override キーワードが必須なため 意図しないオーバーライドが防⽌されている Kotlin @Override public boolean equals(Object obj) { … } OK fun equals(other: Any?): Boolean { … } コンパイルエラー

Slide 51

Slide 51 text

項⽬39 必要な場合には、防御的にコピーする List と MutableList など、コレクションインタフェースとし て読み取り専⽤用とミュータブルが⽤用意されている。 (読み取り専⽤用のインタフェースでも防御的コピーされてい るわけではないので注意が必要) Kotlin

Slide 52

Slide 52 text

項⽬46 従来の for ループより for-each ループを選ぶ for (int i = 0; i < a.length; i++) { doSomething(a[i]); } NG OK for (Element e : elements) { doSomething(e); } for-each のみ Kotlin for (e in elements) { doSomething(e) }

Slide 53

Slide 53 text

Java で for-each ループが使⽤できないよくある状況 1. フィルタリング 2. 変換 3. 並列イテレーション Kotlin val even = list.filter { it % 2 == 0 } val square = list.map { it * it } for (i in 0 until min(list1.size, list2.size)) { val a = list1[i] val b = list2[i] }

Slide 54

Slide 54 text

項⽬49 ボクシングされた基本データより基本データ型を選ぶ 「選択できる場合には、ボクシングされた基本データ(Integer, Double, Boolean など)ではなく、基本データ型(int, double, boolean)を使⽤してください。」 プリミティブ型とラッパークラスを区別しないため、選択を 誤ることがない Kotlin

Slide 55

Slide 55 text

項⽬51 ⽂字列結合のパフォーマンスに⽤⼼する 「パフォーマンスが重要でない場合以外は、数個以上の⽂字列を 結合するのに⽂字列結合演算⼦を使⽤しないでください。代わり に、StringBuilder の append メソッドを使⽤してください。」 ⽂字列の結合処理は StringBuilder + append にコンパイル される Kotlin

Slide 56

Slide 56 text

fun main() { val a = "A" val b = "B" val c = a + b val d = "$a, $b" } public static final void main() { String a = "A"; String b = "B"; (new StringBuilder()).append(a).append(b).toString(); (new StringBuilder()).append(a).append(", ").append(b).toString(); } Decompile

Slide 57

Slide 57 text

項⽬59 チェックされる例外を不必要に使⽤するのを避ける 「API を使⽤しているプログラマがこれ以上のことができな いならば、チェックされない例外のほうが適切でしょう。」 } catch (TheCheckedException e) { throw new AssertionError(); // 決して起きるべきではない! } } catch (TheCheckedException e) { e.printStackTrace(); // まあいいや、負けだ。 System.exit(1); }

Slide 58

Slide 58 text

Kotlin 検査例外と⾮検査例外を区別しない Java Kotlin public String read(String filename) throws IOException { … } fun read(filename: String): String? { … }

Slide 59

Slide 59 text

class MainActivity : AppCompatActivity() { private fun versionName(): String { return packageManager.getPackageInfo(packageName, 0) .versionName } } public class MainActivity extends AppCompatActivity { private String versionName() { try { return getPackageManager() .getPackageInfo(getPackageName(), 0) .versionName; } catch (PackageManager.NameNotFoundException e) { // ここには来ないはず return ""; } } } Java Kotlin

Slide 60

Slide 60 text

まとめ • Effective Java の多くの項⽬について、Kotlin では⾔語仕 様で対応している • Kotlin に書き換えることで、「明瞭で、正しく、再利⽤可 能で、頑強で、柔軟性があり、保守可能なプログラムを書 く」エッセンスが⾃然と取り⼊れられる

Slide 61

Slide 61 text

おわり • blog : Y.A.M の雑記帳 • y-anz-m.blogspot.com • twitter : @yanzm (やんざむ) • uPhyca Inc. (株式会社ウフィカ)