Veel mensen leren Ruby door te beginnen met het Rails framework en helaas is dit de slechtst mogelijke manier om deze taal te leren. Begrijp me niet verkeerd: Rails is geweldig, het helpt je om snel en efficiënt webapplicaties te bouwen zonder dat je je in veel technische details hoeft te verdiepen.
Leuk je te ontmoeten!
Veel mensen leren Ruby door te beginnen met het Rails framework en helaas is dit de slechtst mogelijke manier om deze taal te leren. Begrijp me niet verkeerd: Rails is geweldig, het helpt je om snel en efficiënt webapplicaties te bouwen zonder dat je je in veel technische details hoeft te verdiepen. Ze bieden veel "Rails-magie" waardoor dingen gewoon werken. En voor een beginnende programmeur is dit echt geweldig, want het leukste moment van het proces is wanneer je kunt zeggen "het leeft!", en ziet dat alle onderdelen in elkaar passen en mensen je app gebruiken. We zijn graag "makers" 🙂 Maar er is één ding dat goede programmeurs onderscheidt van de gemiddelde: de goede begrijpen hoe de tools die ze gebruiken werken. En met "je tools begrijpen" bedoel ik niet alle methodes en modules kennen die een framework biedt, maar begrijpen hoe het werkt, begrijpen hoe de "Rails-magie" gebeurt. Alleen dan kan je je comfortabel voelen met het gebruik van objecten en programmeren met Rails. De basis van object-georiënteerd programmeren, en het geheime wapen dat de ingewikkelde Rails applicatie gemakkelijker maakt, is het al in de titel genoemde PORO, dat is Plain Old Ruby Object.
Wat gaat er werkelijk schuil achter deze naam? Wat is dit grote geheime wapen? Het is een eenvoudige Ruby klasse die nergens van overerft. Ja, precies dat, en nog veel meer.
klasse AwesomePoro
einde
Hoe kan ik u helpen?
Je ontwikkelt je applicatie voortdurend en voegt nieuwe functionaliteiten toe terwijl het aantal gebruikers en hun verwachtingen groeien. Je komt op een punt waar je steeds meer duistere plekken van extreem verdraaide logica tegenkomt, plekken die zelfs door de dapperste ontwikkelaars als de pest worden vermeden. Hoe meer van dergelijke plaatsen, hoe moeilijker het wordt om de applicatie te beheren en te ontwikkelen. Een standaardvoorbeeld is de actie van het registreren van een nieuwe gebruiker, die een hele groep andere acties activeert die aan deze gebeurtenis zijn gekoppeld:
- het IP-adres controleren in een spamdatabase,
- een e-mail sturen naar de nieuwe gebruiker,
- een bonus toevoegen aan een account van een aanbevelende gebruiker,
- accounts aanmaken in gerelateerde services,
- en nog veel meer...
Een voorbeeld code die verantwoordelijk is voor de registratie van gebruikers kan er als volgt uitzien:
klasse RegistratieController < ApplicationController
def aanmaken
gebruiker = gebruiker.nieuw(registratie_params)
if user.valid? && ip_valid?(registration_ip)
gebruiker.opslaan!
user.add_bonussen
gebruiker.synchroniseer_gerelateerde_accounts
gebruiker.verstuur_email
einde
einde
einde
Oké, je hebt het gecodeerd, alles werkt, maar... is al deze code echt goed? Misschien kunnen we het beter schrijven? Ten eerste breekt het het basisprincipe van programmeren - Single Responsibility, dus we kunnen het zeker beter schrijven. Maar hoe? Dit is waar de al genoemde PORO je komt helpen. Het is voldoende om een klasse RegistrationService op te splitsen, die verantwoordelijk is voor slechts één ding: het aanmelden van alle gerelateerde services. Met services bedoelen we de individuele acties die we hierboven al hebben genoemd. In dezelfde controller hoef je alleen maar een object RegistrationService te maken en de methode "fire!" aan te roepen. De code is veel duidelijker geworden, onze controller neemt minder ruimte in beslag en elk van de nieuw aangemaakte klassen is nu verantwoordelijk voor slechts één actie, zodat we ze gemakkelijk kunnen vervangen als dat nodig mocht zijn.
klasse RegistratieService
def fire(params)
gebruiker = Gebruiker.nieuw(params)
if user.valid? && ip_validator.valid?(registration_ip)
user.save!
na_geregistreerde_gebeurtenissen(gebruiker)
einde
gebruiker
einde
privé
def na_geregistreerde_gebeurtenissen(gebruiker)
BonussenCreator.new.fire!(gebruiker)
AccountsSynchronizator.fire!(gebruiker)
EmailSender.fire!(gebruiker)
einde
def ip_validator
@ip_validator ||= IpValidator.new
einde
einde
klasse RegistratieController < ApplicationController
def aanmaken
gebruiker = RegistrationService.new.fire!(registration_params)
einde
einde
Maar Plain Old Ruby Object kan niet alleen nuttig zijn voor controllers. Stel je voor dat de applicatie die je maakt gebruik maakt van een maandelijks factureringssysteem. De exacte dag waarop zo'n factuur wordt aangemaakt is niet belangrijk voor ons, we moeten alleen weten dat het om een specifieke maand en jaar gaat. Natuurlijk kun je de dag instellen voor de eerste dag van elke maand en deze informatie opslaan in het object van de klasse "Datum", maar dit is geen echte informatie en je hebt het ook niet nodig in je applicatie. Door PORO te gebruiken kun je een klasse "MonthOfYear" maken, waarvan de objecten precies de informatie opslaan die je nodig hebt. Als je bovendien de module "Comparable" toepast, is het mogelijk om de objecten te itereren en te vergelijken, net als wanneer je de klasse "Date" gebruikt.
klasse MaandJaar
include Vergelijkbaar
attr_lezer :jaar, :maand
def initialiseer(maand, jaar)
raise ArgumentError tenzij month.between?(1, 12)
@jaar, @maand = jaar, maand
einde
def (other)
[jaar, maand] [ander.jaar, ander.maand]
end
einde
Laat me kennismaken met Rails.
In de Rails-wereld zijn we gewend dat elke klasse een model, een view of een controller is. Ze hebben ook hun precieze locatie in de directorystructuur, dus waar kun je ons kleine PORO-leger plaatsen? Overweeg een paar opties. De eerste gedachte die bij je opkomt is: als de aangemaakte klassen geen modellen, views of controllers zijn, moeten we ze allemaal in de map "/lib" zetten. Theoretisch is dit een goed idee, maar als al je PORO-bestanden in één map terechtkomen en de applicatie groot wordt, wordt deze map al snel een donkere plek die je niet durft te openen. Daarom is het ongetwijfeld geen goed idee.
GeweldigProject
├──app
│ ├─controllers
Modellen
│ └─weergaven
│
└─lib
└─diensten
#all poro hier
Je kunt ook een aantal van je klassen niet-ActiveRecord modellen noemen en ze in de map "app/models" zetten, en degenen die verantwoordelijk zijn voor het afhandelen van andere klassen services noemen en ze in de map "app/services" zetten. Dit is een vrij goede oplossing, maar het heeft één nadeel: wanneer je een nieuwe PORO maakt, moet je elke keer beslissen of het meer een model of een service is. Op deze manier kun je een situatie bereiken waarin je twee donkere plekken in je applicatie hebt, alleen kleinere. Er is nog een derde benadering, namelijk: namespaced klassen en modules gebruiken. Het enige wat je hoeft te doen is een map aanmaken die dezelfde naam heeft als een controller of een model, en daarin alle PORO-bestanden plaatsen die door de gegeven controller of het gegeven model worden gebruikt.
GeweldigProject
├──app
│ ├─controllers
│ │ ├─registration_controller
│ │ └─registration_service.rb
│ │ └─registration_controller.rb
│ ├─modellen
│ │ ├─settlement
│ │ └─month_of_year.rb
│ │ └─settlement.rb
│ └─weergaven
│
└─lib
Dankzij deze indeling hoef je bij gebruik de naam van een class niet te laten voorafgaan door een namespace. Je hebt kortere code en een logischer georganiseerde mappenstructuur gekregen.
Bekijk mij!
Het is een aangename verrassing dat wanneer je PORO gebruikt, de unit tests van je applicatie sneller en eenvoudiger te schrijven zijn en later ook beter begrepen worden door anderen. Omdat elke klasse nu verantwoordelijk is voor slechts één ding, kun je de randvoorwaarden eerder herkennen en er gemakkelijk passende testscenario's aan toevoegen.
beschrijven MonthOfYear doen
subject { MonthOfYear.new(11, 2015) }
it { should be_kind_of Comparable }
beschrijf "nieuwe instantie maken" do
it "initialiseert met het juiste jaar en de juiste maand" do
expect { described_class.new(10, 2015) }.to_not raise_error
einde
it "raises error when given month is incorrect" do
expect { described_class.new(0, 2015) }.to raise_error(ArgumentError)
expect { described_class.new(13, 2015) }.to raise_error(ArgumentError)
einde
einde
einde
Ik hoop dat we elkaar nog eens ontmoeten!
De voorbeelden die we hebben laten zien laten duidelijk zien dat het gebruik van PORO de leesbaarheid van applicaties verbetert en ze modulairder maakt, en daardoor makkelijker te beheren en uit te breiden. Het omarmen van het principe van Single Responsibility vergemakkelijkt het uitwisselen van bepaalde klassen indien nodig, en dit zonder andere elementen te verstoren. Het maakt ook het testen ervan een eenvoudigere en snellere procedure. Bovendien is het op deze manier veel eenvoudiger om Rails modellen en controllers kort te houden, en we weten allemaal dat ze de neiging hebben om onnodig groot te worden tijdens het ontwikkelingsproces.