An introduction to Kotlin
with an eye to clean code
Damiano Salvi
@PiDayDev
https://github.com/PiDayDev/
P.S.: brace yourselves - Emojis and GIFs are coming
... I warned you
codekata.com/kata/kata01-supermarket-pricing
github.com/jesuswasrasta/KataSupermarketRefactoring
Hi Nando!
Checkout system
Simple price:
1 apple = 50 cents
๐ ๐ต
Special offers:
3 apples = 120 cents
๐๐๐ ๐ธ๐ธ
Prices | ๐ | 50 ยข | |
---|---|---|---|
Offers | ๐๐๐ | 120 ยข | |
If customers buy... | ๐๐๐๐๐ | ||
..they pay | 120 ยข | + 50 ยข | + 50 ยข |
๐๐๐ | ๐ | ๐ |
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
@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
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
clean up with style!
fun times() =
with (colleagues) {
studying {
Kotlin in guild
}
}
val Kotlin = "K"
val guild = "OK!"
val colleagues = "Intrรฉ"
fun studying(
what: () -> Boolean
) = what()
class ImprovedCheckout
one-line class
; is optional
โ
on dark background: Kotlin
package it.intre.sal.kotlin101
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> { /* ... */ }
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 ๐
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
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"
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
}
}
//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!
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
"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
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
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)
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 ๐ฉโ๐ง๐จโ๐ง
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
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
data class Offer(val quantity: Int, val price: Int)
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, ...
expression body
operator overload
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
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
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
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!
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 */
Stack Overflow survey 2019: 4th most loved ๐ & 5th most wanted ๐ช
https://insights.stackoverflow.com/survey/2019/ (in 2018 was 2nd most loved)
Stack Overflow blog 2017: 2nd least hated ๐คท
Kotlin 2019 - The state of Developer Ecosystem ๐
...although it may not be the right solution for everyone
The Kotlin Foundation was created by JetBrains and Google with the mission to protect, promote and advance the development of the Kotlin programming language.
๐ป
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
@PiDayDev
https://github.com/PiDayDev/