design de aplicações
orientadas a objeto
uma visão rubista
olar
Elaine Naomi Watanabe
Desenvolvedora de Software (Plataformatec)
Mestre em Ciência da Computação (USP)
github.com/elainenaomi
twitter.com/elaine_nw
http://careers.plataformatec.com.br
quais são 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 abstração de uma parte do domínio/problema
estados/valores específicos para seus atributos
Classe template/gabarito para definição de objetos
um conjunto de atributos (características) e
métodos (comportamentos)
Herança herdar atributos/métodos comuns de uma
classe base (ou superclasse) e
adicionar novos atributos/métodos
Polimorfismo ter uma interface única de acesso para diferentes
classes/objetos
Composição combinar objetos simples de modo a ter
objetos mais complexos
objetos + mensagens = app
no mundo do Ruby...
$ gem install rails
$ gem install rails
+ Rails way
ViewModel
Controller
apresentação
intermediador
dados +
lógica de negócio
e se o projeto começa a crescer?
+ Funcionalidades
+ Funcionalidades
+ Modificações
+ Funcionalidades
+ Modificações
+ Bugs
Tamanho da base de código
Tempoparadeploy
como reconhecer os
sintomas?
Muitas regras de negócio nos controllers
Métodos muito longos
Classes muito grandes
Excesso de callbacks (controllers/models)
Dificuldade para escrever testes
Classes de difícil manutenção
Ctrl + F / ⌘ + F para qualquer alteração
Arquivos constantemente alterados
e agora, como faz?
DRY
don't repeat yourself
Helpers
Presenters
Single Table Inheritance
Polymorphic Association
Concerns (cuidado!)
Service Objects
Uso de Plain Old Ruby Object (PORO)
Casos de uso
Regras de negócio explícitas
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
SOLID
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?
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
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)
Tell, don't ask
Não exponha a regra de negócio
Não deixe quem usa o objeto tomar decisões
por ele com base no seu estado
Confie no seu objeto!
Encapsule estados
Crie uma interface pública mínima
class Subscription
def charge(user)
if user.has_credit_card?
user.charge(total)
else
false
end
end
end
class Subscription
def charge(user)
if user.has_credit_card?
user.charge(total)
else
false
end
end
end
class User
def charge(total)
if has_credit_card?
payment_gateway.charge(credit_card, total)
true
else
false
end
end
end
class User
def charge(total)
if has_credit_card?
payment_gateway.charge(credit_card, total)
true
else
false
end
end
end
class Subscription
def charge(user)
user.charge(total)
end
end
TL;DR;
TL;DR:
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
cuidado com big design up front
e não esqueçam:
codar é um processo de comunicação
Mais sobre design?
Padrões de projeto
TDD, DDD
minhas referências
Refactoring rails apps
Flavia Fortes
http://bit.ly/rubyconfbr-refactoring
https://speakerdeck.com/flaviafortes
RAILS GIRLS SP
17 e 18 de agosto
railsgirls.com/saopaulo
obrigada
speakerdeck.com/elainenaomi

Design de aplicações orientadas a objeto