Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Writing a library in Kotlin by Vincent Carrier

Writing a library in Kotlin by Vincent Carrier

GDG Montreal

May 08, 2018
Tweet

More Decks by GDG Montreal

Other Decks in Technology

Transcript

  1. The problem with grids val chessboard = arrayOf( arrayOf(ChessPiece(BLACK, ROOK),

    ChessPiece(BLACK, KNIGHT), …), … ) Verbose, hardcoded implementation, hard to visualize
  2. So I made a library val chessboard = """ |rnbqkbnr

    |pppppppp |________ |________ |________ |________ |PPPPPPPP |RNBQKBNR """.toMutableGrid { char -> char.toChessPiece() }
  3. So I made a library val e2 = Cell(4, 6)

    val e4 = Cell(4, 4) chessboard.swap(e2, e4) assertEquals( ChessPiece(WHITE, PAWN), chessboard[e4] ) // True
  4. Structure of this talk 1. Maintainability 2. Readability 3. Performance

    4. Benchmarking with JMH 5. Publishing on jCenter
  5. Class hierarchy interface Grid<out E> interface MutableGrid<E> abstract class BaseGrid<out

    E> open class ListGrid<out E> open class MutableListGrid<E> open class ArrayGrid<out E> open class MutableArrayGrid<E>
  6. Consider specifying types explicitly • Type inference is great, but

    it forces you to use concrete types and can lead to confusing bugs when refactoring. Use with care! • Be sure to enable type hints within Android Studio : Preferences -> Editor -> General -> Appearance -> Show parameter type hints -> Configure -> Language -> Kotlin
  7. Be aware of the Kotlin’s modifiers • Kotlin classes and

    members are public by default • Everything is implicitly final except protected / interface members / override • internal stands for module-private visibility • private is either class-private or file-private for top-level members
  8. Consider extension functions instead of default interface method implementation or

    static class utilities interface Grid<out E> { … fun cellsAdjacentTo(x: Int, y: Int): List<Cell> { … } } BAD: Can be overridden
  9. Consider extension functions instead of default interface method implementation or

    static class utilities object Grids { fun <E> adjacent(x: Int, y: Int, grid: Grid<E>): List<Cell> { … } } BAD: Ugly and hidden in an utility class
  10. Consider extension functions instead of default interface method implementation or

    static class utilities interface Grid<out E> { … } fun <E> Grid<E>.cellsAdjacentTo(x: Int, y: Int): List<Cell> { … }
  11. 1. Superclass constructor 2. Superclass final properties initialization 3. Superclass

    init {…} 4. Subclass constructor 5. Subclass properties initialization 6. Subclass init {…} Avoid using open members in the constructors, property initializers, and init blocks in a base class
  12. Avoid using open members in the constructors, property initializers, and

    init blocks in a base class abstract class BaseGrid<out E> : Grid<E> { final override val rows = 0 until height final override val columns = 0 until width … } BAD: Accessing non-final properties in constructor
  13. abstract class BaseGrid<out E> : Grid<E> { final override val

    rows by lazy { 0 until height } final override val columns by lazy { 0 until width } … } Avoid using open members in the constructors, property initializers, and init blocks in a base class
  14. Use the inline and reified keywords when tackling type erasure

    Reified Erased Array<E> List<E> Runtime type safety Compile-time type safety Since Java 1 Since Java 5 reify | ˈriːɪfʌɪ, ˈreɪɪfʌɪ | verb (reifies, reifying, reified) [with object] formal make (something abstract) more concrete or real: these instincts are, in man, reified as verbal constructs.
  15. Use the inline and reified keywords when tackling type erasure

    fun <E> String.toMutableArrayGrid(toValue: (Char) -> E) : MutableArrayGrid<E> { val s = trimMargin().filter { it != '\n' } val array = Array(s.length) { i -> toValue(s[i]) } val width = trimMargin().lines().first().length return MutableArrayGrid(array, width) } Cannot use ‘E’ as reified type parameter. Use a class instead.
  16. Use the inline and reified keywords when tackling type erasure

    inline fun <reified E> String.toMutableArrayGrid(toValue: (Char) -> E) : MutableArrayGrid<E> { val s = trimMargin().filter { it != '\n' } val array = Array(s.length) { i -> toValue(s[i]) } val width = trimMargin().lines().first().length return MutableArrayGrid(array, width) }
  17. When using type parameters, think of T? as Optional<T> open

    class ArrayGrid<E>( protected val array: Array<E?>, final override val width: Int) : BaseGrid<E?>() open class ArrayGrid<E>( protected val array: Array<E>, final override val width: Int) : BaseGrid<E>() BAD: Only supports nullable types
  18. Always code as if the guy who ends up maintaining

    your code will be a violent psychopath who knows where you live. - John Woods
  19. Consider using the language features to replace Java design patterns

    when possible • by keyword for Decorator pattern (i.e. class delegation) • object for Singleton pattern • Higher-order functions for Strategy pattern • Delegated properties for simple Observer pattern • sealed class for State pattern • Type-safe builder / default arguments for Builder pattern https://github.com/dbacinski/Design-Patterns-In-Kotlin
  20. Use check() and require()instead of if (…) throw {…} when

    appropriate • require(…) throws IllegalArgumentException • check(…) throws IllegalStateException • Use assert(…) for internal or very expensive sanity checks. Won’t be compiled unless the -ea (enable assertions) flag is passed.
  21. open class ArrayGrid<E>(protected val array: Array<E>, final override val width:

    Int) : BaseGrid<E>() { init { require(array.size % width == 0) { "Grid must be a rectangle but array of size ${array.size} " + "wasn't divisible by width $width” // Lazy-evaluated } } … } Use check() and require()instead of if (…) throw {…} when appropriate
  22. Consider implementing operator functions operator fun <E> Grid<E>.get(c: Cell): E

    { get(c.x, c.y) } Usage: val e4 = Cell(4, 4) chessboard[e4]
  23. Consider implementing operator functions operator fun <E> Grid<E>.component1() = width

    operator fun <E> Grid<E>.component2() = height Usage: val (w, h) = chessboard // val w = chessboard.width; val h = chessboard.height
  24. Sort the contents of a class logically https://kotlinlang.org/docs/reference/coding-conventions.html “Do not

    sort the method declarations alphabetically or by visibility, and do not separate regular methods from extension methods. Instead, put related stuff together, so that someone reading the class from top to bottom would be able to follow the logic of what's happening. Choose an order (either higher-level stuff first, or vice versa) and stick to it.”
  25. Keep it simple, stupid • Consider grouping related elements (like

    a type and its extensions) into a single file, as long as the file stays short enough. • If you can’t find a good name for a variable, consider inlining it. Use let {…}, apply {…}, also {…}, run {…} or with(…) if need be.
  26. Don’t optimize yet (but plan for it) • Premature optimization

    is the root of all evil • Once your core structure is in place and your library is public, it’s too late to change it without introducing breaking changes for its users.
  27. Consider performance limitations • Lambdas / Higher-order functions involve creation

    of a Function object, along with primitive boxing (unlike Java 8). Use inline keyword when appropriate. Be wary of local functions as they can’t be inlined. • companion object generate a lot of extra methods • Auto-generated null checks (can be disabled with ProGuard) • lazy properties are synchronized by default • Prefer for-loops over forEach {…} https://medium.com/@BladeCoder/exploring-kotlins-hidden-costs-part-1-fbb9935d9b62
  28. Benchmarking with JMH Why Are Java Microbenchmarks Hard? Writing benchmarks

    that correctly measure the performance of a small part of a larger application is hard. There are many optimizations that the JVM or underlying hardware may apply to your component when the benchmark executes that component in isolation. These optimizations may not be possible to apply when the component is running as part of a larger application. Badly implemented microbenchmarks may thus make you believe that your component's performance is better than it will be in reality. http://tutorials.jenkov.com/java-performance/jmh.html
  29. Benchmarking with JMH open class MutableArrayGridBenchmark { @State(Thread) open class

    MyState { val grid = """ |abc |def |ghi """.toMutableArrayGrid { it } } @Benchmark fun swap(state: MyState) { state.grid.swap(0, 0, 1, 1) } }
  30. Benchmarking with JMH org.openjdk.jmh.infra.Blackhole 
 
 public class Blackhole extends

    Object 
 Black hole "consumes" the values, conceiving no information to JIT whether the value is actually used afterwards. This can save from the dead-code elimination of the computations resulting in the given values.

  31. Benchmarking with JMH # Run complete. Total time: 00:22:30 Benchmark

    Mode Cnt Score Error Units CharGridBenchmark.swap thrpt 200 362148372.636 ± 3391525.749 ops/s MutableArrayGridBenchmark.swap thrpt 200 228289634.510 ± 941099.976 ops/s MutableListGridBenchmark.swap thrpt 200 132345179.610 ± 1009663.525 ops/s kgrid-benchmark $ java -jar target/benchmarks.jar CharArray > Array<Char> > MutableList<MutableList<Char>>
  32. A word of warning I’m not a Gradle/Maven genius, I

    just copy-paste things from the Internet until they work (i.e. StackOverflow- Driven Development) THIS IS GRADLE. IT’S A BUILD SYSTEM FOR THE JVM THAT WORKS THROUGH A BEAUTIFUL DIRECTED ACYCLIC GRAPH MODEL.
  33. Publishing to jCenter apply plugin: 'maven' apply plugin: ‘maven-publish’ apply

    plugin: 'com.jfrog.bintray' group = 'com.vincentcarrier' version = '1.0.1'
  34. Publishing to jCenter publishing { publications { DefaultPublication(MavenPublication) { from

    components.java groupId group artifactId name version this.version } } }
  35. Publishing to jCenter bintray { user = 'vincent-carrier' key =

    System.getenv('BINTRAY_KEY') configurations = ['archives'] publish = true pkg { repo = 'maven' name = 'kgrid' version { name = this.version released = new Date() } } }