Science for all






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

Właściwy sposób na kod DCI w Ruby

Wiele artykułów znalezionych w społeczności Ruby znacznie upraszcza korzystanie z DCI. Artykuły te, w tym mój własny , podkreślają, w jaki sposób DCI wstrzykuje Role do obiektów w czasie wykonywania, istotę architektury DCI.Wiele postów dotyczy DCI w następujący sposób:

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

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

Istnieje kilka wad związanych z takimi przykładami. Po pierwsze, brzmi jak to zrobić DCI. DCI to coś więcej niż tylko rozszerzanie obiektów. Po drugie, podkreśla #extend jako środek do dodawania metod do obiektów w czasie wykonywania. W tym artykule chciałbym omówić ten pierwszy problem: DCI poza rozszerzaniem obiektów. Następny post będzie zawierał porównanie technik wstrzykiwania Roli do obiektów za pomocą #extend i innych.

DCI (Data-Context-Interaction)

Jak wcześniej wspomniano, DCI to coś więcej niż tylko rozszerzanie obiektów w czasie wykonywania. Chodzi o uchwycenie modelu mentalnego użytkownika końcowego i odtworzenie go do kodu serwisowego. Jest to podejście → zewnętrzne, podobne do BDD, w którym interakcja użytkownika jest traktowana jako pierwsza, a model danych drugi.Zewnętrzne → podejście jest jednym z powodów, dla których kocham architekturę; dobrze pasuje do stylu BDD, co dodatkowo sprzyja testowalności.

Ważną rzeczą, jaką należy wiedzieć o DCI, jest to, że chodzi o coś więcej niż tylko kod. Chodzi o proces i ludzi.Zaczyna się od zasad stojących za Agile i Lean i rozszerza je na kod. Prawdziwą korzyścią wynikającą z podążania za DCI jest to, że gra dobrze z Agile i Lean. Chodzi o łatwość konserwacji kodu, reagowanie na zmiany i oddzielenie tego, co robi system (jego funkcjonalność) od tego, czym jest system (jego model danych).

Przyjmę podejście oparte na zachowaniu do wdrożenia DCI w aplikacji Rails, zaczynając od interakcji i przechodząc do modelu danych.

Historie użytkownika

Historie użytkowników ważną cechą DCI, choć nie różnią się od architektury. Są punktem wyjścia do określenia, co robi system. Jednym z uroków zaczynania od historii użytkownika jest to, że dobrze pasuje do procesu Agile.Zazwyczaj otrzymamy historię, która definiuje naszą funkcję użytkownika końcowego. Uproszczona historia może wyglądać następująco:

  Jako użytkownik chcę dodać książkę do mojego koszyka.

W tym momencie mamy ogólne pojęcie o funkcji, którą będziemy wdrażać.

Poza: bardziej formalne wdrożenie DCI wymagałoby przekształcenia historii użytkownika w przypadek użycia.Przypadek użycia dostarczyłby nam wtedy więcej wyjaśnień na temat danych wejściowych, wyników, motywacji, ról itp.

Napisz kilka testów

W tym momencie powinniśmy mieć dość pisania testu akceptacyjnego dla tej funkcji. Użyjmy RSpec i kapibary :

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

W duchu BDD zaczęliśmy określać, jak będzie wyglądał nasz model domeny (nasze dane). Wiemy, że książka będzie zawierała atrybut tytułu . Również w duchu DCI zidentyfikowaliśmy kontekst, dla którego ten przypadek dotyczy, oraz aktorów, którzy grają kluczowe części. Kontekst dodaje książkę do koszyka. Aktorem, którego zidentyfikowaliśmy, jest Użytkownik.

Realistycznie dodalibyśmy więcej testów, aby jeszcze lepiej opisać funkcję, ale powyższe na razie nam odpowiada.

Role

Aktorzy grają role. W przypadku tej konkretnej funkcji mamy tylko jednego Aktora, Użytkownika. Użytkownik gra rolę klienta, który chce dodać element do koszyka. Role opisują algorytmy używane do definiowania tego, co robisystem.

Kodujmy to:

app / roles / customer.rb

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

Utworzenie naszej roli klienta pomogło nam uzyskać więcej informacji na temat naszego modelu danych, czyli użytkownika. Teraz wiemy, że będziemy potrzebować metody #cart na dowolnych obiektach danych, które odgrywają rolę klienta.

Rola klienta zdefiniowana powyżej nie ujawnia wiele na temat tego, czym jest #cart . Jedną z decyzji projektowych, które podjąłem wcześniej, dla uproszczenia, jest założenie, że wózek będzie przechowywany w bazie danych, a nie w sesji. Metoda #cart zdefiniowana dla każdego aktora grającego rolę klienta nie powinna być skomplikowanym wdrożeniem koszyka. Zakładam jedynie proste skojarzenie.

Role również dobrze grają z polimorfizmem. Rola klienta może być odtwarzana przez dowolny obiekt, który odpowiada na metodę #cart . Sama rola nigdy nie wie, jakiego typu obiekt będzie powiększał, pozostawiając ​​decyzję do kontekstu.

Napisz kilka testów

Wróćmy do trybu testowego i napiszmy testy wokół naszej nowo utworzonej roli.

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

Powyższy kod testowy wyraża również, w jaki sposób będziemy korzystać z tej roli, klienta, w ramach danego kontekstu, dodając książkę do koszyka. To sprawia, że ​​segway w rzeczywistości pisze Context martwy.

Kontekst"

