Slide 1

Slide 1 text

Hanami - нова надія Ruby чи "імперія ходить по тим самим граблям"? Євген Кузьмінов Ruby Team Lead

Slide 2

Slide 2 text

Хто я ● 10 років у веб розробці ● 6 роки у PHP ● 4 роки у Ruby ● з них 3 роки по коліно у Trailblazer ● люблю Elixir ● не люблю RoR і Blockchain ● останні півроку моя команда пише великий проект на Hanami...

Slide 3

Slide 3 text

Hanami, ти хто такий? Давай, до допобачення! ● все що ви хочете бачити від веб-фреймворка ● роутінг ● контроллери ● релоад коду ● робота з БД ● міграції ● генерація коду ● консоль / дебаг ● компіляція ассетів

Slide 4

Slide 4 text

Воу, навіщо, якщо є Рельси?! а ще в “Рельсах” є: ● гострі ножі / “sharp knives” © DHH ● товсті контролери ● товсті моделі ● товсті вьюшки ● strong parameters ● бізнес логіка тонким шаром по всьому цоьму ● рельс-деформація: вписати все у сутність АР, якщо не лізе - це проблеми бізнес логіки ● вихід за рамки Rails-way прирівнюється до самогубства проекту

Slide 5

Slide 5 text

Що пропонує Hanami? ● контейнерну архітектуру (моноліт, що легко розділити) ● структуру для специфічного і загальновживаного коду ● окремий клас на кожен екшн (гнучка настройка на кожен) ● інтерактори aka service objects ● Entity - Repository замість ActiveRecord ● middleware орієнтованість, навіть екшн - middleware ● dry-gems ● свобода від ActiveSupport

Slide 6

Slide 6 text

Базова структура Hanami застосунка, контейнери

Slide 7

Slide 7 text

Rails: ActiveRecord class Task < ApplicationRecord scope :high_priority, ->(limit = 3) { where('priority <= 1').limit(limit) } end Task.create {title: 'homework', priority: 1} Task.high_priority

Slide 8

Slide 8 text

Hanami: Entity - Repository class Task < Hanami::Entity end class TaskRepository < Hanami::Repository def high_priority(limit: 3) tasks.where('priority <= 1').limit(limit) end end task = Task.new {title: 'homework', priority: 1} # task = {title: 'homework', priority: 1} TaskRepository.new.create(task) TaskRepository.new.high_priority

Slide 9

Slide 9 text

Увага! "Батарейки до комплекту не входять!" ● немає “Hanami” адаптованих ліб, але завжди працюють просто “rack middleware” орієнтовані ● автентифікація - немає Device-подібного рішення, потребує кастомне ● кеш - кастомна реалізація з Redis, readthis + hiredis gems ● Sidekiq - працює, але треба трошки загуглити ● Sidekiq::Web - працює тільки як окремий процесс (Docker рятує) ● RSpec/Minitest є з короби, а от Capybara (Hanami <=1.2) / DBCleaner / WebDriver / WebMock доведеться дороблювати методом проб та помилок

Slide 10

Slide 10 text

Увага! "Батарейки до комплекту не входять!" ● factory_bot - тягне ActiveSupport, на щастя є Fabricator ● pagination - через плагін ROM.rb ● soft delete - через monkey patching Hanami::Repository ● enum у моделях - через ruby-enum ● WebPack(er) - через WebPack и свій хелпер ● бізнес логіка - через ваш улюблений сервіс об’єкт ● Trailblazer йде окремо, але працює без “танців з бубном” ● структура файлів і папок - майже яку забажаєте!

Slide 11

Slide 11 text

Книга рецептів (дуже рання стадія) http://hanami-cookbook.stdout.in

Slide 12

Slide 12 text

No content

Slide 13

Slide 13 text

Гарна сторона Hanami Не така вже й гарна сторона Hanami

Slide 14

Slide 14 text

Один класс на один екшн - сумнівно ... context 'with valid params' do it 'redirects the user to the books listing' do response = action.call(params) expect(response[0]).to eq(302) expect(response[1]['Location']).to eq('/books') end end context 'with invalid params' do it 're-renders the books#new view' do response = action.call(params) expect(response[0]).to eq(422) end ...

Slide 15

Slide 15 text

Interactor-и не тягнуть на гарний service object require 'hanami/interactor' class AddBook include Hanami::Interactor expose :book def initialize(repository: BookRepository.new) @repository = repository end def call(book_attributes) @book = @repository.create(book_attributes) end end

Slide 16

