itechart_logo

Hanami – современный веб-фреймворк на Ruby

#DEVELOPMENTANDQA
19 октября 2017
hanami-3

Евгений
Гарлукович

Ruby-разработчик

Почему, задумываясь о долгосрочной поддерживаемости системы, Rails лучше заменить на Hanami? Отвечает Ruby Developer iTechArt.


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

Hanami – современный и достаточно молодой веб-фреймворк на Ruby. Хотя в апреле 2017 года вышла версия 1.0, у него уже большая армия поклонников и внушительное количество контрибьюторов на GitHub.

Почему же стоит обратить внимание на Hanami? На сайте фреймворка можно найти детальное описание преимуществ. Из них я бы выделил одно, на мой взгляд, самое главное – чистая архитектура.

По словам итальянского разработчика и автора Hanami Luca Guidi, основная проблема, которую призван решить фреймворк, – предоставить возможность долгосрочной поддерживаемости системы. И это действительно проблема для Rails-приложений. Так называемый Rails-way, которому учат нас официальные гайды, со временем перестает решать бизнес-задачи, а в определенный момент он может и вовсе стать помехой для дальнейшего развития проекта.

Что же предлагает Hanami? Поговорим о трех составляющих.


1. Разделение ответственности

Основная идея архитектуры Hanami – разделение ответственности между ядром приложения (application core) и механизмом доставки (delivery mechanism). Ядро хранит всю бизнес-логику, в которой реализованы сценарии использования (use cases) нашей системы. Причем бизнес-логика не зависит от того, каким образом она будет вызываться: из клиентской части приложения, из портала администратора или через API. Можно смело сказать, что это не просто ядро, а фактически сердце нашей системы.

Механизмами доставки в данной архитектуре являются интерфейсы, через которые use cases становятся доступными для пользователей приложения. Давайте посмотрим, как Hanami организует эти части приложения.

Ядро

После того как новое Hanami-приложение (назовем его bookshelf) было создано, помимо остальных директорий, в корне проекта появятся две наиболее интересующие нас поддиректории: lib и appsLib – для хранения бизнес-логики, apps – для реализации механизмов взаимодействия с внешним миром. 

Содержимое lib:

$ tree lib
lib
├── bookshelf
│   ├── entities
│   ├── mailers
│   │   └── templates
│   └── repositories
└── bookshelf.rb

5 directories, 1 file

Внутри этой директории только три сущности: entities, mailers и repositories. Назначение mailers понятно из названия – они служат для отправления электронных писем. А вот назначение entities и repositories может оказаться неочевидным для разработчиков, знакомых с Rails.

Entities и repositories являются двумя основными сущностями в реализации уровня моделей нашего приложения. Они есть в библиотеке Hanami::Model (основывается на другой библиотеке для работы с базой данных – ROM-RB).

Entities – это Ruby-объекты, которые служат для представления сущностей доменной области проекта и не привязаны к базе данных. Ответственность за взаимодействие с базой данных несут repositories – посредники между нею и сущностями доменной области. Такой подход отличается от привычного большинству Ruby-разработчиков паттерна Active Record, в котором сущности доменной области тесно связаны с базой данных и знают, как с ней взаимодействовать.

Подход, используемый в Hanami, имеет существенный плюс – за счет инкапсуляции работы с базой данных в repositories логика, использующая repositories и entities, никак не зависит от базы данных. В Rails мы наблюдаем обратную ситуацию: «уши» реляционных баз данных торчат как минимум на уровне контроллеров (вспомните вызовы метода includes для избегания проблемы N+1 запроса), а иногда доходят до уровня представления.

Помимо хранения доменных сущностей и логики взаимодействия с базами данных, директория lib предназначена для хранения остальной части ядра нашего приложения. Так, мы можем добавить подкаталог lib/bookshelf/interactors для реализации сценариев использования приложения. Для данных компонентов системы Hanami предоставляет свою реализацию паттерна Interactor – Hanami::Interactor.

