Mange lærer Ruby ved å starte med Rails-rammeverket, og dessverre er dette den dårligst mulige måten å lære dette språket på. Ikke misforstå meg: Rails er flott, det hjelper deg med å bygge webapplikasjoner raskt og effektivt uten å måtte sette deg inn i mange tekniske detaljer.
Hyggelig å treffe deg!
Mange lærer Ruby ved å starte med Rails-rammeverket, og dessverre er dette den dårligst mulige måten å lære dette språket på. Ikke misforstå meg: Rails er flott, det hjelper deg med å bygge webapplikasjoner raskt og effektivt uten å måtte sette deg inn i mange tekniske detaljer. De tilbyr mye "Rails-magi" som gjør at ting bare fungerer. Og for en nybegynner er dette veldig bra, for det hyggeligste øyeblikket i prosessen er når du kan si "den lever!", og se at alle delene passer sammen og at folk bruker appen din. Vi liker å være "skapere" 🙂 Men det er én ting som skiller gode programmerere fra gjennomsnittet: De gode programmererne forstår hvordan verktøyene de bruker, fungerer. Og med "forstå verktøyene dine" mener jeg ikke å kjenne til alle metodene og modulene som tilbys av et rammeverk, men å forstå hvordan det fungerer, forstå hvordan "Rails-magien" skjer. Først da kan du føle deg komfortabel med å bruke objekter og programmere med Rails. Grunnlaget for objektorientert programmering, og det hemmelige våpenet som gjør kompliserte Rails-applikasjoner enklere, er det allerede nevnte PORO, det vil si Plain Old Ruby Object
Hva skjuler seg egentlig bak dette navnet? Hva er dette store hemmelige våpenet? Det er en enkel Ruby-klasse som ikke arver fra noe som helst. Ja, akkurat det, og så mye mer.
klasse AwesomePoro
slutt
Hvordan kan jeg hjelpe deg?
Du utvikler kontinuerlig applikasjonen din og legger til nye funksjoner i takt med at antallet brukere og deres forventninger øker. Du kommer til et punkt der du støter på flere og flere mørke steder med ekstremt kronglete logikk, steder som selv de modigste utviklerne unngår som pesten. Jo flere slike steder, jo vanskeligere er det å administrere og utvikle applikasjonen. Et standardeksempel er registreringen av en ny bruker, som utløser en hel gruppe andre handlinger knyttet til denne hendelsen:
- sjekker IP-adressen i en spamdatabase,
- sende en e-post til den nye brukeren,
- legge til en bonus på kontoen til en anbefalende bruker,
- opprette kontoer i relaterte tjenester,
- og mange flere...
Et utvalg kode ansvarlig for brukerregistrering kan se slik ut:
class RegistrationController < ApplikasjonsController
def create
user = User.new(registration_params)
if user.valid? && ip_valid?(registrering_ip)
user.save!
user.add_bonuses
user.synchronize_related_accounts
user.send_email
end
end
end
Ok, du har kodet det, alt fungerer, men ... er all denne koden virkelig i orden? Kanskje vi kan skrive den bedre? For det første bryter den med det grunnleggende prinsippet for programmering - Single Responsibility, så vi kan helt sikkert skrive den bedre. Men hvordan? Det er her den allerede nevnte PORO kommer til hjelp. Det er nok å skille ut en klasse RegistrationService, som bare vil være ansvarlig for én ting: å varsle alle relaterte tjenester. Med tjenester vil vi vurdere de individuelle handlingene som vi allerede har utpekt ovenfor. I samme kontroller er alt du trenger å gjøre å opprette et objekt RegistrationService og kalle "fire!" -metoden på den. Koden har blitt mye tydeligere, kontrolleren vår tar mindre plass, og hver av de nyopprettede klassene er nå ansvarlig for bare én handling, slik at vi enkelt kan erstatte dem hvis det skulle oppstå behov for det.
klassen RegistrationService
def fire!(params)
user = User.new(params)
if user.valid? && ip_validator.valid?(registration_ip)
user.save!
after_registered_events(user)
end
user
end
private
def etter_registrerte_hendelser(bruker)
BonusesCreator.new.fire!(bruker)
AccountsSynchronizator.fire!(bruker)
EmailSender.fire!(bruker)
end
def ip_validator
@ip_validator ||= IpValidator.new
end
end
class RegistrationController < ApplicationController
def create
user = RegistrationService.new.fire!(registration_params)
end
end
Men Plain Old Ruby Object kan vise seg å være nyttig ikke bare for kontrollere. Tenk deg at applikasjonen du lager bruker et månedlig faktureringssystem. Den nøyaktige dagen for å lage en slik fakturering er ikke viktig for oss, vi trenger bare å vite at det gjelder en bestemt måned og år. Du kan selvfølgelig angi dagen for den første dagen i hver måned og lagre denne informasjonen i objektet i klassen "Date", men det er verken en sann informasjon, og du trenger den heller ikke i applikasjonen din. Ved å bruke PORO kan du opprette en klasse "MonthOfYear", hvis objekter vil lagre den nøyaktige informasjonen du trenger. Når du bruker modulen "Comparable" i den, vil det dessuten være mulig å iterere og sammenligne objektene, akkurat som når du bruker Date-klassen.
klasse MonthOfYear
include Sammenlignbar
attr_reader :year, :month
def initialize(måned, år)
raise ArgumentError med mindre month.between?(1, 12)
@year, @month = år, måned
end
def (annet)
[år, måned] [annet.år, annen.måned]
end
end
Introduser meg for Rails.
I Rails-verdenen er vi vant til at hver klasse er en modell, en visning eller en kontroller. De har også sin nøyaktige plassering i katalogstrukturen, så hvor kan du plassere vår lille PORO-hær? Tenk på noen alternativer. Den første tanken som dukker opp er: Hvis de opprettede klassene verken er modeller, visninger eller kontrollere, bør vi plassere dem alle i "/lib"-katalogen. Teoretisk sett er det en god idé, men hvis alle PORO-filene dine havner i én katalog, og applikasjonen blir stor, vil denne katalogen raskt bli et mørkt sted som du er redd for å åpne. Derfor er det utvilsomt ikke en god idé.
AwesomeProject
├──app
│ ├─kontroller
│ ├─modeller
│ └─visninger
│
└─lib
└─tjenester
#all poro her
Du kan også kalle noen av klassene som ikke er ActiveRecord-modeller, for modeller og legge dem i katalogen "app/models", og kalle de som er ansvarlige for å håndtere andre klasser, for tjenester og legge dem i katalogen "app/services". Dette er en ganske god løsning, men den har en ulempe: Når du oppretter en ny PORO, må du hver gang bestemme deg for om den er mer en modell eller en tjeneste. På denne måten kan du komme i en situasjon der du har to mørke steder i applikasjonen din, bare mindre. Det finnes også en tredje tilnærming, nemlig å bruke navngitte klasser og moduler. Alt du trenger å gjøre er å opprette en katalog som har samme navn som en kontroller eller en modell, og legge alle PORO-filer som brukes av den gitte kontrolleren eller modellen inni den.
AwesomeProject
├──app
│ │ ├─kontroller
│ │ ├─registrering_controller
│ │ │ └─registration_service.rb
│ │ └─registration_controller.rb
│ ├─modeller
│ │ │ ├─settlement
│ │ │ └─måned_av_år.rb
│ │ └─settlement.rb
│ └─visninger
│
└─lib
Takket være denne ordningen trenger du ikke å sette et namespace foran navnet på en klasse når du bruker den. Du har fått kortere kode og en mer logisk organisert katalogstruktur.
Sjekk meg ut!
Det er en gledelig overraskelse at når du bruker PORO, blir enhetstestene til applikasjonen din raskere og enklere å skrive, og senere mer sannsynlig at andre forstår dem. Ettersom hver klasse nå bare er ansvarlig for én ting, kan du gjenkjenne grensebetingelsene tidligere og enkelt legge til passende testscenarier for dem.
describe MonthOfYear do
subject { MonthOfYear.new(11, 2015) }
it { should be_kind_of Comparable }
describe "oppretter ny instans" do
it "initialiseres med riktig år og måned" do
expect { described_class.new(10, 2015) }.to_not raise_error
end
it "utløser feil når oppgitt måned er feil" do
expect { described_class.new(0, 2015) }.to raise_error(ArgumentError)
expect { described_class.new(13, 2015) }.to raise_error(ArgumentError)
end
end
end
Jeg håper vi møtes igjen!
Eksemplene vi har presentert, viser tydelig at bruk av PORO forbedrer lesbarheten til applikasjonene og gjør dem mer modulære, og følgelig enklere å administrere og utvide. Ved å ta i bruk prinsippet om Single Responsibility blir det lettere å bytte ut enkelte klasser hvis det er nødvendig, uten å forstyrre andre elementer. Det gjør det også enklere og raskere å teste dem. Dessuten er det mye enklere å holde Rails-modeller og -kontrollere korte, og vi vet alle at de har en tendens til å bli unødvendig store i løpet av utviklingsprosessen.