Wikimapia

What if you tried Kotlin?

An introduction to Kotlin

with an eye to clean code

Damiano Salvi

@PiDayDev

https://github.com/PiDayDev/

unsplash-logoMarvin Esteve

Disclaimer

Code is a fact

Opinions are my own

P.S.: brace yourselves - Emojis and GIFs are coming

๐Ÿ˜œ

... I warned you

unsplash-logoDamla ร–zkan

What are we going to see?

Hi Nando!

unsplash-logonrd

The kata

Checkout system

Simple price:

1 apple = 50 cents
    ๐ŸŽ           ๐Ÿ’ต

Special offers:

3 apples = 120 cents
๐ŸŽ๐ŸŽ๐ŸŽ        ๐Ÿ’ธ๐Ÿ’ธ

 
  • Products in any order
    ๐Ÿ๐ŸŽ๐Ÿ๐Ÿ๐Ÿ๐ŸŒ๐Ÿ  ๐Ÿก†  4๐Ÿ = ๐Ÿค‘
  • Receive offers with each transaction
    5๐Ÿ = ๐Ÿ’ฐ๐Ÿ’ฐ
  • Example:
    Prices ๐ŸŽ 50 ยข  
    Offers ๐ŸŽ๐ŸŽ๐ŸŽ 120 ยข  
    If customers buy... ๐ŸŽ๐ŸŽ๐ŸŽ๐ŸŽ๐ŸŽ
    ..they pay 120 ยข + 50 ยข + 50 ยข
      ๐ŸŽ๐ŸŽ๐ŸŽ ๐ŸŽ ๐ŸŽ
unsplash-logoja ma

Starting point

  • JUnit tests
  • Java implementation:
    • Tests ๐Ÿ‘
    • Quality ๐Ÿ‘Ž
unsplash-logoRamiro Mendes

The interface

package it.intre.sal.kotlin101;

import java.util.*;
import java.util.Map.Entry;

public interface Checkout {
  int pay(
    List<String> items,
    Map<String, Entry<Integer, Integer>> offers
  );
}

on light background: Java

unsplash-logoRamiro Mendes

A few tests

@Test
void aBananaCosts60() throws Exception {
  assertEquals(60, checkout.pay(
    forFruits("banana"), withOffers(1, "banana", 60)
  ));
}

@Test
void fruits() {
  Map<String, Entry<Integer, Integer>> ll = withOffers(3, "apple", 130);
  ll.put("pear", new SimpleEntry<>(2, 45));
  int expectedPrice = 130 /* 3 apples */
                    +  45 /* 2 pears */
                    +  60 /* 1 banana */
                    + 220 /* 1 pineapple */;
  assertEquals(expectedPrice, checkout.pay(
    forFruits("apple", "pear", "apple", "pear", "lychee", "apple", "banana", "pineapple"),
    ll));
}

simplest offer (discount)

two offers, many items

unsplash-logoRamiro Mendes

The implementation

public class UglyCheckout implements Checkout {

