Muitas pessoas estão a aprender Ruby começando com a framework Rails e, infelizmente, esta é a pior forma possível de aprender esta linguagem. Não me interpretem mal: O Rails é excelente, ajuda-o a criar aplicações Web de forma rápida e eficiente sem ter de entrar em muitos pormenores técnicos.
É um prazer conhecer-vos!
Muitas pessoas estão a aprender Rubi começando com Carris e, infelizmente, esta é a pior forma possível de aprender esta língua. Não me interpretem mal: O Rails é excelente, ajuda-o a criar aplicações Web de forma rápida e eficiente sem ter de entrar em muitos pormenores técnicos. Eles fornecem um monte de "magia Rails" que faz as coisas simplesmente funcionarem. E para um programador novato isso é realmente ótimo, porque o momento mais agradável do processo é quando você pode dizer "está vivo!", e ver que todas as partes se encaixam e as pessoas usam seu aplicativo. Gostamos de ser "criadores" 🙂 Mas há uma coisa que distingue os bons programadores da média: os bons programadores compreendem como funcionam as ferramentas que utilizam. E por "entender suas ferramentas" não quero dizer conhecer todos os métodos e módulos fornecidos por um framework, mas entender como ele funciona, entender como a "mágica do Rails" acontece. Só então você poderá se sentir confortável com o uso de objetos e a programação com Rails. A base da programação orientada a objectos, e a arma secreta que torna mais fácil a complicada aplicação Rails, é o já mencionado no título PORO, que é Plain Old Ruby Object
O que é que está realmente por detrás deste nome? O que é esta grande arma secreta? É uma simples classe Ruby que não herda de nada. Sim, apenas isso, e muito mais.
classe AwesomePoro
fim
Como é que o posso ajudar?
Está continuamente a desenvolver a sua aplicação e a acrescentar novas funcionalidades à medida que o número de utilizadores e as suas expectativas aumentam. Chega-se a um ponto em que se encontram cada vez mais lugares obscuros de lógica extremamente distorcida, os lugares que são evitados como a peste até pelos programadores mais corajosos. Quanto mais lugares assim, mais difícil é gerir e desenvolver a aplicação. Um exemplo normal é a ação de registo de um novo utilizador, que desencadeia todo um grupo de outras acções associadas a este evento:
- verificar o endereço IP numa base de dados de spam,
- enviar uma mensagem de correio eletrónico para o novo utilizador,
- adicionar um bónus a uma conta de um utilizador que recomenda,
- criar contas em serviços conexos,
- e muito mais...
Uma amostra código responsável pelo registo do utilizador pode ter o seguinte aspeto:
classe 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
utilizador.enviar_email
fim
fim
fim
Muito bem, já está codificado, tudo funciona, mas... este código está mesmo correto? Talvez o pudéssemos escrever melhor? Antes de mais, quebra o princípio básico da programação - Responsabilidade Única, por isso, certamente que o podemos escrever melhor. Mas como? É aqui que o já mencionado PORO vem ajudar. Basta separar uma classe RegistrationService, que será responsável por apenas uma coisa: notificar todos os serviços relacionados. Por serviços, vamos considerar as acções individuais que já identificámos acima. No mesmo controlador, basta criar um objeto RegistrationService e chamar-lhe o método "fire!". O código ficou muito mais claro, o nosso controlador está a ocupar menos espaço e cada uma das classes recém-criadas é agora responsável por apenas uma ação, pelo que podemos substituí-las facilmente em caso de necessidade.
classe RegistrationService
def fire!(params)
user = User.new(params)
if user.valid? && ip_validator.valid?(registration_ip)
user.save!
after_registered_events(user)
fim
utilizador
fim
privado
def after_registered_events(user)
BonusesCreator.new.fire!(utilizador)
AccountsSynchronizator.fire!(utilizador)
EmailSender.fire!(utilizador)
fim
def ip_validator
@ip_validator ||= IpValidator.new
end
end
classe RegistrationController < ApplicationController
def create
utilizador = RegistrationService.new.fire!(registration_params)
end
end
No entanto, o Plain Old Ruby Object pode revelar-se útil não só para controladores. Imagine que a aplicação que está a criar utiliza um sistema de faturação mensal. O dia exato da criação dessa faturação não é importante para nósSó precisamos de saber que se trata de um mês e de um ano específicos. Claro que pode definir o dia para o primeiro dia de cada mês e guardar esta informação no objeto da classe "Date", mas não é uma informação verdadeira, nem precisa dela na sua aplicação. Ao utilizar a PORO, pode criar uma classe "MonthOfYear", cujos objectos armazenarão a informação exacta de que necessita. Além disso, ao aplicar nela o módulo "Comparable", será possível iterar e comparar os seus objectos, tal como quando utiliza a classe Date.
class MonthOfYear
include Comparável
attr_reader :ano, :mês
def initialize(mês, ano)
raise ArgumentError unless month.between?(1, 12)
@ano, @mês = ano, mês
fim
def (outro)
[ano, mês] [outro.ano, outro.mês]
end
fim
Apresenta-me o Rails.
No mundo Rails, estamos habituados ao facto de cada classe ser um modelo, uma vista ou um controlador. Também têm a sua localização exacta na estrutura de diretórios, por isso, onde pode colocar o nosso pequeno exército PORO? Considere algumas opções. O primeiro pensamento que nos vem à cabeça é: se as classes criadas não são modelos, nem vistas, nem controladores, devemos colocá-las todas no diretório "/lib". Teoricamente, é uma boa ideia, no entanto, se todos os seus ficheiros PORO forem parar a uma diretoria e a aplicação for grande, esta diretoria tornar-se-á rapidamente um lugar escuro que tem medo de abrir. Portanto, sem dúvida, não é uma boa ideia.
AwesomeProject
├──app
│ ├─controladores
│ ├─modelos
│ └─visualizações
│
└─lib
└─serviços
1TP69Poro alto aqui
Também pode dar o nome de Models a algumas das suas classes não-ActiveRecord e colocá-las no diretório "app/models", e dar o nome de services às classes responsáveis pelo tratamento de outras classes e colocá-las no diretório "app/services". Esta é uma solução bastante boa, mas tem um inconveniente: ao criar um novo PORO, terá de decidir sempre se se trata mais de um modelo ou de um serviço. Desta forma, pode chegar a uma situação em que tem dois sítios obscuros na sua aplicação, só que mais pequenos. Existe ainda uma terceira abordagem, nomeadamente: a utilização de classes e módulos com espaço de nome. Tudo o que precisa de fazer é criar um diretório com o mesmo nome de um controlador ou de um modelo, e colocar todos os ficheiros PORO utilizados por esse controlador ou modelo.
AwesomeProject
├──app
│ ├─controladores
│ │ ├─controlador_de_registo
│ │ └─registration_service.rb
│ │ └─registration_controller.rb
│ ├─models
│ │ ├─settlement
│ │ └─month_of_year.rb
│ │ └─settlement.rb
│ └─visualizações
│
└─lib
Graças a esta disposição, quando a utiliza, não tem de preceder o nome de uma classe com um espaço de nomes. Ganhou um código mais curto e uma estrutura de diretórios mais organizada logicamente.
Vejam-me!
É uma agradável surpresa que, ao utilizar a PORO, os testes unitários da sua aplicação sejam mais rápidos e fáceis de escrever e, mais tarde, mais susceptíveis de serem compreendidos por outros. Como cada classe é agora responsável por apenas uma coisa, pode reconhecer as condições de limite mais cedo e adicionar facilmente cenários de teste apropriados a elas.
describe MonthOfYear do
subject { MonthOfYear.new(11, 2015) }
it { should be_kind_of Comparable }
describe "criar nova instância" do
it "inicializa com o ano e o mês corretos" do
expect { described_class.new(10, 2015) }.to_not raise_error
end
it "levanta um erro quando o mês fornecido está incorreto" do
expect { described_class.new(0, 2015) }.to raise_error(ArgumentError)
expect { described_class.new(13, 2015) }.to raise_error(ArgumentError)
end
end
end
Espero que nos voltemos a encontrar!
Os exemplos que apresentámos mostram claramente que a utilização da PORO melhora a legibilidade das aplicações e torna-as mais modulares e, consequentemente, mais fáceis de gerir e expandir. A adoção do princípio da Responsabilidade Única facilita a troca de classes específicas, se necessário, e fá-lo sem interferir com outros elementos. Também torna os testes mais simples e rápidos. Além disso, desta forma é muito mais fácil manter os modelos e controladores Rails curtos, e todos sabemos que eles tendem a ficar desnecessariamente grandes no processo de desenvolvimento.