W DCI kontekst jest środowiskiem, w którym obiekty danych wykonują swoje role. Zawsze istnieje co najmniej jeden kontekst dla każdej historii użytkownika. W zależności od złożoności historii użytkownika może istnieć więcej niż jeden kontekst, co może wymagać załamania historii. Celem kontekstu jest połączenie ról (co system robi ) z obiektami danych (czym jest system).

W tym momencie znamy rolę, którą będziemy używać, klienta, i mamy silny pomysł na obiekt danych, który będziemy rozszerzać - użytkownika.

Kodujmy to:

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

Aktualizacja: implementacja Contextów przez Jima Copliena wykorzystuje wykonanie kontekstowe AddToCartContext #. Aby obsługiwać idiomy Ruby, procs i lambdas, przykłady zostały zmienione, aby użyć wywołania AddToCartContext #.

Jest kilka kluczowych punktów do odnotowania:

Napisz kilka testów

Zasadniczo nie jestem wielkim zwolennikiem kpienia i stubowania, ale myślę, że jest to właściwe w przypadku Contextów, ponieważ przetestowaliśmy już działający kod w naszych specyfikacjach ról. W tym momencie testujemy integrację.

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

Głównym celem powyższego kodu jest upewnienie się, że wywołujemy metodę #add_to_cart z poprawnymi argumentami. Robimy to, ustawiając oczekiwanie, że użytkownik Actor w AddToCartContext powinien mieć wywołaną metodę #add_to_cart z książką jako argument.

DCI nie ma wiele więcej. Omówiliśmy interakcję między obiektami a kontekstem, z którym oddziałują. Ważny kod został już napisany. Pozostało tylko głupie dane.

Dane"

Dane powinny być niewielkie. Dobrą zasadą jest nigdy nie definiować metod w swoich modelach. Nie zawsze tak jest.Lepiej: Interfejsy obiektów danych proste i minimalne: wystarczy, aby przechwycić właściwości domeny, ale bez operacji, które są unikalne dla konkretnego scenariusza (Lean Architecture). Dane powinny składać się wyłącznie z metod poziomu trwałości, nigdy w jaki sposób wykorzystywane utrwalone dane. Spójrzmy na model Book, dla którego już wyodrębniliśmy podstawowe atrybuty.

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

Brak metod. Tylko definicje klasy trwałości, asocjacji i walidacji danych. Sposób, w jaki książka jest używana, nie powinien dotyczyć modelu książki. Moglibyśmy napisać kilka testów wokół modelu i prawdopodobnie powinniśmy.Testowanie walidacji i skojarzeń jest dość standardowe i nie będę ich tutaj omawiać.

Trzymaj swoje dane głupie.

Dopasowanie do szyn

Nie ma wiele do powiedzenia na temat dopasowania powyższego kodu do Rails. Mówiąc najprościej, uruchamiamy nasz kontekst w kontrolerze.

app / controllers / book_controller.rb

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

Oto diagram ilustrujący, w jaki sposób DCI komplementuje Rails MVC. Kontekst staje się bramą między interfejsem użytkownika a modelem danych.

MVC + DCI

Co zrobiliśmy

Następujące elementy mogą uzasadniać własny artykuł, ale chcę krótko przyjrzeć się niektórym zaletom kodu strukturyzującego za pomocą DCI.

Tak, dodajemy kolejną warstwę złożoności. Musimy śledzić konteksty i role na szczycie naszego tradycyjnego MVC.Konteksty, w szczególności, wykazują więcej kodu. Wprowadziliśmy trochę więcej kosztów ogólnych. Jednak z tym obciążeniem wiąże się duży stopień dobrobytu. Jako deweloper lub zespół programistów zależy Ci na tym, czy te korzyści mogą rozwiązać twoje problemy biznesowe i inżynierskie.

Ostatnie słowa

Istnieją również problemy z DCI. Po pierwsze, wymaga dużej zmiany paradygmatu. Został zaprojektowany w celu uzupełnienia MVC (Model-View-Controller), więc dobrze pasuje do Rails, ale wymaga przeniesienia całego kodu poza kontroler i model. Jak wszyscy wiemy, społeczność Railsów ma fetysz do umieszczania kodu w modelach i kontrolerach. Zmiana paradygmatu jest duża, co wymagałoby dużego refaktora dla niektórych aplikacji. Jednak prawdopodobnie DCI może zostać zreformowany na zasadzie indywidualnej, umożliwiając aplikacjom stopniowe przechodzenie od grubych modeli, chudych kontrolerów do DCI. Po drugie, potencjalnie niesie ze sobą pogorszenie wydajności , ponieważ obiekty rozszerzane ad hoc.

Główną zaletą DCI w odniesieniu do społeczności Ruby jest to, że zapewnia ona strukturę do omawiania kodu do konserwacji. Ostatnio odbyło się wiele dyskusji w stylu grubych modeli, złe kontrolery są złe, nie umieszczaj kodu w kontrolerze LUB w swoim modelu, umieszczaj go gdzie indziej. Problem polega na tym, że brakuje nam wskazówek, gdzie powinien żyć nasz kod i jak powinien być zorganizowany. Nie chcemy tego w modelu, nie chcemy go w kontrolerze i na pewno nie chcemy tego w widoku. Dla większości przestrzeganie tych wymogów prowadzi do nieporozumień, nadmiernej inżynierii i ogólnego braku spójności. DCI daje nam plan przełamania formy Railsów i stworzenia łatwego w obsłudze, testowalnego, odsprzężonego kodu.

Poza tym: w tej dziedzinie były inne prace. Avdi Grimm ma książkę fenomenalną o nazwie Obiekty na szynach, która proponuje alternatywne rozwiązania.

Szczęśliwa architektura!