Понимание чистого кода в Android

Обложка поста

Прежде чем начать писать свой код, нужно понять, как его масштабировать и как им управлять.

Как сказал Дядя Боб в своей книге:

Вы читаете эту статью по двум причинам. Во-первых, вы программист. Во-вторых, вы хотите программировать лучше.

Роберт С. Мартин

Представьте, что вы ищете книги в библиотеке. Если книги отсортированы и классифицированы, вы найдёте их гораздо быстрее. Кроме того, благодаря хорошему дизайну интерьера вы будете чувствовать себя комфортно.

Если вы хотите создать что-то стоящее, нужно уметь не только писать, но и аккуратно организовывать свой код. Программист со стороны, получив ваш код, должен увидеть имена переменных, пакетов или классов и сразу всё понять. Он не должен ругаться и начинать проект с нуля.

Что такое «чистый код»?

Нехорошо наспех заканчивать проект, если другие не смогут понять ваш код. Это ваша недоработка и ваша ответственность.

Код можно назвать «чистым», если в нём легко разбирается вся команда. А другой разработчик может прочитать его и улучшить. С понятностью приходит читаемость, изменяемость, расширяемость и лёгкость сопровождения.

Должен ли я заботиться об этом?

Вы должны заботиться о своём коде, потому что именно он будет описывать ваш мыслительный процесс другим. Нужно думать о том, как сделать код более простым и читабельным.

Характеристики чистого кода

  • Ваш код элегантен: он должен вызывать у вас улыбку, как от хорошо сделанной музыкальной шкатулки или машины с красивым дизайном.
  • О вашем коде позаботились: кто-то нашёл время, чтобы сделать его простым и упорядоченным и уделил внимание деталям.
  • Ваш код сфокусирован: каждая функция, каждый класс, каждый модуль выполняют одну конкретную задачу и не перегружены лишними фичами.
  • Не содержит дубликатов;
  • Работает на всех тестах;
  • Имеет минимальное количество объектов, таких как классы, методы, функции и тому подобное.

Одно из различий между умным и профессиональным программистом заключается в том, что профессионал понимает, что чистота — это главное. Профессионалы используют свои способности во благо и пишут код, понятный другим.

Роберт С. Мартин

Задавайте уместные имена

Выбор хороших имён требует времени, но экономит больше. Имя переменной, функции или класса должно отвечать на все важные вопросы: зачем нужно, что делает и как используется. Если имя нужно комментировать, то оно не выполняет свою задачу.

Например:

// Плохое название переменных
var a = 0 // user ages
var w = 0 // user weight
var h = 0 // user height
 
// Плохое название функций
fun age()
fun weight()
fun height()

// Плохое название классов для получения пользовательских данных
class UserInfo

  
// Хорошие варианты названия переменных
var userAge = 0
var userWeight = 0
var userHeight = 0
 
// Хорошие варианты названия функций
fun setUserAge()
fun setUserWeight()
fun setUserHeight()

// Хорошие варианты названия классов для получения пользовательских данных
 
class Users()

Имена классов

Классы и объекты должны называться существительными или фразами из них, например Customer, WikiPage, Account и AddressParser. Избегайте таких слов, как Manager, Processor, Data или Info в названии класса. Имя класса не должно быть глаголом.

Имена методов

Методы должны называться глаголами или фразами из них, например postPayment(), deletePage() или save(). Аксессоры, мутаторы и предикаты должны быть названы по их значению, иметь префикс get, set и соответствовать стандарту JavaBean.

Используйте доменные названия для задач

Если не можете подобрать название, используйте доменные названия для задач. По крайней мере, программист, который поддерживает ваш код, может спросить эксперта по домену, что оно значит.

Написание кода с использованием принципов S.O.L.I.D.

Принципы придуманы Робертом К. Мартином (Дядя Боб). SOLID — это термин, описывающий принципы проектирования хорошего кода.

Принцип единой ответственности — SRP

Это означает, что каждый класс должен отвечать за что-то одно. Никогда не должно быть более одной причины для изменения класса. То, что вы можете добавлять в класс всё, что хотите, не означает, что нужно это делать. Разделите большие классы на более мелкие и избегайте классы Бога.

Например:

У нас есть RecyclerView.Adapter с логикой внутри 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)
    }
}
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)
    }
}

RecyclerView.Adapter противоречит принципу единой ответственности, потому что он имеет логику внутри onBindViewHolder.

