Decorator vs Strategy, Composite, Presenter

Очень понравилась статья Dan Croak из ThoughtBot, про сравнение паттернов, которые часто путают( Декоратор, Презентер, Стратегия, Композиция). По сути этот пост это перевод статьи для себя.

Decorator

Следуя определению GoF, суть декоратора заключается в следующем: Динамически расширить возможности декорируемого объекта.

Пример:

coffee = Coffee.new  
Sugar.new(Milk.new(coffee)).cost  

или

coffee = Coffee.new  
coffee.extend Milk  
coffee.extend Sugar  
coffee.cost  

Вопрос в чем же отличие декоратора от остальных вышеперечисленных паттернов?

Strategy

GoF:

  • Декоратор меняет наружность
  • Стратегия меняет внутренности

Другими словами декоратор добавляет некоторую функциональность объекту, а стратегия меняет функциональность, оставляя прежний интерфейс.

Пример:

class Coffee  
  def initialize(brewing_strategy = DripBrewingStrategy.new)
    @brewing_strategy = brewing_strategy
  end

  def brew
    @brewing_strategy.brew
  end
end

Coffee.new(SteepBrewingStrategy.new)  

Основное отличие, что объект пробрасываемый в конструктор не "декорируется", а подменяется, т.е. интерфейс остается тем же, без расширения, но реализация при этом может кардинально отличаться.

Composite

  • Декоратор это композиция с одним объектом
  • Декоратор не предназначен для аггрегации объектов

Пример из ActivePresenter:

class SignupPresenter < ActivePresenter::Base  
  presents :user, :account
end  

Декоратор может послужить оберткой только для одного объекта в отличие от композиции, смысл которой собрать несколько.

Presenter

Самый непонятный из паттернов:

  • Декоратор это класс, добавляющий функциональность другому классу
  • Презентер это класс добавляющий функциональность, отвечающую за представление, другому классу
  • Презентер иногда декоратор
  • Презентер иногда композиция

Определения паттерна Презентер нет в книги GoF. Истоки использования данного термина в rails сообществе идут от статьи Jay Fields 2007 года. По большому счету, единственное что делает презентер презентером, так это его "представительная" составляющая.

class HumanizedStat  
  def initialize(component)
    @component = component
  end

  def to_s
    # большое выражение, которое обычно хранят в модели
  end
  # хелпер методы необходимы для #to_s
  # которым возможно следовало бы находиться в app/helpers
end  

В этом примере Презентер очень похож на Декоратор, но тем не менее не попадает под определение Gang of Four.

Однако функциональность полностью относится к представлению объекта, то можно считать данную конструкцию презентером и сложить её в app/presenters.

Прочитать

[Rails] Exception handling

Продолжаю свои увлекательные (или не очень) рассказы про рабочие будни и задачи, которые надо было решать. Сразу к делу:

Задача

Сделать кастомные страницы исключений для апи и веб интерфейсов. Стандартизировать подход к отлову и отдаче исключений.

№ Контекст

Иногда в приложении происходит неведомая х**ня что-то странное, и вылезает исключение, с ответом в виде статичной html страницы и в апи тоже, отчего случается веселье на всех концах.

А что там под капотом?

Под капотом Rack-based приложение на Rails. Rack был изначально придуман, чтобы упростить всем жизнь, он представляет из себя слоеный пирог middleware. Каждый слой это Ruby объект, принимающий в конструктор объект app, отвечающий на метод call, в который прокидывается environment, а возвращается массив(Array), состоящий ровно из трех элементов: status, headers, body. Начиная с версии руби 1.9 body должно реализовывать метод each, т.е. по сути быть тем же массивом.

Middleware является реализацией то ли паттерна Chain of Responsibility, то ли Pipeline, этого момента я сам до конца не понял. С одной стороны, оно как бы отдает на выход то, что принимает следующий слой, с другой, реквест, который пришел, обрабатывают не все слои, а только те, которые активированы или требованиям которых запрос удовлетворяет. Ну как-то так...

А как это делают?

Готовые решения уже есть, но чтобы все самому прочувствовать, надо залезть во внутренности Rails и посмотреть, закатали рукава и вперед.

Чтобы получить весь список middleware из приложения набираем в консоли:

$ rake middleware

Смотрим на понятно-непонятные буковки и нас интересует: use ActionDispatch::ShowExceptions