Механизмы доставки 

Обратим внимание на директорию apps:

$ tree apps
apps
└── web
    ├── application.rb
    ├── assets
    │   ├── favicon.ico
    │   ├── images
    │   ├── javascripts
    │   └── stylesheets
    ├── config
    │   └── routes.rb
    ├── controllers
    ├── templates
    │   └── application.html.erb
    └── views
        └── application_layout.rb

9 directories, 5 files

По умолчанию Hanami создает приложение Web, которое размещается в директории apps/web. Для разработчиков, знакомых с Rails, такая структура может напомнить содержимое директории app в Rails-приложении. Однако Hanami добавляет существенное отличие: использует директорию apps вместо app, а это значит, apps в нашем веб-приложении может быть много. Содержимое apps и будет механизмом доставки, о котором говорилось выше: это может быть клиентское веб-приложение (apps/web), портал администратора (apps/admin) или API (apps/api). Hanami призывает нас разделять не только бизнес-логику и интерфейсы предоставления доступа к сценариям использования, но и сами интерфейсы.

Ниже – пример приложения, созданного для портала администратора (apps/admin) и имеющего точно такую же структуру, как и изначально создаваемое apps/web.

$ bundle exec hanami generate app admin

$ tree apps
apps
├── admin
│   ├── application.rb
│   ├── assets
│   │   ├── favicon.ico
│   │   ├── images
│   │   ├── javascripts
│   │   └── stylesheets
│   ├── config
│   │   └── routes.rb
│   ├── controllers
│   ├── templates
│   │   └── application.html.erb
│   └── views
│       └── application_layout.rb
└── web
    ├── application.rb
    ├── assets
    │   ├── favicon.ico
    │   ├── images
    │   ├── javascripts
    │   └── stylesheets
    ├── config
    │   └── routes.rb
    ├── controllers
    ├── templates
    │   └── application.html.erb
    └── views
        └── application_layout.rb

18 directories, 10 files

2. Организация контроллеров и уровня представления

Как было показано выше, Hanami уже на уровне структуры приложения придерживается принципа разделения ответственности: бизнес-логика отделена от механизмов доставки, а механизмы доставки отделены друг от друга.

Однако принципы разделения ответственности и чистой архитектуры реализованы и в самих компонентах приложения. Рассмотрим пример реализации контроллеров и уровня представления.

Контроллеры

В Rails мы привыкли, что существует один контроллер, который содержит все методы для работы с какой-нибудь моделью. Например, если у нас есть модель Book, для реализации с ней CRUD-действий мы бы создали один BooksController c семью методами: index,  show,   new,  create,  editupdate и  delete. Во что со временем могут превратиться такие контроллеры, опытные Rails-разработчики знают и без меня...

Hanami в организации контроллеров придерживается подхода «одно действие – один класс», т. е. в описанном выше случае с моделью Book вместо одного контроллера с семью методами-действиями мы получаем семь классов. И каждый из них реализует одно-единственное действие.

Например, так будет выглядеть класс, реализующий действие show:

# apps/web/controllers/books/show.rb

module Web::Controllers::Books
  class Show
    include Web::Action
    expose :book
    def call(params)
      repository = BookRepository.new
      @book = repository.find(params[:id])
    end
  end
end

Это обычный Ruby-класс, реализующий единственный метод call, который принимает параметры запроса. В методе call такие параметры обрабатываются, в данном случае с помощью репозитория BookRepository мы получаем сущность модели Book, соответствующую переданному в параметрах id. Далее найденная сущность модели Book записывается в переменную @book и с помощью конструкции expose :book передается на уровень представления.

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

Уровень представления

После того как мы нашли запрашиваемую сущность, ее необходимо отобразить. На уровне представления в Hanami используются два типа сущностей: views и templates.

Template – это шаблон в нужном формате (HTML, XML, JSON, обычный текст и т. д.). Например, HTML-шаблон для описанного ранее действия show может выглядеть таким образом: 

# apps/web/templates/books/show.html.erb
<div id='book'>
  <h2><%= book_title %></h2>
  <p><%= book_author %></p>
</div>

Если вы знакомы с Rails, ничего непривычного в этом шаблоне не найдете. Однако главное отличие заключается в еще одной абстракции, используемой на уровне представления, – views. 

Views – обычные Ruby-объекты, которые инкапсулируют логику представления, используемую в шаблонах. Вот так будет выглядеть объявление класса view-объекта для описанного выше шаблона:

# apps/web/views/books/show.rb
module Web::Views::Books
  class Show
    include Web::View

    def book_author
      book.author
    end

    def book_title
      book.title
    end
  end
end

Ничего магического тут нет: простой Ruby-объект, реализующий определенную логику.

На примере видно, каким образом можно получить представление сущности Book, используя описанную выше логику: 

template =
Hanami::View::Template.new('apps/web/templates/books/show.html.erb')

book = Book.new(title: 'Refactoring', author: 'Martin Fowler')
exposures = { book: book }

view = Web::Views::Books::Show.new(template, exposures)

view.render
#=> "<div id='book'>\n <h2>Refactoring</h2>\n <p>Martin Fowler</p>\n</div>\n"

Здесь мы просто передаем шаблон представления (переменная template) и необходимые параметры (exposures) в конструктор, где будет создан объект view, и вызываем у этого объекта метод #render. В итоге получаем описанное нами представление сущности модели Book. Именно такая процедура происходит в случае вызова Hanami-действий, когда строится ответ на определенный запрос с использованием views и templates.

На примере реализации этих уровней нашей системы – контроллеров и уровня представления – можно увидеть, что разделение ответственности в Hanami применяется не только в структуре всего приложения, но и в реализации конкретных его частей. Это способствует поддержанию чистоты их реализации.

Здесь я несколько раз использовал фразу «простой Ruby-объект» не только для того, чтобы показать простоту данного подхода. Использование простых Ruby-объектов облегчает процесс тестирования кода.


3. Тестируемость кода

Покрытие кода тестами – очень важный этап в реализации любого программного продукта. Со временем любая система, как бы хорошо она ни была реализована, нуждается в рефакторинге разной степени глубины. И покрытие тестами дает уверенность, что рефакторинг существующего кода не приведет к появлению ошибок в системе. Ну а  если в разработке вы применяете подходы TDD/BDD, то о важности тестов вам и вовсе рассказывать не стоит. Использование простых Ruby-объектов в Hanami для реализации различных компонентов системы значительно облегчает задачу по написанию тестов. На примере объектов, показанных выше, рассмотрим, насколько простым будет тестирование компонентов в Hanami-приложении.

Начнем с уровня представления. Вот как может выглядеть тест для объекта класса Web::Views::Books::Show, который служит для представления сущности модели Book

# spec/web/views/books/show_spec.rb

require 'spec_helper'
require_relative '../../../../apps/web/views/books/show'

describe Web::Views::Books::Show do
  let(:exposures) { Hash[book: book] }
  let(:book) { Book.new(title: 'Refactoring', author: 'Martin Fowler') }
  let(:template) {
Hanami::View::Template.new('apps/web/templates/books/show.html.erb') }
  let(:view)     { Web::Views::Books::Show.new(template, exposures) }
  let(:rendered) { view.render }

  it 'is rendered correctly' do
    rendered.must_include('Refactoring')
    rendered.must_include('Martin Fowler')
  end
end

Здесь и далее примеры тестов написаны с использованием библиотеки minitest.

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

Поскольку наиболее важные зависимости этого объекта (шаблон и параметры для представления) задаются в конструкторе, мы можем влиять на поведение объекта извне. Это позволяет нам абстрагироваться от деталей реализации при написании тестов. Кроме того, появляется возможность передавать в объект более легковесные реализации зависимостей вместо реальных объектов для ускорения выполнения тестов. Посмотрев, как здесь используется такой подход, мы сможем применить аналогичный для тестирования соответствующего действия на уровне контроллеров.

Единственная внешняя зависимость в реализации класса Web::Controllers::Books::Show – репозиторий BookRepository. Вынесем эту зависимость в конструктор. Поскольку мы имеем дело с обычным Ruby-классом, сделать это довольно просто: 

# apps/web/controllers/books/show.rb

module Web::Controllers::Books
  class Show
    include Web::Action

    expose :book

    def initialize(books_repo: BookRepository.new)
      @books_repo = books_repo
    end

    def call(params)
      repository = BookRepository.new
      @book = repository.find(params[:id])
    end

    private

    attr_reader :books_repo
  end
end

Теперь репозиторий для сущностей модели Book передается через параметр books_repo конструктора класса. И мы можем полностью управлять всеми зависимостями класса извне. Воспользуемся этим при написании теста для класса: 

# spec/web/controllers/books/show_spec.rb

require 'spec_helper'
require_relative '../../../../apps/web/controllers/books/show'

describe Web::Controllers::Books::Show do
  let(:repository) { BookRepository.new }
  let(:action) { Web::Controllers::Books::Show.new(books_repo:
repository) }

  let(:book) { repository.create(title: 'TDD', author: 'Kent Beck') }
  let(:params) { Hash[id: book.id] }

  before do
    repository.clear
  end

  it 'exposes the book' do
    action.call(params)
    action.exposures[:book].must_equal(book)
  end
end

Здесь мы использовали реальный репозиторий, который будет производить реальное обращение в базу данных. Однако ничто не мешает заменить реальный репозиторий на сущность, симулирующую такое же поведение, только без непосредственной связи с базой данных. Это позволит нам ускорить выполнение и без того достаточно быстрого теста.


Заключение 

В этой статье я показал, как фреймворк Hanami заботится о долгосрочной поддерживаемости вашей системы.

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

 

  • разделения ответственности на уровне всей системы (разделение ядра приложения и механизмов доставки);
  • отделения друг от друга механизмов доставки (приложения в директории apps); 
  • использования принципа единственной ответственности для реализации компонентов приложения;
  • широких возможностей по управлению зависимостями компонентов; 
  • хорошей тестируемости кода вашей системы.

Нельзя сказать, что Hanami – идеальный инструмент, лишенный каких-либо недостатков. Например, фреймворк имеет еще довольно слабую экосистему. Если для Ruby on Rails можно найти библиотеку с готовым решением для практически любой задачи, то Hanami таким количеством совместимых библиотек пока похвастаться не может. Но не стоит забывать, что фреймворку Ruby on Rails уже более 10 лет, а Hanami добрался до версии 1.0 совсем недавно. Кроме того, за этим фреймворком стоят такие энтузиасты open-source-движения, как, например, Антон Давыдов. Потому я не сомневаюсь, что ситуация будет в ближайшее время исправлена и Hanami ждет большое будущее.

Даже если вы не планируете использовать Hanami в работе, я думаю, вам все равно не помешает изучить более детально подходы, которые в нем предлагаются. Помимо их ценности с точки зрения архитектуры системы, вы сможете расширить свои знания и просто научиться чему-то новому. А в результате – более эффективно работать с инструментами, которые вы используете в своей работе.

Я не призываю никого переписать свои имеющиеся Rails-приложения на Hanami. Однако если сейчас вы стоите перед выбором, какой Ruby-фреймворк выбрать для следующего проекта – Hanami или Ruby on Rails, я бы сказал: хотите идти быстро – выбирайте Rails, хотите идти далеко – Hanami.

Вдохновил рассказ Евгения? С радостью ждем в рядах Remarkable People! Более 100 интересных вакансий по самым разным технологиям: от DevOps Engineer до Front-e‎nd Developer, от Java Developer до Golang Developer и не только. Все актуальные предложения Ruby программисты найдут тут

CATEGORIES

Development & QA Students Lab Company News

Похожие
статьи

arrow_left БЛОГ