Den rigtige måde at kode DCI i Ruby

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

Mange artikler fundet i Ruby samfund i høj grad forsimpler brugen af DCI. Disse artikler, herunder min egen, fremhæve, hvordan DCI injects Roller til objekter på runtime, essensen af DCI arkitektur. Mange indlæg betragter DCI på følgende måde:

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

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


Der er et par fejl med oversimpilfied eksempler som dette. For det første lyder "dette er, hvordan man gør DCI". DCI er langt mere end blot strækker objekter. For det andet fremhæver #extend som gå-til hjælp af tilføjelse af metoder til objekter på runtime. I denne artikel, vil jeg gerne specifikt den tidligere udgave: DCI end blot strækker objekter. En opfølgning indlæg vil indeholde en sammenligning af teknikker til at injicere Roller i objekter ved hjælp #extend og andet.

DCI (Data-Kontekst-Interaction)

Som tidligere nævnt, DCI handler om meget mere end blot at udvide objekter på runtime. Det handler om at fange slutbrugerens mentale model og rekonstruere det til vedligeholdelsesvenlig kode. Det er en ekstern → i tilgang, svarende til BDD, hvor vi betragter brugerinteraktion først og datamodellen sekund. Den udvendige → i tilgang er en af ​​de grunde, jeg elsker arkitekturen; det passer godt ind i et BDD stil, som yderligere fremmer testbarhed.

Det vigtige ting at vide om DCI er, at det handler om mere end bare kode. Det handler om processen og mennesker. Det starter med principperne bag Agile og Lean og udvider dem i kode. Den virkelige fordel ved følgende DCI er, at det spiller fint med Agile og Lean. Det handler om kode vedligeholdelsesevne, at reagere på forandring, og afkobling, hvad systemet gør (det er funktionalitet) fra, hvad systemet er (det er datamodel).

Jeg vil tage en opførsel tilgang til gennemførelsen af ​​DCI inden for en Rails app, begyndende med Interaktioner og flytter til datamodellen. For det meste, vil jeg skrive kode først derefter prøve. Selvfølgelig, du engang har en solid forståelse af komponenterne bag DCI, kan du skrive test først. Jeg ved bare ikke føler test-først er en fantastisk måde at forklare begreber.

User Stories

User historier er et vigtigt element i DCI selvom ikke adskiller sig til arkitekturen. De er udgangspunktet for at definere, hvad systemet gør. En af de skønheder startende med brugernes historier er, at den passer godt ind i en Agile proces. Typisk vil vi få en historie, der definerer vores slutbrugeren funktion. En forenklet historie kunne se ud på følgende:

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

På dette tidspunkt har vi en generel idé om funktionen vi vil gennemføre.

Bortset: En mere formel gennemførelse af DCI ville kræve at dreje en bruger historie i en use case. Brugen tilfælde ville så give os mere afklaring på input, output, motivation, roller etc.

Skriv nogle tests

Vi skal have nok på dette punkt til at skrive en accept test for denne funktion. Lad os bruge RSpec og 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


I ånden af BDD, har vi begyndte at identificere, hvordan vores domæne model (vores data) vil se ud. Vi ved, at bog vil indeholde en titel attribut. I ånden af DCI, har vi identificeret den kontekst, som denne use case retskraftig og skuespillerne, der spiller centrale dele. Baggrund er at tilføje en bog til vognen. Den skuespiller, vi har identificeret, er brugeren.

Realistisk set vil vi tilføje flere test til yderligere dækker denne funktion, men de ovennævnte passer os godt for nu.

De "Roller"

Skuespillere spille roller. For denne specifikke funktion, vi virkelig kun har én skuespiller, brugeren. Brugeren spiller rollen som en kunde ønsker at tilføje et element til deres vogn. Roller beskrive algoritmer, der anvendes til at definere, hvad systemet gør.

Lad os kode det op:

app / roller / customer.rb

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


Skabe vores Kundens rolle har hjulpet drille ud af mere information om vores data model, brugeren. Vi ved nu, at vi har brug for en #cart metode på alle data genstande, som spiller Kunden Rolle.

