Правильний спосіб кодування DCI у Ruby

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

Багато статей, знайдених у співтоваристві Ruby, у значній мірі спрощують використання DCI. Ці статті, у тому числі  моя власна, показують, як DCI наділяє ролями об’єкти під час виконання, тобто суть архітектури 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 ми почали визначити, яким чином наша модель предметної області (наші дані) буде виглядати. Ми знаємо, що книга буде містити  назву  атрибут. У дусі DCI, ми визначили контекст, для якого цей випадок використання розігрує і актори, які грають ключові ролі. Контекст додавання книги в кошик. Актор ми визначили є користувач.

Насправді, ми могли б додати більше тестів для подальшого покриття цієї функції, але вищезгадані костюми нам добре зараз.

«Ролі»

Актори грають роль. Для цієї конкретної функції, ми на самому справі є тільки один актор, користувач. Користувач грає ролі клієнта шукає, щоб додати елемент в їх кошик. Ролі описують алгоритми, використовувані для визначення того, що система  робить.

Давайте закодуємо його:

app/roles/customer.rb

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

Створення нашої ролі клієнта допомагає дражнити більше інформації про нашу моделі даних користувача. Тепер ми знаємо, що ми будемо мати потребу в  #cart  методі на будь-яких об’єктах даних, які грають роль клієнта.

Роль клієнта визначено вище не розкриває багато про те, що  #cart  є. Одне рішення, дизайн я зробив завчасно, для простоти, щоб припустити, віз буде зберігатися в базі даних замість sesssion. #cart  метод, визначений в будь-який актор, який грає роль клієнта не повинна бути складною реалізації возі. Я просто припустити просту асоціацію.

Ролі також дружать з поліморфізмом. Роль клієнта може грати  будь-який  об’єкт, що відповідає #cart  методу. Сама роль ніколи не знає, який тип об’єкта буде збільшувати, залишаючи це рішення до контексту.

Написання певних тестів

Давайте стрибати назад в режим тестування і написати кілька тестів навколо нашої недавно створеної ролі.

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

Вище тестовий код висловлює також, як ми будемо використовувати цю роль, клієнт, в даному контексті, додавши книгу в кошик. Це робить скутер справді писати контекстне мертву просто.

«Контекст»

У DCI, контекст середовища, для яких об’єкти даних виконують свої ролі. Існує завжди принаймні один контекст для кожної історії одного користувача. В залежності від складності користувальницької історії, може бути більше, ніж один контекст, можливо, що вимагає розповіді ломка. Метою контексту є підключення ролей (що система  робить) для об’єктів даних (то, що система  є).

На даний момент ми знаємо, роль, яку ми будемо використовувати, клієнт, і ми маємо сильну ідею про об’єкт даних ми будемо примноженням, користувач.

Давайте закодуємо його:

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

Оновлення: реалізація Джим Коплі по контексти використовує AddToCartContext#execute в якості контексту  тригера. Для підтримки Ruby, ідіоми, пуття і лямбда, приклади були змінені для використання AddToCartContext#call.

Кілька ключових моментів слід відзначити:

• Контекст визначається як клас. Акт екземпляра класу і виклику це  #call  метод відомий як  запуск.

• Маючи метод класу  AddToCartContext.call  просто зручний метод для полегшення запуску.

• Суть DCI в  @user.extend Customer. Об’єкти збільшують даних з ролями спеціальним що дозволяє сильну розв’язку. Там уже мільйон способів ін’єкційні ролей в об’єкти,  #extend  будучи один. В  наступній статті я буду розглядати інші способи, якими це може бути досягнуто.

• Передача призначених для користувача і книжкові об’єктів в контекст може привести до колізії імен методів ролей. Щоб полегшити це, було б прийнятно, щоб пройти user_id і book_id в контексті і дозволяють контексту інстанціювати пов’язані об’єкти.

• Контекст повинен викрити діячів, для яких він є сприятливим. В цьому випадку  attr_reader використовується, щоб виставити @user і @book. @book не діяч в цьому контексті, проте вона схильна до для повноти картини.

Найважливіше: ви повинні рідко доводиться (до незмоги) #unextend ролі з об’єкта. Об’єкт даних, як правило, грають тільки одну роль в той час, в даному контексті. Там повинен бути тільки один контекст в разі використання (акцент: в разі використання, а не користувач історія). Таким чином, ми повинні рідко потрібно видалити функціональні можливості або вводити конфлікти імен. У DCI, це  є  прийнятним для введення декількох ролей в об’єкт в межах даного контексту. Таким чином, проблема конфліктів імен до цих пір живе, але має відбуватися рідко.

