Ничего серьезного, или дико умопомрачительного я не опишу, но покажу, что даже злостный
eval
(да-да, питонисты, да и вообще, сразу косо посмотрят на меня), может быть полезен, и он довольно таки вписался в Ruby. Точнее был красиво вписан туда.Покажу я применение, на простой задаче, с которой я встретился. Не то чтобы, тут нет других выходов, не то чтобы, тут по другому не выйти из ситуации, но мне показалось более красивым это решение. Итак, пристегнитесь покрепче. Тем, кто не хочет даже слышать об
eval
прошу пропустить этот пост, и не читать дальше.Пример я буду показывать из дипломного на Rails, но это не умоляет применения метода в каких-то насущных задачах.
Имеется контроллер
MicroblogController
. У него имеется семь основных методов: пять агрегаторов постов, и два относятся к подписке.В моем приложении все ссылки для текущего пользователя - без префикса в виде его имени пользователя. Приведу кусок из
config/routes.rb
1 # Microblog Controller 2 get "microblog/feeds/global" => "microblog#global_feed", 3 :default => {:page => 1}, 4 :as => :microblog__global_feed 5 get "((:username)/)microblog/feeds/local" => "microblog#local_feed", 6 :default => {:page => 1}, 7 :as => :microblog__local_feed 8 get "((:username)/)microblog/feeds/personal" => "microblog#personal_feed", 9 :default => {:page => 1}, 10 :as => :microblog__personal_feed 11 get "((:username)/)microblog/feeds/followings" => "microblog#followings_feed", 12 :default => {:page => 1}, 13 :as => :microblog__followings_feed 14 get "((:username)/)microblog/feeds/followers" => "microblog#followers_feed", 15 :default => {:page => 1}, 16 :as => :microblog__followers_feed 17 get "((:username)/)microblog/subscribes/followings" => "microblog#followings", 18 :default => {:page => 1}, 19 :as => :microblog__followings 20 put "microblog/subscribes/followings/add/:following" => "microblog#add_following", 21 :as => :microblog__add_following 22 put "microblog/subscribes/followings/remove/:following" => "microblog#remove_following", 23 :as => :microblog__remove_following 24 get "((:username)/)microblog/subscribes/followers" => "microblog#followers", 25 :default => {:page => 1}, 26 :as => :microblog__followers 27 put "microblog/create_post" => "microblog#create_post", 28 :as => :microblog__create_post 29 put "microblog/delete_post/:id" => "microblog#delete_post", 30 :as => :microblog__delete_postЗдесь мы видим, что у меня каждый роут именнованный. Ну и имеет некоторые параметры по умолчанию. Генерация ссылки на персональную ленту микроблога моего микроблога будет выглядеть примерно следующим образом:
<% link_to "Demiazz Microblog", microblog__personal_feed_path(:username => "demiazz") %>
. Нравится? Думаю нет. Как бы удобно не было, как бы красиво не выглядело, но все равно, что то не то.Но это еще цветочки. В моем приложении, сверху расположено меню, которое предоставляет ссылки на те или иные ленты. Меню изображается в виде табов, и хочется как бы показать пользователю, где он собственно находится.
Какое решение можно придумать? Ну проверить по action_name и вывести либо ссылку с каким-нить CSS классом, либо без него. В итоге в шаблоне мы можем получить что-то типа:
<% if action_name == "personal_feed" %> <%= link_to "Personal Feed", microblog__personal_feed_path(:username => "demiazz"), :class => "selected" %> <% else %> <%= link_to "Personal Feed", microblog__personal_feed_path(:username => "demiazz") %> <% end %>Я что-то не вижу элегантности чуть больше, чем совсем. Это одна ссылка, а их может быть 5 к примеру. Причем два варианта, для текущего пользователя, и для указанного пользователя в URL. Можно конечно, оставить и так, но мне кажется это не совсем элегантно. Какой выход? Написать хелперы. Сказано сделано. Первоначальный вариант хелперов:
1 module MicroblogHelper 2 3 def to_microblog_home(name="Microblog", html_options = {}) 4 if action_name == "local_feed" 5 html_options[:class] = html_options[:class].nil? ? "selected" : html_options[:class].concat(" selected") 6 end 7 link_to name, url_for(:controller => "microblog", 8 :action => "local_feed", 9 :username => nil), html_options 10 end 11 12 def to_microblog_user_home(user, name="Microblog", html_options = {}) 13 if action_name == "personal_feed" 14 html_options[:class] = html_options[:class].nil? ? "selected" : html_options[:class].concat(" selected") 15 end 16 link_to name, url_for(:controller => "microblog", 17 :action => "personal_feed", 18 :username => user.username), html_options 19 end 20 21 def to_microblog_global_feed(name="Global", html_options = {}) 22 if action_name == "global_feed" 23 html_options[:class] = html_options[:class].nil? ? "selected" : html_options[:class].concat(" selected") 24 end 25 link_to name, url_for(:controller => "microblog", 26 :action => "global_feed", 27 :username => nil), html_options 28 end 29 30 def to_microblog_local_feed(name="Local", html_options = {}) 31 if action_name == "local_feed" 32 html_options[:class] = html_options[:class].nil? ? "selected" : html_options[:class].concat(" selected") 33 end 34 link_to name, url_for(:controller => "microblog", 35 :action => "local_feed", 36 :username => nil), html_options 37 end 38 39 def to_microblog_personal_feed(name="Personal", html_options = {}) 40 if action_name == "personal_feed" 41 html_options[:class] = html_options[:class].nil? ? "selected" : html_options[:class].concat(" selected") 42 end 43 link_to name, url_for(:controller => "microblog", 44 :action => "personal_feed", 45 :username => nil), html_options 46 end 47 48 def to_microblog_followings_feed(name="Followings", html_options = {}) 49 if action_name == "followings_feed" 50 html_options[:class] = html_options[:class].nil? ? "selected" : html_options[:class].concat(" selected") 51 end 52 link_to name, url_for(:controller => "microblog", 53 :action => "followings_feed", 54 :username => nil), html_options 55 end 56 57 def to_microblog_followers_feed(name="Followers", html_options = {}) 58 if action_name == "followers_feed" 59 html_options[:class] = html_options[:class].nil? ? "selected" : html_options[:class].concat(" selected") 60 end 61 link_to name, url_for(:controller => "microblog", 62 :action => "followers_feed", 63 :username => nil), html_options 64 end 65 66 def to_microblog_user_local_feed(user, name="Local", html_options = {}) 67 if action_name == "local_feed" 68 html_options[:class] = html_options[:class].nil? ? "selected" : html_options[:class].concat(" selected") 69 end 70 link_to name, url_for(:controller => "microblog", 71 :action => "local_feed", 72 :username => user.username), html_options 73 end 74 75 def to_microblog_user_personal_feed(user, name="Personal", html_options = {}) 76 if action_name == "personal_feed" 77 html_options[:class] = html_options[:class].nil? ? "selected" : html_options[:class].concat(" selected") 78 end 79 link_to name, url_for(:controller => "microblog", 80 :action => "personal_feed", 81 :username => user.username), html_options 82 end 83 84 def to_microblog_user_followings_feed(user, name="Followings", html_options = {}) 85 if action_name == "followings_feed" 86 html_options[:class] = html_options[:class].nil? ? "selected" : html_options[:class].concat(" selected") 87 end 88 link_to name, url_for(:controller => "microblog", 89 :action => "followings_feed", 90 :username => user.username), html_options 91 end 92 93 def to_microblog_user_followers_feed(user, name="Followers", html_options = {}) 94 if action_name == "followers_feed" 95 html_options[:class] = html_options[:class].nil? ? "selected" : html_options[:class].concat(" selected") 96 end 97 link_to name, url_for(:controller => "microblog", 98 :action => "followers_feed", 99 :username => user.username), html_options 100 end 101 102 def to_microblog_subscribes(name="Subscribes", html_options = {}) 103 if action_name == "followings" 104 html_options[:class] = html_options[:class].nil? ? "selected" : html_options[:class].concat(" selected") 105 end 106 link_to name, url_for(:controller => "microblog", 107 :action => "followings", 108 :username => nil), html_options 109 end 110 111 def to_microblog_user_subscribes(user, name="Subscribes", html_options = {}) 112 if action_name == "followings" 113 html_options[:class] = html_options[:class].nil? ? "selected" : html_options[:class].concat(" selected") 114 end 115 link_to name, url_for(:controller => "microblog", 116 :action => "followings", 117 :username => user.username), html_options 118 end 119 120 def to_microblog_followings(name="Followings", html_options = {}) 121 if action_name == "followings" 122 html_options[:class] = html_options[:class].nil? ? "selected" : html_options[:class].concat(" selected") 123 end 124 link_to name, url_for(:controller => "microblog", 125 :action => "followings", 126 :username => nil), html_options 127 end 128 129 def to_microblog_followers(name="Followers", html_options = {}) 130 if action_name == "followers" 131 html_options[:class] = html_options[:class].nil? ? "selected" : html_options[:class].concat(" selected") 132 end 133 link_to name, url_for(:controller => "microblog", 134 :action => "followers", 135 :username => nil), html_options 136 end 137 138 def to_microblog_user_followings(user, name="Followings", html_options = {}) 139 if action_name == "followings" 140 html_options[:class] = html_options[:class].nil? ? "selected" : html_options[:class].concat(" selected") 141 end 142 link_to name, url_for(:controller => "microblog", 143 :action => "followings", 144 :username => user.username), html_options 145 end 146 147 def to_microblog_user_followers(user, name="Followers", html_options = {}) 148 if action_name == "followers" 149 html_options[:class] = html_options[:class].nil? ? "selected" : html_options[:class].concat(" selected") 150 end 151 link_to name, url_for(:controller => "microblog", 152 :action => "followers", 153 :username => user.username), html_options 154 end 155 156 endЯ намерено привел весь код, чтобы вы ужаснулись от того, сколько копипасты тут. А ее чуть больше, чем просто огромное количество. Если присмотреться, различия тут только в названии, одном параметре, и применении этого параметра. Все. Но мы пока посмотрим, как теперь будет выглядеть код меню для этого случая.
<article class="content-menu"> <ul> <% if @personal %> <li><%= to_microblog_global_feed %></li> <li><%= to_microblog_local_feed %></li> <li><%= to_microblog_personal_feed %></li> <li><%= to_microblog_followings_feed %></li> <li><%= to_microblog_followers_feed %></li> <li class="separator"></li> <li><%= to_microblog_subscribes %></li> <% else %> <li><%= to_microblog_user_local_feed @user %></li> <li><%= to_microblog_user_personal_feed @user %></li> <li><%= to_microblog_user_followings_feed @user %></li> <li><%= to_microblog_user_followers_feed @user %></li> <li class="separator"></li> <li><%= to_microblog_user_subscribes @user %></li> <% end %> </ul> </article>Ну что ж. Это лучше, чем куча условий. Замечу, что это меню и для текущего пользователя, и для указанного в URL. С условиями это выходило где то в 5 раз больше. Что вовсе не радовало глаз, и вообще было страшно и некрасиво.
С одной задачей мы разобрались. Мы избавились от ужасных
link_to
и условий, скрыв это все в хелперах. Теперь надо подумать, как изменить собственно хелперы, да так, чтобы это выглядело красиво. Для нетерпеливых, я прилагаю ссылку на то, что я использую для решения этой задачи: Module::module_eval. Надо заметить, что есть еще методы
Module#method_exec
, Module#define_method
. Но это вряд ли, то, что я бы сейчас использовал. Налицо большое применение шаблонов, и руки так и чешутся, чтобы объединить этот код в шаблоны, да по ним делать функции. Прежде, чем я покажу результат рефакторинга, я вкратце расскажу про
Module#module_eval
. Эта функция выполняет код в контексте модуля. Что это может нам дать? Это может позволить нам, выполнить код, который объявит функцию, и она в дальнейшем будет доступна так же, как и любая другая в контексте модуля, вплоть до возможностей отладки. Ну разве что, вполне возможно, что отладчик не покажет вам строку где возникла ошибка, но сможет намекнуть на имя функции, которая вызвала ошибку.Итак. Я немного сократил код хелпера, используя метод module_eval, и в итоге получили примерно следующий код:
1 module MicroblogHelper 2 3 def MicroblogHelper.included(mod) 4 %w[global local personal followings followers].each do |feed| 5 pattern = " 6 def to_microblog_#{feed}_feed(name=\"#{feed.capitalize}\", html_options = {}) 7 if action_name == \"#{feed}_feed\" 8 html_options[:class] = html_options[:class].nil? ? \"selected\" : html_options[:class].concat(\" selected\") 9 end 10 link_to name, url_for(:controller => \"microblog\", 11 :action => \"#{feed}_feed\", 12 :username => nil), html_options 13 end" 14 mod.module_eval pattern 15 end 16 %w{local personal followings followers}.each do |feed| 17 pattern = " 18 def to_microblog_user_#{feed}_feed(user, name=\"#{feed.capitalize}\", html_options = {}) 19 if action_name == \"#{feed}_feed\" 20 html_options[:class] = html_options[:class].nil? ? \"selected\" : html_options[:class].concat(\" selected\") 21 end 22 link_to name, url_for(:controller => \"microblog\", 23 :action => \"#{feed}_feed\", 24 :username => user.username), html_options 25 end" 26 mod.module_eval pattern 27 end 28 %w{followings followers}.each do |subscribes| 29 pattern = " 30 def to_microblog_#{subscribes}(name=\"#{subscribes.capitalize}\", html_options = {}) 31 if action_name == \"#{subscribes}\" 32 html_options[:class] = html_options[:class].nil? ? \"selected\" : html_options[:class].concat(\" selected\") 33 end 34 link_to name, url_for(:controller => \"microblog\", 35 :action => \"#{subscribes}\", 36 :username => nil), html_options 37 end" 38 mod.module_eval pattern 39 pattern = " 40 def to_microblog_user_#{subscribes}(user, name=\"#{subscribes.capitalize}\", html_options = {}) 41 if action_name == \"#{subscribes}\" 42 html_options[:class] = html_options[:class].nil? ? \"selected\" : html_options[:class].concat(\" selected\") 43 end 44 link_to name, url_for(:controller => \"microblog\", 45 :action => \"#{subscribes}\", 46 :username => user.username), html_options 47 end" 48 mod.module_eval pattern 49 end 50 end 51 52 def to_microblog_home(name="Microblog", html_options = {}) 53 if action_name == "local_feed" 54 html_options[:class] = html_options[:class].nil? ? "selected" : html_options[:class].concat(" selected") 55 end 56 link_to name, url_for(:controller => "microblog", 57 :action => "local_feed", 58 :username => nil), html_options 59 end 60 61 def to_microblog_user_home(user, name="Microblog", html_options = {}) 62 if action_name == "personal_feed" 63 html_options[:class] = html_options[:class].nil? ? "selected" : html_options[:class].concat(" selected") 64 end 65 link_to name, url_for(:controller => "microblog", 66 :action => "personal_feed", 67 :username => user.username), html_options 68 end 69 70 def to_microblog_subscribes(name="Subscribes", html_options = {}) 71 if action_name == "followings" 72 html_options[:class] = html_options[:class].nil? ? "selected" : html_options[:class].concat(" selected") 73 end 74 link_to name, url_for(:controller => "microblog", 75 :action => "followings", 76 :username => nil), html_options 77 end 78 79 def to_microblog_user_subscribes(user, name="Subscribes", html_options = {}) 80 if action_name == "followings" 81 html_options[:class] = html_options[:class].nil? ? "selected" : html_options[:class].concat(" selected") 82 end 83 link_to name, url_for(:controller => "microblog", 84 :action => "followings", 85 :username => user.username), html_options 86 end 87 88 endКод сократился. Согласитесь. Если вам нужно будет внести изменения, то все функции в одном месте, в одном паттерне. Меняете одну, меняются 2/4/5 функций. Удобно. И код сократился. И они вполне отлаживаемые и выполняемые хоть сотню раз. И да. При инклуде в модуль или класс - они успешно появятся и там и там.
Но у нас остались четыре функции близнеца тех, что мы генерируем шаблонами. Не проблема. Если бы не дефолтный параметр name, значение которого отличается от близнецов, то мы могли бы использовать метод
Module#alias_method
. Для решения подобной проблемы, мы просто объявляем четыре функции обертки в теле модуля MicroblogHelper
. И теперь модуль выглядит следующим образом:1 module MicroblogHelper 2 3 def MicroblogHelper.included(mod) 4 %w[global local personal followings followers].each do |feed| 5 pattern = " 6 def to_microblog_#{feed}_feed(name=\"#{feed.capitalize}\", html_options = {}) 7 if action_name == \"#{feed}_feed\" 8 html_options[:class] = html_options[:class].nil? ? \"selected\" : html_options[:class].concat(\" selected\") 9 end 10 link_to name, url_for(:controller => \"microblog\", 11 :action => \"#{feed}_feed\", 12 :username => nil), html_options 13 end" 14 mod.module_eval pattern 15 end 16 %w{local personal followings followers}.each do |feed| 17 pattern = " 18 def to_microblog_user_#{feed}_feed(user, name=\"#{feed.capitalize}\", html_options = {}) 19 if action_name == \"#{feed}_feed\" 20 html_options[:class] = html_options[:class].nil? ? \"selected\" : html_options[:class].concat(\" selected\") 21 end 22 link_to name, url_for(:controller => \"microblog\", 23 :action => \"#{feed}_feed\", 24 :username => user.username), html_options 25 end" 26 mod.module_eval pattern 27 end 28 %w{followings followers}.each do |subscribes| 29 pattern = " 30 def to_microblog_#{subscribes}(name=\"#{subscribes.capitalize}\", html_options = {}) 31 if action_name == \"#{subscribes}\" 32 html_options[:class] = html_options[:class].nil? ? \"selected\" : html_options[:class].concat(\" selected\") 33 end 34 link_to name, url_for(:controller => \"microblog\", 35 :action => \"#{subscribes}\", 36 :username => nil), html_options 37 end" 38 mod.module_eval pattern 39 pattern = " 40 def to_microblog_user_#{subscribes}(user, name=\"#{subscribes.capitalize}\", html_options = {}) 41 if action_name == \"#{subscribes}\" 42 html_options[:class] = html_options[:class].nil? ? \"selected\" : html_options[:class].concat(\" selected\") 43 end 44 link_to name, url_for(:controller => \"microblog\", 45 :action => \"#{subscribes}\", 46 :username => user.username), html_options 47 end" 48 mod.module_eval pattern 49 end 50 end 51 52 def to_microblog_home(name="Microblog", html_options = {}) 53 to_microblog_local_feed(name, html_options) 54 end 55 56 def to_microblog_user_home(user, name="Microblog", html_options = {}) 57 to_microblog_user_personal_feed(user, name, html_options) 58 end 59 60 def to_microblog_subscribes(name="Subscribes", html_options = {}) 61 to_microblog_followings(name, html_options) 62 end 63 64 def to_microblog_user_subscribes(user, name="Subscribes", html_options = {}) 65 to_microblog_user_followings(user, name, html_options) 66 end 67 68 endИтак. Наш модуль сократился почти в три раза, нам не нужны теперь именованные route'ы (хотя можно и оставить для красоты, тут дело вкуса). И, кроме того, мы последовали принципу повторного использования кода, правда несколько не стандартным путем.
В данном коде возможны еще два улучшения: 1) сократить количество циклов до одного, с вынесением одного из паттернов, и предварительной обработкой к примеру (global_feed). 2) так как условие, которое обрабатывает css класс зависит от action_name, и привязан к модулю MicroblogController, то в принципе, при include в другие модули/классы это условие просто не нужно, и его генерацию можно отключить.
На этом все. ) Программируйте с удовольствием.