Kunden rolle defineret ovenfor ikke afsløre meget om, hvad #cart er. Et design beslutning gjorde jeg i god tid, for overskuelighedens skyld, er at antage vognen vil blive gemt i databasen i stedet for sesssion. Den #cart metode defineret på en skuespiller spille Kundens rolle bør ikke være en udførlige implementering af en vogn. Jeg blot antager en simpel forening.

Roller også spille pænt med polymorfi. Kunden Rolle kunne spilles af ethvert objekt, der svarer på #cart metoden. Rolle selv aldrig ved, hvad type objekt, vil forøge, efterlader denne afgørelse op til Context.

Skriv nogle tests

Lad os springe tilbage i test-mode og skrive nogle tests omkring vores nyoprettede rolle.

spec / roller / 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


Ovenstående test-kode giver også udtryk for, hvordan vi vil bruge denne Rolle, Kunden, inden for en given kontekst, tilføje en bog til vognen. Dette gør segway ind faktisk skriver den Context døde enkel.

Den "Context"

I DCI, Konteksten er miljøet for hvilke data objekter udføre deres roller. Der er altid mindst én sammenhæng for hver bruger historie. Afhængigt af kompleksiteten af brugerens historien, kan der være mere end én sammenhæng, eventuelt nødvendiggør en historie break-down. Målet med Kontekst er at forbinde Roller (hvad systemet gør) til dataobjekter (hvad systemet er).

På dette tidspunkt ved vi, den rolle, vi skal bruge, Kunden, og vi har et stærkt idé om data objekt vi vil forstærke, brugeren.

Lad os kode det op:

app / sammenhænge / 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


Opdatering: Jim Coplien gennemførelse sammenhænge bruger AddToCartContext # udføre som led aftrækkeren. For at understøtte Ruby idiomer, procs og lambdas er eksemplerne blevet ændret til at bruge AddToCartContext # opkald.

Der er en par vigtige punkter at bemærke:

En Kontekst er defineret som en klasse. Det handler om at instantiere klassen og kalde det #call metode er kendt som udløser.
Under klassen metoden AddToCartContext.call er simpelthen en bekvemmelighed metode til at hjælpe med at udløse.
Essensen af ​​DCI er i @ user.extend Kunden. Forstærke data objekter med roller ad hoc er, hvad der giver mulighed for en stærk afkobling. Der er en million måder at injicere Roller i genstande, #extend bliver en. I en opfølgning artikel vil jeg tage fat på andre måder, hvorpå dette kan ske.
Passing bruger- og book objekter til konteksten kan føre til navngivning sammenstød på Role metoder. Til at afhjælpe dette, ville det være acceptabelt at passere user_id og book_id ind i Kontekst og lad Kontekst at instantiere de tilknyttede objekter.
En Kontekst bør udsætte Aktører, som det er der gør det muligt. I dette tilfælde er attr_reader bruges til at eksponereuser ogbook. book er ikke en skuespiller i denne sammenhæng, men det er udsat for fuldstændighedens.
Mest noteably: Du bør sjældent til (håbløst) #unextend en rolle fra et objekt. Et dataobjekt vil normalt kun spille en rolle på et tidspunkt i en given kontekst. Der bør kun være én Kontekst pr use case (vægt: pr use case, ikke bruger historie). Derfor bør vi sjældent brug for at fjerne funktionalitet eller indføre navngivning kollisioner. I DCI, er det acceptabelt at injicere flere roller til et objekt inden for en given kontekst. Så problemet med at navngive kollisioner stadig opholder men bør sjældent forekomme.
Skriv nogle tests

Jeg er generelt ikke en stor tilhænger af spottende og stubbing men jeg synes det er hensigtsmæssigt i tilfælde af Contexts, fordi vi allerede har testet kører kode i vores rolle specs. På dette punkt er vi bare teste integrationen.

spec / sammenhænge / 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


Det vigtigste mål for ovenstående kode er at sikre, at vi kalder den #add_to_cart metoden med de korrekte argumenter. Det gør vi ved at sætte forventning om, at brugeren Actor i AddToCartContext skal have det #add_to_cart metode kaldes med bogen som et argument.

Der er ikke meget mere at DCI. Vi har dækket Samspillet mellem objekter og den kontekst, som de interagerer. Det vigtige koden er allerede blevet skrevet. Det eneste tilbage, er de dumme data.

"Data"

