Pull to refresh

Как можно легко и просто реализовать опции в Ruby on Rails

Reading time 3 min
Views 1.5K

Пролог


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

Поля для представлений точно также можно генерировать в цикле.
Надеюсь небольшая заметка навела вас на некоторые мысли :) Всего вам доброго!
Tags:
Hubs:
+19
Comments 32
Comments Comments 32

Articles