Принцип Открытости-Закрытости — OCP

Программные объекты должны быть открыты для расширения, но закрыты для модификации. Это означает, что если вы пишете класс A, а затем ваши товарищи по команде хотят внести изменения в его функцию, они могут легко сделать это, расширив класс A и не внося изменения.

Простым примером будет класс RecyclerView.Adapter. Вы можете легко расширить этот класс и создать собственный адаптер с настраиваемым поведением, не изменяя существующий класс RecyclerView.Adapter.

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

Принцип подстановки Лисков — LSP

Дочерние классы никогда не должны нарушать определения типов родительского класса.

Это означает, что подкласс должен переопределять методы родительского класса, которые не нарушают функциональность родительского класса. Например, вы создаёте интерфейсный класс с прослушивателем onClick (), а затем применяете прослушиватель в MyActivity и назначаете ему всплывающее действие при вызове onClick ().

interface ClickListener {
   fun onClick()
}

class MyActivity: AppCompatActivity(), ClickListener {

   //........
   override fun onClick() {
       // Творим волшебство
       toast("OK button clicked")
   }
}

Принцип разделения интерфейса — ISP

Принцип разделения интерфейса (ISP) гласит, что ни один клиент не должен зависеть от методов, которые он не использует.

Это означает, что если вы хотите создать класс A и реализовать его в другом классе B, не нужно переопределять все методы класса A внутри класса B. Чтобы стало понятнее, давайте рассмотрим пример:

Внутри кода нужно выполнить SearchView.OnQueryTextListener() и только метод onQuerySubmit().

mSearchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener{
    override fun onQueryTextSubmit(query: String?): Boolean {
        // Нужен только этот метод
        return true
    }

    override fun onQueryTextChange(query: String?): Boolean {
        // Нам не нужно реализовывать этот метод
        return false
    }
})

Как этого добиться? Просто создайте обратный вызов и класс, который распространяется на 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
    }
}

А вот как это реализовать:

val listener = SearchViewQueryTextListener(
   object : SearchViewQueryTextCallback {
       override fun onQueryTextSubmit(query: String?) {
            // Творим волшебство
       }
   }
)
mSearchView.setOnQueryTextListener(listener)

С 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?) {
       // Творим волшебство
   }
}
mSearchView.setupQueryTextSubmit(listener)

​Принцип инверсии зависимостей — DIP

Зависит от абстракций. Не зависит от конкрементов.
Принцип обращения зависимостей определяется двумя пунктами:

Модули высокого уровня не должны зависеть от модулей низкого уровня. Оба должны зависеть от абстракций.

Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

Модули высокого уровня со сложной логикой, должны легко использоваться повторно и не подвергаться изменениям в модулях низкого уровня, которые предоставляют служебные функции. Чтобы достичь этого, нужно ввести абстракцию, которая отделяет модули высокого и низкого уровня друг от друга.

Простой пример — шаблон MVP. У вас есть объект интерфейсов, который помогает взаимодействовать с конкретными классами. Это означает, что классам пользовательского интерфейса (Activity / Fragment) не нужно знать фактическую реализацию методов в Presenter. Если есть какие-либо изменения внутри, классы пользовательского интерфейса не должны знать об изменениях.

Посмотрим на примере кода:

interface UserActionListener {
    fun getUserData()
}

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

Теперь посмотрим на это в UserActivity:

class UserActivity : AppCompatActivity() {
   
   //.....
   val presenter = UserPresenter()
   
   override fun onCreate(savedInstanceState: Bundle?) {
      super.onCreate(savedInstanceState)
      
// Activity не нужно знать, как работает presenter
// он просто знает, как вызывать функции для получения данных, 
// Итак, если вы добавите метод в Presenter, он не сломает пользовательский интерфейс.
// даже пользовательский интерфейс не вызывает метод.

      
      presenter.getUserData()
   }
   
   //....
}

Поэтому мы создаём интерфейс, который абстрагирует реализацию presenter, а наш класс представления сохраняет ссылку на PresenterInterface.

Заключение

Опытные программисты знают, что все объекты — миф. Иногда действительно нужны простые структуры данных с работающими на них процедурами. С этого момента вы должны начать продумывать, как реализовать проект, чтобы в перспективе его можно было легко обновлять.

Удачного кодинга 🙂

На основе статьи «Understanding Clean Code in Android»

Иван Капцов