Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Callbacks do ActiveRecord - o mal secreto ou apenas mal compreendidos?

Callbacks do ActiveRecord - o mal secreto ou apenas mal compreendidos?

Callbacks do ActiveRecord são um dos recursos mais controversos da comunidade Ruby/Rails e são muitas vezes associados a sinônimo de manutenção custosa. Afinal, é possível reduzir sofrimento e fricção sem abrir mão dessa ~ferramenta afiada~? Nessa talk, eu pretendo explorar o que faz esse tema ser tão polêmico, alguns cenários em que callbacks podem ser um recurso valoroso e, principalmente: o que eles tem a nos dizer sobre design de software.

Rondy Sousa

March 19, 2019
Tweet

More Decks by Rondy Sousa

Other Decks in Programming

Transcript

  1. “if you do Rails for a living, you have to

    get along[1] with ActiveRecord callbacks” [1] get along: neither being indifferent or resistant to TÍTULO ALTERNATIVO #1: !
  2. about me: @Rondy Desenvolvedor/consultor de software Cota da diáspora paraense

    na Plataformatec Atualmente ganhando a vida escrevendo mindmaps Mordido algumas vezes por callbacks (morreu mas passa bem)
  3. Ecto, Trailblazer, Hanami, ROM.rb… A comunidade Ruby/Rails (e seu vizinho

    Elixir) já deram algumas respostas pra esse assunto
  4. Callbacks do ActiveRecord vão perdurar por bastante tempo, entretanto… “On

    Writing Software Well #2: Using callbacks to manage auxiliary complexity” https://www.youtube.com/watch? v=m1jOWu7woKM
  5. • DRY e callbacks em controllers do Rails • DRY

    em testes (DRY vs DAMP) • Service objects • Mensagens de commits do Git • Documentação de código
  6. Sharp knives https://rubyonrails.org/doctrine/#provide-sharp- knives We enforce such good senses by

    convention, by nudges, and through education. Not by banning sharp knives from the kitchen and insisting everyone use spoons to slice tomatoes.
  7. Roteiro geral desta talk • Recapitular cenários em que callbacks

    são geralmente utilizados. • Refletir sobre o pensamento “callbacks, tal qual o Rails, não escalam”. Verdade absoluta? Existem apenas desvantagens? • Fornecer um modelo mental e racional teórico pra tomada de decisão.
  8. “Incidental soundtrack” • Pirâmide de testes • High level vs

    low level components (“uso” vs “reuso”) • 50 shades of service objects • DDD Aggregates
  9. • Os dados de uma pessoa devem ser persistidos no

    banco de dados: e-mail e CPF, ambos como String. • O CPF deve ser persistido apenas como dígitos. Isto é, caso o input do usuário siga o formato "999.999.999-99", esse valor deve ser persistido como "99999999999". • Após os dados da pessoas serem persistidos no banco, um e-mail de boas-vindas deve ser enviado para o endereço fornecido.
  10. def create person = Person.new(params[:person].permit(:cpf, :email)) person.cpf = person.cpf.gsub(/\D/, '')

    person.save! PersonMailer.welcome_email(person.email).deliver_now render :ok end Implementação #1:
  11. def create person = Person.new(params[:person].permit(:cpf, :email)) person = normalize_person_cpf(person) person.save!

    notify_welcome_email(person) render :ok end private def normalize_person_cpf(person) person.cpf = person.cpf.gsub(/\D/, '') person end def notify_welcome_email(person) PersonMailer .welcome_email(person.email) .deliver_now end Refatorando com extract method:
  12. def create person = Person.new(…) person = normalize_person_cpf(person) person.save! notify_welcome_email(person)

    render :ok end private def normalize_person_cpf(person); end def notify_welcome_email(person); end API geral após o primeiro refactoring:
  13. Controller Model Dois comportamentos desse fluxo são comumente extraídos da

    camada de controller para uma camada de domínio: notify_welcome_email - a lógica de envio de e-mail. - a lógica de normalizar o valor do CPF; e normalize_cpf
  14. class Person < ApplicationRecord end def create person = Person.new(…)

    person = normalize_person_cpf(person) person.save! notify_welcome_email(person) render :ok end private def normalize_person_cpf(person); end def notify_welcome_email(person); end Saindo do “fat controllers, skinny models”…
  15. def create person = Person.create!(…) render :ok end class Person

    < ApplicationRecord before_save :normalize_cpf after_create :notify_welcome_email private def normalize_cpf; end def notify_welcome_email; end end para o “skinny controllers, fat (domain rich) models…”
  16. class Person < ApplicationRecord # a `before_validation` callback could also

    be applied here. before_save :normalize_cpf after_create :notify_welcome_email private def normalize_cpf self.cpf = self.cpf.gsub(/\D/, '') self end def notify_welcome_email PersonMailer .welcome_email(self.email) .deliver_now end end Model Person com as lógicas agora controladas via callbacks
  17. def create person = Person.new(…) person = normalize_person_cpf(person) person.save! notify_welcome_email(person)

    render :ok end private def normalize_person_cpf(person); end def notify_welcome_email(person); end def create person = Person.create!( params[:person].permit(:cpf, :email) ) render :ok end class Person < ApplicationRecord before_save :normalize_cpf after_create :notify_welcome_email private def normalize_cpf; end def notify_welcome_email; end end Antes de extrair pra callbacks do AR: Após extrair pra callbacks do AR:
  18. “o que levar em consideração para o uso ou não

    de callbacks do ActiveRecord?”
  19. VANTAGEM #1 Callbacks provém DSLs que ajudam a estruturar e

    comunicar o ciclo de vida de uma entidade de domínio (ex: antes de persistir uma entidade de domínio, faça X, e após persisti-la, faça Y)
  20. class Person < ApplicationRecord before_save :normalize_cpf after_create :notify_welcome_email private def

    normalize_cpf; end def notify_welcome_email; end end - “Antes de persistir uma pessoa, o seu CPF deve ser normalizado (apenas caracteres numéricos permanecem).” - “Após persistido com sucesso, devemos enviar um e-mail de boas-vindas.”
  21. VANTAGEM #2 Eles auxiliam no controle transacional das operações de

    banco de dados (i.e.: um erro em um callback gera um rollback da operação, garantindo consistência do banco de dados)
  22. O seu uso descuidado pode ser foco de complexidade, principalmente

    quando o model é exposto a diferentes contextos de negócio
  23. Ex 1: “no cenário A, onde tudo é bonito e

    todos são felizes, todas as lógicas, X, Y, Z devem acontecer” Ex 2: “no cenário B (um contexto levemente diferente), entretanto, algumas regras devem ser evitadas, enquanto outras lógicas adicionais devem acontecer”
  24. Ex 1: “no cenário A, onde tudo é bonito e

    todos são felizes, todas as lógicas, X, Y, Z devem acontecer” Ex 2: “no cenário B (um contexto levemente diferente), entretanto, algumas regras devem ser evitadas, enquanto outras lógicas adicionais devem acontecer”
  25. “Registrar usuários de migração de usuários legados via lote (ex:

    rake task), em que a notificação de boas-vindas é indesejada” Exemplo de requisito futuro:
  26. Um reflexo dessa complexidade pode ser refletido também na suíte

    de testes: Ex: “em alguns testes, eu não preciso que alguma operação dispendiosa aconteça em todos os test cases, como por exemplo, fazer alguma chamada de API ou indexação de ElasticSearch”).
  27. class Product < ApplicationRecord def launch_without_notifications Notification.suppress do launch! end

    end end https://github.com/rails/rails/pull/18910 https://github.com/rails/rails/issues/18847
  28. Responsabilidades: - Remove alguns caracteres do valor fornecido - Atribui

    o novo valor normalizado ao campo CPF Reflexões/efeitos colaterais: - Apenas o estado interno do objeto é alterado - O resultado dessa operação é previsível: "999.999.999-99" => “99999999999” - Testes, em teoria, não precisariam persistir o banco de dados Lógica que normaliza o CPF class Person < ApplicationRecord before_save :normalize_cpf after_create :notify_welcome_email private def normalize_cpf self.cpf = self.cpf.gsub(/\D/, '') self end def notify_welcome_email; end end
  29. - Operação um pouco mais sensível - Interage com um

    serviço que depende de uma operação de rede (IO bound) - Pode gerar side-effects inesperados (ou, no mínimo, onde é difícil se ter controle) - Outras operações semelhantes poderiam ser: chamadas de API (RPC em geral), manipulação de arquivos, execução de processos OS, etc. Lógica que envia e-mail de boas-vindas class Person < ApplicationRecord after_create :notify_welcome_email private def notify_welcome_email PersonMailer .welcome_email(self.email) .deliver_now end end Reflexões/efeitos colaterais:
  30. Person PersonMailer -Depende de estado externo fora do objeto em

    questão. -Operação com um colaborador externo (PersonMailer) que pode gerar side-effects. -Complexidade manifesta em comentários do tipo: “todos os nossos testes unitários que persistem um model Person devem tratar o envio de e- mail, seja aceitando a operação ou mockando-a de alguma forma”. class Person < ApplicationRecord after_create :notify_welcome_email private def notify_welcome_email PersonMailer .welcome_email(self.email) .deliver_now end end
  31. Person; after_create :notifity_welcome_email Acopla o ciclo de vida de um

    objeto, o model Person que controla seu próprio estado interno, com um outro objeto, o PersonMailer, que também mantém seu próprio estado.
  32. Infraestrutura, código de suporte Casos de uso / Fluxos de

    negócios Lógica de domínio (ciclo de vida de entidades, validações, regras de negócio) Uso Reuso def normalize_cpf; end def notify_welcome_email; end def normalize_cpf; end + def notify_welcome_email; end Business centric Tech centric
  33. - “Fluxo de registrar pessoa” Business centric Business people def

    normalize_cpf; end + def notify_welcome_email; end “Isso me interessa! Esse fluxo bem feito (usável, rápido, confiável, etc.) pode aumentar a taxa de conversão da base de clientes…”
  34. “Music is the space between the notes” - Claude DeBussy

    def create person = Person.new(…) person = normalize_person_cpf(person) person.save! notify_welcome_email(person) render :ok
  35. Essa lógica pertence a um fluxo de negócios de “registrar

    nova pessoa”, e não diretamente do ciclo de vida de uma entidade de domínio. Essa diferença é crucial, pois o model Person pode viver independente do fato que o fluxo de registro de pessoas envia um e-mail.
  36. Registrar pessoas Pessoa normalizar CPF da pessoa Mailer da Pessoa

    enviar email de boas-vindas high level components low level components
  37. class Person < ApplicationRecord before_save :normalize_cpf after_create :notify_welcome_email private def

    normalize_cpf; end def notify_welcome_email; end end O orquestrador de um fluxo de negócios sendo acoplado numa camada de infraestrutura Usando callbacks pra controlar fluxos de negócios:
  38. Ou: manter a lógica de envio de e-mail fora do

    model vai potencialmente ser um atrativo de complexidade ao longo do tempo
  39. A lógica de enviar o e-mail de boas- vindas pode/deve

    ser movida para fora do model Person Recomendação do tribunal do juri:
  40. Três caminhos naturais de evolução (ou “50 shades of service

    objects”): 1.Ela pode ser movida de volta para o controller. 2.Ela pode ser movida para um novo método do model Person, focado no fluxo de registrar pessoas. 3.Ela pode ser movida para um novo objeto em nível de serviço (a.k.a., o pattern “service object”).
  41. 1) Movendo a lógica de e-mail de volta para o

    controller: def create person = Person.create!( params[:person].permit(:cpf, :email) ) PersonMailer.welcome_email(person.email).deliver_now render :ok end class Person < ApplicationRecord before_save :normalize_cpf private def normalize_cpf self.cpf = self.cpf.gsub(/\D/, '') self end end
  42. 2) Movendo a lógica para um método especializado de Person:

    def create Person.register_person!(params[:person].permit(:cpf, :email)) render :ok end class Person < ApplicationRecord before_save :normalize_cpf # it is expected to be refactored as things grow up. def self.register_person!(person_params) person_instance = Person.create!(person_params) PersonMailer .welcome_email(person_instance.email) .deliver_now end private def normalize_cpf self.cpf = self.cpf.gsub(/\D/, '') self end end Lógica presente no model porém não acoplado ao seu ciclo de vida
  43. 3) Movendo a lógica de e-mail para um service object:

    def create RegisterPerson.new.call!(params[:person].permit(:cpf, :email)) render :ok end class RegisterPerson def call!(user_params) person = Person.create!(person_params) PersonMailer.welcome_email(person.email).deliver_now end end class Person < ApplicationRecord before_save :normalize_cpf private def normalize_cpf self.cpf = self.cpf.gsub(/\D/, '') self end end
  44. - ❌ Abstração do fluxo de negócios implícita - ❌

    Acoplada à camada de infraestrutura do framework - ❌ Baixo reuso - Favorece design emergente - Abstração do fluxo de negócios explícita - Baixo nível de indireção - Garante reuso - ❌ Acopla high level com low level - Abstração do fluxo de negócios explícita - ❌ Introduz indireção - Garante reuso - Bom nível de separation of concerns Lógica do fluxo no controller PeopleController#create Lógica do fluxo no método especializado no model Person Person.register_person Lógica do fluxo em um service object RegisterPerson#call 1) 2) 3)
  45. Uma nota sobre service objects • Service Objects (Rails) •

    DHH Basecamp controllers (Rails) - http://jeromedalbert.com/how-dhh-organizes-his-rails- controllers/ • Operation (Trailblazer, Hanami) • Interactor & use case (Hexagonal) • Command (DDD, CQRS) Business people
  46. A escolha de uma delas vai depender do contexto do

    seu projeto. Ex: • O quanto esse fluxo é passível de mudanças? • O quanto ele pode evoluir para um caminho mais complexo? • Esse trecho de código representa um fluxo do core business da sua aplicação? • Esse trecho de código já foi implantado em produção ao menos uma vez? (YAGNI)
  47. • O ponto principal é que ele não estaria mais

    vinculado ao ciclo de vida de um objeto de domínio (i.e., em nível unitário, possivelmente reutilizável em outros contextos), • e sim a um cenário em nível de integração, que expressa um comportamento de negócios do sistema (i.e., em camadas de controller ou service object).
  48. Após esses exercícios de refatorações, é possível finalmente chegar a

    um modelo mental que indique quando callbacks são aceitáveis ou não…
  49. •controle de estado presente na interação entre diferentes objetos… •…acoplamento

    dessa interação entre elementos high level / low level Aspectos relevantes de se considerar
  50. É recomendável evitar callbacks em lógicas que exercitem um colaborador

    externo e que gere um possível side-effect Sendo mais específico:
  51. class Person < ApplicationRecord before_save :normalize_cpf after_create :notify_welcome_email private def

    normalize_cpf; end def notify_welcome_email; end end - altera o estado interno do model Person - pertence ao ciclo de vida do model Person - uso de callbacks é oportuno - exercita colaborador externo que gera side- effect - independe do ciclo de vida de uma entidade de domínio - vinculado a um fluxo de negócios - não deveria ser acionado via callbacks
  52. class Person < ApplicationRecord before_save :normalize_cpf private def normalize_cpf; end

    end def create person = Person.create!(…) notify_welcome_email(person) render :ok end private def notify_welcome_email(person); end Versão possivelmente recomendada
  53. “O que aconteceria se o callback pra normalize_cpf fosse removido

    da camada de domínio?” Não existe pergunta boba:
  54. def create person = Person.new(…) person = normalize_person_cpf(person) person.save! notify_welcome_email(person)

    render :ok end private def normalize_person_cpf(person); end def notify_welcome_email(person); end Anemic domain model class Person < ApplicationRecord end https://www.martinfowler.com/bliki/ AnemicDomainModel.html
  55. # Person is responsible to keep its own internal data

    in a valid state. # Outside world should not be aware of how it does so. class Person def initialize(params = {}) @cpf = normalize_cpf(params.fetch(:cpf)) end def cpf=(new_cpf) @cpf = normalize_cpf(new_cpf) end private def normalize_cpf(cpf) cpf.gsub(/\D/, '') end end
  56. “Se um dia houver necessidade de alterar o algoritmo de

    normalização de CPF, ou essa normalização simplesmente não for mais relevante, a mudança estaria centralizada (i.e., apenas um ponto de mudança) e protegida”
  57. Benefícios esperado desse racional - É esperado que a base

    de código seja mais simples de manter e manusear: •Separação de responsabilidades •Distribuição de efeitos colaterais em camadas mais externas e integradas da aplicação Menos bikeshedding!
  58. Callbacks do ActiveRecord são aplicáveis em alguns cenários e podem

    trazer ganhos sim de legibilidade e consistência ✨ (ex: família de callbacks before_[save| create|update]) PONTO CHAVE #1
  59. Fique atento a lógicas de fluxo de negócio sendo incorporadas

    ao ciclo de vida de uma entidade de domínio ⚠ PONTO CHAVE #2
  60. Callbacks after_ geralmente representam pontos futuros de integração/reação numa arquitetura

    distribuída, reativa, orientada a eventos https://blog.arkency.com/2016/05/domain-events-over- active-record-callbacks/ PONTO CHAVE BONUS (observers, wisper…)
  61. - manipula apenas estado interno do meu model? - a

    lógica de manipulação está presente fora do meu model? - manipula algum colaborador que gera side-effects? - manipula algum membro filho da entidade agregadora? Callbacks before_* Callbacks after_*