Автор: Джеймс Беннет (James Bennett)
Примечания переводчика:
Обычно не задумывался над тем, как лучше оформлять кортежи для параметра choices в полях моделей Django.Часто бывает нужно воспользоваться полем модели, которое будет принимать ограниченный набор значений. Например, поле выбора штата США, что логично, должно разрешать только значения, соответствующие действительно существующим штатам США. В ORM Django это реализуется при помощи аргумента choices в определении поля. В общем случае это - наиболее простое решение.
Не задумывался, видимо, потому что никогда до этого не приходилось фильтровать записи по значениям этого поля. И вот однажды такая необходимость возникла. Подумал, что использование в QuerySet'ах "магических чисел" не способствует наглядности кода и решил поискать - как же лучше оформить эти кортежи? В результате нашёл эту статью, которую и решил впоследствии перевести - вдруг кому-нибудь ещё пригодится? Многие ссылки в исходной статье протухли, поэтому заменил их на то, что удалось найти из ещё доступного.
Но это решение не всегда идеальное: хотя со строковыми значениями (упомянутые выше штаты США в Django могут быть реализованы в виде простого двухбуквенного почтового кода вроде "VA" или "KS") не возникает проблем, с числовыми значениями возникают сложности. Например, в моём блоге используется модель Entry (если интересно - см. полный исходный текст в репозитории Google Code), в которой есть поле "status", позволяющее отличать три разных типа записей:
- "Live" - записи, которые опубликованы на сайте.
- "Draft" - записи, которые находятся в процессе подготовки и ещё не опубликованы. Когда они будут дописаны, их статус можно будет поменять на "Live".
- "Hidden" - записи, которые по какой-либо причине были скрыты. Если записи нужно оставить в базе данных, но они не должны быть опубликованными.
Теперь запишем этот набор опций в виде кортежа choices наиболее очевидным образом:
STATUS_CHOICES = ( (1, 'Live'), (2, 'Draft'), (3, 'Hidden'), )Для этих значений не так-то просто придумать строковые аббревиатуры (по крайней мере, ни одно из них не может быть интернационализировано), поэтому выходом может оказаться использование целочисленных значений. В таком случае его можно просто добавить к модели:
class Entry(models.Model) # ...несколько других полей... status = models.IntegerField(choices=STATUS_CHOICES)Можно чуть-чуть улучшить этот код, так чтобы по умолчанию у записей проставлялось значение “Live”:
status = models.IntegerField(choices=STATUS_CHOICES, default=1)Теперь можно легко выбрать только опубликованные записи, выполнив такой запрос:
live_entries = Entry.objects.filter(status=1)Можно даже реализовать собственный менеджер запросов, который заменит метод get_query_set() так, чтобы он возвращал только опубликованные записи (как было сделано здесь).
Но у этого подхода есть большая проблема: теперь в приложении повсюду используются "магические числа".
Плохая магия
"Магические числа" - это любые числовые константы (или любые другие непонятные значения), которые явным образом встречаются в коде. В данном случае значение статуса "Live" - 1 - используется по меньшей мере уже в двух местах: первый раз в объявлении поля модели (в качестве значения по умолчанию) и, даже если мы напишем собственный менеджер запросов для выбора опубликованных записей, как минимум ещё один раз для фильтрации в условии status=1. И это даже если мы просто хотим удобно работать с опубликованными записями. Если же нам когда-нибудь понадобится, скажем, метод-менеджер или QuerySet для черновиков, нам придётся разбросать во множестве мест ещё одно число.Это плохо по нескольким причинам. Во-первых, что самое важное, нарушается принцип DRY (Don't repeat yourself - не повторяйся), так как нужно повторять одно и то же "магическое" число во множестве мест. Во-вторых, есть риск нарушения родственного принципа "Однажды и только единожды", поскольку легко попасть в ловушку при написании filter(status=1) более чем в одном месте. Наконец, в-третьих, возникают проблемы с поддержкой: нужно держать в уме - что означает это "магическое" значение и список всех мест, где оно используется (поскольку любое будущее изменение потребуется внести во все эти места одновременно).
Удаляем магию
Но остаётся проблема: как ссылаться на это значение без жёстко-закодированных повсюду "магических" чисел? В языках с перечисляемыми типами (такими как enum в C и его родственниках), это - легко решаемая проблема. В одном из этих языков нужно просто определить enum с именами различных вариантов и позволить языку использовать значениями.Но в Python нет подходящей замены для enum. Мы можем импортировать кортеж STATUS_CHOICES и записать фильтрацию следующим образом:
live_entries = Entry.objects.filter(status=STATUS_CHOICES[0][0])Но, несмотря на то, что мы больше не используем жёстко закодированные числовые значения, теперь мы опираемся на точное определение кортежа STATUS_CHOICES. Если в первый слот будет помещено какое-то другое значение (например, статус подтверждения записи главным редактором), то сломается всё, что ссылается на STATUS_CHOICES[0][0].
Однако, можно просто определить набор констант:
LIVE_STATUS = 1 DRAFT_STATUS = 2 HIDDEN_STATUS = 3Теперь можно переопределить кортеж STATUS_CHOICES так, чтобы он опирался эти константы:
STATUS_CHOICES = ( (LIVE_STATUS, 'Live'), (DRAFT_STATUS, 'Draft'), (HIDDEN_STATUS, 'Hidden'), )А также, теперь можно импортировать и ссылаться на эти константы. Например, поле статуса теперь должно быть записано следующим образом:
status = models.IntegerField(choices=STATUS_CHOICES, default=LIVE_STATUS)Этими константами можно воспользоваться и для фильтрации только опубликованных записей:
live_entries = Entry.objects.filter(status=LIVE_STATUS)Черновики теперь можно отбирать, фильтруя их по условию status=DRAFT_STATUS. А скрытые записи можно фильтровать по условию status=HIDDEN_STATUS. Код становится более наглядным в двух аспектах:
- Больше нет разложенных повсюду "магических чисел", которые нужно специально обновлять. Одно изменение в определении одной или более констант - это всё, что нужно, чтобы обновить код (важно также обновить и базу данных, но это можно легко исправить одним запросом UPDATE).
- Код теперь стал гораздо понятнее: запрос с условием status=1 может значить что угодно, но запрос с условием status=LIVE_STATUS практически объясняет сам себя.
Инкапсуляция
Однако, можно внести ещё одно улучшение: инкапсулировать варианты статуса внутри модели Entry. Эти варианты логически "принадлежат" модели Entry и поэтому не стоит определять их отдельно или импортировать их отдельно от самой модели Entry. Поэтому можно переместить константы и кортеж с вариантами вовнутрь класса Entry:class Entry(models.Model): LIVE_STATUS = 1 DRAFT_STATUS = 2 HIDDEN_STATUS = 3 STATUS_CHOICES = ( (LIVE_STATUS, 'Live'), (DRAFT_STATUS, 'Draft'), (HIDDEN_STATUS, 'Hidden'), ) # ...несколько других полей... status = models.IntegerField(choices=STATUS_CHOICES, default=LIVE_STATUS)Теперь можно просто импортировать модель Entry и писать запросы следующим образом:
live_entries = Entry.objects.filter(status=Entry.LIVE_STATUS) draft_entries = Entry.objects.filter(status=Entry.DRAFT_STATUS)Можно также сравнивать записи со значениями констант:
if entry_object.status == Entry.LIVE_STATUS: # делаем что-то с опубликованной записью
Комментариев нет:
Отправить комментарий