Understanding Clean Code on Android

Original author: Yoga C. Pranata
  • Transfer
As an introduction, I would like to recall Uncle Bob 's quote
You are reading this article for two reasons. The first - you are a programmer, the second - you want to be the best programmer.
Imagine you are in a library and looking for some books. If the library is sorted, has categories of books, then you will quickly find the one you need. In addition, the cool interior design and architecture will make your stay in this library quite comfortable for you.

As with writing books, if you want to create something great, then you need to know how to write and how to organize your code. If you have team members or someone else who has your (obsolete) code, they just need to see the variable names or packages or classes, and they will immediately understand. They don’t need to say “E ** l” I this code and start it again from scratch.

What is a Clean Code?




Transfer
Dear programmer:

When I wrote this code, only God and I knew how it works!
Now only God knows this!

Before you try to optimize this routine and it will not succeed (most likely), please increase the time counter to warn the next developer.

Total hours spent: 567

As you can see, it’s not enough to finish development faster. If in the future they cannot figure out this code, then it will also become a technical debt.

Your code is in a “Pure” state if every member of your team can understand it. Clean code can be read and improved by any developer other than the original author. With understanding comes readability, mutability, extensibility and maintainability.

Should I take care of this?


The reason you should care about the cleanliness of your code is because you describe your thoughts to other developers. That is why you should take care that your code is more elegant, simple and readable.

Signs of Clean Code


  • Your code should be elegant: Your code should make you smile like a well-made music box or a well-designed car
  • Your code should be focused: each function, each class, each module implements one task, which remains completely not distracted and unpolluted by surrounding details.
  • Code does not contain duplicates
  • Pass all tests successfully
  • Minimized the number of entities, methods, classes, and so on

The difference between a good programmer and a professional is that a professional programmer understands that code comprehensibility is paramount. A professional uses this power to write code that everyone understands - Robert C. Martin

Write the conscious names


Choosing a good name can take a lot of time, but later it will save more. The name of a variable, function, class should answer all significant questions. It should tell you why it exists, why it is needed and how to use it. If the name requires comment, the name does not reveal its purpose.

Simple example

// Bad variables naming
var a = 0 // user ages
var w = 0 // user weight
var h = 0 // user height
// Bad functions naming
fun age()
fun weight()
fun height()
// Bad classes naming to get user data
class UserInfo()
// Best practices varibales naming
var userAge = 0
var userWeight = 0
var userHeight = 0
// Best practices functions naming
fun setUserAge()
fun setUserWeight()
fun setUserHeight()
// Best practices classes naming to get user data
class Users()

Class names


Classes and objects must be nouns, such as Custom, WikiPage, Account, and AddressParser. Avoid words like Manager, Processor, Data, or Info. Also remember that the class name should not be a verb.

Method Names


Method names should be verbs, for example postPayment, deletePage or save. Access modifiers, predicates should be named by their value and with the prefix get, set and according to the JavaBean standard.

Before we continue, take a short break, stock up on coffee and cookies.



Okay, now let's move on to SOLID principles.

Write code adhering to SOLID principles


These principles were developed by Uncle Bob, SOLID is an abbreviation that describes a set of principles designed to write good code.

Single Responsibility Principle (S)


This means that each class should bear only one responsibility. There should never be more than one reason for changing a class. No need to add everything to your class, simply because you can do it. Break large classes into smaller ones and avoid God Classes.

Example:

We have a RecyclerView.Adapter with business logic inside onBindViewHolder

class MyAdapter(val friendList: List) :
    RecyclerView.Adapter() {
    inner class MyViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        var name: TextView = view.findViewById(R.id.text1)
        var popText: TextView = view.findViewById(R.id.text2)
    }
    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        val friend = friendList[position]
        val status = if(friend.maritalStatus == "Married") {
            "Sold out"
        } else {
            "Available"
        }
        holder.name.text = friend.name
        holder.popText.text = friend.email
        holder.status.text = status
    }
    override fun getItemCount(): Int {
        return friendList.size
    }
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.item_friendlist, parent, false)
        return MyViewHolder(view)
    }
}

This makes RecyclerView.Adapter not solely responsible because it contains business logic inside the onBindViewHolder. This method is only responsible for inserting data into the view.

The principle of openness / closure (O)


Software entities must be open for expansion, but closed for modification. This means that if you are developing class A and your colleagues will want to change the function inside this class. They can easily do this by expanding this class without changing the class itself.
A simple example is the RecyclerView.Adapter class. You can easily expand it and create your own adapter with non-standard behavior without modifying the RecyclerView.Adapter itself.