Slide 16 text

Trailblazer + dry-validation без макросів Багато коду без готовых макросів, а документації на макроси немає :( def validate(ctx, params:, **) sch = Dry::Validation.Form(PostSchema) do required(:type).filled(included_in?: ::Post::Types.values) optional(:body).value(:length_permitted?) optional(:tags).maybe end ctx[:validation_result] = sch.with(type: params[:embed_type]).call(params) ctx[:params] = ctx[:validation_result].to_h handle_validation_errors(ctx) end

Slide 17

Slide 17 text

Немає кастомних CLI команд Напевно, можна робити rake таски, але ми використали gem ‘hanami-cli’ Мінус - різні типи викликів: $ hanami db create $ ./hanami-cli db seed

Slide 18

Slide 18 text

No content

Slide 19

Slide 19 text

Hanami::Repository < ROM.rb ● загальне враження - не розумієш як воно працює ● постійно перебираєш варіанти ● не підтримується більше одного БД коннекта ● вкладені Entity не працюють, все через Hash ● не працює кастомний мапінг, завжди “мапить” до однойменного з репозиторієм Entity ● як результат - у нашому плані “перейти на Sequel + dry-struct mapping”

Slide 20

Slide 20 text

Hanami::Repository < ROM.rb по різному працює PostRepo.update(1, embed_data: { ... }) та class PostRepo < Repo def some_update() posts.where(uuid: 1).update(embed_data: { ... }) end end

Slide 21

Slide 21 text

Hanami::Repository < ROM.rb Raw SQL через одну “дивну” функцію class UserRepository < Hanami::Repository ... def find_special(params) users.read("SELECT * FROM users INNER JOIN...

Slide 22

Slide 22 text

Hanami::Repository < ROM.rb def suggest_users_by_query(query, user, count=5) sql = <<-SQL (firstname ILIKE :query OR lastname ILIKE :query OR nickname ILIKE :query) AND id != :requester_id SQL users.where( Sequel.lit(sql, query: "#{query}%", requester_id: user.id) ).limit(count).as(User) end

Slide 23

Slide 23 text

Трохи екзотики jRuby + Celluloid + Hanami > GEMFILE ... gem 'celluloid' gem 'reel-rack' gem 'hanami-router' gem 'hanami-controller' gem 'hanami-validations' gem 'dry-system' gem 'sequel'

Slide 24

Slide 24 text

config.ru require 'rubygems' require 'bundler/setup' require 'celluloid' Bundler.require(:default) require_relative './system/boot.rb' require_relative './router.rb' Supervisors::Main.run! run WEB_ROUTER

Slide 25

Slide 25 text

router.rb Hanami::Controller.configure do handle_exceptions false end WEB_ROUTER = Hanami::Router.new do namespace 'api' do namespace 'meetup' do post '/start', to: ApiActions::Meetup::Start end end end

Slide 26

Slide 26 text

action module ApiActions::Meetup class Start include Hanami::Action params do required(:user_id).filled(:str?) end def call(params) render_errors(params) unless params.valid? self.body = “render smth.” end end end

Slide 27

Slide 27 text

Моє бачення ідеального фреймворка ● жоден поважний Рубі-захід не обійдеться без реклами Elixir та Phoenix! ● складається з незалежних бібліотек (вітання зі світу PHP!) ● повна прозорість ходу запиту від проксі, до роутінгу і екшна ● middleware на всіх рівнях, middleware pipelines на роутинг ● гнучкий data mapper ● базовий DDD структуру тек / файлів / модулів

Slide 28

Slide 28 text

Ruby / Hanami Elixir / Phoenix

Slide 29

Slide 29 text

“model” “model” “service” “context” “context” “service” “model” “model”

Slide 30

Slide 30 text

Підсумок ● з Hanami можна написати як великий, так і маленький застосунок ● доведеться “покопати” ● дуже мало готових рішень, але вони з’являються ● ви знову згадаєте як це - “програмувати” :) ● все можна підлаштувати під себе, а не гнутися під фреймворк ● добрий вибір для: ○ довгограючих проектів ○ API ○ мікро-сервісних проектів ○ застосунків без “людського” інтерфейсу

Slide 31

Slide 31 text

Hanami не ідеальний, але заслуговує на те, щоб ви дали йому шанс!

Slide 32

Slide 32 text

ДЯКУЮ ЗА УВАГУ! ПИТАННЯ? Євген Кузьмінов Ruby Team Lead http://stdout.in https://twitter.com/iJackUA