Pull to refresh

Tips & Tricks

Ruby on Rails *
Практически во всех языках программирования одну и ту же задачу можно решить несколькими способами. Однако какие-то из них лучше, какие-то хуже. Для некоторых требуется написать 10 строк кода, для других можно обойтись и одной.

Совершенствование кода и его оптимизация порой отнимает больше времени, чем заняло написание первой версии. Вы часто встречали новый для вас код или интересную реализацию, и говорили себе: «Оказывается, это можно сделать стандартными средствами, а я изобретал велосипед»? Лично я — да. Поэтому в этой статье я собрал свою коллекцию «велосипедов», и рассказал, как от них можно избавиться.

Методы массивов


Простой пример — допустим, у вас имеется массив объектов класса User. У них есть свойство activated, которое выставляется на 1, если юзер активировал свою учетную запись. Вам нужно проверить, все ли юзеры из массива активированы. Не берем во внимание ActiveRecord (там можно и по-другому сделать), моя цель — показать работу с массивами.

Способ первый, самый примитивный:
  1. @users = User.find(:all)
  2. activated_users = 0
  3. foreach user in @users
  4.   activated_users += 1 if user.activated == 1
  5. end
  6.  
  7. # если количество совпадает, значит все юзеры активированы
  8. activated_users == @users.size

Первое, что я тут сделал — вместо цикла стал использовать метод массива each:
  1. @users = User.find(:all)
  2. activated_users = 0
  3.  
  4. # лично мне такой способ записи нравится больше,
  5. # чем стандартный для многих языков цикл foreach
  6. @users.each do |user|
  7.   activated_users += 1 if user.activated == 1
  8. end
  9.  
  10. # или же в одну строку:
  11. @users.each{ |user|  activated_users += 1 if user.activated == 1}
Но даже эта запись меня в последствии не устроила. Я нашел более рациональный вариант:
  1. # Метод select позволяет вытащить элементы массива по заданному условию
  2. # И потом мы сравниваем размеры обоих массивов
  3. @users = User.find(:all)
  4. @users.select{ |user| user.activated == 1 }.size == @users.size
Однако все хорошее уже придумано за нас, достаточно лишь прочитать документацию. У массивов есть замечательный метод all?, который проверяет, все ли элементы удовлетворяют условию. К слову, еще есть метод any?, возвращающий true если хотя бы один элемент соответствует условию.

Весь наш код можно записать всего лишь одной строкой:
  1. @users.all?{ |user| user.activated == 1 }
  2. # или даже так:
  3. @users.all?(&:activated) # при условии что activated принимает значение true/false, т.к. 0 в Ruby считается true

Кроме этого, есть еще несколько малоизвестных, но полезных методов:
  1. # Взять первые 5 элементов
  2. @array.take(5)
  3.  
  4. # Выбрать случайный элемент
  5. @array.choice
  6.  
  7. # Разбросать элементы в случайном порядке
  8. @array.shuffle!
  9.  
  10. # Взять элемент массива по его номеру
  11. # Причем этот метод работает быстрее, чем @array[1]
  12. @array.at(1)

Каким способом лучше всего подсчитать количество элементов в массиве?


Сount — самый функциональный. Можно задать параметры в скобках:
  1. array = [1,1,2,2,3,4,5]
  2. array.count # => 7
  3. array.count(1) # подсчитает количество элементов "1" => 2
  4. array.count{ |p| p > 2 } # подсчитает элементы по условию => 3


Size или length (для массивов они идентичны) — самый быстрый способ, потому что единственное его назначение — просто подсчитать количество всех элементов.

Частоиспользуемые запросы ActiveRecord


Еще один «велосипед», с которым я встречался — это своя реализация named scope. Например, у вас есть таблица Cars. Вам нужно часто обращаться к базе, и вытаскивать машины определенного цвета. Можно написать в модели свой метод:
  1. class Car < ActiveRecord::Base
  2.     def self.only_red
  3.         self.find(:all, :conditions => "color = 'red'")
  4.     end
  5. end
