SOLID através de BDD um guia prático para rubistas. Lucas Húngaro software developer

Documentos relacionados
#10 PRODUZIR CONTEÚDO SUPER DICAS ATRATIVO DE PARA COMEÇAR A

Transações Seguras em Bancos de Dados (MySQL)

Como escrever melhor em 5 passos simples

UNIVERSIDADE FEDERAL DO PARANÁ UFPR Bacharelado em Ciência da Computação

Diminua seu tempo total de treino e queime mais gordura

Orientação a Objetos

Microsoft Access: Criar relações para um novo banco de dados. Vitor Valerio de Souza Campos

Como fazer contato com pessoas importantes para sua carreira?

3 Dicas MATADORAS Para Escrever s Que VENDEM Imóveis

TESTES AUTOMATIZADOS COM JUNITE MOCKITO

Autor: Marcelo Maia

Como fazer seu blog se destacar dos outros

PROGRAMAÇÃO SERVIDOR PADRÕES MVC E DAO EM SISTEMAS WEB. Prof. Dr. Daniel Caetano

Padrões de projeto 1

Antes de tudo... Obrigado!

RELATÓRIO DA ENQUETE SOBRE INTERNET MÓVEL

Como melhorar a Qualidade de Software através s de testes e nua. Cláudio Antônio de Araújo 22/11/2008

Compreendendo a dimensão de seu negócio digital

5 Equacionando os problemas

Cinco principais qualidades dos melhores professores de Escolas de Negócios

Aspectos técnicos do desenvolvimento baseado em componentes

5 Dicas de marketing para iniciantes

Python Intermediário. terça-feira, 4 de agosto de 15

MÓDULO 5 O SENSO COMUM

Como montar uma boa apresentação de slides?

Microsoft Access: Criar consultas para um novo banco de dados. Vitor Valerio de Souza Campos

Na Internet Gramática: atividades

W W W. G U I A I N V E S T. C O M. B R

VERSÃO DEMO DO MÉTODO DE GUITARRA: CURE SEU IMPROVISO: MODOS GREGOS POR ROBERTO TORAO

Dicas para Ganhar Dinheiro Online

Análise e Desenvolvimento de Sistemas ADS Programação Orientada a Obejeto POO 3º Semestre AULA 03 - INTRODUÇÃO À PROGRAMAÇÃO ORIENTADA A OBJETO (POO)

Os desafios do Bradesco nas redes sociais

A importância da Senha. Mas por que as senhas existem?

Problemas em vender? Veja algumas dicas rápidas e práticas para aumentar suas vendas usando marketing

Institucional. Realização. Patrocínio. Parceria

edirectory Plataforma ios / Android

Tutorial do ADD Analisador de Dados Dinâmico.

internetsegura.fde.sp.gov.br

COMO CRIAR UMA ESTRATÉGIA DE MARKETING

O CAMINHO PARA REFLEXÃO

Falhar em se preparar é se preparar para falhar. (Benjamin Franklin).

os botões emocionais Rodrigo T. Antonangelo

Microsoft Access Para conhecermos o Access, vamos construir uma BD e apresentar os conceitos necessários a cada momento

Programação de Computadores - I. Profª Beatriz Profº Israel

Duração: Aproximadamente um mês. O tempo é flexível diante do perfil de cada turma.

JOSÉ DE SOUZA CASTRO 1

C Por que é preciso fazer rápido o produto web?

Usando o Google Code como repositório para projetos no Eclipse com SubClipse.

PASSO A PASSO: CRIAÇÃO DE PERSONAS

INTRODUÇÃO: 1 - Conectando na sua conta

DESENVOLVENDO APLICAÇÃO UTILIZANDO JAVA SERVER FACES

CURSO DE PROGRAMAÇÃO EM JAVA

Transcriça o da Entrevista

APRENDA AS MUDANÇAS DE FORMA FÁCIL

SUMÁRIO 1. AULA 6 ENDEREÇAMENTO IP:... 2

COMO FAZER A TRANSIÇÃO

MVC e Camadas - Fragmental Bliki

Prefeitura Municipal de São Luís Manual de uso dos serviços da SEMFAZ. Prefeitura Municipal de São Luís Manual de uso dos serviços da SEMFAZ

Top Guia In.Fra: Perguntas para fazer ao seu fornecedor de CFTV

