Let's be honest, the program or code that you wrote can also be scribbled (As long as can it can do the work right?). But this makes it harder for the new person that comes in and reads your code, as they don't have the context of what and why you wrote such a way. And this is even worse when comes to debugging if issues rise up.
And generally, codes are read more often than written, and existing features are maintained more often than developing a new feature or product. So, If you want to write well or be easy to read, you have to spend a lot of effort to learn writing skills. But quality codes come with big advantages, such as:
- Improved the workflow efficiency
- Increased the testability of the app
- Building a professional and structured way of writing code
This section will share some basic knowledge points with you, and practical skills can basically be used in contemporary programming language development.
Everything in code hides a meaning behind its use; it has an intention and the developer must reflect this meaning using an appropriate name for it. Follow the Name Convention of the project. For more information click here.
- On the whole, naming should follow Java standards, as Kotlin is a JVM-compatible language.
- Good naming makes it easier to read and understand code.
- All naming has to be simple, clear and mnemonic (short and meaningful).
- Abbreviations and acronyms should use camel case as normal words (e.g., XmlHttpRequest, QrCodeReader, userId).
- Avoid fully short-form naming unless the name or context is widely known by others. But still please avoid it and write the full name as it gives more clarity of what the variable actually does.
- For example,
val ea = “” // Email AddressThis can cause confusion as new people wouldn't know what isea - Another example is
password. Some people love writing it as short-formpwd. But some people might prefer the full-namepassword. - Also, most of the IDE now comes with auto-complete. It wouldn't hurt to type a few words more on your keyboard
- For example,
Generally, there are parameters that we need to pass into the function to do certain operations. But as requirement increase, the number of parameters also increase as well, and this will bloat the function to accept a lot of parameters and do a lot of things together. This makes it harder for the people who read the function to spend even more time trying to understand what the function do and debug if something goes wrong.
So we need to maintain the number of parameters pass into the function. As a rule of thumb, 3 is the maximum recommended parameter to pass into. If there are more than 3, please write a new function and refactor the code with better logic
Simplify the return condition whenever possible. Also, most of the IDE now can help you to simplify the return so that it's easier to read and understand.
For example, given a function that checks whether the user can drive a car in Malaysia, you would check the logic as
fun canPersonDriveCar(person) : Boolean {
if (person.age ≥ 18 && person.hasDrivingLicense)
return true
else
return false
}But in actual fact, you can simplify the function logic, and it will still do the same thing
fun canPersonDriveCar(person): Boolean {
return person.age ≥ 18 && person.hasDrivingLicense
}As time increase, there are certain scenarios whereby there are multiple functions are required to access a particular variable many times. Hence the variable is better to place on the top of the class as a global variable so that every function can access it. But this makes it hard to debug as the variable you need to trace can scroll until the bottom and never-ending. And you might forget what the variable was initially assigned for. To add on, global variables makes it harder to debug as the scope is wider.
So nowadays, it's becoming a norm and practice to try and limit the variable in a function scope whenever possible so that it's easier to trace and debug. This also avoids dirty mutating the value in an unexpected way. So if the variable is scoped within a function, it will just live within the function, and after finishing it will be destroyed and garbage-collected
loadUserAndSendProfileToServer() This is still a bad design but a bare minimum to avoid any confusion
Avoid Heavily Nested Code. Break into steps and Return Early. Nested codes are harder to read and as a result, you often forget to handle certain edge cases. Nested code also makes your code march to the right-hand side of the editor, which makes hard-to-read code even harder to read on a small screen.
In short, this is to Keep It Simple and Stupid. And will helps makes the logic check and return fast so that It can fully be done its operation. This is also used with Test Driven Development, whereby it considers the Exception or Bad path first before writing happy flow.
This section will refer to other examples so that you will understand the Design Patterns as well.
https://medium.com/swlh/return-early-pattern-3d18a41bba8
https://careers.per-angusta.com/blog/common-mistakes-with-guard-clause-pattern/
https://www.rockandnull.com/guard-clauses-vs-nested-conditionals/
https://in-kotlin.com/design-patterns/
For example, the following is bad:
if (!validationFail) {
// Do A
if (!secondValidationFail) {
// Do B
if (!thirdValidationFail && !fourthValidationFail) {
// Do C
}
else {
return;
}
}
else {
// Do D
}
}Instead, break the code into clear Steps and fail fast if any step cannot be performed (The logic in this example is identical to the bad code above):
if (validationFail) {
return;
}
// Do A
if (secondValidationFail) {
// Do D
return;
}
// Do B
if (thirdValidationFail || fourthValidationFail) {
return;
}
// Do C
Duplicated code means more lines of code that can lead to bugs, and extra maintenance effort later down the road (especially when the same piece of code need to be changed everywhere where it was copy-pasted — which means an even higher possibility of introducing new bugs).
Common functionalities should be broken down into small, sharable parts and organised into functions. Write unit-tests for these functions.
Another example of duplicated code smell:
nameTextView.setText(someArray.get(position).getSchedule().getCompany().getName());
idTextView.setText(someArray.get(position).getSchedule().getCompany().getId());
outletNameTextView.setText(someArray.get(position).getSchedule().getOutlet().getName();
addressTextView.setText(someArray.get(position).getSchedule().getOutlet().getAddress();
...
// Do other things with Outlet
...
blah blahSchedule, Company, and Outlet should be saved in local variables instead
Schedule schedule = someArray.get(position).getSchedule();
Outlet outlet = schedule.getOutlet();
Company company = schedule.getCompany();Static variables & singletons can be very convenient at times, but they have many downsides:
- Static variables & singletons are global to your app and they persist in the memory until the app is closed/killed. Sometimes this is what we want (UIApplication / Custom Application, LocationManager), but these use cases are rare. In the context of mobile app, static variables’ lifecycle is NOT tied to the lifecycle of your Activity / ViewController. Therefore using static variable to store information within an Activity / ViewController is wrong, and for communication between Activities/ViewControllers, there are much better native platform specific mechanisms.
- Static variables & singletons are impossible* to mock and stub. You cannot easily unit-test a methods that reads from a static variable. For singleton, because the initialization is one-time and there’s no public constructor, you cannot create a mock and/or stub its methods. Basically, use static variables & singletons only if you are okay about forgetting unit-testing all the codes that uses them.
- Static variables & singletons break many things we know about good software architecture practices. For example, introducing a global state that makes code execution unpredictable especially in a multi-threaded environment. They are also an anti-pattern to Dependency Injection (DI) or the Inversion of Control (IoC). Your code will be harder to maintain due to the complex web of dependencies between all the singletons and their users, and all the interactions that can potentially happen among them.
- Singleton initialization can be tricky and incorrectly implemented. This is a common source of bugs.
In mobile, this normally means that the representation of the data varies based on the device’s locale. For example, ‘2015-04-20T15:25:56.061+08:00’ is an RFC3339 standard DateTime which doesn’t change no matter where you are in the world.
Use Machine Readable Data when you are sending/receiving data from different systems, and also for your internal representation, processing & storage in the app. On the other hand, when we present the data to the user in the user interface, normally we want the data to be in a Human Readable format that takes into account the user’s locale and so on.
For example, the same Date Time above could be represented as ‘ 2015 **** 4 **** 20 **** ’ in Taiwan and ‘20 April 2015’ in English. Be careful not to accidentally send non-Machine Readable Data when communicating with other systems. Normally this is not obvious during development because developers’ machines are in the English Locale, and only in Production do we encounter all kinds of different locales.
One of the most common bugs in relation to this is initializing DateFormatter with Default Locale, or Upper Casing strings using the Default Locale. Android even has a Lint Check that warns you about this.
// Default Locale?? Careful... Many bugs come from this
SimpleDateFormat formatter = new SimpleDateFormat("YYMMDD");
SimpleDateFormat formatter = new SimpleDateFormat("YYMMDD", Lo
cale.getDefault());
// Both of these also use Default locale. Careful...
"somestring".toUpperCase();
"somestring".toUpperCase(Locale.getDefault())Instead, think about what you are going to use the data for
// Human Readable, explicitly tell people you want the Default Locale
SimpleDateFormat formatter = new SimpleDateFormat("YYMMDD", Locale.getDefault());
"somestring".toUpperCase(Locale.getDefault());
// Machine Readable, en_US POSIX Locale is your friend
SimpleDateFormat formatter = new SimpleDateFormat("YYMMDD", Locale.US);
"somestring".toUpperCase(Locale.US);When dependencies of the libraries are managed thru Cocoapods / Gradle, make sure that the versions are locked to a specific version. This is to ensure that you can rebuild the same app with the same version of all its libraries in the future. Also, prevent surprises when upstream update the library with some unexpected changes.
// Don't do this
implementation 'com.awesome:library: 1. 10. 1 +'
// Do this
implementation 'com.awesome:library: 1. 10. 1 'Generally, we should avoid using any libraries whenever possible because, in the real-life production environment, it's important for the app to be robust and self-sustain without depending on any dependencies.
Because libraries tend to get outdated quickly, hence there is overhead to maintain the library as well (Such as updating the version). Also, not all libraries get equal efforts in updating or bug fixing
Hence, please try to avoid libraries and dependencies as much as possible to prevent your work from dying. You can read this wiki that explains the dependency debts https://www.explainxkcd.com/wiki/index.php/2347:_Dependency
Think about Security while coding
https://www.youtube.com/watch?v=GmXPwRNIrAU