Он просто ищет все машины красного цвета. Обращаться к нему просто — Car.only_red. Но самый главный недостаток — если вам захочется все это еще и отсортировать (или добавить что-то типа: limit => 5), то придется или дорабатывать метод в модели, или использовать стандартный find.

На помощь приходит очень удобный и простой в использовании named_scope.
  1. class Car < ActiveRecord::Base
  2.     named_scope :red, :conditions => 'color = "red"'
  3. end

Использовать его практически также — Car.red. Но кроме этого, можно использовать его вместе с методом find, например:
  1. Car.red.find(:all, :limit => 10, :order => "id DESC")
А еще их можно комбинировать. Сначала добавим новый scope который позволит включить в запрос пользователей, которым принадлежат машины:
  1.  named_scope :with_users, :include => :users

Теперь Car.red.with_users создаст сами-знаете-какой запрос. Удобно? Несомненно.

Link_to и его друзья


Еще несколько интересных аналогов есть у самой обычной ссылки link_to.

Первая это link_to_if, которая выведет ссылку, только если она совпадает с условием. Если не совпадает — ссылки не будет, вместо нее останется лишь текст.
Например, у нас есть ссылка «Поднять карму». Если юзер уже голосовал, она не должна быть доступна. Некоторые делают для этой цели отдельный стиль, и отображают ссылку как обычный текст. Но можно написать:
  1. link_to_if (user.voted_for_karma? == false), "Поднять карму", :action => "add_karma"
Что тут можно поменять? Конечно же, (user.voted_for_karma? == false) смотрится непонятно, ведь можно просто убрать сравнение с false, используя link_to_unless:
  1. link_to_unless user.voted_for_karma?, "Поднять карму", :action => "add_karma"
Можно сделать еще круче — сообщим что юзер уже голосовал.
  1. # link_name тут (не)используется как название оригинальной ссылки
  2. link_to_unless(user.voted_for_karma?, "Поднять карму", :action => "add_karma") { |link_name| "Вы уже проголосовали" }
А еще можно выдавать другую ссылку, если юзер не залогинился, например перенаправлять на регистрацию:
  1. link_to_if(user.logged_in?, "Создать тему", :action => "add_post") do |link_name|
  2.      # если незалогиненный юзер тыкнет по ссылке, то попадет на страницу логина
  3.      link_to(link_name, :controller => "users", :action => "login")
  4. end

Довольно часто можно встретить замену ссылки текстом, если браузер находится на текущей странице. Делается это с помощью link_to_unless_current:
  1. <!-- index.html.erb -->
  2. <ul id="navbar">
  3.   <li><%= link_to_unless_current("Home", { :action => "index" }) %></li>
  4.   <li><%= link_to_unless_current("About Us", { :action => "about" }) %></li>
  5. </ul>
  6. <!-- если мы перейдем на страницу About Us, то в html коде увидим следующее: -->
  7. <ul id="navbar">
  8.   <li><a href="/controller/index">Home</a></li>
  9.   <li>About Us</li>
  10. </ul>


Русификация валидации


Еще одна проблема, с которой я когда-то столкнулся — это валидация и ошибки. Приложение мое было на русском языке (конечно же), и по умолчанию ошибки выдавались в таком формате: «Title не может быть пустым».

В первую очередь нужно установить гем Russian, я очень надеюсь что вы про него уже знаете. Это позволит локализовать большинство стандартных сообщений.
Затем в папке config/locales надо открыть (или создать, если нет) файл ru.yml. Вот пример из моего файла:
  1. ru:
  2.   activerecord:
  3.     errors:
  4.       full_messages:
  5.         rate:
  6.           exists: "Нельзя оценить одного и того же пользователя дважды"
  7.           number_limit: "Оценка может принмать значения от -2 до 2"
  8.     attributes:
  9.       news:
  10.         title: "Заголовок"
  11.         body: "Текст"
  12.         article: "Статья"
Как вы видите, чтобы локализовать поле title в таблице news, достаточно добавить вышеприведенную запись в файл локализации.

