Save 37% off PRO during our Black Friday Sale! »

KotlinFest.pdf

D2bcabeeb1ddff142fb8988b412cb4d3?s=47 Yuki Anzai
August 25, 2018
10k

 KotlinFest.pdf

D2bcabeeb1ddff142fb8988b412cb4d3?s=128

Yuki Anzai

August 25, 2018
Tweet

Transcript

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

  2. なぜ品質の話をするのか • 動いているアプリを Java から Kotlin に移⾏するのは難 しい • 書き直しのコスト

    • デグレのリスク • コストやリスクを上回る効果があるのか?
  3. Android アプリの品質? • 速い • 落ちない • 使いやすい •

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

  5. None
  6. ソフトウェアの品質 = 要因の組み合わせ • 外的品質要因 • ソフトウェア製品にその性質があるかないかをユーザーが認 識できる性質 • スピード、使いやすさ、…

    • 内的品質要因 • ソフトウェア製品について形容されるそれ以外の性質 • モジュール性、読みやすさ、…
  7. 外的品質要因の特に重要な4要因 • 正確さ(correctness) • 頑丈さ(robustness) • 信頼性 = 正確さ +

    頑丈さ • 拡張性(extendibility) • 再利⽤性(reusability) • モジュール性 = 拡張性 + 再利⽤性
  8. Java と品質 • ⻑年第⼀線で使われている • 品質要因に関連する様々なプラクティス、イデオム、規 則、原則、デザインパターンなど

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

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

    new NutritionFacts.Builder(240, 8) .calories(100) .sodium(35) .carbohydrate(27) .build(); NG OK
  12. 項⽬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 )
  13. 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
  14. 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
  15. 項⽬3 private のコンストラクタか enum 型でシングルトン 特性を強制する オブジェクト宣⾔をつかう object Elvis {

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

    b: Int): Int { … } public class UtilityClass { private UtilityClass() { } public static int util1(int a, int b) { … } } Kotlin
  17. 項⽬5 不必要なオブジェクトの⽣成を避ける Java では書けてしまう new String("hello"); new Boolean(true); Kotlin ではコンパイルエラー

    String("hello") Boolean(true)
  18. 項⽬8 equals をオーバーライドするときは⼀般契約にしたが う public class MyClass { public boolean

    equals(MyClass o) { … } } class MyClass { override fun equals(o: MyClass): Boolean { … } } Java では書けてしまう Kotlin ではコンパイルエラー
  19. 項⽬9 equals をオーバーライドする時は、常に hashCode をオーバーライドする IDE による⽣成を利⽤する or data class

    を利⽤する equals(), hashCode() の実装は⼿で書くのは難しい Kotlin
  20. 項⽬11 clone を注意してオーバーライドする public class Object { … protected native

    Object clone() throws CloneNotSupportedException; } 「オブジェクトのコピーを⾏う何らかの代替⼿段を提供するか、 オブジェクトの複製を単に提供しない⽅がおそらく賢明です。」 Kotlin の Any には clone() がない Kotlin
  21. 項⽬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
  22. data class Person( val name: String, val age: Int )

    : Comparable<Person> { 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
  23. 項⽬12 Comparable の実装を検討する 標準関数を活⽤する data class Person( val name: String,

    val age: Int ) : Comparable<Person> { override fun compareTo(other: Person): Int { return compareValuesBy(this, other, { it.age }, { it.name }) } } 年齢昇順 → 名前ABC順 Kotlin
  24. 項⽬13 クラスとメンバーへのアクセス可能性を最⼩限にする 「各クラスやメンバーをできる限りアクセスできないように すべきです。」 Java Kotlin private, package private (default),

    protected, public private, internal, protected, public (default)
  25. 項⽬14 public のクラスでは、public のフィールドではな く、アクセサーメソッドを使う 「まとめると、publicのクラスは決して可変のフィールドを公開 すべきではありません。」 public class Person

    { String name; int age; public Person(String name, int age) { this.name = name; this.age = age; } } NG
  26. 項⽬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
  27. 項⽬14 public のクラスでは、public のフィールドではな く、アクセサーメソッドを使う 外部からのプロパティアクセスは常に getter setter 経由 なので、⾔語としてアクセサーメソッドが強制されている

    Kotlin class Person(name: String, age: Int) { var name: String = name set(value) { println("set : name = $value") field = value } … }
  28. 項⽬15 可変性を最⼩限にする • 1. オブジェクトの状態を変更するためにいかなるメソッド も提供しない • 2. クラスが拡張できないことを保証する。 •

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

    3. すべてのプロパティを val にする • 4. 可変コンポーネントのプロパティを private にする • 5. 可変コンポーネントに対する独占的アクセスを保証す る。 Kotlin
  30. 項⽬16 継承よりコンポジションを選ぶ 「継承は強⼒ですが、カプセル化を破ってしまうので問題があ ります。」 public class InstrumentedSet<E> extends HashSet<E> {

    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
  31. public class InstrumentedSet<E> implements Set<E> { private final Set<E> s;

    private int addCount = 0; public InstrumentedSet(Set<E> 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
  32. public class InstrumentedSet<E> implements Set<E> { private final Set<E> s;

    private int addCount = 0; public InstrumentedSet(Set<E> 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<E> iterator() { return s.iterator(); } @NotNull @Override public Object[] toArray() { return s.toArray(); } @NotNull @Override public <T> 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(); } } 転送してるだけ
  33. class InstrumentedSet2<E>( private val s: MutableSet<E> ) : MutableSet<E> by

    s { private var addCount = 0 override fun add(element: E): Boolean { addCount++ return s.add(element) } override fun addAll(elements: Collection<E>): Boolean { addCount += elements.size return s.addAll(elements) } } Kotlin
  34. 項⽬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 ファクトリーメソッドを追加する
  35. 項⽬17 継承のために設計および⽂書化する、でなければ継承 を禁⽌する 継承のために設計および⽂書化されたクラスだけ open にする Kotlin

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

    • 抽象⾻格実装を提供することでインタフェースを抽象クラ スの⻑所を組み合わせることができる
  37. 項⽬18 抽象クラスよりインタフェースを選ぶ Kotlin • インタフェースにプロパティを宣⾔できる • インタフェースにデフォルト実装を与えることができる

  38. 項⽬19 型を定義するためだけにインタフェースを使⽤する 定数の修飾を回避するために定数をインタフェースに定義し ている public interface PhysicalConstants { double AVOGADROS_NUMBER

    = 6.02214199e23; } public class Atoms implements PhysicalConstants { public double atoms() { return AVOGADROS_NUMBER * mols; } … } NG
  39. 項⽬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
  40. 項⽬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
  41. 項⽬20 タグ付クラスよりクラス階層を選ぶ タグ付きクラスは、冗⻑で、誤りやすく、⾮効率

  42. 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
  43. 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
  44. 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
  45. 項⽬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<Person> NAME_LENGTH_ORDER = new Comparator<Person>() { @Override public int compare(Person o1, Person o2) { return o1.name.length() - o2.name.length(); } }; } OK
  46. data class Person(val name: String, val age: Int) { companion

    object { val NAME_LENGTH_ORDER: Comparator<Person> = 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<Person> = compareBy { it.name.length } } } 標準関数を使⽤用 Kotlin
  47. 項⽬22 ⾮ static のメンバークラスより static のメンバーク ラスを選ぶ 「エンクロージングインスタンスへアクセスする必要がないメン バークラスを宣⾔言するのであれば、その宣⾔言に static

    修飾⼦子を常 に付ける」 Java メンバークラスの宣⾔言はデフォルトが⾮非 static → 明示的に static を付けないといけない メンバークラスの宣⾔言はデフォルトが not inner Kotlin
  48. 項⽬23 原型を使⽤しない List や Map ではなく、List<String>, Map<String,String> のよ うに常に型パラメータを指定する 移⾏互換性のために原型(List

    や Map)が残されている 原型は使⽤できず、コンパイルエラーになる Java Kotlin List list = new ArrayList<String>(); NG val list :List = ArrayList<String>() コンパイルエラー
  49. 項⽬25 配列よりリストを選ぶ 配列は共変 コンパイルエラーにならない Array は不変 コンパイルエラーになる Object[] objectArray =

    new Long[1]; val objectArray : Array<Any> = Array<Long>(1) { _ -> 0L } Java Kotlin
  50. 項⽬36 常に Override アノテーションを使⽤する オーバーライドするときは override キーワードが必須なため 意図しないオーバーライドが防⽌されている Kotlin @Override

    public boolean equals(Object obj) { … } OK fun equals(other: Any?): Boolean { … } コンパイルエラー
  51. 項⽬39 必要な場合には、防御的にコピーする List と MutableList など、コレクションインタフェースとし て読み取り専⽤用とミュータブルが⽤用意されている。 (読み取り専⽤用のインタフェースでも防御的コピーされてい るわけではないので注意が必要) Kotlin

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

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

    + append にコンパイル される Kotlin
  56. 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
  57. 項⽬59 チェックされる例外を不必要に使⽤するのを避ける 「API を使⽤しているプログラマがこれ以上のことができな いならば、チェックされない例外のほうが適切でしょう。」 } catch (TheCheckedException e) {

    throw new AssertionError(); // 決して起きるべきではない! } } catch (TheCheckedException e) { e.printStackTrace(); // まあいいや、負けだ。 System.exit(1); }
  58. Kotlin 検査例外と⾮検査例外を区別しない Java Kotlin public String read(String filename) throws IOException

    { … } fun read(filename: String): String? { … }
  59. 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
  60. まとめ • Effective Java の多くの項⽬について、Kotlin では⾔語仕 様で対応している • Kotlin に書き換えることで、「明瞭で、正しく、再利⽤可

    能で、頑強で、柔軟性があり、保守可能なプログラムを書 く」エッセンスが⾃然と取り⼊れられる
  61. おわり • blog : Y.A.M の雑記帳 • y-anz-m.blogspot.com • twitter

    : @yanzm (やんざむ) • uPhyca Inc. (株式会社ウフィカ)