Пролог
Наверное всем веб (а может и не только веб) разработчикам рано или поздно приходилось сталкиваться с такой задачей как опции пользователей. Что я имею ввиду проще всего показать на картинке. Под опциями будем понимать чекбоксы (то есть опция либо есть либо нет). Обычно таких опций не слишком много, порядка 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
Поля для представлений точно также можно генерировать в цикле.
Надеюсь небольшая заметка навела вас на некоторые мысли :) Всего вам доброго!