Clean Code Android: Bạn đã thật sự hiểu đúng chưa?

Bài viết được sự cho phép của tác giả Sơn Dương

Clean code Android là gì? Có lẽ bạn đã nghe quá nhiều các đàn anh đi trước nói: Em phải viết clean code thì mã nguồn mới dễ đọc, dễ mở rộng, dễ bảo trì… Nhưng bạn có biết clean code Android là thế nào không? Có phải cứ viết ngắn gọn là clean code?

Với những người đang đọc bài viết này:  Một là, bạn là một lập trình viên. Hai là, bạn muốn trở thành một lập trình viên giỏi. — Robert C. Martin

Thử tưởng tượng bạn đang ở trong một thư viện sách. Bạn muốn tìm một cuốn sách nào đó. Nếu như thư viện được sắp xếp gọn gàng, phân loại sách tốt thì bạn sẽ dễ dàng tìm được cuốn mình cần. Ngoài ra, nếu như thư viện mà được thiết kế nội thất tối, bạn sẽ có hứng thú hơn khi đọc sách.

Cũng giống như ví dụ trên, khi bạn xây dựng một ứng dụng, bạn phải biết cách viết code và tổ chức sao cho gọn gàng, dễ đọc.

Đặc biệt với các dự án có nhiều thành viên, và thời gian maintaince dài. Khi đọc code, các member chỉ cần nhìn tên class, tên hàm, tên package… là hiểu ngay.

Đừng để những tiếng “F***K” vang lên mỗi khi ai đó đọc code của bạn

Thực hành cách viết Clean Code Android

“Clean Code” là gì?

Source code của bạn được gọi là “Clean” khi nó có thể dễ dàng đọc hiểu bởi các member trong dự án. Không chỉ tác giả của mã nguồn mới có trách nhiệm tạo code được clean hơn. Mà tất cả các member trong dự án cũng phải ý thức được cần phải viết code clean.

Với tính dễ hiểu, clean code Android sẽ giúp dự án dễ dàng mở rộng, thay đổi theo yêu cầu mới, cũng như tăng cường khả năng bảo trì của ứng dụng.

Tên biến, hàm, Class… phải có nghĩa.

Có thể việc bạn suy nghĩ về tên biến sao cho có ý nghĩa hơi mất thời gian, nhưng lợi ích của nó mang lại thì vô cùng lớn.

Tên của biến, tên hàm, hay tên class… phải nói lên được tại sao nó tồn tại, nó là gì và sử dụng như thế nào.

Nếu một tên cần phải comment để giải thích thì tên đó vẫn chưa đạt được yêu cầu của “Clean code”.

Mình lấy một số ví dụ:

// Không nên
var a = 0 // user ages
var w = 0 // user weight
var h = 0 // user height

// Không nên 
fun age()
fun weight()
fun height()

// Tên class như này vẫn chưa phải là chuẩn clean code.
class UserInfo()

// Nên
var userAge = 0
var userWeight = 0
var userHeight = 0

// Nên
fun setUserAge()
fun setUserWeight()
fun setUserHeight()

// Nên
class Users()

  Viết clean code: Code “đẹp trai” và code “xấu gái” có gì hay ho?

  Quy tắc viết code dễ đọc, tối ưu và dễ bảo trì nhất

#Tên Class

Tên Class hay Object nên là danh từ hay cụm danh từ. Ví dụ: Customer, WikiPage, Account, hay AddressParser. ( Xem cách khởi tạo class bằng Kotlin)

Tránh thêm những hậu tố vào trong tên class: Manager, Processor, Data, or Info.

Tuyệt đối không sử dụng động từ để làm tên Class.

#Tên hàm

Ngược với tên hàm, tên hàm nên sử dụng động từ để đặt tên. Ví dụ: postPayment()deletePage(), hay save()

OK, giờ chúng ta sẽ bắt đầu học cách viết clean code Android theo quy tắc S.O.L.I.D

Sử dụng quy tắc S.O.L.I.D để viết clean code Android

S.O.L.I.D là bộ quy tắc viết code được phát minh bởi Robert C. Martin (Uncle Bob). Khi bạn ứng dụng bộ quy tắc này vào dự án, đảm bảo mã nguồn của bạn sẽ cực kỳ “clean” luôn.

Vậy bộ quy tắc này có những nguyên lý gì?

Bạn cứ đi pha một cốc cafe rồi quay lại tiếp tục đọc nhé

Tham khảo việc làm Java Developer hấp dẫn trên TopDev

#Single Responsibility Principle — SRP

Single Responsibility Principle 

Nguyên lý đầu tiên, tương ứng với chữ S trong S.O.L.I.D. Nội dung nguyên lý:

Một class chỉ nên giữ 1 trách nhiệm duy nhất 
(Chỉ có thể sửa đổi class với 1 lý do duy nhất)

Để hiểu nguyên lý này, chúng ta lấy ví dụ về 1 class Adapter với các logic được implement trong onBindViewHolder.

class MyAdapter(val friendList: List<FriendListData.Friend>) :
    RecyclerView.Adapter<CountryAdapter.MyViewHolder>() {

    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)
    }
}

Với các viết code này đã vi phạm nguyên lý của S.L.O.I.D. Bởi vì class RecyclerView.Adapter không chỉ chịu một trách nhiệm duy nhất. Vì nó phải implement phần logic cho biến status trong onBindViewHolder.

Hàm onBindViewHolder() chỉ nên làm một nhiệm vụ duy nhất là thiết lập dữ liệu để hiển thị ra view thôi, không xử lý bất kì logic nào cả.

Thêm một ví dụ nữa nhé. Mình có class như sau:

public class Reports ()
{
   public void readDataFromDB();
   public void processData();
   public void printReport();
}