В нашем приложении используется последняя стабильная сборка 3.2.9, поэтому её и будем копать:

actionpack/lib/actiondispatch/middleware/showexceptions.rb

def call(env)  
  begin
    response = @app.call(env)
  rescue Exception => exception
    raise exception if env['action_dispatch.show_exceptions'] == false
  end
  response || render_exception(env, exception)
end  

Ничего особо сложного, прокидываем дальше запрос и ждем пока вылетит исключение, если такая ситуация не возникла, то просто отдадим response, если что-то пошло не так, то поймаем Exception(т.е. любое исключение), т.к. в конфиге рельсов можно настроить все что хочешь, то и этот middleware можно отключить, если так, то ошибка необработанной уйдет дальше по цепочке. Если слой не отключен, то вызовем метод renderexception//. А вот в нем творятся интересные вещи(код приведу не полностью):

def render_exception(env, exception)  
  wrapper = ExceptionWrapper.new(env, exception)
  ...
  env["PATH_INFO"] = "/#{status}"
  response = @exceptions_app.call(env)
rescue Exception => failsafe_error  
  $stderr.puts "..."
  FAILSAFE_RESPONSE
end  

Во-первых, исключение отдается в ExceptionWrapper, который сомтрит готовый маппинг известных ошибок в символьный аналог статус кодов. Во-вторых, в переменную пути складывается статус-код, что нам впоследствии пригодится, и далее, обработка всего и вся отдается некоему @exceptionsapp_. Это еще что такое?

А это ,всего навсего, еще одно middleware, которое по дефолту в Rails занимается обработкой исключений. Если почитаете гайды, то узнаете, что имя ему PublicExceptions. Все, что там происходит, так это прокидывание статуса(404, 422, 500) дальше по цепочке и рендеринг стандартных страниц с ошибками, которые лежат в public.

А дальше что?

А дальше, сконфигурировать можно все, поэтому переопределим exceptions_app. Тут встает вопрос, писать своё middleware или взять что-то готовое?

за свое против
органично вписывается в архитектуру то не знает, не найдет
свобода мысли сложность тестирования

А какое готовое? Есть ли такое приложение или слой, который органично вписывался бы в существующую структуру и при этом решал поставленную задачу? Да.

В application.rb укажем:

config.exceptions_app = self.routes  

Как вы помните в path_info складывается путь /404, /500, /422 их будем ловить в routes.rb. Перенаправим на обычный контроллер, назовем его errors.

match '/404', :to => "errors#not_found"  
match '/422', :to => "errors#unprocessable_entity"  
match '/500', :to => "errors#internal_error"  

Теперь создадим errors_controller.rb в папке /app/controllers/, никакой авторизации, эти страницы должны быть доступны для всех. Добавляем

respond_to :json, :html

def not_found  
  respond_with({:message => 'Not Found'}, :status => :not_found)
 end

Аналогично определяем еще 2 метода, заменив только статусы на соответствующие ошибкам(422 - :unprocessableentity, 500 - :internalserver_error).

Напишем тесты — ссылка на github. Ничего сложного, код опущу для экономии места.

Далее сделаем view отдельно для html и json и сложим их в app/views/errors/ Последнее, что надо сделать, чтобы в режиме development увидеть кастомные страницы, это заменить значение одного параметра в config/environments/development.rb true -> false.

config.consider_all_requests_local = false  

Полный код можно посмотреть и скачать с github demo_exception_handling. После этого можно сделать bundle install, затем запустить сервер(bundle exec rails s) и обратиться по адресам стандартных страничек ошибок:

Там можно увидеть наши простенькие кастомные страницы, а что самое классное, если обратить к этим страницам с форматом json, то получим

{ message: 'Not Found' }

а не статичный html. Если потребуется можно выводить какие-то специфические коды ошибок в апи и отдавать их в json как будет удобно. В общем простор для фантазии. На этом я думаю все.

Откуда черпал информацию:

Прочитать

[Ruby] Читайте документацию или как красиво работать с many-to-many

Поделюсь решением проблемы, возникшей при выполнении очередной задачи на работе.

Ситуация:

Есть список рассылок List, у него может быть много подписчиков Subscribers (связь многие ко многим), реализовано через третью таблицу SubscriberLists.

class List < ActiveRecord::Base

  has_many :subscriber_lists, :dependent => :destroy
  has_many :subscribers, :through => :subscriber_lists
end  