    @Override
    public int pay(List<String> items, Map<String, Entry<Integer, Integer>> offers) {
        int res = 0;
        int a = 0;
        int p = 0;
        int ananas = 0;
        int b = 0;

        Map<String, Integer> map = new HashMap<>();
        map.put("apple", 50);
        map.put("pear", 30);
        map.put("pineapple", 220);
        map.put("banana", 60);

        for (String item : items) {
            switch (item) {
                case "apple":
                    a++;
                    break;
                case "pear":
                    p++;
                    break;
                case "pineapple":
                    ananas++;
                    break;
                case "banana":
                    b++;
                    break;
            }
        }

        //Here I have to cycle through every offer to see if it applies
        for (Entry entry : offers.entrySet()) {
            switch (entry.getKey().toString()) {
                case "apple":
                    int a1 = (int) ((Entry) entry.getValue()).getKey();
                    int c1 = a / a1;
                    if (a >= a1) {
                        res += c1 * (int) ((Entry) entry.getValue()).getValue();
                    }
                    a -= a1 * c1;
                    break;
                //jb 2008-09-12: don't sell lychee anymore, but maybe in the future...
//                case "lychee":
//                    int a2 = (int) ((Entry) entry.getValue()).getKey();
//                    if (p >= a2) { res += (int) ((Entry) entry.getValue()).getValue(); }
//                    p -= a2;
//                    break;
                case "pear":
                    int a2 = (int) ((Entry) entry.getValue()).getKey();
                    int c2 = p / a2;
                    if (p >= a2) {
                        res += c2 * (int) ((Entry) entry.getValue()).getValue();
                    }
                    p -= a2 * c2;
                    break;
                case "pineapple":
                    int a3 = (int) ((Entry) entry.getValue()).getKey();
                    int c3 = ananas / a3;
                    if (ananas >= a3) {
                        res += c3 * (int) ((Entry) entry.getValue()).getValue();
                    }
                    ananas -= a3 * c3;
                    break;
                case "banana":
                    int a4 = (int) ((Entry) entry.getValue()).getKey();
                    int c4 = b / a4;
                    if (b >= a4) {
                        res += c4 * (int) ((Entry) entry.getValue()).getValue();
                    }
                    b -= a4 * c4;
                    break;
            }
        }

        for (Entry entry : map.entrySet()) {
            switch (entry.getKey().toString()) {
                case "apple":
                    res += a * (int) entry.getValue();
                    break;
                case "pear":
                    res += p * (int) entry.getValue();
                    break;
                case "pineapple":
                    res += ananas * (int) entry.getValue();
                    break;
                case "banana":
                    res += b * (int) entry.getValue();
                    break;
            }
        }

        return res;
    }
}

        int res = 0;
        int a = 0;
        int p = 0;
        int ananas = 0;
        int b = 0;

item counters


        Map<String, Integer> map = new HashMap<>();
        map.put("apple", 50);
        map.put("pear", 30);
        map.put("pineapple", 220);
        map.put("banana", 60);

price map


        for (String item : items) {
            switch (item) {
                case "apple":
                    a++;
                    break;
                case "pear":
                    p++;
                    break;
                case "pineapple":
                    ananas++;
                    break;
                case "banana":
                    b++;
                    break;
            }
        }

