Hauska tavata!

Monet ihmiset opettelevat Rubya aloittamalla Rails-kehyksestä, ja valitettavasti tämä on huonoin mahdollinen tapa oppia tätä kieltä. Älä ymmärrä minua väärin: Se auttaa sinua rakentamaan verkkosovelluksia nopeasti ja tehokkaasti ilman, että sinun tarvitsee perehtyä moniin teknisiin yksityiskohtiin. Ne tarjoavat paljon "Rails-taikuutta", joka saa asiat yksinkertaisesti toimimaan. Ja aloittelevalle ohjelmoijalle tämä on todella hienoa, koska prosessin miellyttävin hetki on se, kun voit sanoa "se elää!" ja nähdä, että kaikki osat sopivat yhteen ja ihmiset käyttävät sovellustasi. Haluamme olla "tekijöitä" 🙂 Mutta on yksi asia, joka erottaa hyvät ohjelmoijat keskiverto-ohjelmoijista: hyvät ohjelmoijat ymmärtävät, miten heidän käyttämänsä työkalut toimivat. Ja "työkalujen ymmärtämisellä" en tarkoita kaikkien kehyksen tarjoamien metodien ja moduulien tuntemista, vaan sen ymmärtämistä, miten se toimii, miten "Rails-taika" tapahtuu. Vasta sitten voit tuntea olosi mukavaksi käyttää objekteja ja ohjelmoida Railsilla. Oliopohjaisen ohjelmoinnin perusta ja salainen ase, joka tekee monimutkaisesta Rails-sovelluksesta helpomman, on jo otsikossa mainittu PORO eli Plain Old Ruby Object.

Mitä tämän nimen takana todella piilee? Mikä on tämä suuri salainen ase? Se on yksinkertainen Ruby-luokka, joka ei periydy mistään. Kyllä, juuri sitä, ja niin paljon.

luokka AwesomePoro
end

Miten voin auttaa sinua?

Kehität sovellustasi jatkuvasti ja lisäät siihen uusia toimintoja, koska käyttäjien määrä ja heidän odotuksensa kasvavat. Päädyt pisteeseen, jossa törmäät yhä useampiin pimeisiin paikkoihin, joissa logiikka on äärimmäisen kieroutunutta, paikkoihin, joita rohkeimmatkin kehittäjät välttelevät kuin ruttoa. Mitä enemmän tällaisia paikkoja on, sitä vaikeampi on hallita ja kehittää sovellusta. Tavallinen esimerkki on uuden käyttäjän rekisteröinti, joka käynnistää kokonaisen joukon muita tähän tapahtumaan liittyviä toimintoja:

Näyte koodi joka vastaa käyttäjän rekisteröinnistä, voi näyttää tältä:

class RegistrationController < ApplicationController < ApplicationController
  def create
    user = User.new(registration_params)
    if user.valid? && ip_valid?(registration_ip)
      user.save!
      user.add_bonuses
      user.synkronize_related_accounts
      user.send_email
    end
  end
end

Okei, olet koodannut sen, kaikki toimii, mutta... onko kaikki tämä koodi todella kunnossa? Ehkä voisimme kirjoittaa sen paremmin? Ensinnäkin se rikkoo ohjelmoinnin perusperiaatetta - yksittäistä vastuuta, joten voisimme varmasti kirjoittaa sen paremmin. Mutta miten? Tässä kohtaa jo mainittu PORO tulee avuksesi. Riittää, että erotamme luokan RegistrationService, joka vastaa vain yhdestä asiasta: kaikkien siihen liittyvien palveluiden ilmoittamisesta. Palveluilla tarkoitamme yksittäisiä toimia, jotka olemme jo edellä erottaneet. Samassa kontrollerissa sinun tarvitsee vain luoda objekti RegistrationService ja kutsua siihen "fire!"-metodia. Koodista on tullut paljon selkeämpää, kontrollerimme vie vähemmän tilaa, ja kukin äskettäin luoduista luokista vastaa nyt vain yhdestä toiminnosta, joten voimme tarvittaessa korvata ne helposti.

luokka 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 after_registered_events(user)
    BonusesCreator.new.fire!(user)
    AccountsSynchronizator.fire!(user)
    EmailSender.fire!(user)
  end
  def ip_validator
    @ip_validator ||= IpValidator.new
  end
end
class RegistrationController < ApplicationController
  def create
    user = RegistrationService.new.fire!(registration_params)
  end
end

Plain Old Ruby Object voi kuitenkin osoittautua hyödylliseksi muissakin kuin ohjaimissa. Kuvittele, että luomasi sovellus käyttää kuukausilaskutusjärjestelmää. Tarkka päivä, jolloin tällainen laskutus luodaan, ei ole meille tärkeä, meidän on vain tiedettävä, että se koskee tiettyä kuukautta ja vuotta. Voit tietysti asettaa päivän kunkin kuukauden ensimmäiseksi päiväksi ja tallentaa tämän tiedon "Date"-luokan objektiin, mutta se ei ole todellinen tieto, etkä tarvitse sitä sovelluksessasi. Käyttämällä POROa voit luoda luokan "MonthOfYear", jonka objektit tallentavat juuri tarvitsemasi tiedot. Lisäksi, kun sovellat siihen moduulia "Comparable", voit iteroida ja vertailla sen objekteja, aivan kuten Date-luokkaa käyttäessäsi.

