Пролог

Наверное всем веб (а может и не только веб) разработчикам рано или поздно приходилось сталкиваться с такой задачей как опции пользователей. Что я имею ввиду проще всего показать на картинке. Под опциями будем понимать чекбоксы (то есть опция либо есть либо нет). Обычно таких опций не слишком много, порядка 2-10. Обычно их указывают при регистрации и/или редактировании профиля.
Постановка задачи
Походу разработки какого-либо проекта как правило число этих настроек (да и сами настройки) могут варироваться, так что было бы неплохо реализовать механизм этих опций так, чтобы добавлять/изменять/удалять их было максимально просто, изменив при этом как можно меньше кода. Я расскажу как можно решить и как решил эту задачу я в рамках фреймворка Ruby on Rails.
Варианты реализации
Вообще, этот вопрос я обычно задаю на собеседовании, ответов наслушался всяких. Один из них — использовать две таблицы: одна для опций, другая для связи пользователь-опция (есть запись — значит галочка в форме стоит).
Преимущества:
- Список опций редактируемый
- join таблицы для того, чтобы узнать установлена ли опция
- нетривиальный код в представлении
<%= f.check_box :send_mail_on_friend_request %>
Конечно реализовать это можно, но код в модели не будет тривиален.
Другой вариант, для опций использовать одну колонку — текст, в котором хранятся опции в сериализованном виде
class User serialize :options, Hash end
В этом примере непонятно какие вообще могут быть опции у пользователя, улучшим:
class User
OPTION_1 = :key_1
OPTION_2 = :key_2
serialize :options, Hash
def option_1=(value)
self.options[:key_1] = value
end
def option_2=(value)
self.options[:key_2] = value
end
def option_1
self.options[:key_1]
end
def option_2
self.options[:key_2]
end
end
Уже понятней, но слишком много методов: а вдруг опций будет 20? Эту проблему отлично решает define_method (пример его использования будет ниже). Но все таки давайте подумаем: ведь опция, она же просто чекбокс, просто бит — либо взведен, либо нет (да, да как женская интуиция :), зачем сериализовывать, десериализовывать? Может можно сделать проще?
Моя реализация
Давайте подумаем сколько может быть таких опций? Я думаю максимум 10, ну 20, больше пользователь не будет заполнять. А что если хранить опции обычным числом? Неплохая идея, только придется вспомнить работу с битами:
class User < ActiveRecord::Base
NOTIFY_ON_MAIL = 0b0001
NOTIFY_ON_WALL_POST = 0b0010
NOTIFY_ON_FRIEND_REQUEST = 0b0100
NOTIFY_ON_COMMENT_ANSWER = 0b1000
def notify_on_mail
self.options & NOTIFY_ON_MAIL != 0
end
def notify_on_mail=(value)
if value != "0"
self.options |= const
else
self.options &= ~const
end
end
end
Так, хорошо. Теперь можно обобщить для любого числа опций:
class User < ActiveRecord::Base
NOTIFY_ON_MAIL = 0b0001
NOTIFY_ON_WALL_POST = 0b0010
NOTIFY_ON_FRIEND_REQUEST = 0b0100
NOTIFY_ON_COMMENT_ANSWER = 0b1000
[NOTIFY_ON_MAIL, NOTIFY_ON_WALL_POST, NOTIFY_ON_FRIEND_REQUEST, NOTIFY_ON_COMMENT_ANSWER].each do |notifer|
const = self.const_get(notifer)
define_method notifer.downcase.to_sym do
(mail_options & const) != 0
end
define_method "#{notifer.downcase}=".to_sym do |flag|
if flag != "0"
self.mail_options |= const
else
self.mail_options &= ~const
end
end
end
end
Теперь чтобы добавить новую опцию нужно изменить всего две строчки. Хм, а что если свести все к одной? Результат:
class User < ActiveRecord::Base
NOTIFY_ON_MAIL = 0b0001
NOTIFY_ON_WALL_POST = 0b0010
NOTIFY_ON_FRIEND_REQUEST = 0b0100
NOTIFY_ON_COMMENT_ANSWER = 0b1000
constants.grep(/^NOTIFY_/).each do |notifer|
const = self.const_get(notifer)
define_method notifer.downcase.to_sym do
(mail_options & const) != 0
end
define_method "#{notifer.downcase}=".to_sym do |flag|
if flag != "0"
self.mail_options |= const
else
self.mail_options &= ~const
end
end
end
end
Поля для представлений точно также можно генерировать в цикле.
Надеюсь небольшая заметка навела вас на некоторые мысли :) Всего вам доброго!