Hi, Kotlin: An End to A Beginning

Kotlin for 2018, yummy!

Hello, 2018 and may you be THE year for everything that is good and kind to those who needed them, which is everyone!

Let’s start by looking back at what’s been written about Kotlin so far:

In our introduction, we looked at why Kotlin makes a better language than Java for Android development, saw the required configuration properties to use Kotlin for an Android app project on Android Studio 3.0, and learned on what to do to make sure a newly created Android project can run its unit test cases.

Then, we saw two kinds of testing that can be done on an Android project: unit tests and instrumented tests. We created a basic class and declared constant values, as well as two functions which took advantage of if else and when statements to express flow controls in Kotlin.

Lastly, we wrote our own test cases for Float data type, along with specific test cases for a simple business logic. We also learned on how to update the Kotlin version from 1.1.51 to 1.2.0.

FYI, there’s already another new version 1.2.1!

Thanks to those who have been following along, and may have already moving ahead with their own studies. Note that from now on, I would no longer write the Indonesian version manually. Indonesian readers with limited English skill who wishes to learn Kotlin with me can read through Google Translate instead. It’s not perfect, but their quality have been improving and I’ll try to keep the English version in proper form to make sure minimal mistranslations. Or you could write a response demanding Bahasa Indonesia to be used :)

Without further ado, let’s look at these two updated version of FeeModelUnitTest and FeeModel:

The first thing to look at is how the FeeModel object -or rather, objects- are now created and stored. Remember that Singapore government may increase their GST rate before the 2020 election, and yet it isn’t known what the new rate would be. What we know is that it’ll be a fractional number, so our FeeModel’s primary constructor must take one Float parameter as the tax rate from other source (that we have not define yet) with the current tax rate as its default value:

class FeeModel(private val withTax: Float = 1.07f) {

}

Looking at how we create test cases for a Float parameter on calcuateFees() previously, we get these six values to test our new constructor:

private val taxCases: Array<Float> by lazy {
arrayOf(Float.MIN_VALUE,
Float.MAX_VALUE,
Float.POSITIVE_INFINITY,
Float.NEGATIVE_INFINITY,
Float.NaN,
1f)
}

The use of by lazy here is an interesting feature on Kotlin. It’s a function that execute a lambda block and remember the last value returned. In this case, an array of Float would be returned once, and afterwards taxCases would refer to that array when used instead of creating new array on each use. The lazy part is that no array would be created until there’s a line of code that use taxCases, saving some memory space compared with creating the array directly.

Yes, using by lazy to initialize a constant value with an array of Float is a bit overkill. But it’s an important concept to be aware of on complex object initialization.

We would also need an ArrayList to store FeeModel objects with these tax rates:

private val fees: ArrayList<FeeModel> = ArrayList(7)

Notice that we declared the initial capacity of the ArrayList as 7, one for a FeeModel object with current tax rate, and six for FeeModel object created by mapping taxCases to fees:

@Before
fun setUp() {
fees.add(FeeModel())
taxCases.mapTo(fees) { FeeModel(it) }
}

With FeeModel objects initialized and referred to by fees, we can update our test cases to expect different commissions on different tax rates. For example, in one test case for transaction with smallest worth above zero, we would now expect seven commissions and asserts the returned values in a for loop:

@Test fun feeOnMinFloat() {
val expected = arrayOf(10.700001f,
1.4E-44f,
Float.POSITIVE_INFINITY,
Float.POSITIVE_INFINITY,
0f,
0f,
10f)
val worth = Float.MIN_VALUE
var i = 0
for (fee in fees) {
assertEquals(expected[i++], fee.calculateFees(worth))
}
}

The problem with asserting inside a loop is that the test case would stop at first failure and does not assert the rest of expected values. However, the calculated value must pass on all tax rates regardless and using a loop makes our test case more readable and concise.

Now that we have prepared FeeModel to accept various tax rates, let’s look at how to prepare it for different brokers. We would need to implement another broker as a subclass of FeeModel, which means it must be declared as an open class:

const val DBSV_UPFRONT = 1
const val DBSV_CASH = 2
open class FeeModel(private val withTax: Float = 1.07f) {

fun calculateFees(worth: Float): Float {

}
 var accountType = DBSV_UPFRONT
 protected open fun brokers_fee(worth: Float): Float {
val minimum = if (DBSV_UPFRONT == accountType) 10f else 25f

}
}

Here we refactored vickers_fee() into a more general name brokers_fee(), and modified its visibility from private to protected open so that any subclass can override its function to use their own business logic. We also replaced its second parameter, upfront, into a class property, accountType, and made it public to allow other classes to set and get its value.

The data type is no longer a Boolean, but an Int to anticipate additional types of account. Currently, there are two constant values for account type, DBSV_CASH and DBSV_UPFRONT.

One example to test the changes would be to implement a subclass of FeeModel for a broker that does not charge a fee, let’s call it FreeModel:

class FreeModel: FeeModel() {
override fun brokers_fee(worth: Float) = 0f
}

And then create a test suite for FreeModel on FreeModelUnitTest:

class FreeModelUnitTest {
private lateinit var fee: FeeModel
@Before
fun setUp() {
fee = FreeModel()
}

}

Bear in mind that just because our new broker does not charge a fee, the expected values for each test case may not be zero. The CDP, SGX, and GST charges still applied, sadly.

But wait, there is another thing missing. We still need to figured out how to change the account type when calculating fees: the accountType should not be set as a parameter for the constructor (like what we did for tax rate) because account types only affect the minimum fee charged, not how the calculation is performed. The property is also broker’s specific, so it cannot be set as another parameter for calculateFees() either. For example, our new broker FreeModel does not use accountType at all. Finally, it should be applied before brokers_fee(), which means before calculateFees() are called.

Based on these conditions, the accountType can be set once for all test cases:

class FeeModelUnitTest {

@Before
fun setUp() {
fee = FeeModel()
fee.accountType = DBSV_CASH
}

}

But since there’s only one test case affected by account type, we could change it right before calling calculateFees() on a new test case:

class FeeModelUnitTest {

@Test fun feeOnCashSmallFloat() {
val worth = 100 * 0.001f
val commission = fee.apply {
accountType = DBSV_CASH
}.calculateFees(worth)
assertEquals(26.750044f, commission)
}
}

The best part is other test cases of calculateFees()are not disturbed!

This .apply() function, which is part of Kotlin’s standard library, allows extension of FeeModel even further. For example, if some brokers supports multiple exchange markets then we can have another property on FeeModel, exchangeCode, that may also affect how the broker calculate their fees, and set them inside the lambda block.

With that being said, now that we have made FeeModel more future-proof and see how it would be used, our next step is to create a UI layout where users can calculate their broker’s fee from an Android device!

I would like to use that opportunity to learn about Anko as well, but in the mean time.. Enjoy 2018! Thumbs up for getting this far, please leave a response or advice on any shortcomings.