Требуется:

Выводить списки рассылки в порядке обновления, т. е. те, кто был недавно обновлен должны быть выше. Под обновлением в нашем случае понимается как изменение атрибутов List, так и добавление или удаление подписчиков в список рассылки. Решение: Для вывода списка в апи делаем:

lists = current_client.lists.by_updated_at

Кого смутило by_updated_atсюда. Великолепный гем UsefullScopes от Kaize.

Подпишем желающих(в классе List):

def add_subscriber(subscriber)  
  self.subscribers << subscriber
end  

Вот оно счастье, но тесты мне ответили красным светом. Добавление подписчика вносило изменения лишь во вспомогательную таблицу.

Тут возникла проблема: как выстроить списки в нужном порядке, если без лишних телодвижений обновления не будет.

Держа в голове, что я уже почти 3 месяца как пишу на руби, решил слазить в гугл, а не костылять сразу. Но запросы вида «как мне обновить связанную запись» выдавали исключительно информацию про accepts_nested_attributes_for, что к моей проблеме отношения не имело. Попытав удачу в течение минут 15, решил подойти к проблеме с другой стороны — можно повесить действие на изменение объекта в SubscriberLists:

def after_commit(subscriber_list)  
  subscriber_list.list.update_attributes(:updated_at => Time.zone.now)
end  

У List даем доступ на запись в updated_at

attr_accessible :updated_at

Выглядит монструозно, но работает... Но это же Ruby, но это же Rails, тут должно быть красивое решение! И оно нашлось: http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html (ищем belongs_to и далее :touch)
Решением большинства проблем является фраза: RTFM, и тут прямо в точку! Пишем в SubscriberLists:

belongs_to :list, :touch => true

И все... Теперь, при добавлении подписчика происходит апдейт списка с подписчиками, и он автоматом попадет на первое место в выборке. Читайте доку внимательнее.

Прочитать

[Ruby]: Сортировка и фильтрация по связанным ресурсам в разных локалях с Ransack

Задача была проста: есть модель User и связанная с ней модель Role, нужно организовать поиск и сортировку, и все бы ничего, да только связаны они many-to-many связью:

has_many :roles, :through => :user_roles

Истоки данного варианта соединения мне, к сожалению, были неизвестны, а задачу надо было решить.

Почему именно ransack?

Это gem, призванный облегчить поиск и сортировку по нужным полям, в тех местах, где пользоваться такими мощными средствами как ElasticSearch + Tire вроде бы нет смысла. Скачать его можно тут, плюс есть demo и код его на github, при работе очень помогло описание построения простейших запросов. Это переписанный MetaSearch, удобный для встраивания поиска в формы вашего приложения. На этом хватит лирики, все, кто хочет более подробно ознакомиться, может пройтись по ссылкам, прочитать там все, и потом залезть в исходный код, ничего особо сложного там нет, а для общего понимания будет полезно. Перейдем непосредственно к проблеме. Для начала подключим и установим гем:

gem 'ransack'

bundle install  

Как сортировать по nested resources?

Nested resources не совсем корректный термин, но нравится как звучит, оно применимо в контексте роутинга приложений на RoR. В Readme гема на github очень скудно описаны варианты сортировки, и в принципе работы с гемом, очень помогает в этом отношении код демо приложения и статься про Basic Searching(ссылки смотри выше). Ситуация, есть таблица пользователей в админке приложения:

Id Name Status Role
1 V. Pupkin Opened Company admin
2 I. Ivanov Opened User
3 S. Sergeev Opened Visitor

Сортировку надо организовать по полю Role. Для начала сформируем action index в контроллере:

def index  
  params[:search] = { s: 'created_at desc' }.merge(params[:search] || {})
  @search = ::User.search(params[:search])
  @users = @search.result
end  

Теперь перейдем во view и будем творить магию там:

sort_link @search, "roles_name_#{I18n.locale}", User.human_attribute_name(:roles)  

На этом все, кажется очень просто, но на написание этой строчки я убил 2 часа :) У гема есть helper sort_link, в который мы передаем объект @search, инициализированный в контроллере, он будет производить поиск и сортировку. Следующим параметром передаем поле для сортировки.

