$30 off During Our Annual Pro Sale. View Details »

KotlinFest.pdf

Yuki Anzai
August 25, 2018
12k

 KotlinFest.pdf

Yuki Anzai

August 25, 2018
Tweet

Transcript

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  5. View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  10. View Slide

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

    View Slide

  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
    )

    View Slide

  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

    View Slide

  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

    View Slide

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

    }
    }
    Kotlin

    View Slide

  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

    View Slide

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

    View Slide

  18. 項⽬8 equals をオーバーライドするときは⼀般契約にしたが

    public class MyClass {
    public boolean equals(MyClass o) {

    }
    }
    class MyClass {
    override fun equals(o: MyClass): Boolean {

    }
    }
    Java では書けてしまう
    Kotlin ではコンパイルエラー

    View Slide

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

    View Slide

  20. 項⽬11 clone を注意してオーバーライドする
    public class Object {

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

    View Slide

  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

    View Slide

  22. 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

    View Slide

  23. 項⽬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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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
    }

    }

    View Slide

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

    View Slide

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

    View Slide

  30. 項⽬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

    View Slide

  31. 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

    View Slide

  32. 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();
    }
    }
    転送してるだけ

    View Slide

  33. 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

    View Slide

  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 ファクトリーメソッドを追加する

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    }
    NG

    View Slide

  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

    View Slide

  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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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 NAME_LENGTH_ORDER
    = new Comparator() {
    @Override
    public int compare(Person o1, Person o2) {
    return o1.name.length() - o2.name.length();
    }
    };
    }
    OK

    View Slide

  46. 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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  50. 項⽬36 常に Override アノテーションを使⽤する
    オーバーライドするときは override キーワードが必須なため
    意図しないオーバーライドが防⽌されている
    Kotlin
    @Override
    public boolean equals(Object obj) {

    }
    OK
    fun equals(other: Any?): Boolean {

    }
    コンパイルエラー

    View Slide

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

    View Slide

  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)
    }

    View Slide

  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]
    }

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

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

    View Slide

  58. Kotlin
    検査例外と⾮検査例外を区別しない
    Java
    Kotlin
    public String read(String filename) throws IOException {

    }
    fun read(filename: String): String? {

    }

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide