Mês: junho 2010

Acessando DLL’s não gerenciadas no .Net – Parte 1

Objetivo

O objetivo desta primeira parte é descrever como utilizar DLL’s escritas em C/C++ (ou qualquer outra linguagem não-gerenciada) em C#. É um procedimento relativamente simples, mas existem algumas técnicas diferentes para carregamento estático e dinâmico, e ainda como compatibilizar os tipos entre as duas linguagens.

Criando uma DLL exemplo no C++

Para criar uma DLL no C++, devemos seguir os seguintes passos:

  • File | New | Project
  • Visual C++ (Muitas vezes dentro de “other languages”, principalmente para quem configurou o C# como ambiente principal no Visual Studio).
  • Win32 Project
  • Escolha o diretório
  • O wizard do Win32 Project vai ser iniciado.
  • Clique em Next
  • Escolha DLL
  • Empty Project

Crie um novo “header file”, chame ele de stdafx.h.

Crie um novo source file de nome SampleDLL.cpp, com o seguinte conteúdo:

#include "stdafx.h"
#include "windows.h"
#include "tchar.h"

int sum(int a, int b){
	return a + b;
}

void otherSum(int a, int b, int &result){
	result = a + b;
}

No caso temos dois métodos, um “sum” que retorna a soma de dois números, e outro “otherSum” que retorna a soma de dois números, por referência.

Adicione um novo “Module Definition File” (.def). Esse arquivo serve para colocar algumas definições na DLL. Nesse caso, para exportar os nomes das funções com nomes mais “amigáveis”. Vamos chamá-lo de SampleDLL.def colocar o seguinte conteúdo neste arquivo:

LIBRARY	"SampleDLL"
EXPORTS
  sum @1
  otherSum @2

Vamos compilar o projeto e partir para o lado C#.

Criando uma aplicação no C#

File | New | Project | Windows Application.

Vamos criar uma nova classe chamada CppInterop.

Dentro dessa classe, vamos colocar o seguinte código:

[DllImport("SampleDLL.dll")]
public static extern int sum(int a, int b);

[DllImport("SampleDLL.dll")]
public static extern void otherSum(int a, int b, ref int result);

O atributo “DllImport” é quem identifica qual a DLL que vai ser carregada para executar o método. A seguir a assinatura do método da DLL vai ser praticamente copiada para o C#. Nesse caso, a tradução é manual, ou seja, é necessário entender como funciona a chamada da função e usar tipos que são intercambiáveis. Falaremos mais sobre a tipagem adiante.

  • Arraste um botão para dentro do form.
  • Clique duas vezes neles.

Dentro do método button1_click, adicione o código:

int res = CppInterop.sum(10, 20);
MessageBox.Show(res.ToString());

CppInterop.otherSum(5, 2, ref res);
MessageBox.Show(res.ToString());

Agora execute e clique no botão. Vai receber um DllNotFoundException com o erro: “Não é possível carregar a DLL ‘SampleDLL.dll’: Não foi possível encontrar o módulo especificado. (Exceção de HRESULT: 0x8007007E)”

Para explicar esse erro, vamos ao próximo tópico.

Static Load x Dynamic Load

A diferença básica entre o carregamento estático (static load) e o carregamento dinâmico (dynamic load) está na dependência direta da DLL. Se você tem uma aplicação escrita em C++ e faz referências diretas a funções “externas”, está fazendo um static load.

Quando a aplicação é executada, automaticamente a DLL é procurada e caso não encontre a aplicação nem sobe.

A seqüência de busca de uma DLL é praticamente padrão no Windows:

  • Diretório System32
  • Diretório da aplicação
  • Qualquer diretório no PATH (variável de ambiente).

Já nas aplicações com carregamento dinâmico, a aplicação chega a subir, e ocorre a tentativa de subir a DLL. Nesse caso é possível a aplicação continuar executando ou mesmo mandar erros mais amigáveis. Geralmente usa-se essa abordagem em plugins.

No caso de aplicações em C# Windows Forms, percebemos um comportamento um pouco diferente. O erro só ocorre quando tentamos executar o método “extern” com o atributo DllImport, o que nos faz entender que no momento da execução do método, a DLL é carregada e o método é executado.

Vamos agora então copiar a SampleDLL.dll para dentro do diretório bin/debug e atender o segundo critério listado acima para carregamento da DLL.

Executamos a aplicação e percebemos que ela funciona. Executamos o código C++ dentro do C# através da DLL.

Outro comportamento interessante que observamos aqui é que mesmo após a execução dos métodos, se tentarmos apagar ou renomear a DLL (que teoricamente está em uso), conseguimos realizar a operação. Isso nos faz pensar que o carregamento e descarregamento da DLL a cada execução, porém examinando o resultado do executável com o ProcessExplorer, percebemos que a DLL continua carregada.

Com isso, percebemos que quando fazemos o DllImport diretamente, internamente o Platform Invoke (como a Microsoft chama essa feature de intercambiar código gerenciado com não-gerenciado) sempre utiliza carregamento dinâmico.

O atributo DllImport também nos dá outras opções para especificar diferentes entry points ou calling conventions. Não vou explorar todos os detalhes aqui.

Diferenças de Tipagens

Existem formas completamente diferentes de tratar informações no mundo gerenciado e não gerenciado. Muitos tipos são exatamente iguais, como o usado no exemplo acima, o Int32. Ele acaba tendo exatamente a mesma representação nas duas linguagens. O mesmo não corre com alguns tipos.

Esse artigo do msdn nos demonstra a compatibilidade, de forma bem simples: Platform Invoke Data Types .

A parte divertida das tipagens, está no tipo “IntPtr”. Ele foi uma forma de conseguir colocar um ponteiro dentro do C#. Tudo que é um ponteiro em C++ ou outras linguagens não gerenciadas são diretamente intercambiáveis com um IntPtr.

Devolvendo Strings do C++ para o C#

O C++ tem uma forma muito particular de tratar strings. São as famosas strings terminadas em zero ou “Null terminated strings” (ver Caractere Nulo).

Geralmente quem vem do mundo C#, Java ou mesmo Delphi, tem tipos strings que fazem todo o controle de tamanho, memória, etc. Em C++ existe também esse tipo, mas geralmente não é usado, por ser “caro” demais. Programadores C++ geralmente tem performance correndo pelas veias.

Geralmente passa-se um ponteiro para o primeiro caractere da string e a vai-se incrementando o caractere até achar um caractere #0 (null). Isso indica que acabou a string e a memória dali pra frente passa a ser “desconhecida”. Outros níveis de complexidade começam a aparecer quando a string deixa de ser uma string ASCII/ANSI (um byte por caractere) e passa a ser Unicode ou Multibyte. A idéia deste artigo não é tentar explicar isso.

Tendo em mente essa idéia, geralmente quando é necessário devolver uma string terminada em zero em linguagens não gerenciadas, ocorre um problema quanto à alocação da memória necessária para a string.

  • Se dentro do C++ você declarar um array de caracteres como uma variável local e devolver o ponteiro, vai gerar um belíssimo bug, pois a memória da pilha é liberada e o ponteiro passa a ser inválido.
  • Se dentro do C++ você alocar a memória no heap com um new ou um malloc e devolver o ponteiro, quem chamou a função precisa “lembrar” de desalocar a memória. Essa é uma bela forma de gerar um vazamento de memória (memory leak).
  • A solução comumente usada para esse problema é quem está chamando a função alocar um buffer e passar o ponteiro para o início do buffer e o seu tamanho máximo. É praticamente uma convenção no mundo C/C++ essa abordagem.

Com isso, vamos fazer um novo método na nossa DLL C++ para concatenar duas strings. É um método bastante inútil, visto que já existe o strcat. Mas para fins didáticos, é interessante. Vamos ao seu conteúdo no C++:


void concat(LPTSTR src1, LPTSTR src2, LPTSTR dest, int destSize){
	_tcscpy_s(dest, destSize, src1);
	_tcscat_s(dest, destSize, src2);
}

O tipo LPTSTR no C++ indica um “ponteiro para uma string terminada em zero”, porém o LPTSTR tem uma série de defines dentro do windows.h e tchar.h, para fazer com que, caso a aplicação seja compilado com _UNICODE, os tipos sejam convertidos internamente para 2 bytes por caractere (O Windows trata todos os caracteres com 2 bytes em Unicode). Caso seja compilado sem _UNICODE, vai um byte por caractere. Aqui vamos assumir que vai ser compilado como _UNICODE.

A idéia desse método é devolver em “dest” as strings src1 e src2 concatenadas. “destSize” é o tamanho do buffer pré-alocado em dest.

_tscpy_s é uma macro que traduz para uma função que copia uma string para outro buffer de forma “segura” (evitando o estouro do buffer). Por isso o parâmetro destSize é passado. A macro é para garantir a questão da compilação com ou sem _UNICODE.

_tsccat_s é uma macro que traduz para uma função que concatena duas strings de uma forma segura.

Como fica isso no C#?

Vamos alterar o nosso CppInterop.cs e adicionar o seguinte método:

[DllImport("SampleDLL.dll")]
public static extern void concat([MarshalAsAttribute(UnmanagedType.LPWStr)]string src1,
  [MarshalAsAttribute(UnmanagedType.LPWStr)]string src2, IntPtr dest, int destSize);

O MarshalAsAttribute (UnmanagedType.LPWStr) é um atributo usado no argumento da função, indicando que no momento da tradução, no lado “Unmanaged” temos um LPWStr (ponteiro para caractere de 2 bytes), ou seja, no momento do “Invoke” da função, o Platform Invoke vai fazer o trabalho sujo de tradução para nós. Aqui se usássemos um LPTStr, o resultado seria o mesmo, já que no lado “Unmanaged” sabemos que temos uma string de caracteres de 2 bytes. Eu coloquei diferente justamente para ilustrar que o Platform invoke não faz “checagens” dos tipos do lado de lá. Ele assume que o tipo do lado de lá, executa o método e vamos ver o que acontece (as vezes cai em memória indevida e dá Access Violation).

Os dois primeiros parâmetros serão passados como strings e traduzidas. Se o terceiro parâmetro é um LPTSTR no C++, por que foi traduzido como um IntPtr no C#?

A resposta é que a idéia desse parâmetro é passar o ponteiro para um “buffer” onde a função vai responder a string concatenada. Ou seja, eu vou dar um espaço de memória para o C++ escrever e depois vou ler o que tem dentro. O destSize é justamente o tamanho desse buffer, para evitar que o C++ escreva em memória indevida.

A chamada dessa função no C# fica da seguinte forma:

IntPtr p = Marshal.AllocHGlobal(100);
CppInterop.concat("String", " concatenation", p, 100);
MessageBox.Show(Marshal.PtrToStringUni(p));
Marshal.FreeHGlobal(p);

Vamos tentar entender o código acima.

A primeira linha, vai alocar um buffer de 100 bytes e retornar o ponteiro para um IntPtr.

A segunda, vai executar o método concat, convertendo os dois primeiros parâmetros e dando o buffer pré-alocado no terceiro parâmetro e indicando pro C++ q ele pode escrever em até 100 bytes.

A terceira linha, usa métodos auxiliares da classe Marshal, o PtrToStringUni, que vai converter o conteúdo do buffer para uma string em C#, considerando que o que tem apontado é uma string terminada em zero Unicode (existem as versões multi-byte e ANSI).

A quarta linha é para liberar a memória alocada na primeira linha. Ou seja, o buffer que eu pré-aloquei. Quem disse que o C# não vaza memória? :-P. É interessante colocar blocos como esse dentro de um try/finally.

Outro exercício interessante que podemos fazer com esse exemplo é mudar o tamanho do buffer em AllocHGlobal e na chamada do concat para “10”. Com isso, vamos perceber que vai estourar uma EAccessViolationException no C#. Por incrível que pareça isso é bom.

A função _tcscat_s, verifica o tamanho do buffer e estoura uma exceção que é retornada para o C#.

Se quisermos ver uma coisa mais divertida, mantemos o tamanho do buffer com 10 e na DLL C++, alteramos as chamadas de _tcscpy_s para _tsccpy e _tcscat_s para _tcscat.

O efeito que obtemos é mais divertido, por que a string é concatenada mas escreve numa área de memória que sabe-se lá o que tem dentro. O C# chega a mostrar a string, porém dá uma exceção EAccessViolationException. O C# consegue identificar que está lendo memória “inválida”, porém, já houve a gravação nessa memória.

Conclusão

Observamos que o C# tem vários recursos interessantes para acessar código escrito em linguagens não gerenciadas. Mesmo funções da API do Windows podem ser chamadas diretamente desta forma.

Percebemos também que é necessário conhecermos muito bem o funcionamento das chamadas em código não gerenciado, já que a tradução dos tipos não é simples e totalmente intuitiva.

Nas próximas partes desse artigo pretendo abordar alguns tópicos um pouco mais avançados sobre o assunto.

Gostaria de agradecer os amigos Marcos Capelini e Rodrigo Strauss por me ajudarem bastante com as práticas e culturas mundo C/C++.

Código fonte

Baixe o código fonte aqui.

Anúncios

Reescrever ou não reescrever, eis a questão

Introdução

Quantas vezes já nos deparamos com um software legado e pensamos: é melhor jogar tudo fora e fazer de novo. É uma triste realidade, mas tem horas que com o nosso próprio código, temos esse sentimento. Quando falamos daquele sistema legado de uns 10 anos de idade, é pior ainda.

Nunca sabemos quando vamos mexer num lugar e quebrar outro imprevisível, ou ainda quantos pontos de entrada com regras de negócio diferentes temos para a mesma entidade, ou ainda impactos de performance. Tem ainda aquele bug que o cliente já se acostumou tanto com ele que se corrigimos, ele reclama.

Por causa desse sentimento, acabamos chegando à conclusão que se começarmos de novo, vamos ter sucesso.

Será que é verdade?

O que torna um software impossível de se manter?

Existem vários fatores, mas uma verdade é inquestionável: Todos os softwares começam bem, e dali algum tempo viram legados que ninguém quer mexer. Por que?

  • Falta de padrões: Raramente começamos um software com padrões arquiteturais bem definidos, sobre como evoluir o software, como nomear variáveis, métodos. Pior ainda, raramente conseguimos disseminar isso e tornar isso parte da cultura das equipes, que inevitavelmente tem várias baixas durante os projetos.
  • Falta de definição de requisitos: O tempo investido no levantamento de requisitos geralmente é incondizente com o projeto. É necessário estressar muito o levantamento de requisitos, documentar muito bem todos os critérios necessários para conseguir um produto final de qualidade.
  • Falta de processos: A gestão do ciclo de vida do software é muito importante. Processo de liberação de versão, gestão de configuração e versões, testes, homologação, deploy, integração contínua, etc.

O outro lado da moeda.

É muito fácil falar em falta de padrões, de requisitos e de processos. Essas reclamações são uma constante quando conversamos com profissionais da área de desenvolvimento de software.

Mas será que se tivéssemos acertado tudo, ainda estaríamos no caminho do software perfeito? Eu particularmente acredito que não, e as razões são:

  • Os padrões mudam: O padrão arquitetural ideal de hoje, provavelmente não será o padrão arquitetural ideal de amanhã. As tecnologias mudam, os padrões mudam.
  • Os requisitos mudam: Raramente o usuário conhece os requisitos. Geralmente os requisitos vêm de vários usuários, áreas, empresas, organizações. Por mais que se estresse o levantamento, é impossível prever 100% das situações. O processo minimiza o retrabalho, mas não elimina. Novamente, tem o fator evolução. Os requisitos que hoje atendem ao negócio de hoje, amanhã podem não atender.
  • Os processos mudam: Antigamente era tolerável colocar uma ferramenta de sincronização para atualizar o schema de um banco de dados em produção. Hoje em dia as equipes de desenvolvimento estão proibidas de colocar a mão em servidores de produção. Antigamente aceitava-se uma compilação proveniente da máquina do desenvolvedor, hoje em dia não. Em resumo, os processos evoluem.

Cada vez que as tecnologias evoluírem vamos reescrever os softwares, mesmo que as linguagens sejam exatamente as mesmas?

De volta ao mérito da questão: Reescrever ou não?

Pergunta difícil, né? Eu vou tentar respondê-la com uma proposição lógica. Se hoje o seu legado está tentando sair de um nível de maturidade caótico e não consegue, quando o novo software se tornar um legado ele vai conseguir?

Pra mim a resposta a maioria das vezes é não.

Se a organização não tem maturidade para conseguir fazer o produto evoluir de forma consistente, estável, sem autodestruir a base do produto, se ela não consegue manter uma documentação andando junto do produto atual, porque conseguiria fazer tudo isso num software novo?

A verdade é que a maioria dos problemas está nos processos. Nas pequenas decisões de implementação “ótima”, “média” ou “quick ‘n’ dirty”, o “quick ‘n’ dirty” acaba sempre ganhando, por causa do patrocínio do cliente, que sempre quer a coisa rápida e funcional para ele. As dificuldades de gestão do produto e de manutenção de código não são problema dele (no fundo são, mas ele não percebe diretamente). Mas sempre essas pequenas decisões acabam carregando o custo intangível do produto se tornar caótico a longo prazo. Esse custo só se torna tangível, quando surge a máxima “é melhor jogar tudo fora e fazer de novo”. Aí é possível sentir o real custo de tornar um software caótico.

Pra mim o único momento em que “reescrever” é uma opção é quando se trata de uma troca de tecnologia completa. As vezes mesmo uma troca parcial de tecnologia (um componente, por exemplo) consegue-se resolver através de processos. No momento em que nada se aproveita, ou seja, a linguagem é diferente, a framework é diferente, aí não tem jeito. É necessário reescrever. Mesmo assim, se a base do produto está boa, é possível fazer um porte diretamente a partir do código fonte, sem mudar conceitualmente o produto e aproveitando todo o trabalho de levantamento de requisitos.

O paradoxo do “eu não sei o que tem lá dentro”

Muitas vezes essa é a grande justificativa para se reescrever um software inteiro. “Eu não sei o que tem lá dentro”, “eu não sei como esse software se comporta”. Sim, estou falando de níveis de caos elevados.

Parte-se do princípio que reescrevendo o software todo, passa-se a conhecer o que está lá dentro. Mas para conhecer o software novo, você vai desprezar toda a experiência, todo o conhecimento já levantado e funcionando no software atual?

Numa das minhas experiências profissionais, tive a oportunidade de trabalhar num projeto de “reescrever” um produto. Segregaram-se as equipes em “novo e atual” e começou-se um levantamento de requisitos do zero. Uma equipe mantinha o produto atual e a outra começava o novo. Existe todo um contexto que impede que a afirmação possa ser científica do tipo “sempre vai dar errado”. Mas nessa experiência, o produto novo nunca atingiu a mesma maturidade do atual, mesmo porque se o atual para de evoluir, raramente a empresa consegue atrair novos clientes, conseqüentemente dinheiro para manter todos os projetos.

Coloca-se o novo produto na frente do cliente e obtém-se o feedback: “E aquele negocinho que tinha no outro?”.

Em resumo, se a motivação para você fazer um novo produto é não conhecer o que já possui dentro de casa, a chance de acertar é bem pequena.

Se reescrever não é a saída, qual é a saída?

É muito difícil formar opinião sobre esse assunto. Acho que atualmente a minha idéia vai mais de encontro com reforçar os processos para fazer o produto atual amadurecer de forma consistente. Ou em outras palavras, criar meios para consertar o avião com ele voando.

Acredito que esse objetivo possa ser conquistado, com os seguintes passos:

  • Estabelecer uma boa política de gestão de configuração e versões. Isso vai permitir que se isole desenvolvimento de alta complexidade, que mexe estruturalmente no produto, de manutenções triviais. Gera meios para o produto se modificar consistentemente.
  • Estabelecer uma documentação de produto. Conseguir um processo que mantenha uma documentação atualizada do que se tem no produto.
  • Gestão de projetos. Cada evolução estrutural do produto é um projeto e deve ser gerido como tal. É muito difícil tornar os benefícios tangíveis, por isso é muito difícil justificar esse tipo de projeto. Os projetos que estão associados com entrada de clientes e fluxo financeiro geralmente ganham na prioridade.
  • Criar processos de testes consistentes. Testes automatizados ajudam muito na evolução do produto, pois sabe-se quando uma nova feature não passa num critério de aceitação definido em versões anteriores.

Em resumo, a estratégia que me parece mais factível é criar um andaime de processos e boas práticas ao lado do produto e à medida que ele começar a tender a ser melhor e mais estável, é um indicador que a organização ganhou a maturidade suficiente para tal. Mesmo que esse produto venha a ser descontinuado, com o aprendizado institucional destes processos é possível construir um novo e ter sucesso. A parte ruim é que tudo isso é intangível.

Dentro do código, existem milhares de pequenas situações, incluídas via bug fixes ou pequenos desenvolvimentos que são extremamente complexos de serem mapeados, mas as vezes representam features chave para os usuários finais. Raramente mapeamos corretamente esses requisitos em novos produtos.

Em resumo, a resposta está nos processos e no bom gerenciamento do ciclo de desenvolvimento.

O “pulo do gato” pra mim está em conseguir dar visibilidade dos ganhos obtidos com projetos de estruturação e que permitem a entrada de novas features sem grandes dificuldades. Exemplo: É sempre muito complicado colocar novas funcionalidades nos pedidos, pois existem lógicas separadas para entrada do pedido por integração via arquivo, por web service ou cadastrado pelo usuário. Não existe um ponto central.

Em resumo, viabilizar um projeto para unificar as três estruturas é muito difícil, pois não permite ganhos financeiros imediatos. Porém, permite que a funcionalidade ganhe estabilidade e projetos futuros sejam muito menos “destrutivos” e suscetíveis a erros. É muito difícil as organizações apostarem nesse tipo e projeto, e pra mim é onde acontecem os grandes erros. Insiste-se no modelo que dá ganhos imediatos e o software deteriora-se por completo no decorrer do tempo, causando perda de clientes por constante insatisfação com a qualidade do software. Até o momento que se chega à máxima: “não vale a pena consertar, tem que fazer de novo”. Nesse ponto faz-se um “go-to” para o início do artigo e volta-se ao círculo vicioso, com a diferença de que todo esse custo “intangível” de deteriorar o produto, começa a se tornar tangível.

Exercitando o modelo

Vamos tornar essa idéia mais prática (quem já acostumou com meus artigos e me conhece pessoalmente sabe que eu sou prático!). Partimos do princípio que temos um sistema de 10 anos de idade, escrito com sucessivas repetições do anti-pattern “magic-push-button”, ou seja, com botões que contém toda a lógica de negócio, acesso a dados, tudo dentro dele. Aí percebemos que além do problema sério de manutenção, precisamos que esse software torne-se multi banco de dados.

O primeiro passo é pensar em como resolver o problema no cenário ideal, construindo um software novo. Parte-se de um pattern em 3 camadas, com apresentação, lógica de negócio e acesso a dados, com a camada de acesso a dados abstraindo o banco de dados.

O próximo passo é fazer uma prova de conceito, com o pattern acessando os dois bancos, estressando todas as formas possíveis que podem gerar problemas: Stored procedures passam a ser indesejadas, pois precisa-se desenvolvê-las para cada um dos bancos de dados. Tipos de dados precisam ser padronizados e podem gerar sérios problemas no modelo de dados, queries que retornam múltiplos datasets tem incompatibilidades, etc.

Uma vez levantados os pontos que precisam ser padronizados, chega-se ao padrão ideal, ou o ponto onde queremos chegar. A partir daqui, partimos para os projetos.

Primeiro projeto para a próxima versão: Converter as stored procedures para a camada de acesso a dados da aplicação. Óbvio que não dá para fazer isso sem testar inputs e outputs. Cada procedure precisa ser mapeada, com entradas e saídas e devidamente testadas, para saber se o “porte” do código tem o mesmo comportamento e não vai gerar problemas na versão atual.

Aí já surgem as vertentes que defendem o pensamento: Mas se eu tiver que fazer testes em todas as procedures, fica inviável.

E porque a idéia de reescrever o software todo parece mais viável que isso? Para que o novo software não se torne um novo legado daqui a cinco anos, não deveríamos ter um processo para isso?

E quando no futuro, esse software precisar de evolução, não passaremos pelo mesmo problema?

A pergunta é mais simples. Se não existe maturidade para evoluir o produto atual, existe maturidade para escrever um novo sem repetir os mesmos erros?

Uma vez que se estabeleça o meio de realizar os testes, tem-se dois ganhos para próxima versão:

  • Procedures portadas para a aplicação, abrindo o caminho para multi-banco.
  • Processo de teste automatizado estabelecido e vivo na próxima versão.

Na próxima iteração, deve-se trocar os tipos de dados para compatibilizar os bancos de dados. Agora não precisamos mais nos preocupar com os casos de teste. Já temos o processo e precisamos somente repetí-lo para a próxima versão.

Conclusão

Os legados impossíveis de se manter surgem da dificuldade com processos de engenharia de software. São processos caros, complexos e que dependem de pessoas com especialidades muito diferentes.

O grande problema é que raramente temos oportunidade de começar um produto novo. Quase tudo é legado. E não poderia ser diferente.

Quando estava lendo um livro sobre a API do Windows, talvez o maior “legado” (não de forma pejorativa) da história, percebi que os headers da API do Windows são praticamente os mesmos desde o Windows 3.1. Hoje estamos no Windows 7. Temos praticamente o mesmo software evoluindo a 20 anos de forma consistente, cada vez mais estável e com mais features, tudo em cima da mesma “casca” da API. Será que o Windows foi inteiro “reescrito”? De uma vez só?

O mesmo observamos em várias ferramentas, compiladores. Vemos um copyright “199x-2010”, indicando que é o mesmo produto, sofrendo sucessivas evoluções. Duvido que eles joguem todo o código fora a cada versão. Reescrevem partes problemáticas, aperfeiçoam outras, mas o conjunto é o mesmo.

Após alguma experiência que já tive com desenvolvimento de software, sempre penso comigo mesmo: Que ações estou tomando para não desenvolver um novo legado (agora de forma pejorativa)?

Reescrever o software gera aquela sensação de confiança no que se está fazendo. Porque você participou de todo o processo e conhece todo o código que está ali dentro. Porém, à medida que a equipe vai aumentando, os requisitos vão se complicando, naturalmente chega-se ao mesmo estágio de evolução da versão anterior. Porque a sensação de controle começa a diminuir. Simplesmente porque o método não mudou.

Aplicando sempre o mesmo método, você acredita que pode obter resultados diferentes?