Здесь ключевой момент в формате записи поля. Очень помог понять, что там вообще происходит вот этот issue. У модели Role есть 2 атрибута, это nameru и nameen, для названий в русской и английской локалях соответственно. Чтобы список выдавался в правильном порядке в зависимости от того, с каким языком мы работаем, надо передавать правильное поле, поэтому на конце стоит name_#{I18n.locale}. Т. о., чтобы правильно выбрать поле для сортировки при связи многие ко многим необходимо указать сначала имя связанной модели, затем через нижнее подчеркивание нужное поле для отображения, т. е. в моем случае roles_name_#{I18n.locale}. При сортировке, выполняется запрос в базу, на проекте используется PostgreSQL, и для разработки я использую Mac, поэтому у меня при выдаче возникли проблемы. База при запросе с ORDER BY выдавала неправильную последовательность, лечиться вот этим постом.

С сортировкой на этом разобрались, перейдем к фильтрации выдачи по нескольким параметрам. Приведу сразу результирующий кусок кода, а затем подробно его разберу:

= simple_form_for :search, :url => admin_users_path, :method => :get, :html => {:class => 'form-inline'} do |f| 
  = f.input :user_roles_role_id_in, :as => :chosen_select, :collection => Role.all, :label => User.human_attribute_name(:roles), :input_html => {:multiple => true }
  = f.input :state_in, :as => :select, :collection => User.state_machine(:state).states.map {|s| [s.human_name, s.name]}, :label => User.human_attribute_name(:state), :input_html => {:multiple => true }
  = f.submit t("web.searches.show.search")

Идем по порядку:

  1. simple_form_for, если кто не знает, прошу сюда. Подробно на самом simple останавливаться не буду, просто скажу, что для поисковой формы можно использовать не стандартную ransack search_form_for, а спокойно заменить на simple_form и ничего страшного не произойдет.

    • Форма строится для объекта :search, который мы в контроллере как раз и принимаем:
    • :url => укажем точный урл, куда уйдет запрос
    • :method => каким методом посылать
    • :html => доп. параметры для верстки
  2. f.input — самое интересное, на это я потратил 3 часа, если не больше...

    • :user_roles_role_id_in — конечный правильный вариант, при котором учитываются поисковые запросы по соединению с roles. Во-первых in это специальное окончание, которое воспринимает Ransack, более подробно про них написано в статье про BasicSearching. Остальное вроде бы очевидно, мы передали связующую таблицу и поле, по которому надо осуществлять поиск. Почему-то просто по :roles_in работать не захотел.
    • :as => :chosen_select — специальный гем, который позволяет делать красивый множественный выбор для select поля
    • :collection => сюда пробрасываются параметры, которые надо превратить в id — option
    • остальное должно быть понятно, label просто для описания и :multiple => true для реализации множественного выбора

Следующий input это сортировка по статусу, по скольку у нас используется state_machine, то особых проблем с ним не возникло. Последнее это кнопка, которую описывать смысла не вижу.

На этом я хочу закончить, статье итак можно присвоить теги TL DR. Вопросы, предложения?

Прочитать

[Ruby] Как написать свой gem #1

Очень много про это написано, поэтому тем, кто знает лучше не читать. Это скорее просто cheat sheet для себя.

Разобью на несколько этапов, чтобы было понятнее, проще и все описать. Все не в понарошку гем configus, упражнение на новой работе.

С чего начать? Для начала нам необходим bundler. Но если вы работаете с RoR, то может быть и не подозреваете, что это отдельная утилита.

bundle gem configus  

Создаст для вас структуру директорий и даже проинициализирует репозиторий git.

В созданных файлах можно заметить Gemfile и configus.gemspec. В исходном коде Gemfile есть строчка gemspec. В чем отличие этих файлов и для чего каждый из них предназначен лучше и понятней всего написано тут. Идем далее, если заглянуть в configus.gemspec, можно увидеть много полей с информацией, которые стоит заполнить, чтобы явить миру своё лицо, когда вы отправите своё детище на всеобщее обозрение. Советую прочитать статью про различие gemspec и Gemfile, после чего можно смело добавлять строчку:

gem.add_development_dependency 'rspec'

сразу после gem.homepage. Это позволит сказать, что для полноценной работы нам потребуется установить gem 'rspec' и найти его можно на http:://rubygems.org/. Тестировать будем через него, хотя как потом выяснилось, нас заставляли им пользовать только в ознакомительных целях. Наберем bundle install и возрадуемся установившемуся RSpec. На этом завершу первую часть из небольшого cheat sheet по написанию gem'а

Прочитать