count items


    //Here I have to cycle through every offer to see if it applies
    for (Entry entry : offers.entrySet()) {
        switch (entry.getKey().toString()) {
            case "apple":
                int a1 = (int) ((Entry) entry.getValue()).getKey();
                int c1 = a / a1;
                if (a >= a1) {
                    res += c1 * (int) ((Entry) entry.getValue()).getValue();
                }
                a -= a1 * c1;
                break;

           /* ... */
    }

apply offers, reduce quantities


        for (Entry entry : map.entrySet()) {
            switch (entry.getKey().toString()) {
                case "apple":
                    res += a * (int) entry.getValue();
                    break;

                /* ... */
            }
        }

residual quantities

 
unsplash-logoWilliam Moreland

How I solved the kata

  • Refactor to a cleaner version! ๐Ÿ”„
  • Could it be done in Java?
    • Absolutely โœ…
    • Ample room for improvement ๐Ÿ˜…
  • But JVM โ‰  "just Java" ๐Ÿ’ก
  • Why not try something else?
    • To learn ๐ŸŽ“
    • To (maybe) overcome some limits of the same old language ๐Ÿš€

clean up with style!

So what?

unsplash-logoAmeen Fahmy

Kotlin is...

  • created by JetBrains
  • modern
  • concise
  • expressive
  • intuitive
  • multi-paradigm
  • 100% compatible with Java & JVM
  • supported for Android development
  • influenced by Effective Java (J.Bloch)
  • ...
fun times() =
  with (colleagues) {
    studying {
      Kotlin in guild
    }
  }

val Kotlin = "K"

val guild = "OK!"

val colleagues = "Intrรฉ"

fun studying(
  what: () -> Boolean
) = what()
unsplash-logoRick Mason

Why I like it

fluent and concise

string templates delegates immutable by default
infix functions Android inline functions { it }
lambda higher-order functions var & val extension functions
operator overload DSL public by default coroutines
non-nullable types data class one-line classes
Java compatibility collection functions properties
unsplash-logoShane Aldendorff

Disclaimer (again)

We are NOT going to...

  • remove every code smell
  • get a flawless implementation
  • see a complete overview of Kotlin

...but we WILL

  • reduce code smells
  • make the code clearer and more expressive
  • discover some Kotlin features
  • spur your curiosity (at least, I hope so ๐Ÿ˜‡๐Ÿคž)
unsplash-logoHal Gatewood

A Kotlin class



class ImprovedCheckout

one-line class

; is optional

โŒ

on dark background: Kotlin

package it.intre.sal.kotlin101
 
 
unsplash-logoHal Gatewood

Implement the Java interface

class ImprovedCheckout : Checkout {

    override fun pay(
        items: List<String>,
        offers: Map<String, Map.Entry<Int, Int>>
    ): Int {
        TODO("not implemented")
    }

}
implements ๐Ÿก† ":"
 
fun name(..): Type
 
 

mandatory override

 

nice TODO function

 

extensions from standard library

 
 
package kotlin.collections

public interface Collection<out E> : Iterable<E> { /* ... */ }
public interface List<out E> : Collection<E> { /* ... */ }
public interface Set<out E> : Collection<E> { /* ... */ }
public interface Map<K, out V> { /* ... */ }
unsplash-logoHal Gatewood

100% interoperability also on Java side

class CheckoutTest {
    // ...

    static class Checkouts implements ArgumentsProvider {
        // ....
        return Stream.of(new UglyCheckout(), new ImprovedCheckout())
                    .map(Arguments::of);
        // ...
    }
}

new Java object

new Kotlin object

fails as expected ๐Ÿ’€

 
 
unsplash-logorawpixel

Copy Java & paste Kotlin

override fun pay(items: List<String>, offers: Map<String, Map.Entry<Int, Int>>): Int {
  var res = 0
  var a = 0
  var p = 0
  var ananas = 0
  var b = 0
  // TODO prices
  for (item in items) {
    when (item) {
      "apple" -> a++
      "pear" -> p++
      "pineapple" -> ananas++
      "banana" -> b++
    }
  }
  // TODO offers
  for ((key, value) in map) {
    when (key) {
      "apple" -> res += a * value
      "pear" -> res += p * value
      "pineapple" -> res += ananas * value
      "banana" -> res += b * value
    }
  }
  return res
}
public int pay(List<String> items, Map<String, Entry<Integer, Integer>> offers) {
    int res = 0;
    int a = 0;
    int p = 0;
    int ananas = 0;
    int b = 0;
    // ...prices...
    for (String item : items) {
        switch (item) {
            case "apple":
                a++;
                break;
            case "pear":
                p++;
                break;
            case "pineapple":
                ananas++;
                break;
            case "banana":
                b++;
                break;
        }
    }
    // ...offers...
    for (Entry entry : map.entrySet()) {
        switch (entry.getKey().toString()) {
            case "apple":
                res += a * (int) entry.getValue();
                break;
            case "pear":
                res += p * (int) entry.getValue();
                break;
            case "pineapple":
                res += ananas * (int) entry.getValue();
                break;
            case "banana":
                res += b * (int) entry.getValue();
                break;
        }
    }

    return res;
}

IntelliJ auto-converts ๐Ÿ˜

type inference

when(..) expression

destructuring assignment

 
 
 
 
 
 
 
 
 
 
unsplash-logoHal Gatewood

//TODO prices

val        var

immutable      mutable

const val APPLE = "apple"
const val PEAR = "pear"
const val PINEAPPLE = "pineapple"
const val BANANA = "banana"
const val

compile-time constants

idiomatic map creation

to: infix function

val prices = mapOf(
    APPLE to 50,
    PEAR to 30,
    PINEAPPLE to 220,
    BANANA to 60
)

type inference ๐Ÿก† can omit : String

const val APPLE: String = "apple"
const val PEAR: String = "pear"
const val PINEAPPLE: String = "pineapple"
const val BANANA: String = "banana"
unsplash-logoHal Gatewood

All together

 
const val APPLE = "apple"
const val PEAR = "pear"
const val PINEAPPLE = "pineapple"
const val BANANA = "banana"

val prices = mapOf(
  APPLE to 50,
  PEAR to 30,
  PINEAPPLE to 220,
  BANANA to 60
)

only one failure

class ImprovedCheckout : Checkout {
  override fun pay(items: List<String>, offers: Map<String, Map.Entry<Int, Int>>): Int {
    var res = 0
    var a = 0
    var p = 0
    var ananas = 0
    var b = 0

    for (item in items) {
      when (item) {
        APPLE -> a++
        PEAR -> p++
        PINEAPPLE -> ananas++
        BANANA -> b++
      }
    }
    // TODO offers
    for ((key, value) in prices) {
      when (key) {
        APPLE -> res += a * value
        PEAR -> res += p * value
        PINEAPPLE -> res += ananas * value
        BANANA -> res += b * value
      }
    }
    return res
  }
}
unsplash-logoHal Gatewood

//TODO offers

//Here I have to cycle through every offer to see if it applies
for ((key, value) in offers) {
  when (key) {
    "apple" -> {
      val a1 = (value as Entry<*, *>).key as Int
      val c1 = a / a1
      if (a >= a1) {
        res += c1 * (value as Entry<*, *>).value as Int
      }
      a -= a1 * c1
    }
//jb 2008-09-12: don't sell lychee anymore, but maybe in the future...
//  case "lychee":
//  int a2 = (int) ((Entry) entry.getValue()).getKey();
//  if (p >= a2) {
//     res += (int) ((Entry) entry.getValue()).getValue(); }
//    p -= a2; //    break;
    "pear" -> {
      val a2 = (value as Entry<*, *>).key as Int
      val c2 = p / a2
      if (p >= a2) {
        res += c2 * (value as Entry<*, *>).value as Int
      }
      p -= a2 * c2
    }
    "pineapple" -> {
      val a3 = (value as Entry<*, *>).key as Int
      val c3 = ananas / a3
      if (ananas >= a3) {
        res += c3 * (value as Entry<*, *>).value as Int
      }
      ananas -= a3 * c3
    }
    "banana" -> {
      val a4 = (value as Entry<*, *>).key as Int
      val c4 = b / a4
      if (b >= a4) {
        res += c4 * (value as Entry<*, *>).value as Int
      }
      b -= a4 * c4
    }
  }
}

๐Ÿ‘Ž ugly casts

๐Ÿ‘Ž smelly dead comments

๐Ÿ‘Ž unclear names

 
 
 
 
 
 
 

๐Ÿ‘Ž duplication

 
 
 

๐Ÿ˜Ž but it works!

Refactoring!

var res = 0
var a = 0
var p = 0
var ananas = 0
var b = 0

for (item in items) {
    when (item) {
        APPLE -> a++
        PEAR -> p++
        PINEAPPLE -> ananas++
        BANANA -> b++
    }
}

//Here I have to cycle through every offer to see if it applies
for ((key, value) in offers) {
    when (key) {
        "apple" -> {
            val a1 = (value as Entry<*, *>).key as Int
            val c1 = a / a1
            if (a >= a1) {
                res += c1 * (value as Entry<*, *>).value as Int
            }
            a -= a1 * c1
        }
//jb 2008-09-12: don't sell lychee anymore, but maybe in the future...
//  case "lychee":
//  int a2 = (int) ((Entry) entry.getValue()).getKey();
//  if (p >= a2) {
//     res += (int) ((Entry) entry.getValue()).getValue(); }
//    p -= a2; //    break;
var res = 0
var apple = 0
var pear = 0
var pineapple = 0
var banana = 0

for (item in items) {
    when (item) {
        APPLE -> apple++
        PEAR -> pear++
        PINEAPPLE -> pineapple++
        BANANA -> banana++
    }
}


for ((item, offer) in offers) {
    when (item) {
        APPLE -> {
            val a1 = offer.key
            val q = apple / a1
            if (apple >= a1) {
                res += q * offer.value
            }
            apple -= a1 * q
        }
        PEAR -> {
            val a2 = offer.key
            val q = pear / a2
            if (pear >= a2) {
                res += q * offer.value
            }
            pear -= a2 * q
 

meaningful names

constants

R.I.P. ๐Ÿ‘ป

no casts

 
 
 
 
 
 
 
 
 
 
 
 
 

Refactoring!

    "pineapple" -> {
      val a3 = (value as Entry<*, *>).key as Int
      val c3 = ananas / a3
      if (ananas >= a3) {
        res += c3 * (value as Entry<*, *>).value as Int
      }
      ananas -= a3 * c3
    }
    "banana" -> {
      val a4 = (value as Entry<*, *>).key as Int
      val c4 = b / a4
      if (b >= a4) {
        res += c4 * (value as Entry<*, *>).value as Int
      }
      b -= a4 * c4
    }
  }
}

for ((key, value) in prices) {
    when (key) {
        APPLE -> res += a * value
        PEAR -> res += p * value
        PINEAPPLE -> res += ananas * value
        BANANA -> res += b * value
    }
}

return res
        PINEAPPLE -> {
            val a3 = offer.key
            val q = pineapple / a3
            if (pineapple >= a3) {
                res += q * offer.value
            }
            pineapple -= a3 * q
        }
        BANANA -> {
            val a4 = offer.key
            val q = banana / a4
            if (banana >= a4) {
                res += q * offer.value
            }
            banana -= a4 * q
        }
    }
}

for ((item, price) in prices) {
    when (item) {
        APPLE -> res += apple * price
        PEAR -> res += pear * price
        PINEAPPLE -> res += pineapple * price
        BANANA -> res += banana * price
    }
}

return res
 

names again

 
 

D.R.Y.

var res = 0
var apple = 0
var pear = 0
var pineapple = 0
var banana = 0

for (item in items) {
    when (item) {
        APPLE -> apple++
        PEAR -> pear++
        PINEAPPLE -> pineapple++
        BANANA -> banana++
    }
}

for ((item, offer) in offers) {
    when (item) {
        APPLE -> {
            val a1 = offer.key
            val q = apple / a1
            if (apple >= a1) {
                res += q * offer.value
            }
            apple -= a1 * q
        }
        //  ... REPEAT THREE MORE TIMES ๐Ÿ˜ฃ
}

for ((item, price) in prices) {
    when (item) {
        APPLE -> res += apple * price
        PEAR -> res += pear * price
        PINEAPPLE -> res += pineapple * price
        BANANA -> res += banana * price
    }
}

return res
var res = 0

val quantities = mutableMapOf<String, Int>()

for (item in items) {
    quantities[item] = 1 + (quantities[item] ?: 0)
}


for ((item, offer) in offers) {
    val (offerQuantity, offerPrice) = offer
    val quantity = quantities[item] ?: 0
    if (item in prices.keys) {
        val repeat = quantity / offerQuantity
        res += repeat * offerPrice
        quantities[item] = quantity - repeat * offerQuantity
    }
}




for ((item, price) in prices) {
    val quantity = quantities[item] ?: 0
    res += quantity * price
}




return res
 

one map ๐Ÿ’

(to count them all)

one computation

?: Elvis operator

destr.assign. again

 
 
 
 
 
 
 
 

Collection methods

val quantities = mutableMapOf<String, Int>()
for (item in items) {
    quantities[item] = 1 + (quantities[item] ?: 0)
}


for ((item, offer) in offers) {
    // ...
}


for ((item, price) in prices) {
    val quantity = quantities[item] ?: 0
    res += quantity * price
}
val quantities = items
        .groupingBy( { item -> item } )
        .eachCount()
        .toMutableMap()


offers.forEach { (item, offer) ->
    // ...
}


res += quantities.entries.sumBy {
    (item, quantity) -> quantity * (prices[item]?:0)
}
 

no-boilerplate collection functions

 
 
 

lambda expressions

 
 
 

mutable? on-demand

 

inline fun: embed lambda โฉ

inline fun <T> Iterable<T>.forEach(action: (T) -> Unit)

Lambda expressions

 items.groupingBy( { item -> item } )



 items.groupingBy() { item -> item }



 items.groupingBy { item -> item }



 items.groupingBy { it }

lambda expressions as an argument

lambda can go outside "( )" if last argument

and you can omit "( )" if only argument

lambda with a single parameter can use implicit it

 
 
 
 
CTRL + ALT + SHIFT + K
public interface Checkout {
    int pay(List<String> items, Map<String, Map.Entry<Integer, Integer>> offers);
}
class CheckoutTest {
    // ...
    void aBananaCosts60(Checkout checkout) {
        assertEquals(60, checkout.pay(
            forFruits("banana"), withOffers(1, "banana", 60)
        ));
    }
}
interface Checkout {
    fun pay(items: List<String>, offers: Map<String, Map.Entry<Int, Int>>): Int
}
class CheckoutTest {
    // ...
    fun aBananaCosts60(checkout: Checkout) {
        assertEquals(60, checkout.pay(
            forFruits("banana"), withOffers(1, "banana", 60)
        ))
    }
}

Disclaimer: may need ๐Ÿ‘ฉโ€๐Ÿ”ง๐Ÿ‘จโ€๐Ÿ”ง

Benefits

fun pay(items: List<String>, offers: Map<String, Map.Entry<Int, Int>>): Int

fun pay(items: List<String>, offers: Map<String, Pair<Int, Int>>): Int


fun withOffers(quantity: Int, fruit: String, offerPrice: Int): MutableMap</*..*/> {
    val offers = HashMap<String, Entry<Int, Int>>()
    offers[fruit] = SimpleEntry(quantity, offerPrice)
    return offers
}

fun withOffers(quantity: Int, fruit: String, offerPrice: Int) =
    mutableMapOf(fruit to (quantity to offerPrice))


fun forFruits(vararg fruits: String): List<String> {
    return Arrays.asList(*fruits)
}

fun forFruits(vararg fruits: String) = fruits.asList()

Pair idioms: a to (b to c)

collection methods

 
 
 
 
 
 

builtin types

We can do better

override fun pay(items: List<String>, offers: Map<String, Pair<Int, Int>>): Int {
    val quantities = items
            .groupingBy { it }
            .eachCount()
            .toMutableMap()

    var offerTotal = 0
    offers.forEach { (item, offer) ->
        val (offerQuantity, offerPrice) = offer
        val quantity = quantities[item]
        if (quantity != null && item in prices.keys) {
            val repeat = quantity / offerQuantity
            offerTotal += repeat * offerPrice
            quantities[item] = quantity - repeat * offerQuantity
        }
    }

    return offerTotal +
           quantities.entries.sumBy { (item, quantity) ->
                quantity * (prices[item] ?: 0)
           }
}

mutable map ๐Ÿ˜•

mutable var ๐Ÿ™„

if ๐Ÿคจ

primitive obsession ๐Ÿคช

long method ๐Ÿ˜ฅ

 
 
 
 
 
 
 
 
 
 

The first rule of functions is that they should be small.

     The second rule of functions is that they should be smaller than that.

Uncle Bob

Domain model

data class Offer(val quantity: Int, val price: Int)
data class

smaller methods

new instance (no new)

named arguments

override fun pay(items: List<String>, offers: Map<String, Pair<Int, Int>>): Int {
    val quantities = items.groupingBy { it }.eachCount().toMutableMap()








    var offerTotal = 0
    offers.forEach { (item, offer) ->
        val (offerQuantity, offerPrice) = offer
        // ...
    }
    // ...
    val offersMap = offers.mapValues {
         (_, v) -> Offer(quantity = v.first, price = v.second)
    }
    return pay(quantities, offersMap)
}

private fun pay(quantities: MutableMap<String, Int>, offers: Map<String, Offer>): Int {

built-in support for destructuring assignment, ...

 
 
 
 
 
 

Let's move business logic into the model

expression body

 

operator overload

 
Offer * 3 โ‡„ Offer.times(3)
data class Offer(val quantity: Int, val price: Int) {

    operator fun times(repeat: Int) = Offer(repeat * quantity, repeat * price)

}
data class Offer(val quantity: Int, val price: Int) {

    operator fun times(repeat: Int) = Offer(repeat * quantity, repeat * price)

    infix fun buying(quantity: Int) : Offer {
        val repeat = quantity / this.quantity
        return this * repeat
    }

}
 

infix function

i call u โ‡„ i.call(u)

and that's how DSL are born ๐Ÿ‘ถ

 
  val (offerQuantity, offerPrice) = offer
  val repeat = quantity / offerQuantity
  offerTotal += repeat * offerPrice
  quantities[item] = quantity - repeat * offerQuantity
 
  val appliedOffer = offer buying quantity
  offerTotal += appliedOffer.price
  quantities[item] = quantity - appliedOffer.quantity

Model for "no offers"

data class Offer(val quantity: Int, val price: Int) {
    operator fun times(repeat: Int) = Offer(repeat * quantity, repeat * price)
    infix fun buying(quantity: Int) : Offer {
        val repeat = quantity / this.quantity
        return this * repeat
    }
}
sealed class SpecialPrice

sealed restricts inheritance

all subclasses in same file

object makes a singleton

data class Offer(val quantity: Int, val price: Int): SpecialPrice() {
    operator fun times(repeat: Int) = Offer(repeat * quantity, repeat * price)
    infix fun buying(quantity: Int) : Offer {
        val repeat = quantity / this.quantity
        return this * repeat
    }
}
object NoOffer : SpecialPrice()
 
 
 
sealed class SpecialPrice {
  companion object {
    fun from(quantityToPrice: Pair<Int, Int>?) =
      when (quantityToPrice) {
        null -> NoOffer
        else -> Offer(
          quantity = quantityToPrice.first,
          price = quantityToPrice.second
        )
      }
  }
}

companion object โ‰ˆ Java static

when & null only in factory method

 
 
 

There may be no more than one switch statement for a given type of selection.
    The cases [...] must create polymorphic objects that take the place of other such switch [...]

Uncle Bob

Polymorphism

sealed class SpecialPrice {
  companion object { /* ... */ }
  abstract fun payItem(quantity: Int, price: Int): Int
}

data class Offer(val quantity: Int, val price: Int): SpecialPrice() {
  /* ... */

  override fun payItem(quantity: Int, price: Int): Int {



  }
}

object NoOffer: SpecialPrice() {
  override fun payItem(quantity: Int, price: Int) =
}
 

Offer API

quantity * price
    val appliedOffer = this buying quantity
    return appliedOffer.price + (quantity - appliedOffer.quantity) * price
? ? ?
? ? ?
 
 

Classes in action

override fun pay(items: List<String>, offers: Map<String, Pair<Int, Int>>): Int {
  val quantities = items
    .groupingBy { it }
    .eachCount()
    .toMutableMap()

  val offersMap = offers.mapValues {
    (_, v) -> Offer(quantity = v.first, price = v.second)
  }
  return pay(quantities, offersMap)
}

private fun pay(quantities: MutableMap<String, Int>, offers: Map<String, Offer>): Int {
  var offerTotal = 0
  offers.forEach { (item, offer) ->
    val (offerQuantity, offerPrice) = offer
    val quantity = quantities[item]
    if (quantity != null && item in prices.keys) {
      val repeat = quantity / offerQuantity
      offerTotal += repeat * offerPrice
      quantities[item] = quantity - repeat * offerQuantity
    }
  }

  return offerTotal +
    quantities.entries.sumBy { (item, quantity) -> quantity * (prices[item] ?: 0) }
}
override fun pay(items: List<String>,
         offers: Map<String, Pair<Int,Int>>) =
  prices.entries.sumBy { (item, price) ->
    val quantity = items.count { it == item }
    val offer = SpecialPrice.from(offers[item])
    offer.payItem(quantity, price)
  }
 

loop once on non-null Pairs (item to price)

 

sumBy: for each entry, add lambda result

 

count each item separately (easier)

 

from Pair? get a non-null SpecialPrice

 

lambda returns value of last expression

 

no if, null, var, Mutable*

๐Ÿคฉ๐Ÿ˜Ž

small!

Recap

  public int pay(List<String> items, Map<String, Entry<Integer, Integer>> offers) {
    int res = 0;
    int a = 0;
    int p = 0;
    int ananas = 0;
    int b = 0;

    Map<String, Integer> map = new HashMap<>();
    map.put("apple", 50);
    map.put("pear", 30);
    map.put("pineapple", 220);
    map.put("banana", 60);

    for (String item : items) {
      switch (item) {
        case "apple":
          a++;
          break;
        case "pear":
          p++;
          break;
        case "pineapple":
          ananas++;
          break;
        case "banana":
          b++;
          break;
      }
    }
    //Here I have to cycle through every offer to see if it applies
    for (Entry entry : offers.entrySet()) {
      switch (entry.getKey().toString()) {
        case "apple":
          int a1 = (int) ((Entry) entry.getValue()).getKey();
          if (a >= a1) {
            res += (int) ((Entry) entry.getValue()).getValue();
          }
          a -= a1;
          break;
        //jb 2008-09-12: don't sell lychee anymore, but maybe in the future...
//                case "lychee":
//                    int a2 = (int) ((Entry) entry.getValue()).getKey();
//                    if (p >= a2) { res += (int) ((Entry) entry.getValue()).getValue(); }
//                    p -= a2;
//                    break;
        case "pear":
          int a2 = (int) ((Entry) entry.getValue()).getKey();
          if (p >= a2) {
            res += (int) ((Entry) entry.getValue()).getValue();
          }
          p -= a2;
          break;
        case "pineapple":
          int a3 = (int) ((Entry) entry.getValue()).getKey();
          if (ananas >= a3) {
            res += (int) ((Entry) entry.getValue()).getValue();
          }
          ananas -= a3;
          break;
        case "banana":
          int a4 = (int) ((Entry) entry.getValue()).getKey();
          if (b >= a4) {
            res += (int) ((Entry) entry.getValue()).getValue();
          }
          b -= a4;
          break;
      }
    }

    for (Entry entry : map.entrySet()) {
      switch (entry.getKey().toString()) {
        case "apple":
          res += a * (int) entry.getValue();
          break;
        case "pear":
          res += p * (int) entry.getValue();
          break;
        case "pineapple":
          res += ananas * (int) entry.getValue();
          break;
        case "banana":
          res += b * (int) entry.getValue();
          break;
      }
    }

    return res;
  }
}
class ImprovedCheckout : Checkout {
  override fun pay(items: List<String>, offers: Map<String, Pair<Int, Int>>) =
    prices.entries.sumBy { (item, price) ->
      val quantity = items.count { it == item }
      val offer = SpecialPrice.from(offers[item])
      offer.payItem(quantity, price)
    }
}
sealed class SpecialPrice {
  companion object {
    fun from(quantityToPrice: Pair<Int, Int>?) = when (quantityToPrice) {
        null -> NoOffer
        else -> Offer(quantity = quantityToPrice.first, price = quantityToPrice.second)
      }
  }
  abstract fun payItem(quantity: Int, price: Int): Int
}
data class Offer(val quantity: Int, val price: Int) : SpecialPrice() {
  operator fun times(repeat: Int) = Offer(repeat * quantity, repeat * price)
  private infix fun buying(quantity: Int): Offer {
    val repeat = quantity / this.quantity
    return this * repeat
  }
  override fun payItem(quantity: Int, price: Int): Int {
    val appliedOffer = this buying quantity
    return appliedOffer.price + (quantity - appliedOffer.quantity) * price
  }
}
object NoOffer : SpecialPrice() {
  override fun payItem(quantity: Int, price: Int) = quantity * price
}








   /* this space intentionally left blank */
unsplash-logoLuca Bravo

Kotlin ecosystem

Stack Overflow survey 2019: 4th most loved ๐Ÿ’œ & 5th most wanted ๐Ÿ’ช

Stack Overflow blog 2017: 2nd least hated ๐Ÿคท

Kotlin 2019 - The state of Developer Ecosystem ๐Ÿ“ˆ

unsplash-logoRobynne Hu

Future is realizing

...although it may not be the right solution for everyone

Kotlin Foundation

The Kotlin Foundation was created by JetBrains and Google with the mission to protect, promote and advance the development of the Kotlin programming language.

unsplash-logoJaredd Craig

References

๐Ÿ’ป Source code (step by step)
      https://github.com/PiDayDev/kotlin-what-if-you-tried

๐ŸŽž๏ธ Slides
      https://pidaydev.github.io/kotlin-what-if-you-tried-slides

๐Ÿ“— "Clean Code: A Handbook of Agile Software Craftsmanship"
      Robert C. Martin (a.k.a. Uncle Bob)

๐Ÿ“™ "Effective Java"
      Joshua Bloch

๐Ÿ“ข Kotlin Programming Language
      https://kotlinlang.org/

๐Ÿงฎ Kotlin 2019 - The state of Developer Ecosystem
      https://www.jetbrains.com/lp/devecosystem-2019/kotlin

๐Ÿ“Š State of Kotlin 2018, Pusher
      https://pusher.com/state-of-kotlin

Thanks

 
unsplash-logoEvan Dennis

@PiDayDev

https://github.com/PiDayDev/

Please give me your feedback at forms.gle/AfhXjyN138sBTDqw6