Kale, apparently has many health benefits such as being high in fiber and water. However, me, personally, I find it on par on eating paper and I refuse to even accept that kale is proper food!
Kale rant aside, this blog post is really about coding patterns for writing conditionals in an AppStudio (QML) app.
I've come up with a simple app that demonstrate the conditional coding pattern.
It has a combo box with the values "apple", "carrot" or "kale" where. Upon a user's selection, the app will respond with the corresponding food type: "fruit", "vegetable" or "unknown" respectively.
To implement the above app, there are many methods I've come across, thus, I take a comparative approach covering the most common to the most peculiar.
Each approach has a complete working code sample and discussed.
import QtQuick 2.7
import QtQuick.Controls 2.1
import ArcGIS.AppFramework 1.0
App {
id: app
property alias food: foodComboBox.currentText
property string foodType: getFoodType(food)
Column {
anchors.fill: parent
anchors.margins: 10
spacing: 10
ComboBox {
id: foodComboBox
model: [ "apple", "carrot", "kale" ]
}
Text {
text: foodType
}
}
function getFoodType(food) {
if (food === "apple") {
return "fruit";
}
if (food === "carrot") {
return "vegetable";
}
return "unknown";
}
}
The above is a complete code sample that determines a type of food given a user's input.
We see there are two visual components, ComboBox component for the user to select "apple", "carrot" or "kale" and a Text component to display the corresponding food type "fruit", "vegetable" or "unknown" respectively.
The visual component is constant in all the apps shown in the blog. The only thing that differs is the implementation of the foodType property.
In this example, it is implemented with the if statement via the getFoodType function.
It's very obvious, very easy to read and maintain.
I'm usually against the if statement pattern:
I've carefully crafted the answer to not utilize any else statements. When using if statements, I prefer to implement early exit with return statement following the guard pattern removing one level of nesting for flatter code and helps avoids errors. See Guard (computer science) - Wikipedia
When there are 3 or more cases if statements can become cumbersome to read and maintain. Terse cases may require a refactor moving code to their own functions.
import QtQuick 2.7
import QtQuick.Controls 2.1
import ArcGIS.AppFramework 1.0
App {
id: app
property alias food: foodComboBox.currentText
property string foodType: food === "apple" ? "fruit" : food === "carrot" ? "vegetable" : "unknown"
Column {
anchors.fill: parent
anchors.margins: 10
spacing: 10
ComboBox {
id: foodComboBox
model: [ "apple", "carrot", "kale" ]
}
Text {
text: foodType
}
}
}
The conditional (ternary) operator is very short and deceptively simple. It is often used in 1-liner solutions.
I tend to use conditional for very simplest of cases.
As your application complexity grows, the conditional expression rapidly becomes hard to read and maintain. They tend to obfuscate what you're trying to achieve.
When there are 3 or more cases, it quickly loses its appeal as a 1-liner solution.
import QtQuick 2.7
import QtQuick.Controls 2.1
import ArcGIS.AppFramework 1.0
App {
id: app
property alias food: foodComboBox.currentText
property string foodType: getFoodType(food)
Column {
anchors.fill: parent
anchors.margins: 10
spacing: 10
ComboBox {
id: foodComboBox
model: [ "apple", "carrot", "kale" ]
}
Text {
text: foodType
}
}
function getFoodType(food) {
switch (food) {
case "apple":
return "fruit";
case "carrot":
return "vegetable";
}
return "unknown";
}
}
The switch statement is designed to handle multiple cases. If the values for each case is concise, then, this approach works well.
In my apps, I find, however, as my requirement grows, the switch statement becomes longer and longer and they it ends up as code that's longer than one screenful. When that happens, we lose the conciseness of this approach. In order to get back down to something that's manageable we refactor the cases by moving the code for each cases to their own function.
import QtQuick 2.7
import QtQuick.Controls 2.1
import ArcGIS.AppFramework 1.0
App {
id: app
property alias food: foodComboBox.currentText
property var foodTypes: {
"apple": "fruit",
"carrot": "vegetable"
}
property string foodType: foodTypes[food] || "unknown"
Column {
anchors.fill: parent
anchors.margins: 10
spacing: 10
ComboBox {
id: foodComboBox
model: [ "apple", "carrot", "kale" ]
}
Text {
text: foodType
}
}
}
This approach implements a lookup table. We leverage a Javascript object's ability to hold key value pairs. The advantages to this approach is the code is declarative with minimal imperative code.
I used this approach when managing large list of keys whose values are simple and defined. We simply hard code the list at the top of the app, and/or deployed the list via JSON object resource that gets loaded when the app starts.
This is approach is easy to read and maintain. We have a limitation that the key to the lookup table must be a string.
import QtQuick 2.7
import QtQuick.Controls 2.1
import ArcGIS.AppFramework 1.0
App {
id: app
property alias food: foodComboBox.currentText
property string foodType: getFoodType(food)
Column {
anchors.fill: parent
anchors.margins: 10
spacing: 10
ComboBox {
id: foodComboBox
model: [ "apple", "carrot", "kale" ]
}
Text {
text: foodType
}
}
function getFoodType_apple() {
return "fruit";
}
function getFoodType_carrot() {
return "vegetable";
}
function getFoodType(food) {
var func = "getFoodType_" + food;
return func in app ? app[func]() : "unknown";
}
}
This approach uses the fact that we can invoke a Javascript function by name. It also requires us to use the name of the parent object (here it is called app). We have to construct the function name, and, if it exists we invoke that function.
I used this approach in app where the requirements were changing. I was building a conversion app. The requirements were changing, i.e. the list of inputs (e.g. the list of foods) will grow over time. This pattern was useful as it allowed me to focus on handling new cases as they occur.
property var bool use_merriam_webster: true
function getFoodType_tomato()
{
// A tomato is actually a fruit -- but it's a vegetable at the same time
// https://www.businessinsider.com.au/tomato-fruit-or-vegetable-2018-5?r=US&IR=T
if (use_merriam_webster) {
return "fruit";
}
return "vegetable";
}
Despite being the driver function being hard to read, it has advantage that it doesn't require maintenance to add new cases. We simply start coding the new cases and they will automatically be picked up.
For the "Kale is not food" app all 5 approaches shown above achieve the task and one may argue that there isn't much differences between them in achieving that task.
For more complex applications, some of the more obscure approaches such as lookup table or invocation by function name may become more appealing in long term ability to maintain and scale.
Generally, whenever I see code involving either the if or switch statement, I ask myself would it be worthwhile to check to see if another approach works better?. Usually, I find: yes, it is.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.