Microsoft Access: Criar relatórios para um novo banco de dados. Vitor Valerio de Souza Campos

Comprar online é uma delícia, direto do meu sofá sem filas!

Escrito por Paulo Ricardo - Gambware Qua, 08 de Setembro de :47 - Última atualização Sex, 24 de Setembro de :18

Iluminação pública. Capítulo VIII. Iluminação pública e urbana. O mundo digital

Orientação a Objetos

O CONCEITO DE TDD NO DESENVOLVIMENTO DE SOFTWARE

Como obter excelentes. Resultados. no Marketing Digital. Aprenda a usar 3 metas matadoras. Publicação SEVEN - SPD

uma representação sintética do texto que será resumido

INTRODUÇÃO. Fui o organizador desse livro, que contém 9 capítulos além de uma introdução que foi escrita por mim.

Seis dicas para você ser mais feliz

Freelapro. Título: Como o Freelancer pode transformar a sua especialidade em um produto digital ganhando assim escala e ganhando mais tempo

Introdução. Bom, mas antes de começar, eu gostaria de me apresentar..

18/04/2006 Micropagamento F2b Web Services Web rev 00

No E-book anterior 5 PASSOS PARA MUDAR SUA HISTÓRIA, foi passado. alguns exercícios onde é realizada uma análise da sua situação atual para

Quando era menor de idade ficava pedindo aos meus pais para trabalhar, porém menor na época não tinha nada e precisei esperar mais alguns anos.

Estrutura de Dados Básica

Evolução do Design através de Testes e o TDD

Como usar o Facebook para catapultar sua lista de clientes?

Meu resultado com SEO para Youtube

Informática I. Aula 6. Aula 6-12/09/2007 1

Introdução a Java. Hélder Nunes

Curso de Java. Orientação a objetos e a Linguagem JAVA. TodososdireitosreservadosKlais

A Tua Frase Poderosa. Coaches Com Clientes: Carisma. Joana Areias e José Fonseca

Rock In Rio - Lisboa

Oração. u m a c o n v e r s a d a a l m a

2 echo "PHP e outros.";

A criança e as mídias

Tabelas vista de estrutura

NOME COMPLETO DA SUA INSTITUIÇÃO. Nome completo do integrante A Nome completo do integrante B Nome completo do integrante C

INSTALANDO E CONFIGURANDO O MY SQL

Desenvolvendo plugins WordPress usando Orientação a Objetos

Equipe OC- Olimpíadas Científicas

Aula 5 Modelo de Roteiro Para Ser Usado nas Suas Entrevistas

PROGRAMAÇÃO AVANÇADA -CONCEITOS DE ORIENTAÇÃO A OBJETOS. Prof. Angelo Augusto Frozza, M.Sc. frozza@ifc-camboriu.edu.br

Como Criar seu produto digital

Barra de ferramentas padrão. Barra de formatação. Barra de desenho Painel de Tarefas

Algoritmos. Objetivo principal: explicar que a mesma ação pode ser realizada de várias maneiras, e que às vezes umas são melhores que outras.

COMO MINIMIZAR AS DÍVIDAS DE UM IMÓVEL ARREMATADO

Transcrição:

SOLID através de BDD um guia prático para rubistas Lucas Húngaro software developer

SOLID Conjunto de princípios desenvolvidos por Bob Martin que devem ser aplicados para melhorar a qualidade do código orientado a objetos.

Rails, MVC Rails, MVC e facilitam muito a vida do desenvolvedor, principalmente em aplicações simples. Mas também deixamos de prestar atenção à aspectos importantes enquanto produzimos código rapidamente.

Fat Models aka programação procedural Assim acabamos desenvolvo alguns anti-patterns. Pior do que isso é quando alguns desses anti-patterns são vistos como boas práticas mas, na verdade, são apenas atalhos enganosos (parecem vantajosos a princípio, mas acabam aumentando o custo de manutenção)

OOP, BDD, SOLID As soluções são simples mas, muitas vezes, acabam ficando de lado porque a maioria dos desenvolvedores pensam que são assuntos chatos.

Acadêmico... (chato) Um dos motivos para isso é a origem acadêmica, onde muitas vezes o assunto é discutido apenas na esfera teórica, com poucos exemplos de aplicação.

Coisa de Javeiro... Outro problema é que muitos associam essas práticas à plataformas como Java e.net que, infelizmente, remetem a sentimentos de excesso de burocracia e baixa produtividade.

Linguagem dinâmica Mas, para nossa sorte, utilizar esses princípios em linguagens dinâmicas é muito mais fácil e menos burocrático.

SRP e DIP Pra facilitar as coisas, vamos falar apenas de dois princípios: Single Responsibility e Depency Inversion - os que acredito causarem o maior impacto

Coesão e Acoplamento Traduzindo esses princípios, identificamos que eles falam sobre duas características do código: coesão e acoplamento.

Baixa coesão: executa parte de uma responsabilidade ou mais de uma Problemas que queremos eliminar

Alto acoplamento: código preso a um caso de uso e suas depências Problemas que queremos eliminar

Exemplos Code!

Processo usual:

rails new myapp

migrations

finders

data-centric apps fat models Começamos pelos dados (schema) e depois tentamos derivar algum comportamento disso, resultando em objetos gordos, pouco coesos e muito acoplados.

Rails apps == model User gigante mais algumas classes sem importância Domínios anêmicos

class User < ActiveRecord::Base attr_accessible :email, :password, :password_confirmation has_secure_password validates_presence_of :password, :on => :create def s_password_reset generate_token(:password_reset_token) self.password_reset_sent_at = Time.zone.now save! UserMailer.password_reset(self).deliver def generate_token(column) begin self[column] = SecureRandom.urlsafe_base64 while User.exists?(column => self[column]) Exemplo clássico (railscasts 275). Falta coesão, sobra acoplamento.

class User < ActiveRecord::Base attr_accessible :email, :password, :password_confirmation has_secure_password validates_presence_of :password, :on => :create def s_password_reset generate_token(:password_reset_token) self.password_reset_sent_at = Time.zone.now save! UserMailer.password_reset(self).deliver def generate_token(column) begin self[column] = SecureRandom.urlsafe_base64 while User.exists?(column => self[column]) Depências

class User < ActiveRecord::Base attr_accessible :email, :password, :password_confirmation has_secure_password validates_presence_of :password, :on => :create def s_password_reset generate_token(:password_reset_token) self.password_reset_sent_at = Time.zone.now save! UserMailer.password_reset(self).deliver def generate_token(column) begin self[column] = SecureRandom.urlsafe_base64 while User.exists?(column => self[column]) Depências

class User < ActiveRecord::Base attr_accessible :email, :password, :password_confirmation has_secure_password validates_presence_of :password, :on => :create def s_password_reset generate_token(:password_reset_token) self.password_reset_sent_at = Time.zone.now save! UserMailer.password_reset(self).deliver def generate_token(column) begin self[column] = SecureRandom.urlsafe_base64 while User.exists?(column => self[column]) Depências

describe User do describe "#s_password_reset" do let(:user) { Factory(:user) } it "generates a unique password_reset_token each time" do user.s_password_reset last_token = user.password_reset_token user.s_password_reset user.password_reset_token.should_not eq(last_token) it "saves the time the password reset was sent" do user.s_password_reset user.reload.password_reset_sent_at.should be_present it "delivers email to user" do user.s_password_reset last_email.to.should include(user.email) Estrutural

describe User do describe "#s_password_reset" do let(:user) { Factory(:user) } it "generates a unique password_reset_token each time" do user.s_password_reset last_token = user.password_reset_token user.s_password_reset user.password_reset_token.should_not eq(last_token) it "saves the time the password reset was sent" do user.s_password_reset user.reload.password_reset_sent_at.should be_present it "delivers email to user" do user.s_password_reset last_email.to.should include(user.email) Estrutural

describe User do describe "#s_password_reset" do let(:user) { Factory(:user) } it "generates a unique password_reset_token each time" do user.s_password_reset last_token = user.password_reset_token user.s_password_reset user.password_reset_token.should_not eq(last_token) it "saves the time the password reset was sent" do user.s_password_reset user.reload.password_reset_sent_at.should be_present it "delivers email to user" do user.s_password_reset last_email.to.should include(user.email) Depente de banco de dados == lento

describe User do describe "#s_password_reset" do let(:user) { Factory(:user) } it "generates a unique password_reset_token each time" do user.s_password_reset last_token = user.password_reset_token user.s_password_reset user.password_reset_token.should_not eq(last_token) it "saves the time the password reset was sent" do user.s_password_reset user.reload.password_reset_sent_at.should be_present it "delivers email to user" do user.s_password_reset last_email.to.should include(user.email) Muitas responsabilidades (isso porque o has_secure_password já esconde outras)

describe User do describe "#s_password_reset" do let(:user) { Factory(:user) } it "generates a unique password_reset_token each time" do user.s_password_reset last_token = user.password_reset_token user.s_password_reset user.password_reset_token.should_not eq(last_token) it "saves the time the password reset was sent" do user.s_password_reset user.reload.password_reset_sent_at.should be_present it "delivers email to user" do user.s_password_reset last_email.to.should include(user.email) Specs lentas, código rígido (difícil de modificar), custo de manutenção alto.

Escrever specs isoladas para AR é difícil

ActiveRecord == Repositório (classe) Model (objeto) BD (reflection) AR faz coisas demais e quebra o SRP por design

Uncle Bob: ActiveRecord is a Data Structure http://goo.gl/oebvx AR deve ser usado como estrutura de dados para não se tornar um buraco negro de comportamento. Minha guideline: AR contém apenas validação, associações e finders, nada mais.

validações associações finders nada mais AR deve ser usado como estrutura de dados para não se tornar um buraco negro de comportamento. Minha guideline: AR contém apenas validação, associações e finders, nada mais.

Comece pelo comportamento Vamos fazer diferente. Primeiro, identificamos o comportamento desejado.

describe UserAuthentication do let(:user) { Factory(:user) } let(:password) { "123456" } context "with valid credentials" do subject { UserAuthentication.new(user.username, password) } it "allows access" do subject.authenticate.should be_true context "with invalid credentials" do subject { UserAuthentication.new(user.username, "invalid") } it "denies access" do subject.authenticate.should be_false Isso é o que eu quero que faça

class UserAuthentication def initialize(username, password) @username = username @password = password def authenticate if user = User.find_by_username(@username) user.password_hash == BCrypt::Engine.hash_secret(@password, user.password_salt) else false Uma primeira implementação

class UserAuthentication def initialize(username, password) @username = username @password = password def authenticate if user = User.find_by_username(@username) user.password_hash == BCrypt::Engine.hash_secret(@password, user.password_salt) else false Melhoramos quanto à coesão... mas ainda há acoplamento

The negative effects on testability in the Active Record pattern can be minimized by using mocking or depency injection Wikipedia

object doubles são essenciais YAY for mocks, stubs, spies, proxies and fris!

BDD == design Eu costumava ser da turma do mocks e stubs são apenas para isolamento de sistemas externos (como gateways) pq geram testes quebradiços quando usados internamente

Testes quebradiços == Design ruim Até que tomei vergonha na cara e fui estudar essa bagaça! ;)

object doubles + depency injection Através disso conseguimos: testes rápidos, objetos desacoplados e coesos, custo de manutenção reduzido.

class UserAuthentication def initialize(username, password) @username = username @password = password def authenticate(user_repo = User, encryption_engine = BCrypt::Engine) if user = user_repo.find_by_username(@username) user.password_hash == encryption_engine. hash_secret(@password, user.password_salt) else false Como estamos apro, vou inverter as coisas e mostrar a implementação primeiro: uma forma de utilizar o DIP é passar as depências como parâmetros.

class UserAuthentication def initialize(username, password) @username = username @password = password def authenticate(user_repo = User, encryption_engine = BCrypt::Engine) if user = user_repo.find_by_username(@username) user.password_hash == encryption_engine. hash_secret(@password, user.password_salt) else false

describe UserAuthentication do let(:user) { double("an user").as_null_object } let(:user_repo) { double("an user repository") } let(:encryption_engine) { double("an encryption engine") } before(:each) do user.stub(:password_hash).and_return "the hash" user_repo.stub(:find_by_username).and_return user

describe UserAuthentication do let(:user) { double("an user").as_null_object } let(:user_repo) { double("an user repository") } let(:encryption_engine) { double("an encryption engine") } before(:each) do user.stub(:password_hash).and_return "the hash" user_repo.stub(:find_by_username).and_return user São objetos dublês, não me importo se stub ou mock (ou outro tipo). Essas decisões tomo sobre mensagens (métodos), não sobre o objeto todo. Isso é uma feature muito legal do framework de mocks do RSpec

describe UserAuthentication do let(:user) { double("an user").as_null_object } let(:user_repo) { double("an user repository") } let(:encryption_engine) { double("an encryption engine") } before(:each) do user.stub(:password_hash).and_return "the hash" user_repo.stub(:find_by_username).and_return user

context "with valid credentials" do before(:each) { encryption_engine. stub(:hash_secret). and_return user.password_hash } subject { UserAuthentication.new("username", "123456") } it "allows access" do subject.authenticate(user_repo, encryption_engine).should be_true E agora as specs: sem banco de dados, depências injetáveis, código simples. No fim das contas, tudo o que preciso é de três objetos que respondam a uma interface bem definida.

context "with valid credentials" do before(:each) { encryption_engine. stub(:hash_secret). and_return user.password_hash } subject { UserAuthentication.new("username", "123456") } it "allows access" do subject.authenticate(user_repo, encryption_engine).should be_true E agora as specs: sem banco de dados, depências injetáveis, código simples. No fim das contas, tudo o que preciso é de três objetos que respondam a uma interface bem definida.

context "with invalid credentials" do before(:each) { encryption_engine. stub(:hash_secret). and_return "another hash" } subject { UserAuthentication.new("username", "invalid") } it "denies access" do subject.authenticate(user_repo, encryption_engine).should be_false

context "with invalid credentials" do before(:each) { encryption_engine. stub(:hash_secret). and_return "another hash" } subject { UserAuthentication.new("username", "invalid") } it "denies access" do subject.authenticate(user_repo, encryption_engine).should be_false

describe MyBCryptAdapter do context "encrypting text with a salt" do it "respects the lib protocol" do BCrypt::Engine.expects(:hash_secret) MyBCryptAdapter.encrypt("text", "salt") class MyBCryptAdapter def self.encrypt(plain_text, salt) BCrypt::Engine.hash_secret(plain_text, salt) Podemos abstrair ainda mais...

describe MyBCryptAdapter do context "encrypting text with a salt" do it "respects the lib protocol" do BCrypt::Engine.expects(:hash_secret) MyBCryptAdapter.encrypt("text", "salt") class MyBCryptAdapter def self.encrypt(plain_text, salt) BCrypt::Engine.hash_secret(plain_text, salt)... e usar um mock para garantir a interface.

def authenticate(user_repo = User, encryption_engine = MyBCryptAdapter) if user = user_repo.find_by_username(@username) user.password_hash == encryption_engine. encrypt(@password, user.password_salt) else false

Até onde? Quanto de abstração vale a pena? Indireção em excesso é tão ruim quanto alto acoplamento. A dica é abstrair quando é um componente que mudará muito ou quando você não tem a mínima ideia disso, pois isso preserva seu direito de mudar caso seja necessário.

def authenticate(encryption_engine = MyBCryptAdapter) if user = User.find_by_username(@username)... Por ex: caso eu tenha certeza de que meu repositório de usuários será sempre um modelo AR (e nunca precisarei buscá-los em um LDAP, por exemplo), podemos eliminar essa injeção e deixar acoplado.

Mudanças... E se precisarmos mudar o algoritmo de criptografia?

describe MyZYCryptAdapter do context "encrypting text with a salt" do it "respects the lib protocol" do ZYCrypt::Engine::Passwords. should_receive(:hash_password_with_salt) MyZYCryptAdapter.encrypt("text", "salt") class MyZYCryptAdapter def self.encrypt(plain_text, salt) ZYCrypt::Engine::Passwords. hash_password_with_salt(plain_text, salt)

def authenticate(user_repo = User, encryption_engine = MyZYCryptAdapter) if user = user_repo.find_by_username(@username) user.password_hash == encryption_engine. encrypt(@password, user.password_salt) else false

WIN! \o/

Logging! Mais uma alteração

def authenticate(user_repo = User, encryption_engine = MyBCryptAdapter logger = MyFancyLogger)... Passando como parâmetro

context "with invalid credentials" do... it "logs the authentication attempt" do my_logger = double("a logger") my_logger.should_receive(:log).with("something") subject.authenticate(user_repo, encryption_engine, logger) Um caso de uso meio forçado, apenas como exemplo. Vamos verificar as interações entre os componentes. O como fica à cargo do teste unitário do logger, não do autenticador.