Data bør være slank. En god tommelfingerregel er aldrig at definere metoder på dine modeller. Dette er ikke altid tilfældet. Bedre put: "dataobjekt grænseflader er enkle og minimal: lige nok til at fange de domæne egenskaber, men uden operationer der er unikke for en bestemt scenarie" (Lean Architecture). Data bør virkelig kun bestå af vedholdenhed niveau metoder, aldrig hvordan varet data bliver brugt. Lad os se på den bog-modellen, som vi allerede har drillet de grundlæggende attributter.

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


Ingen metoder. Bare definitioner af vedholdenhed, forenings- og validering af data klasse-niveau. De måder, hvorpå Bog anvendes bør ikke være et anliggende for Bog-modellen. Vi kunne skrive nogle tests omkring modellen, og vi sandsynligvis bør. Test valideringer og foreninger er temmelig standard, og jeg vil ikke dække dem her.

Hold dine data stum.

Passer ind Rails

Der er ikke en hel masse at sige om montering af ovenstående kode ind Rails. Kort sagt, vi få vores Kontekst i controlleren.

app / controllere / book_controller.rb

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


Her er et diagram, der illustrerer, hvordan DCI komplimenter Rails MVC. Baggrund bliver en gateway mellem brugergrænsefladen og datamodellen.

MVC + DCI

Hvad vi har gjort

Følgende kunne berettige sin egen artikel, men jeg vil kort se på nogle af fordelene ved at strukturere koden med DCI.

Vi har stærkt afkoblet funktionaliteten af ​​systemet fra, hvordan dataene er faktisk gemt. Det giver os den ekstra fordel af kompression og nem polymorfi.
Vi har oprettet læsbar kode. Det er nemt at ræsonnere om koden både af filnavne og algoritmer indenfor. Det er alle meget godt organiseret. Se Onkel Bobs gripe om fil-niveau læsbarhed.
Vores data model, hvad systemet er, kan forblive stabile, mens vi videre og refactor Roller, hvad systemet gør.
Vi er kommet tættere på at repræsentere slutbrugeren mentale model. Dette er det primære mål for MVC, noget, der er blevet skæv over tid.
Ja, vi tilføjer endnu et lag af kompleksitet. Vi er nødt til at holde styr på Contexts og roller på toppen af ​​vores traditionelle MVC. Sammenhænge, ​​specifikt udviser mere kode. Vi har indføre en lidt mere overhead. Men med denne overliggende kommer en høj grad af velstand. Som udvikler eller et team af udviklere, er det din descretion om, hvorvidt disse fordele kunne løse dine forretningsmæssige og tekniske lidelser.

Afsluttende ord

Problemer med DCI eksisterer så godt. For det første kræver en stor paradigmeskift. Den er designet til at komplimentere MVC (Model-View-Controller), så det passer godt ind i Rails, men det kræver, at du flytte alle din kode uden for controller og model. Som vi alle ved, den Rails samfund har en fetich for at sætte kode i modeller og controllere. Paradigmeskiftet er stort, noget, der ville kræve en stor Refactor for nogle apps. Men DCI kunne sandsynligvis refactored i fra sag til sag tillade apps til gradvist skifte fra "fedt modeller, mager controllere" for DCI. For det andet potentielt bærer nedsættelser af ydeevnen, som følge af, at objekter er forlænget ad hoc.

Den største fordel ved DCI i forhold til Ruby samfund er, at det giver en struktur for at diskutere vedligeholde kode. Der har været en masse af de seneste drøftelser i stil med "'fede modeller, mager controllere er dårlig« ikke sætte kode i din controller eller din model, sætte den et andet sted. " Problemet er, vi mangler vejledning til hvor vores koden skal leve, og hvordan det skal struktureres. Vi vil ikke have det i modellen, ønsker vi ikke det i controlleren, og vi bestemt ikke ønsker det i visningen. For de fleste, der tilslutter sig disse krav fører til forvirring, overengineering, og en generel mangel på konsekvens. DCI giver os en plan til at bryde Rails skimmel og skabe vedligeholde, testes, afkoblet kode.

Bortset: Der har været andet arbejde på dette område. Avdi Grimm har en phenominal bog kaldet objekter on Rails, der foreslås alternative løsninger.

Glad architecting!

Comments are closed.