sobre code smells,
refactoring e design
como SOLID pode te ajudar no dia a dia
hello
1996
Elaine Naomi Watanabe
Desenvolvedora de Software (Plataformatec)
Mestre em Ciência da Computação (USP)
twitter.com/elaine_nw
speakerdeck.com/elainenaomi
2019
http://careers.plataformatec.com.br
analisar os conceitos básicos de orientação a objeto
como identificar problemas na nossa base de código
como melhorar o design do nosso código
o que vamos ver?
orientação a objeto
uma versão super resumida
Objeto
Classe
class Pessoa
def initialize(nome_recebido, email_recebido)
@nome = nome_recebido
@email = email_recebido
end
def contato
@email
end
end exemplo de uma classe
p1 = Pessoa.new("Ana", "ana@email.com")
p1.contato
=> "ana@email.com"
p1 = Pessoa.new("Ana", "ana@email.com")
p1.contato
=> "ana@email.com"
exemplo de um objeto
p2 = Pessoa.new("Bia", "beatriz@email.com")
p2.contato
=> "beatriz@email.com"
exemplo de outro objeto
Herança
Polimorfismo
Composição
exemplo de herança
class PessoaFisica < Pessoa
def diga_oi
'olar'
end
end
p1 = PessoaFisica.new("Ana", "ana@email.com")
p1.contato
=> "ana@email.com"
método herdado
p1 = PessoaFisica.new("Ana", "ana@email.com")
p1.contato
=> "ana@email.com"
p1.diga_oi
=> "olar"
p1 = Pessoa.new("Ana", "ana@email.com")
p1.contato
=> "ana@email.com"
p1.diga_oi
=> NoMethodError (undefined method `diga_oi'
for #<Pessoa:0x00007fd9cb0f0760 @nome="Ana",
@email="ana@email.com">)
p1 = Pessoa.new("Ana", "ana@email.com")
p1.contato
=> "ana@email.com"
p1.diga_oi
=> NoMethodError (undefined method `diga_oi'
for #<Pessoa:0x00007fd9cb0f0760 @nome="Ana",
@email="ana@email.com">)
class PessoaJuridica < Pessoa
def contato
@nome
end
end
exemplo de polimorfismo
class PessoaJuridica < Pessoa
def contato
@nome
end
end
método redefinido
p1 = PessoaFisica.new("Ana", "ana@email.com")
p1.contato
=> "ana@email.com"
p2 = PessoaJuridica.new("Umbrella Academy",
"contato@email.com")
p2.contato
=> "Umbrella Academy"
[p1, p2].each do |pessoa|
puts pessoa.contato
end
não importa qual pessoa estou
chamando
class PessoaAnonima < Pessoa
def contato
'...'
end
end
homenagem a talk da Cybelle
class Pessoa
def initialize(nome_recebido, forma_de_contato_recebida)
@nome = nome_recebido
@contato_favorito = forma_de_contato_recebida
end
def contato
@contato_favorito.identificador
end
end exemplo de composição
class Twitter
def initialize(nome_do_usuario)
@nome = nome_do_usuario
end
def identificador
@nome
end
end
exemplo de composição
twitter = Twitter.new("@ana")
pessoa = Pessoa.new("Ana", twitter)
pessoa.contato
=> "@ana"
class WhatsApp
def initialize(numero_telefone_recebido)
@numero_telefone = numero_telefone_recebido
end
def identificador
@numero_telefone
end
end
zapzap = WhastApp.new(5511999999999)
pessoa = Pessoa.new("Ana", zapzap)
pessoa.contato
=> 5511999999999
Reúso
Coesão
Acoplamento
Encapsulamento
class MeuChatBot
def contato
oi
end
private
def oi
'olar'
end
end
exemplo de encapsulamento
class MeuChatBot
def contato
oi
end
private
def oi
'olar'
end
end
bot = MeuChatBot.new
bot.contato
=> "olar"
bot.oi
=> NoMethodError (undefined method `olar` for
#<MeuChatBot:0x00007fd9cb0f0760>)
método oi está encapsulado/oculto
objetos + mensagens = app
e no dia a dia?
Frameworks MVC
ViewModel
Controller
apresentação
intermediador
dados +
lógica de negócio
+ Funcionalidades
+ Funcionalidades
+ Modificações
+ Funcionalidades
+ Modificações
+ Bugs
Tamanho da base de código
Tempodeentregas
Nota: gráfico ilustrativo, mas baseado em fatos reais
"Esse código é difícil de entender"
"Joga isso fora e começa do zero"
Quando reescrever um sistema?
Uso de tecnologia desatualizada
Contratação de pessoas está difícil
Existência de tecnologias mais vantajosas
Fonte: http://blog.plataformatec.com.br/2016/07/key-points-to-consider-when-doing-a-software-rewrite/
que tal refatorar o código?
Refatoração
processo de melhorar o design do código existente,
alterando o software sem alterar o seu comportamento
Por que refatorar?
Código fácil de ler
Código fácil de entender
Código fácil de manter
Código limpo
precisamos mesmo refatorar?
Dívida Técnica
Custodamudança
Tempo
Fonte: How to Monetize Application Technical Debt, Gartner, 2011
Dívida Técnica
Valor de negócio
Custodamudança
Tempo
Fonte: How to Monetize Application Technical Debt, Gartner, 2011
e como evitar isso?
Custodamudança
Tempo
dívida técnica
refatoração
caso ótimo
Fonte: An Empirical Model of Technical Debt and Interest, MTD' 11, Ariadni Nugroho et al.
Como medir a necessidade de refatoração?
Quantidade de código duplicado
Dificuldade para escrever testes
Desempenho da suíte de testes
Identificação dos code smells
Code smells
Sintomas no código que indicam possíveis
problemas de design em sistemas orientados a objeto
Code smells
Indícios de que o código precisa ser refatorado
exemplos de code smells
Duplicated Code
Código duplicado
Regras de negócio duplicadas
Regra de negócio duplicada != Texto duplicado
Mesma alteração em vários arquivos,
Buscas globais para toda alteração
Shotgun Surgery
Vários comentários explicando o
funcionamento do código
Comments
# a é o valor total a ser cobrado
def charge(a)
# cartão com status = 3 é o cartão ativo
if credit_card.status == 3
payment_gateway.charge(a)
end
end
def charge(total)
# cartão com status = 3 é o cartão ativo
if credit_card.status == 3
payment_gateway.charge(total)
end
end segundos de vida economizados! o/
def charge(total)
# cartão com status = 3 é o cartão ativo
if credit_card.status == 3
payment_gateway.charge(total)
end
end
def charge(total)
# cartão com status = 3 é o cartão ativo
if credit_card.status == 3
payment_gateway.charge(total)
end
end
def charge(total)
# cartão com status = 3 é o cartão ativo
if credit_card.active?
payment_gateway.charge(total)
end
end
def charge(total)
if credit_card.active?
payment_gateway.charge(total)
end
end
def active?
status == 3
end
mais segundos de vida economizados!
o/
comentário != documentação
SECONDS_FOR_CONNECTION_TIMEOUT_IN_INTEGER = 60
# Public: Integer number of seconds to wait
# before connection timeout.
CONNECTION_TIMEOUT = 60
# Public: A summary of how much some user has consumed in a
certain plan.
#
# Examples
# plan_consumption_summary(contracted_plan)
# # => '2.44% (500 MB of 20 GB)'
#
# Returns a String.
def plan_consumption_summary(contracted_plan)
total_contracted = contracted_plan.plan_storage_limit
total_consumed = contracted_plan.total_consumed
# ...
Fonte: http://blog.plataformatec.com.br/2018/06/the-anatomy-of-code-documentation/
Mysterious Name
Long Function
Long Parameter List
Global Data
Mutable Data
Divergent Change
Outros code smells 2019
Feature Envy
Data Clumps
Primitive Obsession
Repeated Switches
Loops
Lazy Element
Speculative Generality
Temporary Field
2019
Message Chains
Middle Man
Insider Trading
Large Class
Alternative Classes with Different Interfaces
Data Class
Refused Bequest
2019
e como evitar os code smells?
reduzir
reconheça esses sintomas
Muitas regras de negócio nos controllers
Métodos muito longos
Classes muito grandes
Excesso de indireção/callbacks
Dificuldade para escrever testes
Classes de difícil manutenção
Ctrl + F / ⌘ + F para qualquer alteração
Arquivos constantemente alterados
reconheci, e agora?
Para cada code smell, existe
um conjunto de refatorações sugeridas
Martin Fowler e
Kent Beck listam
22 code smells e
sugerem como
refatorá-los
1999
Martin Fowler e
Kent Beck listam
22 code smells e
sugerem como
refatorá-los
2019
24
Refatoraram?
Mas vamos tentar olhar
para o design do código
Finalmente, SOLID o/
Single Responsibility Principle
Open/Closed Principle
Liskov Substitution Principle
Interface Segregation Principle
Dependency Inversion Principle
Single Responsibility Principle
O que a minha classe faz?
Indicativo de problemas: usar "e" ou "ou" na explicação
Métodos relacionados?
Uma única razão para mudar?
Alterações com efeitos previsíveis?
class BooksUser < ApplicationRecord
belongs_to: :book
belongs_to: :user
after_commit :send_notification_reservation_completed
def send_notification_reservation_completed
NotificationService.reservation_completed(user, book)
end
end
class BooksUser < ApplicationRecord
belongs_to: :book
belongs_to: :user
after_commit :send_notification_reservation_completed
def send_notification_reservation_completed
NotificationService.reservation_completed(user, book)
end
end
Uma classe de persistência não deveria saber sobre
notificações a um usuário, por ex.
Que tal um Service Object para isso?
Mais exemplos em: codeclimate.com/blog/7-ways-to-decompose-fat-activerecord-models/
class ReserveBookService
attr_reader :user, :book, :notification_service
def initialize(user, book, notification_service)
@user = user
@book = book
@notification_service = notification_service
end
def confirm!
user.books << book
notification_service.reservation_completed(user, book)
end
end
class ReserveBookService
attr_reader :user, :book, :notification_service
def initialize(user, book, notification_service)
@user = user
@book = book
@notification_service = notification_service
end
def confirm!
user.books << book
notification_service.reservation_completed(user, book)
end
end
Open/Closed Principle
Adicionar nova regra = modificar uma ou mais classes?
Se sim, é um indicativo de problema
Aberto para extensão, fechado para modificação
Defina interfaces/super classes
Reduza o acoplamento
class FinancialReport
def generate(account, file_format)
case file_format
when :csv
file = FormatCSV.generate_file(account.transactions)
when :xml
file = XML.parse_list(account.transactions)
end
Mailer.send(account.email, file)
end
end
class FinancialReport
def generate(account, file_format)
case file_format
when :csv
file = FormatCSV.generate_file(account.transactions)
when :xml
file = XML.parse_list(account.transactions)
when :pdf
file = PDFGenerator.create(account.transactions)
end
Mailer.send(account.email, file)
end
end edição
class FinancialReport
def generate(account, file_format)
case file_format
when :csv
file = FormatCSV.generate_file(account.transactions)
when :xml
file = XML.parse_list(account.transactions)
when :pdf
file = PDFGenerator.create(account.transactions)
end
Mailer.send(account.email, file)
end
end
class FinancialReport
def generate(account, file_creator)
file = file_creator.create(account.transactions)
Mailer.send(account.email, file)
end
end
class FileCreator
def create(items)
raise NotImplementedError
end
end
contrato
class FileCreatorXML < FileCreator
def create(items)
XML.parse(items)
end
end
class FileCreatorCSV < FileCreator
def create(items)
FormatCSV.generate_file(items)
end
end
class FileCreatorPDF < FileCreator
def create(items)
PDFGenerator.generate(items)
end
end
adição
class FinancialReport
def generate(account, file_creator)
file = file_creator.create(account.transactions)
Mailer.send(account.email, file)
end
end
FinancialReport.new.generate(account, FileCreatorPDF.new)
Liskov Substitution Principle
Barbara Liskov
Institute Professor from MIT
The 2008 Turing Award winner
liskov at csail.mit.edu
Liskov Substitution Principle (1987)
Let φ(x) be a property provable about objects x
of type T. Then φ(y) should be true for objects y
of type S where S is a subtype of T.
Tradução em: https://speakerdeck.com/elainenaomi/hacking-evening-liskov-substitution-principle
Eita!
Design by contract
respeitar os contratos definidos pela classe base
Pré-condições: dados de entrada
classes derivadas só podem ser mais permissivas
Pós-condições: dados de saída
classes derivadas só podem ser mais restritivas
Não podemos criar comportamentos inesperados ou incorretos!
O comportamento da super classe precisa ser mantido
class CheckingAccount
# ...
def deposit(value)
raise InvalidValueError if value <= 0
self.balance = self.balance + value
end
def compute_bonus
self.balance = self.balance * 1.01
end
end
class PayrollAccount < CheckingAccount
class OperationNotAllowed < StandardError; end
# ...
def compute_bonus
raise OperationNotAllowed
end
end
CheckingAccount.all.each do |account|
account.compute_bonus
end
CheckingAccount.all.each do |account|
begin
account.compute_bonus
rescue PayrollAccount::OperationNotAllowed
false
end
end
CheckingAccount.all.each do |account|
begin
account.compute_bonus
rescue PayrollAccount::OperationNotAllowed
false
end
end contrato quebrado
class PayrollAccount < CheckingAccount
# ...
def deposit(value)
raise InvalidValueError if value <= 100
self.balance = self.balance + value
end
def compute_bonus
self.balance = self.balance * 1.01
end
end
class PayrollAccount < CheckingAccount
# ...
def deposit(value)
raise InvalidValueError if value <= 100
self.balance = self.balance + value
end
def compute_bonus
self.balance = self.balance * 1.01
end
end
contrato quebrado
Deveriam ser classes diferentes!
Interface Segregation Principle
Uma classe derivada não deveria ser obrigada
a implementar métodos que ela não usa
class CoffeeMachine
def brew_coffee
# brew coffee logic
end
def fill_coffee_beans
# fill coffee beans
end
end
class Person
attr_reader :coffee_machine
def initialize
@coffee_machine = CoffeeMachine.new
End
def quero_cafe
coffee_machine.brew_coffee
end
end
class Staff
attr_reader :coffee_machine
def initialize
@coffee_machine = CoffeeMachine.new
end
def fill_coffee_beans
coffee_machine.fill_coffee_beans
end
end
Várias interfaces específicas é melhor do
que uma interface generalizada
class CoffeeMachineUserInterface
def brew_coffee
# brew coffee logic
end
end
class CoffeeMachineServiceInterface
def fill_coffee_beans
# fill coffee beans
end
end
class CoffeeMachineUserInterface
def brew_coffee
# brew coffee logic
end
end
class CoffeeMachineServiceInterface
def fill_coffee_beans
# fill coffee beans
end
end
class Person
attr_reader :coffee_machine
def initialize
@coffee_machine = CoffeeMachineUserInterface.new
end
def quero_cafe
coffee_machine.brew_coffee
end
end
class Person
attr_reader :coffee_machine
def initialize
@coffee_machine = CoffeeMachineUserInterface.new
end
def quero_cafe
coffee_machine.brew_coffee
end
end
class Staff
attr_reader :coffee_machine
def initialize
@coffee_machine = CoffeeMachineServiceInterface.new
end
def fill_coffee_beans
coffee_machine.fill_coffee_beans
end
end
class Staff
attr_reader :coffee_machine
def initialize
@coffee_machine = CoffeeMachineServiceInterface.new
end
def fill_coffee_beans
coffee_machine.fill_coffee_beans
end
end
Dependency Inversion Principle
Dependa de abstrações,
não de implementações
class FinancialReport
def generate(account, file_creator)
file = file_creator.create(account.transactions)
Mailer.send(account.email, file)
end
end
class FileCreatorCSV < FileCreator
def create(items)
FormatCSV.generate_file(items)
end
End
FinancialReport.new.generate(account, FileCreatorCSV.new)
class FileCreatorCSV < FileCreator
def create(items)
FormatCSV.generate_file(items)
NewCSVGenerator.parse(items, header: false)
end
End
FinancialReport.new.generate(account, FileCreatorCSV.new)
use o encapsulamento e polimorfismo a seu favor
TL;DR
Princípios para a criação de um código
mais flexível e adaptável a mudanças
SOLID
No fundo, o que queremos?
Alta coesão
Baixo acoplamento
Encapsulamento
vamos refatorar tudo?
esses conceitos nos ajudam a criar
aplicações mais flexíveis
converse com seu time
analisem juntos os trade-offs
verifique a cobertura de testes
testes automatizados são essenciais
para garantir a evolução do seu sistema
refatoração != revolução
cuidado com big design up front
e overengineering
e não esqueçam:
codar é um processo de comunicação
Donald Knuth. "Literate Programming (1984)" in Literate Programming. CSLI, 1992, pg. 99.
Mais sobre design?
Padrões de projeto
TDD, DDD
minhas referências
guidelines.plataformatec.com.br
thoughtbot.com/upcase/clean-code
refactoring.guru
mundopodcast.com.br/podprogramar
Mais referências:
Refactoring rails apps - Flavia Fortes
http://bit.ly/2zDADhe
Evitando o Jenga Driven Development - João Britto
http://bit.ly/2DFvB3v
Mais referências:
Por que (às vezes) você deve reinventar a roda -
Paulo Silva
https://youtu.be/kdNf2abcP5E?t=11640
Callbacks do ActiveRecord: o mal secreto ou
apenas mal compreendidos? - Rondy
https://youtu.be/kdNf2abcP5E?t=13214
Mais referências:
Sororidade
Empatia, solidariedade,
companheirismo, respeito
Juntas somos mais fortes
Até a próxima!
muito obrigada
speakerdeck.com/elainenaomi

Sobre code smells, refactoring e design: como SOLID pode te ajudar no dia a dia