python

Пара слов об именовании переменных и методов

  • четверг, 2 июля 2020 г. в 00:26:57
https://habr.com/ru/post/508238/
  • JavaScript
  • Python
  • Программирование
  • Проектирование и рефакторинг



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


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


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


Не будем затягивать и, пожалуй, начнем.


Переменные


Один из самых раздражающих видов переменных — это такие переменные, что дают ложное представление о природе данных, которые они хранят. Эдакие переменные-мошенники.


В среде Python-разработчиков крайне популярна библиотека requests, и если вы когда-либо искали что-то связанное с requests, то наверняка натыкались на подобный код:


import requests

req = requests.get('https://api.example.org/endpoint')
req.json()

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


Когда вы делаете запрос (requests.Request), то получаете ответ (requests.Response), так отразите это у себя в коде:


response = requests.get('https://api.example.org/endpoint')
response.json()

Не r, не res, не resp и уж точно не req, а именно response. res, r, resp (про req и вовсе молчу) — это все переменные, содержание которых можно понять, только взглянув на их объявление, а зачем прыгать к объявлению, когда можно изначально дать подходящее название?


Давайте рассмотрим еще один пример, но теперь из Django:


users_list = User.objects.filter(age__gte=22)

Когда вы видите где-то в коде users_list, то вы совершенно справедливо ожидаете, что сможете сделать так:


users_list.append(User.objects.get(pk=3))

Но нет, вы этого сделать не можете, так как .filter() возвращает QuerySet, а QuerySet далеко не list:


Traceback (most recent call last):
# ...
# ...
AttributeError: 'QuerySet' object has no attribute 'append'

Если вам очень важно указывать суффикс, то укажите хотя бы такой, который соответствует действительности:


users_queryset = User.objects.all()
users_queryset.order_by('-age')

Если же вам очень хочется написать именно _list, то будьте добры позаботиться о том, чтобы в переменную и правда попадал список:


users_list = list(User.objects.all())

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


users = User.objects.all()

или, если вам уж совсем неймется и вы в любом случае намерены использовать суффиксы, то лучше добавить суффикс _seq (меньшее из зол), чтобы отметить, что это последовательность:


users_seq = [1, 2, 3]
# или 
users_seq = (1, 2, 3)
# или
users_seq = {1, 2, 3}

Таким образом, вы будете знать, что в переменной хранится последовательность и что по ней можно итерировать, но не будете делать предположений о прочих свойствах и методах объекта.


Еще одним видом раздражающих переменных являются переменные с сокращенными именами.


Вернемся к requests и рассмотрим этот код:


s = requests.Session()
# ... 
# ... 
s.close()

Это яркий пример совершенно неоправданного сокращения названия переменной. Это ужасная практика, и ее ужасы становятся еще более очевидны, когда такой код занимает больше 10-15 строк кода.


Конкретно в случае requests скрипя зубами можно простить подобное сокращение, когда код занимает не более 5-10 строк и записывается вот так:


with requests.Session() as s:
    # ...
    # ...
    s.get('https://api.example.org/endpoint')

Тут контекстный менеджер позволяет дополнительно выделить объемлющий блок для переменной s.


Но гораздо лучше написать как есть, а именно:


session = requests.Session()
# ...
# ...
session.get('https://api.example.org/endpoint')
# ...
# ...
session.close()

или


with requests.Session() as session:
    session.get('https://api.example.org/endpoint')

Вы можете возразить, что это ведь более многословный вариант, но я вам отвечу, что это окупается, когда вы читаете код и сразу понимаете, что session — это Session. Поймете ли вы это по переменной s, не взглянув на ее определение?


Рассмотрим еще один пример:


info_dict = {'name': 'Isaak', 'age': 25}
# ...
# ... 
info_dict = list(info_dict)
# ...
# ...

Вы видите dict и можете захотеть сделать так:


for key, value in info_dict.items():
    print(key, value)

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


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


В приведенном выше примере выбор переменной достаточно неудачный, и можно было бы дать имя, точнее выражающее контекст (не нужно бояться использовать имена, относящиеся к предметной области), однако даже в этом случае можно было бы сделать этот код лучше:


info_dict = {'name': 'Isaak', 'age': 25}
# ...
# ... 
info_keys = list(info_dict)
# ...
# ...

или даже так, что более идиоматично:


info_dict = {'name': 'Isaak', 'age': 25}
# ...
# ... 
info_keys = info_dict.keys()
# ...
# ...

Комментарии-кэпы


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


Возьмем небольшой пример из JavaScript:


// Remove first five letters
const errorCode = errorText.substr(5)

Первое, что мелькает в голове, когда видишь такое — это "Спасибо, кэп!". Зачем описывать то, что и так понятно без комментария? Зачем дублировать информацию, которую нам уже рассказывает код? Этим и отличается хороший комментарий от плохого — хороший комментарий заставляет вас испытать благодарность тому, кто его написал, а плохой — лишь раздражение.


Давайте попробуем сделать этот комментарий полезным:


// Remove "net::" from error text
const errorCode = errorText.substr(5)

А еще лучше прибегнуть к более декларативному подходу и избавиться от комментария вообще:


const errorCode = errorText.replace('net::', '')

Говоря о комментариях, нельзя не упомянуть мертвый код. Мертвый код, пожалуй, раздражает куда больше, чем бесполезные комментарии, так как вы еще и должны разбираться, был ли код закомментирован временно (для отладки каких-то частей системы) или все же разработчик просто забыл его удалить?


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


Методы


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


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


>>> person = Person()
>>> person.has_publications()
['Post 1', 'Post 2', 'Post 3']

В коде мы выразили весьма однозначный вопрос: "Имеются ли у этого человека публикации?", но какой ответ мы получили?


Мы не спрашивали, какие у человека есть публикаций. Название этого метода подразумевает, что возвращаемое значение должно иметь булевый тип, а именно True или False:


>>> person = Person()
>>> person.has_publications()
True

А для получения постов вы можете использовать более подходящее название:


>>> person.get_publications()
['Post 1', 'Post 2', 'Post 3']

или


>>> person.publications()
['Post 1', 'Post 2', 'Post 3']

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


Список литературы для изучения вопроса


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


  1. Robert Martin — Clean Code
  2. Robert Martin — Clean Architecture
  3. Robert Martin — The Clean Coder: A Code of Conduct for Professional Programmers
  4. Martin Fowler — Refactoring: Improving the Design of Existing Code
  5. Colin J. Neill — Antipatterns: Managing Software Organizations and People