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 становится шлюзом между пользовательским интерфейсом и моделью данных.
Что мы сделали
Следующее может гарантировать его собственную статью, но я хочу кратко рассмотреть некоторые преимущества структурирования кода с помощью 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 и создать поддерживаемый, проверяемый, развязанный код.
Кроме того: Там была другая работа в этой области. У Авди Гримма есть феноменологическая книга под названием «Объекты на рельсах», в которой предлагаются альтернативные решения.
Хорошей арихтектуры!