Working effectivetly with legacy code
Written by Michael C. Feathers
Chapter 1 - Changing Software
Etapas de alterações em software costumam ser:
adicionar nova funcionalidade
correção de bugs
melhoria da arquitetura
otimização de uso dos recursos do sistema.
A maior diferença entre corrigir bugs e novas funcionalidades é que uma altera o comportamento antigo e novas funcionalidade acresce novo comportamento ao já existente. "Behavior is the most important thing about software. It is what users depend on. Users like it when we add new behavior (provided it is what they really wanted), but if we change or remove behavior they depend on (introduce bugs), they stop trusting us."
Segue um exemplo: public class CdPlayer { public void addTrackListing(Track track) { ... }
public void replaceTrackListing(String name, Track track) { ... } }
Adotar técnicas que facilitem o entendimento e a manutenabilidade, mantendo o comportamento.
Melhorar o desing removendo e alterar o comportamento antigo se chama bug. O oposto disso se chama refactoring.
Improving design
Alterações representam muito menos riscos que as reformas, e em geral deve ser o caminho a ser seguido ao se trabalha com código legado.
Alterações sem grandes riscos devem ser feitas em pequenas parcelas de código e com cobertura de testes para garantir o comportamento.
No entanto, muitas vezes a tentação de alterar um código é grande, mas aprender as técnicas certas são de suma importância.
Refactoring
Optimization
Tem como objetivo usar menos recursos, aumentar a performance, usar ferramentas que facilitam o trabalho(frameworks).
Testes também podem garantir que algumas otimizações mais invasivas não mudem o comportamento do sistema.
Adding a feature
Fixing a Bug Refactoring Optimizing
Structure Alterações Alterações Alterações
Funcionality Alterações
New funcionality Alterações
Resource usage Alterações
Área de atuação das alterações
Adicionar novas funcionalidades parece-se muito mais com refactoring e otimização do que com correção de bugs
Em geral tem de se alterar algumas funcionalidades e até alguns comportamentos, mas deve-se manter muito mais comportamentos do que alterar algum.
Chapter 2 - Working with Feedback
Em muitas equipes de software existem algumas reações
Edit and pray(Edite e reze)
Cover and modify (Cubra e modifique)Em geral uma equipe de testes escreve um teste contra o código e roda-o contra o código(geralmente a noite)
Covering SoftwareSoftwares cobertos por testes que dá um feedback rápido sobre as alterações.
Software ViseSoftwares com comportamento fixado por testes.
•Testes que verificam o comportamentos de componentes de forma isolada devem testar o comportamento a nível "atômico".
•Dão feedback rápido do resultado pois rodam rapidamente.
•Test Harness: Termo utilizado para definir um sistema sob uma espécie de “armadura de testes”.
Teste unitários
Não costumam ser muito eficientes pois costumam ser difícieis de entender e de manter
Dificulta a localização exata do erro pois muitas vezes tem comportamentos aninhados.
São executados de forma lenta, possuem diversas funcionalidades ambíguas.
Difícil de terem boa cobertura porque tende a ter seu comportamento alterado de forma indireta muitas vezes não coberto por um outro teste.
Large Tests
Identificação de change points(Locais onde a alteração ocorrerá)
Encontrar os test points
Quebra dependências
Escrever os testes
Fazer as alterações e refatorações necessárias.
O passo a passo de alterações de código legado
Chapter 3 - Sensing and separation
Sensing: Quebrar dependências quando não se pode acessar determinados valores através de testes.
Separation: Quebrar dependências quando não se pode rodar um determinado teste isoladamente.
Faking Colaborator e Mock Objects Segue o exemplo abaixo:
public classe Sale { private Display display; public Sale(Display display) { this.display = display; }
public void scan(String barCode) { ... display.showLine(itemName); ... } }
A classe display recebe um código de barras e o exibe em um display de preços.
Classes como a anterior são difíceis de testar por possuirem uma interface de usuário. Usa-se então um fake colaborator para simular a saída.
public class FakeDisplay implements Display { private String lastLine = "";
public void showLine(String line) { this.lastLine = line; }
public String getLastLine() { return lastLine;
} }
Segue o teste: public class SaleTest { @Test public void displayAnItemShouldShowTheNameAndPrice() { FakeDisplay fakeDisplay = new FakeDisplay(); Sale sale = new Sale(fakeDisplay);
sale.scan("152"); assertEquals("Lixo $1,59", display.getLastLine()); } }
Mock objects
São objetos poderosos que simulam um objeto para abstrair o comportamento dos objetos que não estão sendo testados.
Chapter 4 - The Seam Model
Seam é um local onde se pode mudar o comportamento de um programa mais sem alterar o local original.
A maior vantagem de seam model é que muitas vezes não é necessário alterar nada que mude o comportamento no método original.
Exemplo: bool CAsyncSslRec::init() { ... if (!m_bFailureSent) { m_bFailureSent = TRUE; PostReceiveError(SOCKETCALLBACK, SSL_FAILURE); } ... }
O método PostReceiveError é um método global pode-se criar uma implementação desse método na própria classe podemos ter uma com o método original e uma com um método vazio, sem comportamento. Com o comportamento original: void CAsyncSslRec::PostReceiveError(UINT type, UINT errorCode) { ::PostReceiveError(type, errorCode); }
Comportamento para o teste:
void TestingAsyncSslRec:: public CAsyncSslRec { virtual void PostReceiveError(UINT tyoe, UINT errorCode) { //nothing here! } }
A maior vantagem da técnica Seam é que ao trabalhar com código legado
onde muitas vezes quebrar dependências pode ser complicado, esse é uma maneira rápida e com pouca alteração ou nenhuma na classe original sendo a maioria delas na classe(ou classes) para teste.
Vantagem no uso do seam
Enabling points
São locais onde escolhemos qual comportamento usar. Por exemplo, podemos ter um comportamento padrão para o ambiente e um outro comportamento para testes por exemplo.
Algumas linguagens como o C e o C++ ao compilar possuem um estágio de
build anterior a compilação final.
Pode-se criar macros com operadores condicionais para compilar de forma diferentes usando #if de configurações diferentes entre teste e no ambiente e produção.
O enable point são as macros que mudaram as definições antes da compilação.
Preprocessing Seams
Locais onde se podem mudar o caminho de um conjunto de classes no ambiente.
Classes com mesmo nome e caminho podem ser testadas fazendo um “switch” através do classpath por exemplo.
Link Seams
package fitnesse; ... import fit.Parse; import fit.Fixture; ... public class FitFilter { ... tables = new Parse(input) fixture.doTables(tables); .. tables.print(output);
}
As classes ft.Parse e fit.Fixture podem ser determinadas no classpath no ambiente de testes que ao rodar usará uma classe fake que possivelmente terá os mesmo métodos para facilitar a escrita de testes.
É quando usamos uma classe como parâmetro para abstrair um comportamento.
Segue o exemplo:
public class AnyThing { public void doSomething() { Something something = new EspecificSomething("something here"); something.do(); } }
Object Seams
Após pequena refatoração será possível sobrescrever o método doSomething no momento do teste.
public classe AnyThing extends OtherThing { public void doSomething(Something something) { ... do(something); ... }
protected static void do(Something something) { ... } }
Dentre as técnicas apresentadas Object seam é a melhor escolha entre as demais.
Chapter 5 - Tools
refactoring – A change made to the internal structure of software to make it easier to understand and cheaper to modify without changing existing behavior.
Ferramentas de refactoring automático são úteis para refactoring rápidos, mas se atente, use apenas as ferramentas que garantam que o comportamento do código não será alterado.
Classe original
public class A { private int alpha = 0; private int getValue () { alpha++; return 12; }
private void doSomething() { int v = getValue(); int total = 0; for (int n =0; n < 10; n++) { total +=v; } } }
Exemplo de refactoring automático mal sucedido
após refactoring:
public class A { private int alpha = 0; private int getValue () { alpha++; return 12; }
private void doSomething() { int total = 0; for (int n =0; n < 10; n++) { total +=getValue(); } } }
O método getValue() incrementára 10 vezes o valor original
São objetos que simulam o comportamento de outro.
Extremamente úteis em código legado, pois esse tipo de código costuma possuir muitas dependências.
Sempre que possível quebre algumas dependências quando algum método contiver muitas responsabilidades antes de usar mocks.
Existem diversas ferramentas de mock como o jMock, Mockito para java, Mockpp e GoogleMock para C++ .
Mock Objects
Ferramentas de teste unitário
XunitUma das primeira ferramentas de teste unitário, escrito originalmente em
Smaltalk por Kent Beck e depois portado para o Java por Kent Beck e Erich Gamma. Foi escrito em diversas linguagens.
Junit
Ferramenta de teste poderosa, rápida e intuitiva para Java No jUnit 3 estendia-se a classe TestCase e dos métodos tinha o prefixo test. No jUnit 4 usa-se a anotation @Test - no teste em si entre outras como @Before @BeforeClass @After @AfterClass para usar recursos e desmontar recursos em comum entre os testes.
CpunitLite Ferramenta para criar testes no C++. No entanto mais trabalhosa e bem menos intuitiva que o JUnit por exemplo. NUnit Ferramenta de teste para plataformas .NET e em C#. Muito similar ao JUnit onde a classe e métodos possuem uma “marca” para representarem testes.
Framework for Integrated tests (Fit)
Desenvolvido por Ward Cunninghan, basicamente se cria e salva documentos htmls escrita em uma linguagem acessível.
Os valores a serem testados são exibidos em uma tabela html. Testes falhos exibem células em vermelho com valor esperado e em verde ao passarem.
Mesmo leigos em programação podem escrever um testes com as expectativas dos respectivos valores.
Um programador ou designer invocará o método verdadeiro e apontará onde o resultado será mostrado.
Outras informações http://fit.c2.com
Ferramentas de teste global
Chapter 6 - I don't have much time and I have to change it
Algumas refactorings podem parecer perda de tempo, mas se ela for feita com as técnicas corretas a médio prazo os benefícios já começaram a serem vistos.
Lembre-se, mesmo trabalhando com prazos apertados, "Remember, code is your house, and you have live in it."
Ao contrário do que possa parecer testes podem fazer com que o trabalho fique mais rápido, pois o feedback é instantâneo e as chances de ajustes ou correções de bugs nesses locais costumam cair drasticamente.
Citação do autor: "Boy, we aren't going back to that again." (Rapazes, nós não voltaremos para aquilo novamente).
Com prazo apertado, surge dilemas, especialmente quando não se há testes. Gastar um pouco mais agora, ou gastar muito no futuro.
Usa-se quando o método possui trechos de código que tem responsabilidades totalmente distintas.
Muito usada quando há laços for por exemplo e para “aproveitar” o laço mesmo laço coloca-se responsabilidades distintas que dependem dos mesmos valores.
Verifica-se quais são as variáveis locais necessárias e passe-as como parâmetros para o novo método.
Sprout method
Comente a linha com o trecho onde o novo método será chamado e no novo método retorno o valor de mesmo tipo.
Use preferencialmente TDD para criar esse novo método
Rode os testes
Chame o método novo no método no de origem e remova os comentários deixados nesse método antigo.
Vantagens
Em geral a maior vantagem é a legibilidade, geralmente é uma reforma rápida, pois os novos métodos devem ter as mesmas funcionalidades do antigo separadamente.
Novos métodos se tornam testáveis
Desvantagens
• Como se trata basicamente de uma reforma, soa muitas vezes como desperdício de tempo
• O método original nem sempre ficará facilmente testável
• Obviamente se perde um pouco de tempo, no entanto, facilidade no entendimento poderá justificar o uso.
São como os sprout methods, mas a nível de classe.
Usada quando um há um mesmo comportamento no sistema diversas vezes.
Cria-se uma nova classe com os métodos repetitivos e os invoca no lugar.
Passe as variáveis locais como construtor da nova classe.
Use comentários antes de remover os métodos originais.
Utilize TDD preferencialmente para criar novos métodos.
Após a substituição, remova os comentários e código duplicado.
Sprout class
Vantagens
• Código bem intuitivo aos olhos do programador e a novas funcionalidades ficam em um único lugar
• Extremante fácil de testar devido a remoção das repetições e similaridades.
Desvantagens
• Difícil de determinar, pois pode se tornar algo abstrato e a equipe deve entender bem essa abstração para saber que ela está disponível no sistema.
• Generalizar demais uma funcionalidade pode dificultar o uso de funcionalidades muito similares.
Wrap method
Renomeia-se o antigo método antigo.
Cria-se um novo com a mesma assinatura do antigo.
O novo método deve invocar o antigo.
Coloque as novas funcionalidades no método novo
Recomenda-se usar a técnica do Extract method que muitas IDEs possuem e apenas renomear para um nome mais apropriado.
public class Employee {
...
public void pay() {
Money amout = new Money();
...
payDispatcher.pay(this, date, amount);
}
...
}
public class Employee {
...
public void dispatchPayment() {
Money amout = new Money();
...
payDispatcher.pay(this, date, amount);
}
public void pay() {
logPayment();
dispatchPayment();
}
...
}
Vantagens
O método antigo renomeado não crescerá em tamanho.
Usa-se o princípio de conservação de parâmetros(Preserve signatures) evitando-se os erros de compilação e alteração do comportamento.
Desvantagens
Os métodos em código legado costumam ter nomes e assinaturas ruins, nesse caso é sempre mantido as antigas.
Geralmente usa-se essa técnica quando não se possui testes, apenas para evitar que se quebre a antiga funcionalidade.
O código ainda se mantém pobre e frágil como no antigo.
Essa técnica também é chamada de 'decorator pattern'.
Recomenda quando se quer adicionar novo comportamento a uma mesma funcionalidade e continuar com comportamento antigo
Cria-se uma classe abstrata que estenda e recebe no construtor a classe com as funcionalidades principais.
Na classe abstrata sobrescreve-se os métodos da superclasse e invoca-os os métodos de mesmo nome na classe passada no construtor.
Wrap class
Classes que necessitarem dessas funcionalidades estenderão a classe decorator e receberão a classe principal no construtor.
As classe filhas sempre deverão herdar da classe abstrata e passada no construtor novas classes com as novas funcionalidades e invoca-se o construtor da superclasse.
Exemplo: Main class:
public class ToolController { public ToolController() { }
public void on() { //anything here }
public void off() { //anything here } }
A classe abstrata:
public abstract class ToolControllerDecorator extends ToolController { protected Controller controller; public ToolControllerDecorator(ToolController controller) { this.controller = controller; }
public void on() {controller.on();} public void off() {controller.off();} }
public class LogController extends ToolControllerDecorator { private Logger logger;
public LogController(ToolController controller, Logger logger) { super(controller); this.logger = logger; } public void on() { // anything method in the logger class
controller.on(); } ... }
Sumário
Embora muitas vezes seja um pouco difícil separar as responsabilidades de uma classe, ao utilizar essas técnicas se tornará uma tarefa cada vez mais fácil.
Recomendado-se também, que além da melhora visual, comece sempre a testar o novo código para não proliferar códigos sem testes.
Chapter 7: it takes forever to make a change
Quando é preciso alterar código, inicialmente precisamos entender o que ele faz. Especialmente ao lidar com código no qual não se está muito familiarizado há um sentimento de insegurança
Understanding( Entendimento)
Na maioria das vezes ao olhar um código saberemos se ele é um código legado ou com boa manutenabilidade.
Boa manutenabilidade(well-maintained): Mais segurança e confiança para novas features ou alterações.
Legacy code: Desespero, muitas vezes piora-se o código pois não o compreendemos bem.
Tempo que se leva após fazer uma alteração saber qual a resposta o sistema teve a essa alteração.
Sem testes, é necessário subir o sistema, e testar como um usuário diversas vezes, e sempre que algo dá errado tudo de novo, isso costuma ser frustrante em muitos casos.
Lag time
A mente humana trabalha melhor quando se tem um feedback rápido de pequenas alterações por vez e isso culminará em um entendimento muito melhor com o todo.
Imagine que código sem teste é como o robo Spirit que esteve em Marte. O comando parte da Terra e somente 7 minutos depois a máquina responderá ao comando, e só saberá se ela realmente respondeu 14 minutos depois.
No aspecto de tempo de resposta, as linguagens interpretadas costumam levar vantagens perante as compiladas.
Quebrar dependências em código legado muitas vezes é um desafio.
Detectar interception points antes da alteração.
Existem alguns técnicas como PassNull, FakeConnections e getInstance.
Ao simular um comportamento evite fazê-las no ambiente de produção e sim no ambiente de testes.
Breaking Dependencies
Interception point
Um simples ponto onde se pode detectar os efeitos de uma simples alteração.
Métodos as vezes um tanto confusos quando detectados os interceptions points, facilita o entendimento e o uso de testes, mesmo que indiretamente.
Por exemplo: Um método que calcule preço e taxas que variam de estado para estado com essa lógica juntas no mesmo método
public class Invoice { ... public Money getValue() {
Money total = itemsSum(); if (billing.after(someDate)) { if (originator.getState().equals('NY') { total.add(getLocalShipping() ); //--> Interception point here }else { total.add(getDefaultShipping() ); //--> Interception point here } }else{ total.add( getDefaultShipping() ) ; //--> Interception point here } total.add(getTax()); return total; } ... }
Pinch points
Um pinch point é uma pequena área de um equema, métodos que ao serem testados podem detectar alterações em vários outros. O termo pinch point muitas vezes é difícil de se entender. Seria como um funil e é justamente nesse "afunilamento" que as bases dos testes serão criadas.
Pinch point é o extremo do encapsulamento.
Build Dependencies
Quebrar o sistema em pacotes com menos classes acelera o processo de building dos testes.
Usar interfaces ao invés de classes concretas. Para facilitar isso, muitas IDEs possui as opções Extract implementor and Extract interface.
Quebrar diversas classes com responsabilidades distintas, diminui tempo de build dos testes. Isso poderá fazer com que os testes em geral rodem mais rápido.
Extract Interface
É uma das técnicas mais úteis para se trabalhar com código legado.
Consiste em criar uma interface e os métodos que se deseja testar coloque os na interface com a mesma assinatura.
Substituir as chamadas que usem a classe concreta pela interface.
Outra boa razão de usar interfaces e que em geral interfaces sofrem poucas alterações diminuindo o risco de erros de compilação.
Muitas IDEs possuem Extract Interface automático, onde e necessário informar apenas o nome da interface e selecionar os métodos que se deseja implementar.
Extract Method
Outra técnica bem útil e prática para se trabalhar com código legado.
Seleciona-se um trecho de código, cria-se um método e cole o conteúdo antigo nesse método.
Passe como parâmetro todas as variáveis locais utilizadas no novo método.
Comente o código no método antigo, invoque o método novo e então rode os testes.
Remova todas os comentários e códigos duplicados se houver.
Inversão de dependência
Geralmente quando usamos interfaces ao invés de classes concretas utilizamos o princípio da inversão de dependência.
Em geral essas interfaces são passadas no construtor ou setter ao invés de serem construídas dentro da própria classe.
Muitas vezes usar muitas interfaces pode soar confuso, pelo menos dois arquivos, mas os benefícios para tempo de rebuild e quebra de dependência valem o preço.
Sumário
O autor recomenda ler o livro Agile Software Development: Principles, Patterns, and Pratices de Robert C. Martin's. Lá há diversas técnicas além das mostradas nesse livro.
Outras técnicas abordadas nesse livro
• Notes/Sketching
• Listing markup
• Scratch Refactoring
• Delete Unused Code
• Adapt parameter
• Encapsule Global References
• Extract and Override Call
• Extract and Factory Method
• Introduce Instance Delagator
• Link Substitution
• Parameterize Contructor
• Parameterize Method
• Extract Implementor
Michael Feathers Senior Trainer, Mentor and Consultant
Michael Feathers is a senior member of Object Mentor team. He provides training, coaching and mentoring services in Agile/XP programming practices, test-driven development, refactoring, object-oriented design, Java, C#, and C++.
Michael has over 12 years of experience in developing world-class software solutions. Prior to joining Object Mentor, Michael designed a proprietary programming language and compiler as well as a large multi-platform class library and a framework for instrumentation control.
Michael is an active member of the Agile/XP community. As a contribution to this community, he developed and maintains the CPPUnit — an open source C++ port of the JUnit testing framework. He is a member of the ACM and IEEE. He regularly speaks at software conferences around the world and has been the acting chair for the Codefest event at the last three OOPSLA conferences.
When Michael isn't engaged with a team, he spends his time investigating new ways of altering design over time in codebases. His key passion is helping teams surmount problems in large legacy code bases and connecting with what makes developing software fun and enriching.
Top Related