Наверное самым странным и непонятным (по моему мнению) компонентом
Rails являются хелперы (Helpers). Когда мы приходим в мир Rails мы много
читаем про MVC и как она реализована в Rails, прозоны ответственности
этих компонентов. Но что представляют собой Helpers, которые содержатся
в папке app/helpers?
Что предгагают нам рельсы
Хелперы представляют собой обычные модули с набором функций. Например:
12345
moduleApplicationHelperdefsome_useful_method(arg1)# some logic hereendend
Модули не являются самостоятельными единицами и в практически всегда они
инклудятся в определенный контекст для расширения.
Хелперы придумали для выноса части различной мелкой (в некоторых случаях
не такой уж и мелкой) логики из контролера и представления (view).
Например:
app/helpers/application_helper.rb
12345
moduleApplicationHelperdefhello_text'Hello from Helper!'endend
app/controllers/welcome_controller.rb
12345
classWelcomeController<ApplicationControllerdefindex# here `hello_text` will be availableendend
app/views/welcome/index.html.erb
1
<%=hello_text%>
Какие проблемы
Основная проблема в том, что хелперы превращаются в неконтролируемую
свалку ни к чему не привязанных функций, ведь по умолчанию все хелперы
инклудятся в любой контроллер. При этом возникает вероятность
существования нескольких функций с одинаковыми именами и тогда удачной
отладки. До определенного момента в контроллер инклудился хелпер имя
модуля которого является часть имени контроллера без Controller части.
Так как хелперы инклудятся в контекст view, то в них доступны instance variables объявленные в
контроллере. Поэтому это уменьшает возможность повторного использования,
а также увеличивает непонимание того, какие объекты являются “игроками”
в той или иной функции, и возвожность протестировать. Например:
Также из-за особенности, что хелперы доступны и в контроллере и в
представлении, иногда трудно сказать в каком логическом контексте текущая функция
должна вызываться, хотя она может быть универсальной и вызываться в
обеих.
Очень часто хелперы используют для формирования html сниппетов:
Мне кажется, что формирование HTML кода необходимо оставлять на view.
Резюмирую: в итоге получается модули, в которых масса отдельно
сущестующих методов, которые привязаны к разным логическим контекстам выполнения
(controller или view), которые могут генерировать HTML код и много всего
другого, чего они не должны делать.
Как уменьшить боль?
Я бы не рекомендовал использовать хелперы вообще. Но бывают ситуации,
особенно на старте проекта, когда нужно быстро накидать рабочий
прототип, при этом особо не заморачиваясь с продумыванием сложной
объектной иерархии с распределением обязанностей. Поэтому я выделю
несколько маленьких рецептов:
Не использовать instance variables, объявленные в контроллере или во
view. Передавайте явно параметры. При этом можно избежать side
effects, когда вы вызываете функцию, а там используется instance
variable, которой еще нет. В идеале функции, объявленные в хелперах,
должны быть pure functions.
Выносите формирование HTML сниппетов в отдельный partial. Опять же
бывают ситуации, когда сниппет - это буквально одна строчка,
например, в зависимости от статуса выводить разную иконку.
Отказаться от глобального инклудинга всех хелперов. Для этого
необходимо добавить следующую строку, например в
config/application.rb:
Ruby не перестает меня удивлять! Как по мне, то самое удивительное и
мощное в нем - метапрограмминг. После статического языка, в котором твой
арсенал ограничен заложенными ключевыми словами и конструкциями, просто
невозможно вообразить насколько развязаны твои руки и что ты можешь со
всем этим делать! Используя динамику и выразительность Ruby можно
превратить кусок кода практически в понятную фразу на английском.
Элементы конструкций не ограничены! Можно создавать их самому сколько
угодно, получая в результате высокоуровневый и понятный почти любому
человеку DSL.
Самое главное вовремя остановится… Но не сейчас)
Варианты объявления класса
Все мы привыкли объявлять наши классы используя элемент синтаксиса ruby,
а именно ключевое слово class:
12
classUserend
Но существует еще один вариант. Т.к. класс является экземпляром класса
Class, то справедлива следующая запись:
123
User=Class.newdoattr_accessor:nameend
Используя способ выше также можно указать суперкласс, просто передав его
как параметр в метод Class.new:
Когда вы несколько раз объявляете класс используя ключевое слово class, то реально
класс инициализируется только один раз. Все следующие разы класс
открывается для внесения изменений. Например:
В случае с объявлением класса, используя метод Class.new, ситуация
немного другая. На самом деле слово User, которое мы используем для
инициализации экземпляров, является практически такой же переменной,
которая ссылается на экземпляр класса Class. Поэтому код ниже дает,
может быть, вполне очевидные результаты:
12345678910111213141516171819202122232425
classUserdefname'Sergey'endendUser.object_id# => 70162158343080User.new.name# => SergeyUser=Class.newdodefsecond_name'Gernyak'enddeffull_name"#{second_name}#{name}"endendUser.object_id#=> 70162144228540User.new.full_name# => NameError: undefined local variable or method `name' for #<User:0x007fdb3b028b18>User.new.second_name# => Gernyak
Получается мы взяли и заменили значение нашей переменной User на
другое. Об этом также свидетельствует значение object_id. Поэтому наш
новый класс User ничего не знает о методе #name.
Правда и тут можно сделать хитрость: добавить наш существующий класс в
цепочку наследования:
Данную фичу я использовал в одной из наших библиотек exracted_validator.
У меня есть некоторый базовый класс, который содержит общую логику для
возможности вынесения валидаций из модели:
12345
moduleExtractedValidatorclassBase<SimpleDelegator# Some logic hereendend
Но с некоторыми видами валидаций могут возникнуть проблемы, т.к. они
жестко завязаны на именование класса, в котором они объявлены. Например,
валидация uniqueness. Для того, чтобы иметь возможность использовать
такую валидацию в кастомном валидаторе, необходимо переопределить метод
.model_class:
Используя последний код, все будет работать как и ожидается и все
останутся довольны.
Ну почти все… Я не доволен тем, что мне принудительно нужно добавлять
еще один метод каждый раз, когда буду объявлять новый валидатор. Поэтому
я решил сделать небольшой рефакторинг используя метапрограмминг :-)
В данном примере динамически создается класс-обертка, в котором
объявляется требуемый метод .model_class и этот метод возвращает
переданный класс, целевой для данного валидатора, модели. Цепочка
наследования может выглядеть следующим образом:
Фреймворк - это логическое упорядочивание кода, которое реализует
определенную идею, а также набор вспомогательных средств для удобной
разработки программных продуктов. Но главную роль для фреймворка, в том
числе того, что он может позволить для разработчика, является язык
программирования, на котором он написан.
Phoenix Framework написан на языке elixir. В сущности своей язык
является “синтаксическим сахаром” для другого известного ЯП erlang. Но
самое главное в нем то, что он написан рубистом (Jose Valim) и смотря
на код elixir нельзя не заметить насколько велико влияние ruby на
elixir. Но некоторое сходство можно найти только в синтаксисе. Все
остальное - структуры данных, управляющие блоки - все здесь по другому.
Основное, что нужно помнить про elixir - это функциональный язык
программирования! Это значит никаких объектов! Но, как оказалось, и
без них можно обойтись.
Структура фреймворка
Phoenix является MVC фреймворком. Структурирование очень напонимает
рельсы:
web/controllers - здесь находятся контроллеры;
web/models - здесь находятся модели;
web/views - здесь находятся вьюхи.
Но если вы зайдете в web/views, то не найдете там ни одного темплейта.
Здесь начинается интересная особенность phoenix.
Для шаблонов имеется другое место в фреймворке - web/templates. А что
же тогда views? Оказывается views - это не то, что мы привыкли видеть
в рельсах. В Phoenix Framework views выполняют следующие функции (по
крайней мере то, что мне удалось выяснить на данный момент):
рендеринг темплейтов. В рельсах рендерингом теплейтов занимается
контроллер. Здесь данная функциональность вынесена в отдельный класс. И,
на мой взгляд, это очень интересное решение, т.к. в рельсах нельзя было,
кроме контроллера, отрендерить темплейт;
содержит набор вспомогательных функций, которые можно использовать в
темплейтах. В рельсах для этого создавались так называемые helpers.
Доступ к данным
Есть несколько вариантов того, где хранить данные:
использовать БД mnesia, которая написанная на erlang и является
родной для elixir также;
использовать аналог ActiveRecord, который называется Ecto. На данный
момент есть адаптеры для postgresql и mysql. ActiveRecord эта
библиотека, конечно, мало напоминает, но она сразу идет из коробки и
является, таким себе, стандартным решением.
Ecto поддерживает практически весь необходимый функционал по работе с
данным, которым мы привыкли пользоваться в рельсах:
создание БД с помощью специального mix таска;
создание моделей;
генерирование миграций;
ассоциации;
валидации;
CRUD операции и т.д.
Ассеты
Из коробки компиляция ассетов происходит с помощью nodejs пакета
brunch. Т.е. если вы хотите писать ассеты на coffeescript и sass, то
нужно также ставить nodejs. При деплое на продакшн необходимо
принудительно выполнять специальную команду для прекомпиляции. На первый
взгляд все понятно и прозрачно, еще не успел наспотыкаться)
На момент написания статьи вышла новая версия Phoenix Framework 0.12.0. В ней появился новый mix таск для прекомпиляции ассетов, наподобие, `rake assets:precompile` в rails.
Разработка
В процессе разработке я не заметил каких-то значимых особенностей. Очень
похожая инфраструктура с рельсами:
есть свой пакетный менеджер;
большое количество библиотек, плагинов;
есть свой аналог rake тасков - mix;
генерирование приложения с помощью mix таска;
различные scaffold генераторы;
есть свой аналог Gemfile’а - mix.exs.
В общем с точки зрения рельсовика все довольно похоже. Команды только
называются по другому, но семантически они выполняют теже действия.
Деплой
Очень важным шагом является деплой своего приложения на продакшн.
Инфраструктура rails немного разбаловала нас, что деплой можно делать
всего одной командой. Хотя начальная настройка бывает затяжной. А также
разгребание после очередного деплоя тоже :-) Но все же все понятно и
очевидно. В официальной документации нет целостной картины, что из себя
представляет деплой phoenix приложения и как его готовить.
Я столкнулся с множеством интересных моментов, когда пытался залить
приложение на сервер, но все же через несколько часов попыток все
заработало. На самом деле проблемы связаны даже не с самим фреймворком,
а с незнанием, что делать с языком и его окружением - erlang и его
виртуальной машиной. Для людей знающих все это труда не составит, но вот
программисту-рельсовику придется голову поломать.
В своей следующей статье я более подробно остановлюсь на
этом вопросе.
Заключение
Я не пытался сделать подробное описание всех особенностей или сравнение
с рельсами. Это просто вольные мысли насчет первого опыта создания
приложения с использованием Phoenix Framework. Учитывая все факторы, а
также, что еще идет активная разработка, то я считаю этот опыт
положительным и вселяющим надежду, что Phoenix Framework станет отличной
альтернативой Ruby on Rails. Будем с охотой наблюдать за развитием!
Несколько дней назад вышел апдейт известного гема для менеджмента
зависимостей bundler. Вот пруф линк.
Я сразу же поспешил обновить его, т.к. у меня, как оказалось, стояла еще
версия 1.7.7. Но оказалось не все так радостно.
После апдейта перестали запускаться rails приложения. Вывод после выполнения команды rails s стал следующим:
12345678910111213141516171819202122
/Users/sergio/.rvm/gems/ruby-2.2.0/gems/activesupport-4.0.4/lib/active_support/values/time_zone.rb:283: warning: circular argument reference - now
/Users/sergio/.rvm/gems/ruby-2.2.0/gems/newrelic_rpm-3.8.1.221/lib/new_relic/agent/configuration/default_source.rb:674: warning: duplicated key at line 985 ignored: :disable_mongo
bin/rails:6: warning: already initialized constant APP_PATH
/Users/sergio/Work/railsapps/tiporbet/bin/rails:6: warning: previous definition of APP_PATH was here
Usage: rails COMMAND [ARGS]
The most common rails commands are:
generate Generate new code (short-cut alias: "g")
console Start the Rails console (short-cut alias: "c")
server Start the Rails server (short-cut alias: "s")
dbconsole Start a console for the database specified in config/database.yml
(short-cut alias: "db")
new Create a new Rails application. "rails new my_app" creates a
new application called MyApp in "./my_app"
In addition to those, there are:
application Generate the Rails application code
destroy Undo code generated with "generate" (short-cut alias: "d")
plugin new Generates skeleton for developing a Rails plugin
runner Run a piece of code in the application environment (short-cut alias: "r")
All commands can be run with -h (or --help) for more information.
Я подумал, что проблема в spring и перезапустил его командой spring
stop, но ничего не изменилось.
sergio@sergios-MacBook-Pro ~/Work/railsapps/tiporbet (master●)$ bundle config --delete bin [ruby-2.2.0]
sergio@sergios-MacBook-Pro ~/Work/railsapps/tiporbet (master●)$ rake rails:update:bin [ruby-2.2.0]
/Users/sergio/.rvm/gems/ruby-2.2.0/gems/activesupport-4.0.4/lib/active_support/values/time_zone.rb:283: warning: circular argument reference - now
/Users/sergio/.rvm/gems/ruby-2.2.0/gems/newrelic_rpm-3.8.1.221/lib/new_relic/agent/configuration/default_source.rb:674: warning: duplicated key at line 985 ignored: :disable_mongo
rake aborted!
LoadError: Please require this file from within a Capistrano recipe
/Users/sergio/.rvm/gems/ruby-2.2.0/gems/capistrano-2.15.4/lib/capistrano/configuration/loading.rb:18:in `instance'
/Users/sergio/.rvm/gems/ruby-2.2.0/gems/rvm-capistrano-1.5.1/lib/rvm/capistrano/helpers/base.rb:16:in `rvm_with_capistrano'
/Users/sergio/.rvm/gems/ruby-2.2.0/gems/rvm-capistrano-1.5.1/lib/rvm/capistrano/helpers/_cset.rb:3:in `<top (required)>'
/Users/sergio/.rvm/gems/ruby-2.2.0/gems/activesupport-4.0.4/lib/active_support/dependencies.rb:229:in `require'
/Users/sergio/.rvm/gems/ruby-2.2.0/gems/activesupport-4.0.4/lib/active_support/dependencies.rb:229:in `block in require'
/Users/sergio/.rvm/gems/ruby-2.2.0/gems/activesupport-4.0.4/lib/active_support/dependencies.rb:214:in `load_dependency'
/Users/sergio/.rvm/gems/ruby-2.2.0/gems/activesupport-4.0.4/lib/active_support/dependencies.rb:229:in `require'
/Users/sergio/.rvm/gems/ruby-2.2.0/gems/rvm-capistrano-1.5.1/lib/rvm/capistrano/base.rb:1:in `<top (required)>'
/Users/sergio/.rvm/gems/ruby-2.2.0/gems/activesupport-4.0.4/lib/active_support/dependencies.rb:229:in `require'
/Users/sergio/.rvm/gems/ruby-2.2.0/gems/activesupport-4.0.4/lib/active_support/dependencies.rb:229:in `block in require'
/Users/sergio/.rvm/gems/ruby-2.2.0/gems/activesupport-4.0.4/lib/active_support/dependencies.rb:214:in `load_dependency'
/Users/sergio/.rvm/gems/ruby-2.2.0/gems/activesupport-4.0.4/lib/active_support/dependencies.rb:229:in `require'
/Users/sergio/.rvm/gems/ruby-2.2.0/gems/rvm-capistrano-1.5.1/lib/rvm/capistrano/selector.rb:1:in `<top (required)>'
/Users/sergio/.rvm/gems/ruby-2.2.0/gems/activesupport-4.0.4/lib/active_support/dependencies.rb:229:in `require'
/Users/sergio/.rvm/gems/ruby-2.2.0/gems/activesupport-4.0.4/lib/active_support/dependencies.rb:229:in `block in require'
/Users/sergio/.rvm/gems/ruby-2.2.0/gems/activesupport-4.0.4/lib/active_support/dependencies.rb:214:in `load_dependency'
/Users/sergio/.rvm/gems/ruby-2.2.0/gems/activesupport-4.0.4/lib/active_support/dependencies.rb:229:in `require'
/Users/sergio/.rvm/gems/ruby-2.2.0/gems/rvm-capistrano-1.5.1/lib/rvm/capistrano.rb:3:in `<top (required)>'
/Users/sergio/.rvm/gems/ruby-2.2.0@global/gems/bundler-1.9.1/lib/bundler/runtime.rb:85:in `require'
/Users/sergio/.rvm/gems/ruby-2.2.0@global/gems/bundler-1.9.1/lib/bundler/runtime.rb:85:in `rescue in block in require'
/Users/sergio/.rvm/gems/ruby-2.2.0@global/gems/bundler-1.9.1/lib/bundler/runtime.rb:68:in `block in require'
/Users/sergio/.rvm/gems/ruby-2.2.0@global/gems/bundler-1.9.1/lib/bundler/runtime.rb:61:in `each'
/Users/sergio/.rvm/gems/ruby-2.2.0@global/gems/bundler-1.9.1/lib/bundler/runtime.rb:61:in `require'
/Users/sergio/.rvm/gems/ruby-2.2.0@global/gems/bundler-1.9.1/lib/bundler.rb:134:in `require'
/Users/sergio/Work/railsapps/tiporbet/config/application.rb:6:in `<top (required)>'
/Users/sergio/Work/railsapps/tiporbet/Rakefile:4:in `require'
/Users/sergio/Work/railsapps/tiporbet/Rakefile:4:in `<top (required)>'
/Users/sergio/.rvm/gems/ruby-2.2.0/bin/ruby_executable_hooks:15:in `eval'
/Users/sergio/.rvm/gems/ruby-2.2.0/bin/ruby_executable_hooks:15:in `<main>'
LoadError: cannot load such file -- rvm-capistrano
/Users/sergio/.rvm/gems/ruby-2.2.0@global/gems/bundler-1.9.1/lib/bundler/runtime.rb:76:in `require'
/Users/sergio/.rvm/gems/ruby-2.2.0@global/gems/bundler-1.9.1/lib/bundler/runtime.rb:76:in `block (2 levels) in require'
/Users/sergio/.rvm/gems/ruby-2.2.0@global/gems/bundler-1.9.1/lib/bundler/runtime.rb:72:in `each'
/Users/sergio/.rvm/gems/ruby-2.2.0@global/gems/bundler-1.9.1/lib/bundler/runtime.rb:72:in `block in require'
/Users/sergio/.rvm/gems/ruby-2.2.0@global/gems/bundler-1.9.1/lib/bundler/runtime.rb:61:in `each'
/Users/sergio/.rvm/gems/ruby-2.2.0@global/gems/bundler-1.9.1/lib/bundler/runtime.rb:61:in `require'
/Users/sergio/.rvm/gems/ruby-2.2.0@global/gems/bundler-1.9.1/lib/bundler.rb:134:in `require'
/Users/sergio/Work/railsapps/tiporbet/config/application.rb:6:in `<top (required)>'
/Users/sergio/Work/railsapps/tiporbet/Rakefile:4:in `require'
/Users/sergio/Work/railsapps/tiporbet/Rakefile:4:in `<top (required)>'
/Users/sergio/.rvm/gems/ruby-2.2.0/bin/ruby_executable_hooks:15:in `eval'
/Users/sergio/.rvm/gems/ruby-2.2.0/bin/ruby_executable_hooks:15:in `<main>'
(See full trace by running task with --trace)
Как оказалось - это проблема с гемом rvm-capistrano и началась она еще
с апдейта bundler 1.8.0, но так как у меня стояла более поздняя версия,
то все было нормально. Решением этой проблемы стало указанием require:
false в Gemfile:
1
gem 'rvm-capistrano', '1.5.1', require: false
Затем я нормально смог перегенерить binstubs:
123456789101112
sergio@sergios-MacBook-Pro ~/Work/railsapps/tiporbet (master●)$ rake rails:update:bin [ruby-2.2.0]
/Users/sergio/.rvm/gems/ruby-2.2.0/gems/activesupport-4.0.4/lib/active_support/values/time_zone.rb:283: warning: circular argument reference - now
/Users/sergio/.rvm/gems/ruby-2.2.0/gems/newrelic_rpm-3.8.1.221/lib/new_relic/agent/configuration/default_source.rb:674: warning: duplicated key at line 985 ignored: :disable_mongo
WARNING: Nokogiri was built against LibXML version 2.9.0, but has dynamically loaded 2.8.0
exist bin
identical bin/bundle
conflict bin/rails
Overwrite /Users/sergio/Work/railsapps/tiporbet/bin/rails? (enter "h" for help) [Ynaqdh] Y
force bin/rails
conflict bin/rake
Overwrite /Users/sergio/Work/railsapps/tiporbet/bin/rake? (enter "h" for help) [Ynaqdh] Y
force bin/rake
Во многих языках программирования можно встретить, так называемые,
перечисления или enum, которые представляют из себя набор
именованых констант. Например, в языке C# их можно объявить следующим образом:
land:csharp Листинг 1 - Enum в C#
123
enum Days {Mon, Tue, Wed, Thu, Fri, Sat, Sun};
day = Days.Mon;
В ruby нет встроенного типа enum, но есть несколько приемов как
такое можно реализовать из того, что есть под рукой :-)
Вариант 1 - Быстрый и понятный
Все что нам нужно - модуль и константы. Код будет следующим:
Листинг 2 - Ruby Enum 1
1234567
moduleDaysMON=1TUE=2# и так далееendputsDays::MON# => 1
Я думаю, что здесь особо ничего не нужно комментировать.
Вариант 2
Вот такой интересный вариант я нагуглил:
Листинг 3 - Ruby Enum 2
123
Days=[Mon=1,Tue=2,...]putsMon# => 1
Но данный подход, как по мне, имеет несколько недостатков:
Теряется namespace, для этого нужно код выше поместить еще в модуль
Необходимо вручную прописывать значения для каждого элемента
перечисления
Не очень удобно (но иногда все таки необходимо) использовать числа в
качестве значений
Вариант 3
Необходимо будет написать дополнительно пару строк кода и… вуаля!
Такой вариант очень удобен и читабелен, особенно если вы хотите
устанавливать определенные значения для какой-либо записи в БД. Ведь
сразу понятнее такая информация event.day_of_week # => 'MON', чем
event.day_of_week # => 1.
Заключение
На самом деле можно нагуглить очень много вариантов реализации и вы
можете выбрать именно тот, который вам по душе и лучше всего подходит к
решению конкретной задачи.
P.S. Вот, на мой взгляд, одна из самых лучших реализаций ruby enum
В основе статьи лежит ситуация, с которой я и мои коллеги столкнулись
при работе над одним проектом.
Представим себе следующее. У меня есть модель Item:
Листинг 0 - Модель Item app/models/item.rb
12
classItem<ActiveRecord::Baseend
Затем нам дали задачу на новую фичу: пользователи могут создавать списки
и добавлять туда элементы. Мы сразу видим модель List:
Листинг 1 - Модель List app/models/list.rb
12
classList<ActiveRecord::Baseend
Но для хранения связи между элементом и листом мы решили использовать модель
не ListItem, а положить ее в namespace List, т.е. логически
выделить эту часть подсистемы, т.к. мы предполагаем, что будут еще связанные модели.
И получается следующее:
Листинг 2 - Модель List::Item app/models/list/item.rb
1234
moduleListclassItem<ActiveRecord::Baseendend
Как бы все идет хорошо до тех пор, пока вы не захотите использовать
List::Item модель где-нибудь в коде приложения:
Листинг 3 - Использование List::Item в rails console
1234567891011121314151617
Loading development environment (Rails 4.0.4)2.1.2 :001 > List::Item.new
TypeError: List is not a module
from /Users/sergio/Work/railsapps/namespace_collision_demo/app/models/list/item.rb:1:in `<top (required)>' from /Users/sergio/.rvm/gems/ruby-2.1.2/gems/activesupport-4.0.4/lib/active_support/dependencies.rb:424:in `load' from /Users/sergio/.rvm/gems/ruby-2.1.2/gems/activesupport-4.0.4/lib/active_support/dependencies.rb:424:in `block in load_file' from /Users/sergio/.rvm/gems/ruby-2.1.2/gems/activesupport-4.0.4/lib/active_support/dependencies.rb:616:in `new_constants_in' from /Users/sergio/.rvm/gems/ruby-2.1.2/gems/activesupport-4.0.4/lib/active_support/dependencies.rb:423:in `load_file' from /Users/sergio/.rvm/gems/ruby-2.1.2/gems/activesupport-4.0.4/lib/active_support/dependencies.rb:324:in `require_or_load' from /Users/sergio/.rvm/gems/ruby-2.1.2/gems/activesupport-4.0.4/lib/active_support/dependencies.rb:463:in `load_missing_constant' from /Users/sergio/.rvm/gems/ruby-2.1.2/gems/activesupport-4.0.4/lib/active_support/dependencies.rb:184:in `const_missing' from (irb):1
from /Users/sergio/.rvm/gems/ruby-2.1.2/gems/railties-4.0.4/lib/rails/commands/console.rb:90:in `start' from /Users/sergio/.rvm/gems/ruby-2.1.2/gems/railties-4.0.4/lib/rails/commands/console.rb:9:in `start' from /Users/sergio/.rvm/gems/ruby-2.1.2/gems/railties-4.0.4/lib/rails/commands.rb:62:in `<top (required)>' from bin/rails:4:in `require' from bin/rails:4:in `<main>'
Есть несколько путей решения этой проблемы.
Решение 0 - Внести название модуля в название класса
Я до конца не понимаю почему, но вот если изменить код модели
List::Item на следующий:
Листинг 4 - Решение 0 для модели List::Item app/models/list/item.rb
Решение 2 - К черту всю эту возню с объектной моделью!
В данном случае предлагается поступить следующим образом: выделить
namespace и ложить туда все модели, которые логически связаны. В
соответствии с нашим примером будет следующее:
Листинг 9 - Модель Lists::List app/models/lists/list.rb
1234
moduleListsclassList<ActiveRecord::Baseendend
Листинг 10 - Модель Lists::Item app/models/lists/item.rb
1234
moduleListsclassItem<ActiveRecord::Baseendend
Однозначным плюсом данного подхода является то, что все модели лежат в
одной папке и если вам нужно выпилить данный кусок функционала, то вот
оно все в папке лежит - не нужно шарится среди несколько десятков
моделей в папке app/models.
Вот и наступил 2015 год. Подводя итоги прошлого года можно отметить две
мэйнстримовые тенденции:
Статическая типизация
Функциональное программирование
Сегодня пойдет речь о втором пункте.
Год перемен
2014 год однозначно был функциональным. Вторую жизнь получили многие
забытые функциональные языки, много новых появилось. Их начинали пихать
куда можно и куда нельзя, где уместно их использовать, а где нет. Меня
тоже немного задела эта волна. Совершенно случайно необходимо было
написать слой приложения на любом языке кроме ruby. Задача выбора языка
сейчас очень сложна из-за их огромного выбора. Но не на этот раз! Все
больше и больше информации появлялось о новом молодом языке elixir.
Он привлекал еще больше внимания, т.к. его создатель - Rails Core Team
Member (Jose Valim)!. И как раз вышла стабильная версия 1.0.0! И он как
раз еще и функциональный язык! Бинго! Как раз есть возможность
окунуться в мир ФП на реальном примере! Работка обещала быть интересной
:-)
Другой мир
На первый взгляд все казалось намного проще. Но в самом начале был
тупик… ООПшный тупик… Нет классов и, соответственно, нет
наследования и всех плюшек ООП. Есть только функции и их можно
объединять в модули и все. Совсем другой мир! Но не менее интересный) Но
я не буду полностью все описывать, а остановлюсь на нескольких, как мне
кажется, особо интересных особенностях.
Трансформация данных
Для тех, кто хочет познакомится с elixir и функциональным
программированием, обязательно стоит посмотреть доклад Дейва Томаса
(Dave Thomas) Elixir: Power of Erlang, Joy of Ruby.
Основная идея, которую я подчерпнул из данного доклада - трансформация данных.
Т.е., рассматривая какую-либо часть архитектуры как черный ящик, то на
вход подается данные_1, а на выходе мы ожидаем данные_2. Я думаю,
что это применимо и к ОПП подходу, но в ФП это более выражено и есть
даже специальный оператор!
Pipe-оператор
В elixir есть специальный оператор |>, который называется
pipe-оператор, который выполняет операции над данными слева. Например:
Листинг 1 - Пример использования pipe-оператора
12
str=" sergey "str|>String.strip# => "sergey"
Причем количество операций преобразования может быть несколько
Листинг 2 - Пример комбинирования операций преобразования через pipe-оператор
Это очень напоминает оператор |, используемый в bash.
А что в ruby?
Ruby полностью объектно-ориентированный язык, поэтому в основе лежат
объекты и связь между объектами с помощью сообщений. Но сколько же раз
вы видели подобный код:
Меня всегда он вводит в конфуз, т.к. с первого взгляда сложно
понять, где входные данные, а где список операций обработки.
Давайте попробуем реализовать подобное в ruby. Мы будем использовать здесь
monckeypatching (начиная с ruby 2.0 необходимо использовать
refinements)!
Листинг 4 - Monckeypatching
123456789101112
classStringdefstrip_html# some codeenddefnormalize_name# some codeendenddeffetch_name(html)html.strip_html.normalize_nameend
Вот чем вам не код, как в листинге 2? В отличии от листинга 3, здесь код
намного понятен с первого взгляда, т.к. сразу можно понять: слева
объект, над которым производятся трансформации, а далее идут операции.
Стоит отметить, что в данном примере все операции производятся над типом String.
Если в цепочке обработки используются несколько типов данных, то необходимо патчить
все соответствующие классы.
А как же коллекции?
Pipe-оператор также отлично справляется и с коллекциями. Вот, например,
код, который увеличивает каждый элемент в массиве на 1:
Я очень люблю ruby! Каждый день для себя открываю все больше и больше
возможностей и не перестаю восхищатся его мощью и выразительностью. Но
всегда не покидает чувство, что чего-то не хватает и ты находишь это в
другом языке и пытаешься реализовать в ruby. Кто знает, возможно лет
через N это запилят и в ruby :-)
Приходит время, когда просто сидеть на работе и как робот выполнять
таски по трекеру становится скучно и не интересно. И в этот момент
просыпается необходимость разнообразить свою профессиональную жизнь.
Самое первое, что можно придумать - это вести блог. Так что прошу не
сильно закидывать шишками, т.к. это мой первый подобный пост.
Lets go!
Задача
Сколько людей - столько и мнений. Наверное сейчас очень сложно придумать
такую задачу, чтобы она была уникальной. Поэтому появилось устоявшееся
выражение - писать велосипед. Вот таким велосипедом я сейчас и займусь
(я даже специально не гуглил на эту тему, чтобы заранее себя не
расстраивать). А проблема вот какая:
Последнее время очень часто возникает необходимость сливать базу
данных с продакшена и накатывать ее на девелопменте, т.к. править
различные штуки удобнее локально. Для этого приходится делать
достаточно много шагов: зайти по ssh и сделать дамп базы, зайти через
Filezilla и скачать этот дамп, затем накатить его на dev базу. И тут я
задался вопросом ведь яжпрограммист или кто?!
Поэтому я решил написать на ruby утилиту, которая поможет мне
автоматизировать этот процесс.
Я понимаю, что это велосипед, но ведь он свой :-)
Цель
С самого начала я взял листок бумаги и накидал кусок кода, который,
возможно, должен получится в конце. Можно сказать своеобразный DSL,
при помощи которого я смог бы описывать шаги, которые я делал вручную.
Получилось что-то такое:
Сразу хочу отметить тот факт, что конечный результат может и даже,
скорее всего, будет отличаться от кода выше.
Будем пробовать TDD
Есть многие вещи, которые ты не поймешь зачем они нужны, пока ты сам до
этого не дойдешь, даже если какой-нибудь гуру будет тебе говорить, что это классно.
Такая история и с TDD. Сама идея писать код для проверки кода звучит
дико, не правда ли? Но не нужно смотреть на TDD только через призму,
того что нужно писать дополнительный код, ведь программисты и так
ленивые. Это целая методология со своими условными ‘правилами’ и ее
главная цель уменьшить вашу головную боль. Но не буду заострять внимание
на этом. Просто будте готовы писать тесты!
Ну-с начнем
Самый сложный момент. С чего начать, когда есть только идея? Существуют
два варианта движения:
Inside-out
Outside-in
Выбор зависит индивидуально от программиста (смотря как у кого работает
голова), а также от того, какую информацию содержит ваша первоначальная
идея (листинг 1).
Давайте еще раз посмотрим, что у нас есть - это пример АПИ, которое
должна предоставлять библиотека или, другими словами, ее outside
часть. Для нас очевиден путь (по крайней мере я выбрал этот вариант и
возможно к концу написания библиотеки пойму, что он был неудобный)
outside-in.
Первые шаги
С самого начала хочу определить структуру каталога, чтобы была
возможность ссылаться на нее, а также вам для наглядности:
123
<project_root>
|- lib
|- test
Я думаю тут вопросов не должно возникнуть)
В первую очередь давайте настроем наше test environment. Надеюсь вы
заметили, что я ни слова не сказал насчет какого-нибудь фреймворка? Да,
вы правильно поняли - это будет проект на чистом ruby. Поэтому любые
настройки придется делать вручную. Но это даже к лучшему! Так вот, если
взять rails, то там уже из коробки идет настроенный unit testing, но я
всегда сразу устанавливал Rspec, и файл с настройками назывался
test_helper или spec_helper (для rspec). Давайте и у нас
создадим такой файл, в котором будут содержаться общие настройки для
всех наших тестов:
Подключили фреймворк для unit тестирования - Minitest, который идет в
ruby stdlib
Загрузили все файлы из папки lib
И вот теперь, наконец-то, можно приступать к написанию кода! А если
точнее - теста. Вот тут начинается еще одна интересная вещь - а что
тестировать, если ничего нет?
Небольшое отступление. Сейчас мне в голову пришла мысль и я хочу немного
изменить и дополнить нашу первоначальную идею:
И теперь стало немного понятнее с чего нам начать. Как я уже говорил
выше я выбрал путь outside-in, т.е. мы будем двигаться в сторону
уточнения внутренней реализации. Поэтому начнем с теста внешнего АПИ.
Да, я не опечатался - начнем с ТЕСТА)
Листинг 4 - Первая проба написания теста test/lib/db_fetcher_test.rb
Мы заинклудили наш test_helper, поэтому в данном контексте доступен
Minitest
Написали тест, который проверяет правильно работы метода
define_runner (здесь лучше сказать, что мы проверяем не
правильность, а то, какой результат ожидаем после вызова данного
метода). Все, что мы можем сейчас проверить - метод define_runner
возвращает новый экземпляр класса DbFetcher::Runner.
А давайте запустим наш тест и посмотрим результат? (подсказка: для
запуска теста набирайте ruby test/lib/db_fetcher_test.rb):
Отлично! 1 assertions значит, что наш тест прошел! Фууух.
Итерационность
Главное слово в этом процессе - итерационность. После того, как вы
добились чтобы тест прошел, можете писать тест дальше. Затем вы опять
пишете код, удовлетворяющий новые тесты, но и не сломал предыдущие! И
этот процесс повторяется снова и снова. Самой главной величиной этого
процесса является размер вот этого шага или количество логики,
заложенной в тесте, после написания которого вы приступаете к написанию
кода. Здесь нет никаких пожеланий и вступает в силу индивидуальность
разработчика. Каждый выбирает этот шаг так, чтобы было комфортно: если
напишите много тестов - зациклитесь на написании кода, а также есть
вероятность сделать тесты неактуальными, т.к. при их написании не учли
каких-либо архитектурных моментов; если напишите мало - то придется
слишком часто переключать ваш контекст между кодом и тестами, что тоже
не гуд, т.к. опять же можно что-то упустить.
Продолжим?
Спешу вас разочаровать(а может и обрадовать) - я не буду полностью
описывать весь процесс разработки данной библиотеки, т.к. это будет
слишком много неинтересного текста и чередующихся листингов
тесты-код-тесты-код-тесты… и т.д. Поэтому я предлагаю мне дописать
библиотеку и затем я выложу исходники на github, и затем кину сюда
ссылку, хорошо?) Ну и отлично!
Спасибо за внимание!
UPDATE. Как и обещал, выкладываю ссылку на репозитарий
db_fetcher. Библиотека
еще будет дорабатываться, но базовые концепции уже реализованы и
протестированы.