Mnoho lidí se učí Ruby tak, že začíná s frameworkem Rails, a to je bohužel ten nejhorší možný způsob, jak se tento jazyk naučit. Nechápejte mě špatně: Rails je skvělý, pomáhá vám rychle a efektivně vytvářet webové aplikace, aniž byste museli pronikat do mnoha technických detailů.
Rád vás poznávám!
Mnoho lidí se učí Ruby tím, že začnete Rails a to je bohužel ten nejhorší možný způsob, jak se tento jazyk naučit. Nechápejte mě špatně: Rails je skvělý, pomáhá vám rychle a efektivně vytvářet webové aplikace, aniž byste museli pronikat do mnoha technických detailů. Poskytují spoustu "kouzel Rails", díky kterým věci jednoduše fungují. A pro začínajícího programátora je to opravdu skvělé, protože nejpříjemnějším okamžikem celého procesu je, když můžete říct "žije to!" a vidíte, že všechny části do sebe zapadají a lidé vaši aplikaci používají. Rádi jsme "tvůrci" 🙂 Dobré programátory ale od těch průměrných odlišuje jedna věc: ti dobří rozumí tomu, jak fungují nástroje, které používají. A tím "rozumět svým nástrojům" nemyslím znát všechny metody a moduly, které framework poskytuje, ale rozumět tomu, jak to funguje, rozumět tomu, jak se děje "Rails magic". Teprve pak se můžete cítit pohodlně při používání objektů a programování s Rails. Základem objektově orientovaného programování a tajnou zbraní, která usnadňuje složité aplikace pro Rails, je již v nadpisu zmíněné PORO, tedy Plain Old Ruby Object
Co se pod tímto názvem skutečně skrývá? Co je to za velkou tajnou zbraň? Je to jednoduchá třída v jazyce Ruby, která z ničeho nedědí. Ano, právě to, a tolik.
třída AwesomePoro
konec
Jak vám mohu pomoci?
S rostoucím počtem uživatelů a jejich očekáváním neustále vyvíjíte svou aplikaci a přidáváte nové funkce. Dostáváte se do bodu, kdy narážíte na stále temnější místa extrémně zvrácené logiky, místa, kterým se i ti nejodvážnější vývojáři vyhýbají jako moru. Čím více takových míst, tím obtížnější je aplikaci spravovat a vyvíjet. Standardním příkladem je akce registrace nového uživatele, která spouští celou skupinu dalších akcí spojených s touto událostí:
- kontrola IP adresy v databázi nevyžádané pošty,
- odeslání e-mailu novému uživateli,
- přidání bonusu na účet doporučujícího uživatele,
- vytváření účtů v souvisejících službách,
- a mnoho dalších...
Ukázka kód odpovědný za registraci uživatele může vypadat takto:
třída RegistrationController < ApplicationController
def create
user = User.new(registration_params)
if user.valid? && ip_valid?(registration_ip)
user.save!
user.add_bonuses
user.synchronize_related_accounts
user.send_email
end
end
end
Dobře, máte to nakódované, všechno funguje, ale... je celý ten kód opravdu v pořádku? Možná bychom to mohli napsat lépe? Především to porušuje základní princip programování - Single Responsibility, takže bychom to jistě mohli napsat lépe. Ale jak? Tady přichází na pomoc již zmíněné PORO. Stačí vyčlenit třídu RegistrationService, která bude mít na starosti jedinou věc: notifikaci všech souvisejících služeb. Službami budeme rozumět jednotlivé akce, které jsme již vyčlenili výše. Ve stejném kontroléru stačí vytvořit objekt RegistrationService a zavolat na něm metodu "fire!". Kód se stal mnohem přehlednějším, náš kontrolér zabírá méně místa a každá z nově vytvořených tříd je nyní zodpovědná pouze za jednu akci, takže je můžeme v případě potřeby snadno nahradit.
třída RegistrationService
def fire!(params)
user = User.new(params)
if user.valid? && ip_validator.valid?(registration_ip)
user.save!
after_registered_events(user)
konec
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
třída RegistrationController < ApplicationController
def create
user = RegistrationService.new.fire!(registration_params)
end
end
Nicméně Plain Old Ruby Object se může ukázat jako užitečný nejen pro řadiče. Představte si, že aplikace, kterou vytváříte, používá měsíční fakturační systém. Přesný den vytvoření takového vyúčtování není důležitý pro nás, potřebujeme pouze vědět, že se týká konkrétního měsíce a roku. Samozřejmě můžete nastavit den pro první den každého měsíce a uložit tuto informaci do objektu třídy "Date", ale ani to není pravdivá informace, ani ji ve své aplikaci nepotřebujete. Pomocí PORO můžete vytvořit třídu "MonthOfYear", jejíž objekty budou uchovávat přesně ty informace, které potřebujete. Navíc když v ní použijete modul "Comparable", bude možné iterovat a porovnávat její objekty, stejně jako když používáte třídu Date.
třída MonthOfYear
include Comparable
attr_reader :year, :month
def initialize(month, year)
raise ArgumentError unless month.between?(1, 12)
@rok, @měsíc = rok, měsíc
end
def (other)
[rok, měsíc] [other.year, other.month]
end
end
Představte mi systém Rails.
Ve světě Rails jsme zvyklí, že každá třída je model, view nebo controller. Mají také své přesné umístění v adresářové struktuře, kam tedy umístit naši malou PORO armádu? Zvažte několik možností. První myšlenka, která nás napadne, je: pokud vytvořené třídy nejsou ani modely, ani pohledy, ani kontroléry, měli bychom je všechny umístit do adresáře "/lib". Teoreticky je to dobrý nápad, nicméně pokud všechny soubory PORO přistanou v jednom adresáři a aplikace bude rozsáhlá, stane se tento adresář rychle temným místem, které se budete bát otevřít. Proto to nepochybně není dobrý nápad.
Úžasný projekt
├──── aplikace
│ ├─ ovladače
│ ├─ modely
│ └─ náhledy
│
└─lib
└─služby
#ady je vysoká porota
Některé třídy, které nejsou modely ActiveRecord, můžete také pojmenovat Modely a umístit je do adresáře "app/models" a ty, které jsou zodpovědné za obsluhu jiných tříd, pojmenovat Služby a umístit je do adresáře "app/services". To je docela dobré řešení, ale má jednu nevýhodu: při vytváření nového PORO se budete muset pokaždé rozhodnout, zda se jedná spíše o model, nebo o službu. Tímto způsobem se můžete dostat do situace, kdy budete mít v aplikaci dvě temná místa, pouze menší. Existuje ještě třetí přístup, a sice: použití tříd a modulů se jmenným prostorem. Stačí vytvořit adresář, který se jmenuje stejně jako kontrolér nebo model, a do něj umístit všechny soubory PORO používané daným kontrolérem nebo modelem.
Úžasný projekt
├──── aplikace
│ ├─ ovladače
│ │ ├─registration_controller
│ │ │ └─registration_service.rb
│ │ └─registration_controller.rb
│ ├─modely
│ │ ├─settlement
│ │ │ └─month_of_year.rb
│ │ └─settlement.rb
│ └─pohledy
│
└─lib
Díky tomuto uspořádání nemusíte při použití před název třídy uvádět jmenný prostor. Získali jste kratší kód a logičtěji uspořádanou adresářovou strukturu.
Podívejte se na mě!
Je příjemným překvapením, že při použití PORO se unit testy vaší aplikace píšou rychleji a snadněji a později je pravděpodobnější, že je pochopí i ostatní. Protože každá třída je nyní zodpovědná pouze za jednu věc, můžete dříve rozpoznat okrajové podmínky a snadno k nim přidat vhodné testovací scénáře.
describe MonthOfYear do
subject { MonthOfYear.new(11, 2015) }
it { should be_kind_of Comparable }
describe "creating new instance" do
it "inicializuje se správným rokem a měsícem" do
expect { described_class.new(10, 2015) }.to_not raise_error
end
it "vyvolá chybu, když je daný měsíc nesprávný" do
expect { described_class.new(0, 2015) }.to raise_error(ArgumentError)
expect { described_class.new(13, 2015) }.to raise_error(ArgumentError)
end
end
end
Doufám, že se ještě setkáme!
Příklady, které jsme uvedli, jasně ukazují, že použití PORO zlepšuje čitelnost aplikací a činí je modulárnějšími, a tedy i snadněji spravovatelnými a rozšiřitelnými. Přijetí principu jednotné odpovědnosti usnadňuje výměnu jednotlivých tříd v případě potřeby, a to bez zásahu do ostatních prvků. Zjednodušuje a urychluje také jejich testování. Navíc je tímto způsobem mnohem snazší udržovat modely a kontroléry Rails krátké, a všichni víme, že v průběhu vývoje mají tendenci zbytečně narůstat.