Поле full_messages используется для полных сообщений об ошибках. Вот как они прописываются в модели:
  1. class Rate < ActiveRecord::Base
  2.   validates_uniqueness_of :rate_owner_id, :message => "rate.exists"
  3.   validates_inclusion_of  :value, :in => [-2, -1, 1, 2], :message => "rate.number_limit"
  4. end
Параметр message берет файл локализации, идет по адресу activerecord.errors.full_messages, затем по указанному в модели, и возвращает локализованную строку. Все достаточно просто. Если не указывать message, то на выводе будет стандартная фраза об ошибке, но в моем случае я заменил ее своей.

Методы для контроллера


Если вам надо в экшене отдавать данные и аяксом, и «по-старинке», то проверку на него можно сделать с помощью request.xhr?, например:
  1. def search
  2.     @users = User.find(:all, :conditions => "login LIKE '#{params[:q]}' ", :limit => 30)
  3.     render :json => @users if request.xhr?
  4. end
Еще один полезный метод — это verify. Судя по названию, он служит для удостоверения, что на контроллер пришел правильный запрос. В основном это служит для защиты, и для некоторых методов я настоятельно рекомендую его использовать.
  1. class UsersController < ApplicationController
  2.   # Проверяем метод create - он должен принимать только post запросы и указанные параметры.
  3.   # Если что-то не так, то перенаправляем юзера в начало, и добавляем флеш-сообщение об ошибке.
  4.   verify :only => :create, :method => :post, :params => [:login, :email, :name], :redirect_to => :index, :add_flash => {:error => 'Неверный запрос. У вас точно все в порядке?'}
  5. end
К слову, не стоит расписывать ошибку как можно подробнее — основная валидация должна проходить в модели. Тут же мы проверяем параметры, и ругаемся если что-то не так.

Отладка


Для отладки приложений я использую логгер и гем ruby-debug. Логгер — это простой и быстрый способ узнать «какого хрена он nil вместо users возвращает». Делается он просто:
  1. # inspect я вызываю для удобного отображения объектов в консоли
  2. logger.debug "users = #{users.inspect}"
К слову, можно использовать не только logger.debug, но и logger.info, logger.warn или даже logger.fatal. В собственных классах можно писать RAILS_DEFAULT_LOGGER.debug, потому что на фразу logger.debug он откликаться не будет.

Если надо посмотреть или изменить состояние переменных, то можно воспользоваться дебаггером. Для этого помещаем запись debugger, например:
  1. class UsersController < ApplicationController
  2.  
  3.   def index
  4.     @user = User.find_by_id(params[:id])
  5.     role = @user.role
  6.     debugger
  7.   end
  8. end
После этого нам нужно или запустить сервер с параметром --debugger, или дописать
  1. require 'ruby-debug'
в environment.rb. После этого обновляем страницу index в браузере -, но она остановится и не будет загружена. Самое время заглянуть в консоль, там нас ждет дебаггер.
  1. (rdb:1) irb
  2. irb(#<UsersController:0x1039edd20>):001:0> @user
  3. => #<User id: 1, login: "Vizakenjack">
  4. irb(#<UsersController:0x1039edd20>):001:0> @user.login = 'Admin'
  5. => "Admin">

Сначала переходим к irb, а затем смотрим наши переменные. Кроме просмотра, их можно поменять и уже потом пускать их в приложение.

По дебаггеру можно написать целую книгу, тут я привел примеры лишь базового использования. Для тех, кто хочет «всё и сразу» — читаем ссылки абзацем ниже.

Заключение



Тут я приведу несколько полезных ссылок, которые помогли мне, и, надеюсь, будут полезны и вам.
  • Гайды по рельсам. На английском. Очень полезный сборник статей.
  • Ruby Toolbox — сборник плагинов, гемов и расширений. На все случаи жизни.
  • Rails API и Ruby API. Документация языка и фреймворка. Хоть аналогов и немало, мне приглянулся этот сайт.
  • Викиучебник по Ruby, на русском. Читается очень легко. Для интереса можно зайти в раздел «Сети» — там даже есть описание трояна!
  • Rubular — сайт для создания и тестирования регулярных выражений,
Tags:
Hubs:
Total votes 76: ↑61 and ↓15 +46
Views 4.7K
Comments Comments 45