luokka MonthOfYear
  include Comparable
  attr_reader :year, :month
  def initialize(kuukausi, vuosi)
    raise ArgumentError ellei month.between?(1, 12)
    @vuosi, @kuukausi = vuosi, kuukausi
  end
  def (other)
    [vuosi, kuukausi]  [muu.vuosi, muu.kuukausi]
  end
end

Esittele minulle Rails.

Rails-maailmassa olemme tottuneet siihen, että jokainen luokka on malli, näkymä tai ohjain. Niillä on myös tarkka sijaintinsa hakemistorakenteessa, joten minne voimme sijoittaa pienen PORO-armeijamme? Harkitse muutamia vaihtoehtoja. Ensimmäinen mieleen tuleva ajatus on: jos luodut luokat eivät ole malleja, näkymiä tai ohjaimia, meidän pitäisi laittaa ne kaikki hakemistoon "/lib". Teoriassa se on hyvä ajatus, mutta jos kaikki PORO-tiedostot päätyvät yhteen hakemistoon ja sovelluksesta tulee suuri, tästä hakemistosta tulee nopeasti pimeä paikka, jota pelkäät avata. Siksi se ei epäilemättä ole hyvä idea.

AwesomeProject
├──-sovellus
│ ├─ohjaimet
│ ├─mallit
│ └─views
│
└─lib
  └─palvelut
      #all poro täällä

Voit myös nimetä joitakin luokkia, jotka eivät ole ActiveRecord-malleja, ja sijoittaa ne hakemistoon "app/models", ja nimetä muiden luokkien käsittelystä vastaavat luokat palveluiksi ja sijoittaa ne hakemistoon "app/services". Tämä on melko hyvä ratkaisu, mutta siinä on yksi haittapuoli: kun luot uuden PORO:n, sinun on joka kerta päätettävä, onko se enemmän malli vai palvelu. Näin saatat päästä tilanteeseen, jossa sovelluksessasi on kaksi pimeää paikkaa, vain pienempiä. On vielä kolmaskin lähestymistapa, nimittäin: nimitettyjen luokkien ja moduulien käyttö. Sinun tarvitsee vain luoda hakemisto, jolla on sama nimi kuin ohjaimella tai mallilla, ja sijoittaa kaikki kyseisen ohjaimen tai mallin käyttämät PORO-tiedostot sinne.

AwesomeProject
├──-sovellus
│ ├─ohjaimet
│ │ ├─ rekisteröinnin_valvoja
│ │ │ │ └─registration_service.rb
│ │ │ └─registration_controller.rb
│ ├─models
│ │ │ ├─settlement
│ │ │ │ └─month_of_year.rb
│ │ │ └─settlement.rb
│ └─views
│
└─lib

Tämän järjestelyn ansiosta sitä käytettäessä sinun ei tarvitse edeltää luokan nimeä nimiavaruudella. Olet saanut lyhyempää koodia ja loogisemman hakemistorakenteen.

Tsekkaa minut!

On miellyttävä yllätys, että kun käytät POROa, sovelluksesi yksikkötestit ovat nopeampia ja helpompia kirjoittaa, ja myöhemmin muut ymmärtävät ne todennäköisemmin. Koska jokainen luokka vastaa nyt vain yhdestä asiasta, voit tunnistaa reunaehdot nopeammin ja lisätä niihin helposti sopivia testiskenaarioita.

describe MonthOfYear do
  subject { MonthOfYear.new(11, 2015) }
  it { should be_kind_of Comparable }
  describe "uuden instanssin luominen" do
    it "alustetaan oikealla vuodella ja kuukaudella" do
      expect { described_class.new(10, 2015) }.to_not raise_error_error
    end
    it "herättää virheen, kun annettu kuukausi on väärä" do
      expect { described_class.new(0, 2015) }.to raise_error(ArgumentError)
      expect { described_class.new(13, 2015) }.to raise_error(ArgumentError)
    end
  end
end

Toivottavasti tapaamme vielä!

Esittämämme esimerkit osoittavat selvästi, että PORO:n käyttö parantaa sovellusten luettavuutta ja tekee niistä modulaarisempia, jolloin niitä on helpompi hallita ja laajentaa. Yhden vastuun periaatteen omaksuminen helpottaa tiettyjen luokkien vaihtamista tarvittaessa ja ilman, että se häiritsee muita elementtejä. Se tekee myös niiden testaamisesta yksinkertaisempaa ja nopeampaa. Lisäksi tällä tavoin Rails-mallien ja -kontrollerien pitäminen lyhyinä on paljon helpompaa, ja me kaikki tiedämme, että niillä on taipumus kasvaa tarpeettoman suuriksi kehitysprosessin aikana.

fiFinnish