class FriendListAdapter(val friendList: List) :
    RecyclerView.Adapter() {
    inner class MyViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        var name: TextView = view.findViewById(R.id.text1)
        var popText: TextView = view.findViewById(R.id.text2)
    }
    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        val friend = friendList[position]
        holder.name.text = friend.name
        holder.popText.text = friend.email
    }
    override fun getItemCount(): Int {
        return friendList.size
    }
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
        val view = LayoutInflater.from(parent.context).inflate(R.layout.item_friendlist, parent, false)
        return MyViewHolder(view)
    }
}

The Barbara Lisk Substitution Principle (L)


The child class should complement the parent, and not change it. This means that the subclass must override the parent methods that do not violate the functionality of this parent class. For example, we create a class interface that has an onClick () listener and then you apply the listener in MyActivity and give it a Toast action when onClick () is called.

interface ClickListener {
    fun onClick()
}
class MyActivity: AppCompatActivity(), ClickListener {
    //........
    override fun onClick() {
        // Do the magic here
        toast("OK button clicked")
    }
}

Interface separation principle


This principle states that the client should not be dependent on methods that he does not use.
This means that if you want to write class A and add the functionality of another class B to it. There is no need to redefine all classes A inside class B.

Example: in our activity, we need to implement SearchView.OnQueryTextListener (), but we only need onQuerySubmit ( ) method.

mSearchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener{
    override fun onQueryTextSubmit(query: String?): Boolean {
        // Only need this method
        return true
    }
    override fun onQueryTextChange(query: String?): Boolean {
        // We don't need to implement this method
        return false
    }
})

How do we do this? Easy! Just create a callback and a class extending SearchView.OnQueryTextListener ()

interface SearchViewQueryTextCallback {
    fun onQueryTextSubmit(query: String?)
}
class SearchViewQueryTextListener(val callback: SearchViewQueryTextCallback): SearchView.OnQueryTextListener {
    override fun onQueryTextSubmit(query: String?): Boolean {
        callback.onQueryTextSubmit(query)
        return true
    }
    override fun onQueryTextChange(query: String?): Boolean {
        return false
    }
}

And so we add it to our view

val listener = SearchViewQueryTextListener(
    object : SearchViewQueryTextCallback {
        override fun onQueryTextSubmit(query: String?) {
             // Do the magic here
        } 
    }
)
mSearchView.setOnQueryTextListener(listener)

Or so using Extension Function in Kotlin

interface SearchViewQueryTextCallback {
    fun onQueryTextSubmit(query: String?)
}
fun SearchView.setupQueryTextSubmit (callback: SearchViewQueryTextCallback) {
    setOnQueryTextListener(object : SearchView.OnQueryTextListener{
        override fun onQueryTextSubmit(query: String?): Boolean {
            callback.onQueryTextSubmit(query)
            return true
        }
        override fun onQueryTextChange(query: String?): Boolean {
            return false
        }
    })
}

val listener = object : SearchViewQueryTextCallback {
    override fun onQueryTextSubmit(query: String?) {
        // Do the magic here
    }
}
mSearchView.setupQueryTextSubmit(listener)

Dependency Inversion Principle


Dependence on abstractions, without dependence on something specific.
The definition of inversion of dependency on Uncle Bob consists of two concepts.

Upper level modules should not depend on lower level modules. Both should be tied to abstractions. Abstractions should not depend on the details. Details should depend on abstractions. High-level modules that implement complex logic should be easily reusable without changes to lower-level modules. To do this, you need to enter an abstraction that separates the modules of the upper and lower levels from each other.

A simple example of this MVP pattern. You have an interface object that helps you communicate with specific classes. What is meant - the UI classes (Activity / Fragment) do not need to know the actual implementation of the presenter methods. Thus, if you make changes inside the presenter, UI classes do not have to worry about these changes.

Let's look at an example.

interface UserActionListener {
    fun getUserData()
}
class UserPresenter : UserActionListener() {
    // .....
    override fun getUserData() {
        val userLoginData = gson.fromJson(session.getUserLogin(), DataLogin::class.java)
    }
    // .....
}

And now on the activity

class UserActivity : AppCompatActivity() {
   //.....
   val presenter = UserPresenter()
   override fun onCreate(savedInstanceState: Bundle?) {
      super.onCreate(savedInstanceState)
      // Activity doesn't need to know how presenter works
      // for fetching data, it just know how to call the functions
      // So, if you add method inside presenter, it won't break the UI.
      // even the UI doesn't call the method.
      presenter.getUserData()
   }
   //....
}

In this way, we create an interface that abstracts the implementation of the presenter, and our view class maintains a reference to the PresenterInterface.

Also popular now: