Правильный путь к коду DCI в Ruby

Original:http://mikepackdev.com/blog_posts/24-the-right-way-to-code-dci-in-ruby

Многие статьи, найденные в сообществе Ruby, в значительной степени упрощают использование DCI. Эти статьи, в том числе мои собственные, показывают, как DCI вставляет в Role в объекты во время выполнения, сущность архитектуры DCI. Большинство постов  рассматривают DCI следующим образом:

class User; end # Data
module Runner # Role
  def run
    ...
  end
end

user = User.new # Context
user.extend Runner
user.run

Есть несколько недостатков с подобными примерами. Во-первых, в нем говорится: «Это как сделать DCI». DCI – это нечто большее, чем просто расширение объектов. Во-вторых, он выделяет #extend как средство для добавления методов к объектам во время выполнения. В этой статье я хотел бы конкретно остановиться на первом выпуске: DCI за пределами только расширяющихся объектов. Последующая запись будет содержать сравнение методов ввода ролей в объекты с использованием #extend и в противном случае.

DCI (Data-Context-Interaction)

Как было сказано ранее, DCI представляет собой нечто большее, чем просто расширение объектов во время выполнения. Речь идет о захвате ментальной модели конечного пользователя и ее восстановлении в поддерживаемом коде. Это внешний подход →, похожий на BDD, где мы сначала рассматриваем взаимодействие пользователя и модель данных. Внешний вид → в подходе является одной из причин, по которым мне нравится архитектура; он хорошо вписывается в стиль BDD, что также способствует тестированию.

Важная информация о DCI заключается в том, что речь идет не только о коде. Речь идет о процессе и людях. Он начинается с принципов, лежащих в основе Agile и Lean, и расширяет их в коде. Настоящая выгода от DCI заключается в том, что он отлично играет с Agile и Lean. Речь идет о поддерживаемости кода, ответе на изменение и развязывании того, что делает система (это функциональность) от того, что система (это модель данных).

Я возьму поведенческий подход к внедрению DCI в приложении Rails, начиная с взаимодействия и перейдя к модели данных. По большей части, я собираюсь написать код сначала, затем проверить. Конечно, если у вас есть четкое представление о компонентах DCI, вы можете сначала написать тесты. Я просто не чувствую, что тест – это отличный способ объяснить понятия.

Истории пользователей

Истории пользователей – важная функция DCI, хотя она не отличается от архитектуры. Они являются отправной точкой для определения того, что делает система. Одна из красивейших начинаний с пользовательских историй заключается в том, что он хорошо вписывается в Agile-процесс. Как правило, нам будет предоставлена история, которая определяет нашу функцию конечного пользователя. Упрощенная история может выглядеть следующим образом:

"As a user, I want to add a book to my cart."

На данный момент у нас есть общее представление о функции, которую мы будем реализовывать.

Кроме того: более формальная реализация DCI потребует превращения истории пользователя в прецедент. Вариант использования затем предоставит нам больше разъяснений по поводу ввода, вывода, мотивации, ролей и т. д.

Напишите некоторые тесты

На этом этапе нам должно быть достаточно, чтобы написать приемочный тест для этой функции. Давайте использовать RSpec и Capybara:

spec/integration/add_to_cart_spec.rb

describe 'as a user' do
  it 'has a link to add the book to my cart' do
    @book = Book.new(:title => 'Lean Architecture')
    visit book_path(@book)
    page.should have_link('Add To Cart')
  end
end

В духе BDD мы начали определять, как будет выглядеть наша модель домена (наши данные). Мы знаем, что книга будет содержать атрибут title. В духе DCI мы определили Context, для которого этот случай использования используется, и Actors, которые играют ключевые части. Context добавляет книгу в корзину. Actors, которого мы идентифицировали, является User.

Реалистично, мы добавили бы больше тестов для дальнейшего покрытия этой функции, но выше всего нам подходит.

The “Roles”

Actors играют Roles . Для этой конкретной функции у нас действительно есть только один Actor, User. Пользователь играет роль клиента, который хочет добавить товар в свою корзину. Roles описывают алгоритмы, используемые для определения того, что делает система.

Давайте подберем код::

app/roles/customer.rb

module Customer
  def add_to_cart(book)
    self.cart << book
  end
end

Создание нашей Role клиента помогло вывести дополнительную информацию о нашей модели данных. Теперь мы знаем, что нам понадобится метод #cart для любых объектов Data, которые играют роль клиента.

Определенная выше роль Customer не раскрывает многое о том, что такое #cart. Одно конструктивное решение, которое я сделал раньше времени, ради простоты, состоит в том, чтобы предположить, что cart будет храниться в базе данных, а не в sesssion. Метод #cart, определенный для любого Актера, играющего роль Клиента, не должен быть сложной реализацией cart. Я просто предполагаю простую ассоциацию.

Roles также прекрасно сочетаются с полиморфизмом. The Customer Role может быть воспроизведен любым объектом, который отвечает на метод #cart. The Role сам никогда не знает, какой тип объекта он будет увеличивать, оставив это решение до контекста.

Пишим тесты

Давайте вернемся в режим тестирования и напишем некоторые тесты вокруг нашей вновь созданной Role.

spec/roles/customer_spec.rb

describe Customer do
  let(:user) { User.new }
  let(:book) { Book.new }

  before do
    user.extend Customer
  end

  describe '#add_to_cart' do
    it 'puts the book in the cart' do
      user.add_to_cart(book)
      user.cart.should include(book)
    end
  end
end

Вышеупомянутый тестовый код также выражает, как мы будем использовать этот Role, the Customer, в рамках данного контекста,добавив book в  cart.Это делаем the segway into actually writing the Context dead simple.

The “Context”

В DCI, the Contextэто среда, для которой объекты данных выполняют  Roles. Всегда есть хотя бы один Context для каждой истории пользователя. В зависимости от сложности  user story, может быть больше чем один  Context, возможно, потребует разложения истории. Цель Context является подключение  Roles (что делает система) к Data объкетакм(что есть система).

С этой точки зрения, мы знаем the Role будем использовать, the Customer, и у нас есть сильное представление о Data объете, мы будем дополнять.

Let’s code it up:

app/contexts/add_to_cart_context.rb

class AddToCartContext
  attr_reader :user, :book

  def self.call(user, book)
    AddToCartContext.new(user, book).call
  end

  def initialize(user, book)
    @user, @book = user, book
    @user.extend Customer
  end

  def call
    @user.add_to_cart(@book)
  end
end

Update: Jim Coplien’s реализация  Contexts  используя AddToCartContext#execute как тригер  контекста. Для поддержки  Ruby идиом, procs and lambdas, примеры были изменены для использованияAddToCartContext#call.

Следует отметить несколько ключевых моментов:

  • A Context определяется как класс. Акт создания экземпляра класса и вызов его метода #сall известен triggering.
  • Наличие метода класса AddToCartContext.call  это просто удобный метод, помогающий в triggering.
  • Сущность  DCI  в  @user.extend Customer.Дополнение объектов данных с помощью  Roles ad hoc это то, что позволяет сильно развязать. Есть миллион вариантов  добавить Roles в объекты, #extend является одним из них. В  следующей статье, Я рассмотрю другие способы, которыми это может быть выполнено.
  • Переда user and book объекта в Context може привести к коллизиям в именах  on Role методах. Чтобы облегчить это, было бы приемлимо передать user_id и book_id в Context и разрешить the Context для создания экземпляров связанных объектов.
  • A Context должен вставляться  в the Actors для которого это  позволено.В этом случае, attr_reader используется , чтобы вставить @user and @book. @book не находиться в  Actor in this Context,однако он открыт для полноты.
  • В первую очередь: Вам редко приходиться (возможно) #unextend a Role из объекта .Объект Data обычно воспроизводит только одну Role за раз в данном Context.Должен быть только один Context за использование (акцент: за использование, а не история пользователя). Поэтому нам редко приходится удалять функциональные возможности или вводить коллизии имен.В DCI, допустимо вводить несколько Rolesв объект в рамках данного  Context. Таким образом, проблема именования столкновений все еще сохраняется, но должна встречаться редко.

Напишим тесты

Я вообще не большой сторонник насмешек и stubbing, но я думаю, что это уместно в случае Contexts, потому что мы уже тестировали код в наших спецификациях Role. На этом этапе мы просто проверяем интеграцию.

spec/contexts/add_to_cart_context_spec.rb

describe AddToCartContext do
  let(:user) { User.new }
  let(:book) { Book.new }

  it 'adds the book to the users cart' do
    context = AddToCartContext.new(user, book)
    context.user.should_recieve(:add_to_cart).with(context.book)
    context.call
  end
end

Основная цель вышеуказанного кода – убедиться, что мы вызываем #add_to_cart method с правильными переменными .Мы делаем это, устанавливая ожидание того что  the user Actor внутри  AddToCartContext должен иметь этот  #add_to_cart  метод вызываемый с  book в качесвте аргумента.

Для DCI это не так много.Мы рассмотрели взаимодействие между объектами и Context для которых они взаимодействуют. Важный код уже напиан. Остались только dumb data

The “Data”

Данные должны быть небольшими. Хорошее эмпирическое правило – никогда не определять методы на ваших моделях. Это не всегда так. Лучше: «Интерфейсы объектов данных просты и минимальны: этого достаточно для захвата свойств домена, но без операций, которые уникальны для любого конкретного сценария» (Lean Architecture). Данные действительно должны состоять только из методов уровня настойчивости, никогда не используемых для сохранения данных. Давайте посмотрим на модель книги, для которой мы уже дразнили основные атрибуты.

class Book < ActiveRecord::Base
  validates :title, :presence => true
end

Нет методов. Просто определения уровня персистентности, ассоциации и валидации данных. Способы использования Книги не должны вызывать беспокойства в книжной модели. Мы могли бы написать несколько тестов вокруг модели, и мы, вероятно, должны это сделать. Тестирование валидаций и ассоциаций довольно стандартно, и я не буду здесь их освещать.

держите свои немые данные.

Установка в Rails

Не стоит много говорить об установке вышеуказанного кода в Rails.Проще говоря, мы  запускаем наш  Context внутри  Controller.

app/controllers/book_controller.rb

class BookController < ApplicationController
  def add_to_cart
    AddToCartContext.call(current_user, Book.find(params[:id]))
  end
end

Вот диаграмма, иллюстрирующая, как DCI дополняет Rails MVC. The Context становится шлюзом между пользовательским интерфейсом и моделью данных.

MVC + DCI

Что мы сделали

Следующее может гарантировать его собственную статью, но я хочу кратко рассмотреть некоторые преимущества структурирования кода с помощью DCI.

  • Мы сильно отделили функциональность системы от того, как данные фактически хранятся. Это дает нам дополнительное преимущество сжатия и легкого полиморфизма.
  • Мы создали читаемый код. Легко рассуждать о коде как именами файлов, так и алгоритмами внутри. Все это очень хорошо организовано. Посмотрите Uncle Bob’s gripe about file-level readability.
  • Наша модель данных, что система, может оставаться стабильной, пока мы прогрессируем и реорганизуем роли, что делает система.
  • Мы подошли ближе к представлению ментальной модели конечного пользователя. Это основная цель MVC, которая со временем исказилась.

Да, мы добавляем еще один уровень сложности. Мы должны отслеживать контексты и роли поверх нашего традиционного MVC. Контексты, в частности, демонстрируют больше кода. Мы вводим немного больше накладных расходов. Однако с этим накладными расходами происходит большая степень процветания. Как разработчик или команда разработчиков, это ваше решение о том, могут ли эти преимущества разрешить ваши деловые и инженерные недомогания.

Заключительные слова

Также существуют проблемы с DCI. Первая,это требует большого сдвига парадигмы. Он предназначен для комплимента MVC (Model-View-Controller) поэтому он хорошо вписывается в Rails, но требует, чтобы вы переместили весь свой код за пределы контроллера и модели. Как мы все знаем, сообщество Rails имеет фетиш для размещения кода в моделях и контроллерах. Смещение парадигмы велико, что потребует большого рефакторинга для некоторых приложений. Тем не менее, DCI, вероятно, может быть реорганизована в каждом конкретном случае, позволяя приложениям постепенно переключаться с «толстых моделей, тощих контроллеров» на DCI. Во-вторых, это потенциально может привести к ухудшению производительности из-за того, что объекты расширены ad hoc.

Основное преимущество DCI в отношении сообщества Ruby заключается в том, что он предоставляет структуру для обсуждения поддерживаемого кода. Было много недавних обсуждений в духе «толстых моделей, тощих контроллеров – плохо», не помещайте код в свой контроллер или свою модель, помещайте его в другое место ».Проблема в том, что нам не хватает указаний относительно того, где должен жить наш код и как он должен быть структурирован. Мы не хотим этого в модели, мы не хотим этого в контроллере, и мы, конечно, не хотим этого в представлении. Для большинства, соблюдение этих требований приводит к путанице, чрезмерной подготовке и общей несогласованности. DCI дает нам план разбить плесень Rails и создать поддерживаемый, проверяемый, развязанный код.

Кроме того: Там была другая работа в этой области. У Авди Гримма есть феноменологическая книга под названием «Объекты на рельсах», в которой предлагаются альтернативные решения.

Хорошей арихтектуры!

Leave a Reply

Your email address will not be published. Required fields are marked *