Android

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


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

Вы читаете эту “статью” по двум причинам. Первая — вы программист. Вторая — вы хотите улучшить свои навыки. — Роберт С. Мартин

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

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

Что такое “Чистый код”?

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

— Зачем мне вообще это нужно?

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

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

  • Элегантность: Коддолжен вызывать приятную улыбку на вашем лице так же, как и красивая музыкальная шкатулка или хорошая машина.
  • Код должен быть написан с заботой: Потратьте время на организацию кода. Уделите особое внимание деталям. Отнеситесь к нему с заботой.
  • Целенаправленность: Каждая функция, каждый класс, каждый модуль должен обладать определенной позицией, незагрязненной окружающими деталями.
  • Отсутствие дублированного кода
  • Прохождение всех тестирований
  • Минимальное количество сущностей, таких как классы, методы, функции и тому подобное.

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

Значимые названия

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

Рассмотрим пример:

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

— Названия классов

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

— Названия методов

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

— Использование названий проблемных доменов

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

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

Хорошо. Продолжим тему написания кода с использованием S.O.L.I.D-принципов.

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

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

Single Responsibility Principle — SRP

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

Рассмотрим пример:

У нас есть 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)
}
}

RecyclerView.Adapter не обладает единственной ответственностью, поскольку содержит бизнес-логику в onBindViewHolder. Этот метод ответственен только за установку данных в реализацию связывания представлений.

Open-Closed Principle — OCP

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

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

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

Liskov Substitutions Principle — LSP

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

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

interface ClickListener {
    fun onClick()
}

class MyActivity: AppCompatActivity(), ClickListener {

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

}

Interface Segregation Principle — ISP

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

Это означает, что при создании класса A и реализации его в другом классе (Класс B), он не должен переопределять все методы класса A внутри класса B. 

Рассмотрим пример: SearchView.OnQueryTextListener() нужно реализовать в activity. Понадобится лишь метод onQuerySubmit().

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

Как этого достичь? Все очень просто: создайте обратный вызов и класс, расширяющийся до 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?) {
// Do the magic here
}
}
)
mSearchView.setOnQueryTextListener(listener)

Также можно использовать Extension Function в 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 — DIP

Опирайтесь на абстракцию, а не конкретику.

Дядя Боб разделяет определение принципа инверсии зависимостей на две части:

  • Высокоуровневые модули не должны зависеть от низкоуровневых модулей. Оба они должны зависеть от абстракций.
  • Абстракции не должны зависеть от деталей, а детали должны зависеть от абстракций.

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

Простой пример — шаблон MVP. Объект интерфейсов используется для взаимодействия с конкретными классами. Это означает, что UI-классы (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 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()
}

//....
}

Мы создали интерфейс, абстрагирующий реализацию presenter, а класс представлений содержит ссылку PresenterInterface.

Заключение

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

Возможно, вы уже создавали приложения с нелепыми названиями, God classes, Спагетти-кодом, как и я раньше. Поэтому я решил поделиться своими знаниями о чистом коде от Дяди Боба, чтобы вы задумались о реализации и будущих обновлениях своего кода.

Счастливого программирования 🙂


Перевод статьи Yoga C. Pranata: Understanding Clean Code in Android