Class này giữ tới 3 trách nhiệm: Đọc dữ liệu từ database, xử lý dữ liệu, hiển thị kết quả.

Sau này chỉ cần ta thay đổi DB hay thay đổi cách hiển thị kết quả dữ liệu,… ta sẽ phải sửa đổi class này. Càng về sau class sẽ càng phình to ra.

Theo đúng nguyên lý, ta phải tách class này ra làm 3 class riêng. Tuy số lượng class nhiều hơn những việc update code sẽ đơn giản hơn. Class ngắn hơn nên cũng ít issue hơn.

#Open-Closed Principle — OCP

Open-Closed Principle 

Nguyên lý thứ hai, tương ứng với chữ O trong SOLID. Nội dung nguyên lý:

Có thể thoải mái mở rộng 1 class, nhưng không được sửa đổi bên trong class đó
(open for extension but closed for modification).

Điều này có nghĩa là: Nếu bạn viết một class A, và các members khác muốn chỉnh sửa một function bên trong Class A. Họ có thể dễ dàng extend Class A và sửa đổi tùy thích. Nhưng không nên sửa trực tiếp trong Class A.

Mình tiếp tục với ví dụ với RecyclerView.Adapter class. Bạn có thể dễ dàng extend class này tạo một Adapter cho riêng mình.

class FriendListAdapter(val friendList: List<FriendListData.Friend>) :
    RecyclerView.Adapter<CountryAdapter.MyViewHolder>() {

    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)
    }
}

#Clean code Android: Liskov Substitutions Principle — LSP

Clean code Android: Liskov Substitutions Principle

Nguyên lý thứ ba, tương ứng với chữ L trong SOLID. Nội dung nguyên lý:

Trong một chương trình, các object của class con có thể thay thế class cha 
mà không làm thay đổi tính đúng đắn của chương trình

Điều này có nghĩa là class con khi override hàm mà không làm hỏng chức năng của class.

Mình ví dụ: Bạn tạo một interface có một listener: onClick(). Sau đó bạn apply listener đó trong MyActivity. Khi người click và onClick() được gọi, bạn cho hiển thị một Toast thông báo.

interface ClickListener {
    fun onClick()
}
class MyActivity: AppCompatActivity(), ClickListener {

    //........
    override fun onClick() {
        // Do the magic here
        toast("OK button clicked")
    }

}

#Interface Segregation Principle — ISP

Interface Segregation Principle

Nguyên lý thứ tư, tương ứng với chữ I trong SOLID. Nội dung nguyên lý:

Thay vì dùng 1 interface lớn, ta nên tách thành nhiều interface nhỏ, 
với nhiều mục đích cụ thể

Nghĩa là: nếu bạn muốn  tạo một Interface A và implement nó trong một class B nào đó. Và sẽ class B sẽ phải implement toàn bộ methods trong Interface A. Sẽ ra sao nếu Interface có khoảng 100 methods, trong khi Class B không nhất thiết phải override toàn bộ số methods đó.

Nguyên lý này khuyên bạn nên tách Interface A ra thành nhiều Interface nhỏ khác với các methods liên quan với nhau nhiều nhất. Như vậy sẽ sẽ implement hơn nhiều.

Chúng ta cùng thử một ví dụ nhé. Trong activity của bạn, bạn cần phải implement SearchView.OnQueryTextListener() và bạn chỉ cần đến mỗi hàm onQuerySubmit().

mSearchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener{
    override fun onQueryTextSubmit(query: String?): Boolean {
        // Chỉ muốn override hàm này
        return true
    }

    override fun onQueryTextChange(query: String?): Boolean {
        // Chúng ta không muốn override cả hàm này.

        return false
    }
})

Làm thế nào có thể làm được điều này? Đơn giản là bạn chỉ cần tạo một callback và một class extend từ nó.

>>> Xem thêm: Nguyên lý Solid trong Nodejs

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
    }
}

Và đây là cách implement:

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

#Dependency Inversion Principle — DIP

Dependency Inversion Principle

Nguyên lý cuối cùng, tương ứng với chữ D trong SOLID. Nội dung nguyên lý:

1. Các module cấp cao không nên phụ thuộc vào các modules cấp thấp.
Cả 2 nên phụ thuộc vào abstraction.
2. Interface(abstraction) không nên phụ thuộc vào chi tiết, 
mà ngược lại.
(Các class giao tiếp với nhau thông qua interface,
không phải thông qua implementation.)

Một ví dụ đơn giản đó là mô hình MVP. Bạn có một Interface giúp chúng ta kết nối các class. Tức là, các UI class không cần quan tâm đến logic được implement trong Presenter như thế nào.

Vì vậy, nếu bạn có phải thay đổi logic bên trong Presenter thì UI cũng không biết, không cần phải thay đổi code vì điều đó.

Ví dụ bằng code nhé:

interface UserActionListener {
    fun getUserData()
}

class UserPresenter : UserActionListener() {
    // .....

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

    // .....
}

Bây giờ bạn hãy xem UserActivity:

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()
     }

     //....
}

Tạm kết

Như vậy, qua bài viết này mình chỉ muốn truyền tải đến bạn một thông điệp duy nhất:

“Hãy viết code như thể người maintain là một đứa sát nhân điên cuồng và biết địa chỉ nhà bạn”

Bài viết này mình chủ yếu tập trung vào cách viết clean code Android. Tuy nhiên, nguyên lý S.L.O.I.D hoàn toàn có thể ứng dụng cho những mã nguồn khác.

Mã nguồn của bạn có thực sự “Clean” không? Hãy để lại bình luận bên dưới nhé.

Bài viết gốc được đăng tải tại vntalking.com

Xem thêm:

Xem thêm Việc làm IT hấp dẫn trên TopDev