Написання певних тестів

Я взагалі не великий прихильник глузливий і гасячи, але я думаю, що це доречно в разі контексти, тому що ми вже перевірили виконання коду в нашій ролі специфікації. На даний момент ми тільки тестування інтеграції.

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  метод з правильними аргументами. Ми робимо це, встановивши очікування того, що  користувач  діяч в AddToCartContext повинен мати це  #add_to_cart  метод викликається з  книгою в  якості аргументу.

Там не набагато більше DCI. Ми розглянули взаємодію між об’єктами і контекстом, для яких вони взаємодіють. Важливий код вже написаний. Єдине, що залишилося просто дані.

«Дані»

Дані повинні бути стрункими. Гарне емпіричне правило ніколи не визначити методи ваших моделей. Це не завжди так. Краще покласти: «інтерфейси об’єктів даних прості і мінімальні: досить, щоб захопити властивості домену, але без операцій, які є унікальними для будь-якого конкретного сценарію» (архітектура Lean). Дані дійсно має складатися тільки з методів живучості рівня, ніколи, як використовуються збережені дані. Давайте подивимося на модель книги, для якої ми вже дражнили з основних атрибутів.

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

Немає методів. Всі визначення класу рівня стійкості, асоціація і перевірка достовірності даних. Способи, в яких використовується Книга не повинна бути проблемою моделі книги. Ми могли б написати кілька тестів навколо моделі, і ми, ймовірно, слід. Тестування валідацій і асоціації є досить стандартним, і я не буду покривати їх тут.

Тримайте німий ваші дані.

Вбудовується в Rails

Там не багато, щоб сказати про встановлення вище коду в Rails. Простіше кажучи, ми викликаємо наш контекст всередині контролера.

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. Контекст стає шлюзом між призначеним для користувача інтерфейсом і моделями даних.

MVC + DCI

Що ми зробили

Наступний може гарантувати свою власну статтю, але я хочу коротко розглянемо деякі з переваг структурування коду з DCI.

• Ми високо розв’язані функціональність системи від того, як дані зберігаються. Це дає нам додаткову перевагу стиснення і легкому поліморфізм.

• Ми створили читається код. Легко міркувати про коді як по іменах файлів і алгоритмами всередині. Це все дуже добре організовано. Див.  Роздратування дядька Боба стосовно можливості читання на рівні файлу.

• Наша модель даних, що система  є, може залишатися стабільною в той час як ми прогресуємо і реорганізувати ролі, що система  робить.

• Ми підійшли ближче до подання кінцевого користувача ментальної моделі. Це основна мета MVC, то, що було спотворено протягом довгого часу.

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

Заключні слова

Проблеми з DCI існують також. По- перше, це вимагає великої зрушення парадигми. Він розроблений, щоб доповнити MVC (Model-View-Controller, модель-вид-контролер), щоб він добре вписується в Rails, але вимагає, щоб перемістити весь код поза контролера і моделі. Як ми всі знаємо, співтовариство Rails є фетиш для введення коду в моделі і контролери. Зрушення парадигми є великим, то, що потребуватиме великої рефакторінга для деяких додатків. Проте DCI, ймовірно, може бути перероблено в на індивідуальній основі випадку, дозволяючи додатки поступово переходити від «ситих моделей, худих контролерів» для DCI. По- друге, вона  потенційно несе зниження продуктивності, через те, що об’єкти розширеної спеціальної.

Основна перевага DCI по відношенню до товариства Ruby, є те, що вона забезпечує структуру для обговорення підтримуваного коду. Там було багато недавньої дискусії в дусі «жиру моделей, змарнілі контролери погано»; не ставте код в контролер або моделі, помістіть його в іншому місці. Проблема полягає в тому, що ми не вистачає вказівки для того, де наш код повинен жити і як вона повинна бути структурована. Ми не хочемо це в моделі, ми не хочемо його в контролері, і ми, звичайно, не хочу його в поданні. Для більшості, дотримуючись цих вимог призводить до плутанини, перебудови, і загальна відсутність узгодженості. DCI дає нам план розірвати рейки форму і створити ремонтопрігодни, перевіряються, розв’язку код.

Відступ: є й інші роботи в цій галузі. Авді Грімм має феноменальну книгу під назвою  Об’єкти Rails  яка пропонує альтернативні рішення.

Щасливого проектування!

Leave a Reply