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

Safe and sound code with Kotlin

GDG SPb
June 15, 2017

Safe and sound code with Kotlin

Java достаточно безопасный язык программирования с точки зрения управления памятью и типов. Но с переходом на Kotlin мы можем программировать ещё более безопасно благодаря некоторым фичам языка, недоступных в Java.

Это означает больше времени на бизнес-логику, меньше на велосипеды. И да, с этими фичами некоторые unit-тесты вам больше не понадобятся.

Автор: Руслан Захаров

Multistack developer.

GDG SPb

June 15, 2017
Tweet

More Decks by GDG SPb

Other Decks in Technology

Transcript

  1. Safe and sound code with kotlin Code business not code

    ruX @ GDG Spb “Kotlin HardcoreWhiteNights” 15.06.2017
  2. Background • Came from startups #GSD • Worked in very

    small multifunctional teams • Used different technologies and platforms • Java - 7+ years, kotlin - 4+ • Currently working at Nexmo
  3. “Simple” java bean public class User { private String name;

    private Date birthday; public User(String name) { this.name = name; } public User(String name, Date birthday) { this(name); this.birthday = birthday; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Date getBirthday() { return birthday; } public void setBirthday(Date birthday) { this.birthday = birthday; } @Override public String toString() { return "User{" + "name='" + name + '\'' + ", birthday=" + birthday + '}'; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; User user = (User) o; if (name != null ? !name.equals(user.name) : user.name != null) return false; return birthday != null ? birthday.equals(user.birthday) : user.birthday == null; } @Override public int hashCode() { int result = name != null ? name.hashCode() : 0; result = 31 * result + (birthday != null ? birthday.hashCode() : 0); return result; }
  4. Null safety private void display(String lang, Integer age) { requireNonNull(lang,

    "Language should be non-null"); requireNonNull(age, "Age should be non-null"); System.out.println(lang.toUpperCase() + " is " + age + "years old"); } display(null, 100); display("Java", 22);
  5. Null safety fun display(lang: String, age: Int) { println("${lang.toUpperCase()} is

    $age years old") } display("kotlin", 6) display(null, 6) Kotlin: Null can not be a value of a non-null type String
  6. Optionals class JPerson { String name; JAddress address; public JPerson(String

    name, JAddress address) { this.name = name; this.address = address; } } class JAddress { String street; String zip; Date updatedAt; public JAddress(String street, String zip, Date updatedAt) { this.street = street; this.zip = zip; this.updatedAt = updatedAt; } }
  7. Optionals void display(JPerson person) { StringBuilder sb = new StringBuilder();

    sb.append(person.name).append(" moved to "); if (person.address != null && person.address.street != null) sb.append(person.address.street); else sb.append("unknown place"); if (person.address != null && person.address.zip != null) sb.append(person.address.zip.toUpperCase()); sb.append(" on "); if (person.address != null && person.address.updatedAt != null) sb.append(person.address.updatedAt.toGMTString()); else sb.append("unknown date"); System.out.println(sb.toString()); }
  8. Optionals data class KPerson(val name: String, val address: KAddress? =

    null) data class KAddress(val street: String?, val zip: String?, val updatedAt: Date) fun display(person: KPerson) { println("${person.name} moved to " + "${person.address?.street ?: "unknown place"} " + "${person.address?.zip.orEmpty()} on " + "${person.address?.updatedAt?.toGMTString() ?: "unknown date"} ") }
  9. Numbers int n = 2_000_000_000; long b = n +

    n; System.out.println(b); -294967296
  10. Numbers val n: Int = 2_000_000_000 val b: Long =

    n + n println(b) Error:(9, 19) Kotlin: Type mismatch: inferred type is Int but Long was expected
  11. Numbers val n: Int = 2_000_000_000 val b: Long =

    n.toLong() + n println(b) 4000000000
  12. Collections void mutatorMethod(List<Integer> list) { list.add(5); } void run() {

    List<Integer> list1 = genList1(); mutatorMethod(list1); List<Integer> list2 = genList2(); mutatorMethod(list2); } List<Integer> genList1() { return new ArrayList<>( asList(1,2,3,4) ); } List<Integer> genList2() { return unmodifiableList( asList(1,2,3,4) ); } Exception in thread "main" java.lang.UnsupportedOperationException at java.util.Collections$UnmodifiableCollection.add(Collections.java:1055) at collections.JavaImmutableCollection.mutatorMethod(JavaImmutableCollection.java:31)
  13. Collections fun mutatorMethod(list: MutableList<Int>) { list.add(5) } fun run() {

    val list1 = genList1() mutatorMethod(list1) val list2 = genList2() mutatorMethod(list2) } fun genList1() : MutableList<Int> { return ArrayList( asList( 1, 2, 3, 4) ) } fun genList2() : List<Int> { return asList(1, 2, 3, 4) } Error:(27, 19) Kotlin: Type mismatch: inferred type is List<Int> but MutableList<Int> was expected
  14. Named arguments void run() { checkParcelSize(439394234, 23, 21, 10, 3.5,

    7.8, 200); } boolean checkParcelSize(long id, double width, double height, double length, double weight, double price, double value) { return true; // pretends we checked something }
  15. Named arguments fun run() { checkParcelSize(id = 439394234, weight =

    3.5 width = 23.0, height = 21.0, length = 10.0, price = 7.8, value = 200.0 ) } fun checkParcelSize(id: Long, width: Double, height: Double, length: Double, weight: Double, price: Double, value: Double ): Boolean { return true }
  16. Named constructor arguments class MyService( val config: Map<String, String>, val

    input: File, val output: File, val offsetX: Int, val offsetY: Int, val items: List<String> ) val service = MyService( config = loadConfig(), items = items, input = File("input.txt"), output = File("output.txt"), offsetX = 42, offsetY = 64 ) (for DI haters)
  17. Keys name refactoring class JUser { private final String login;

    private final String password; public JUser(String login, String password) { this.login = login; this.password = password; } public JSONObject toJson() { return new JSONObject() .put( KEY_LOGIN, login) .put( KEY_PASSWORD, password); } public static final String KEY_LOGIN = "login"; public static final String KEY_PASSWORD = "password"; }
  18. Keys name refactoring data class KUser(val login: String, val password:

    String) { fun toJson() = JSONObject() .put(KUser::login.name, login) .put(KUser::password.name, password) } fun main(args: Array<String>) { val user = KUser("Bob", "123456") println(user.toJson()) }
  19. Sealed classes abstract class JEventBase { } class JMouseMove extends

    JEventBase { public int x, y; public JMouseMove(int x, int y) { this.x = x; this.y = y; } } class JMouseClick extends JEventBase { public int x, y, btn; public JMouseClick(int x, int y, int btn) { this.x = x; this.y = y; this.btn = btn; } } class JKeyPressed extends JEventBase { char key; public JKeyPressed(char key) { this.key = key; } }
  20. Sealed classes List<JEventBase> events = Arrays. asList( new JMouseClick(42, 200,

    1), new JMouseMove(46, 203), new JMouseMove(50, 210), new JMouseClick(40, 210, 3), new JKeyPressed('j') ); events.forEach( this::process);
  21. Sealed classes private void process(JEventBase event) { String log =

    ""; if (event instanceof JMouseClick) { log = "Mouse click button #" + ((JMouseClick) event).btn + " at " + ((JMouseClick) event).x + ", " + ((JMouseClick) event).y; } else if (event instanceof JMouseMove) { log = "Mouse move to " + ((JMouseMove) event).x + ", " + ((JMouseMove) event).y; } else if (event instanceof JKeyPressed) { log = "Key '" + ((JKeyPressed) event).key + "' pressed"; } else throw new IllegalArgumentException("Got unknown event, should never happen!"); System.out.println(event.getClass().getSimpleName() + "> " + log); }
  22. Sealed classes sealed class KEventBase data class KMouseMove(val x: Int,

    val y: Int) : KEventBase() data class KMouseClick(val x: Int, val y: Int, val btn: Int) : KEventBase() data class KKeyPressed(val key: Char) : KEventBase() val events = listOf( KMouseClick( 42, 200, 1), KMouseMove( 46, 203), KMouseMove( 50, 210), KMouseClick( 40, 210, 3), KKeyPressed( 'j') ) events.forEach(::process)
  23. Sealed classes fun process(event: KEventBase) { val log = when(event)

    { is KMouseClick -> "Mouse clicked button #${event.btn} at ${event.x}, ${event.y}" is KMouseMove -> "Mouse moved to ${event.x}, ${event.y}" is KKeyPressed -> "Key '${event.key}' pressed" } println("${event::class.java.simpleName}> $log") }
  24. Type safe units math open class DistanceUnit(val multiplier: Double, val

    unitAbbr: String) object Meters : DistanceUnit( 1.0, "m") object Kilometers : DistanceUnit( 1e3, "km") object Millimeters : DistanceUnit( 1e-3, "mm") object Centimetres : DistanceUnit( 1e-2, "cm") object Miles : DistanceUnit( 1609.34, "mi") object Yards : DistanceUnit( 0.9144, "yd") part #1
  25. Type safe units math data class Distance(val distance: Double, val

    unit: DistanceUnit) { override fun toString() = "${distance}${unit.unitAbbr}" fun to(otherUnit: DistanceUnit) = Distance( distance * unit.multiplier / otherUnit.multiplier, otherUnit) operator fun plus(that: Distance) = Distance( this.distance + that.to(unit).distance, unit) operator fun minus(that: Distance) = Distance( this.distance - that.to(unit).distance, unit) } part #1
  26. Type safe units math val twoKm = Distance(2.0, Kilometers) val

    `5m` = Distance(5.0, Meters) val `42mm` = Distance(42.0, Millimeters) println(twoKm) // 2km println("$`42mm` = ${`42mm`.to(Centimetres)}") // 42.0mm = 4.2cm println("$twoKm = ${twoKm.to(Meters)} = ${twoKm.to(Miles)}") // 2.0km = 2000.0m = 1.2427454732996135mi println(twoKm - `5m`) // 1.995km println(Distance(10.0, Meters) - `5m` + `42mm`) // 5.042m part #1
  27. Type safe units math data class Distance(val distance: Double, val

    unit: DistanceUnit) { fun to(otherUnit: DistanceUnit) = Distance( distance * unit.multiplier / otherUnit.multiplier, otherUnit) val asMeters get() = to(Meters) val asKilometers get() = to(Kilometers) val asMillimeters get() = to(Millimeters) val asCentimetres get() = to(Centimetres) val asMiles get() = to(Miles) val asYards get() = to(Yards) override fun toString() = "${distance}${unit.unitAbbr}" } part #2
  28. Type safe units math fun Number.to(unit: DistanceUnit) = Distance( this.toDouble(),

    unit) val Number.asMeters get() = to(Meters) val Number.asKilometers get() = to(Kilometers) val Number.asMillimeters get() = to(Millimeters) val Number.asCentimetres get() = to(Centimetres) val Number.asMiles get() = to(Miles) val Number.asYards get() = to(Yards) part #2 val tenMeters = 10.asMeters val nineYards = 9.asYards
  29. Type safe units math val twoKm = 2.asKilometers val `5m`

    = 5.asMeters val `42mm` = 42.asMillimeters println("$twoKm = ${twoKm.asMeters} = ${twoKm.asMiles}") // 2.0km = 2000.0m = 1.2427454732996135mi println(twoKm - `5m`) // 1.995km println(10.asMeters - `5m` + `42mm`) // 5.042m val snake = (10.asCentimetres + 1.asMeters).asMeters println("snake is $snake(${snake.asYards}) long") // snake is 1.1m(1.2029746281714786yd) long part #2
  30. Type safe units math data class SpeedUnit(val distanceUnit: DistanceUnit, val

    timeUnit: TimeUnit) open class DistanceUnit(val multiplier: Double, val unitAbbr: String) { operator fun div(timeUnit: TimeUnit) = SpeedUnit( this, timeUnit) } data class Speed(val speed: Double, val unit: SpeedUnit) { fun to(speedUnit: SpeedUnit): Speed { val dd = unit.distanceUnit.multiplier / speedUnit.distanceUnit.multiplier val dt = unit.timeUnit.multiplier / speedUnit.timeUnit.multiplier val s = speed * (dd / dt) return Speed(s, speedUnit) } override fun toString() = "$speed${unit.distanceUnit.unitAbbr}/${unit.timeUnit.unitAbbr}" } part #3
  31. Type safe units math println(10.asSeconds + 5.asMinutes) // 310.0s val

    speed = 10.asKilometers / 5.asHours val speedInMPerSecond = speed.to(Meters / Seconds) println("$speed = $speedInMPerSecond") // 2.0km/h = 0.556m/s val speedInMph = speed.to(Miles / Hours) println("$speed = $speedInMph") // 2.0km/h = 1.2427454732996135mi/h val `50mph` = 50.asMiles / 1.asHours println("$`50mph` = ${`50mph`.to(Kilometers / Hours)}") // 50.0mi/h = 80.467km/h part #3