context "with invalid credentials" do... it "logs the authentication attempt" do my_logger = double("a logger") my_logger.should_receive(:log).with("something") subject.authenticate(user_repo, encryption_engine, logger) Um caso de uso meio forçado, apenas como exemplo. Vamos verificar as interações entre os componentes. O como fica à cargo do teste unitário do logger, não do autenticador.

context "with valid credentials" do... it "doesn t log the authentication attempt" do my_logger = double("a logger") my_logger.should_receive(:log).never subject.authenticate(user_repo, encryption_engine, logger) Um caso de uso meio forçado, apenas como exemplo. Vamos verificar as interações entre os componentes. O como fica à cargo do teste unitário do logger, não do autenticador.

context "with valid credentials" do... it "doesn t log the authentication attempt" do my_logger = double("a logger") my_logger.should_receive(:log).never subject.authenticate(user_repo, encryption_engine, logger) Um caso de uso meio forçado, apenas como exemplo. Vamos verificar as interações entre os componentes. O como fica à cargo do teste unitário do logger, não do autenticador.

Sintomas BDD é uma ótima forma de apontar problemas com o design do código - os sintomas costumam ser claros

Essa é uma das principais reclamações dos desenvolvedores que são contra o uso pesado de dublês - criar muitos mocks/stubs nos setups dos cenários de testes. A questão é que isso, na verdade, está revelando problemas com o design. Exemplo: teste de controller fazo stub de vários métods de um model => falta de encapsulamento e model quebrando SRP Muitos mocks/stubs numa mesma depência == superfície de contato muito ampla

Superfície de contato

class BlogController < ApplicationController def index @posts = Post.published.page params[:page] @posts_grouped_by_year = Post. all. group_by { post post. created_at. beginning_of_year } @posts_highlights = Post.published.featured.limit(3) @categories = Category.includes(:posts) @faqs = Faq.tagged_with(@posts.map { post post.tag_list}. join(","). split, :any => true)

tha fuuuuck??!?!?!

class BlogController < ApplicationController def index @posts = Post.published.page params[:page] @posts_grouped_by_year = Post. all. group_by { post post. created_at. beginning_of_year } @posts_highlights = Post.published.featured.limit(3) @categories = Category.includes(:posts) @faqs = Faq.tagged_with(@posts.map { post post.tag_list}. join(","). split, :any => true)

Controller: quanto mais burro, melhor Minha sugestão para esse caso seria extrair toda essa filtragem para uma classe especializada em montar a página inicial do blog (poderia ser utilizado um Presenter).

Muitos contextos em diferentes níveis de abstração == muitas responsabilidades

describe User do context "persistence logic" do it "validates..." context "data gathering" do it "finds records under certain conditions..." context "making payments" do it "register an error in case the key is invalid" it "writes some info to the log in case of success" #...

describe User do context "persistence logic" do it "validates..." context "data gathering" do it "finds records under certain conditions..." context "making payments" do it "register an error in case the key is invalid" it "writes some info to the log in case of success" #... Isso nem sempre é exato, mas pode ser um ótimo indicativo de problemas.

Dicas Algumas formas rápidas de verificar se seu código está sólido :D

Pequenas peças de comportamento facilmente acessíveis Isso garante que não precisemos de setups muito elaborados e nos força a abstrair conceitos de negócio em forma de código.

app console.buy(user, product, cart) Dica: abra o console da sua aplicação e veja se os processos de negócio podem ser executados facilmente na linha de comando

Modele processos, não se pra apenas a entidades.

class Customer def initialize(user, supplier) @user = user @supplier = supplier def pay(amount, gateway = SomeGatewayAdapter)... Exemplo: User assume o papel Customer para o processo de pagamento.

Don't mock types you don't own Write wrappers Evite ao máximo colocar dublês em tipos externos. Caso necessário, crie um wrapper/ adapter

Always check code in better than you checked it out. Uncle Bob Seja um bom menino! :P

Crie o hábito de passar depências como parâmetros

Não escreva fat models OOP

Próximos passos Não basta apenas alguns slides pra mostrar como fazer isso. É preciso ler, estudar e praticar. Por isso, seguem algumas referências.

Uncle Bob Michael Feathers Corey Haines Gary Bernhardt Pat Maddox Avdi Grimm Esses são apenas alguns nomes, que levarão a outros. Há muita gente altamente capacitada falando sobre e praticando essas técnicas.

Ótimas guidelines (!= regras)

Obrigado @lucashungaro github.com/lucashungaro lucashungaro@gmail.com