Como disse hoje ou ontem em outro tópico, o Peter Coad foi o primeiro autor que li recomendando favorecer composição sobre herança e isto foi em 1999. Anos depois o Joshua Bloch e o Rod Johnson enfatizaram muito isto.
Mas foi quando surgiu a onda de testes unitários é que começou a cair a ficha de muita gente porque com herança ficava muito mais difícil testar. É por isto que alguém deve ter escrito que testes unitários não combinam com herança.
Hoje todo mundo sabe dos defeitos da herança mas se este estágio foi alcançado, além da popularização das ideías do Coad, Bloch e Johnson, com certeza a onda de testes unitários tem sua parcela de contribuição.
Sim, hoje todo mundo sabe os defeitos de herança e quando favorecer composição sobre herança. O que pouquíssima gente parece entender é quando e como herança deve ser usada, isto é, quando ela justifica grandeosamente a sua razão de existir. Ou será ela algo totamente inútil que pode ser removido tranquilamente de qualquer linguagem OO?
As pessoas idolatram tanto os testes unitários que fecharam os olhos para a herança e como ela pode “estender sem quebrar”, ou seja, permitir a evolução do sistema e a criação de novas funcionalidades sem necessariamente quebrar o que já existe. Mas ela prejudica testes unitários, o que vc está falando? Está maluco?. Bom, “a única diferença de um maluco e eu é que eu sei que não sou maluco.”
Não é que as pessoas não entendem os defeitos de herança por causa de testes unitários. Mas sim que testes unitários colaboram para que as pessoas reneguem e não entendam herança. Se vc está começando agora a programar OO, testes unitários poderão lhe dar o ilusório conforto de que vc está fazendo a coisa direito. A mensagem parece ser essa: “Programe como quiser, faça uma arquitetura bem interdependente e enroscada, onde tudo depende de tudo. E se der verdinho parabéns!”
Testes unitários é a rodinha da bicicleta. O problema é que pouca gente tem afirmado que é preciso aprender e gostar de andar sem rodinha. E existem outras maneiras, automatizadas ou não, unitárias ou não, de testar se pelo menos o que vc fez está funcionando… Se fez direito aí o computador não tem como dizer e é nessa hora que a programação deixa de ser uma chata ciência exata e vira arte… O meu Hello World é mais bonito que o seu…
Quando aprendi Fortran em 1968 meu primeiro programinha NÃO foi um HelloWorld. Foi multiplicação de matrizes.
Mas já que atualmente sempre se usa o HelloWorld, porque não fazer teste unitário? Eu acho que seria muito bom já incluir teste unitário no primeiro HelloWorld. Se fosse assim a gente não precisava ler o que eu li ontem dizendo que escrever testes unitários consome tempo exagerado.
[]s
Luca[/quote]
Realmente para hello world por um lado pode ser exagero por outro criar a cultura de usar testes unitários é válida. E não apenas para HW, mas para exemplos de códigos, vários frameworks tem dezenas de exemplos de como fazer, mas nenhum deles com teste. Quem está começando se olha o tetse pode se interessar, expandir e até mesmo entender melhor o proprio framework. E não estou comentando de testes dos frameworks e sim dos exemplos em cima deles.
[quote=Rubem Azenha][code]
public class HelloWorld {
public static void main(String[] args) {
System.out.println(“Hello World!”);
}
}
//…
public class HelloWorldTest extends TestCase {
public void testMain() {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
System.setOut(new PrintStream(bos));
HelloWorld.main(null);
assertEquals("Hello World!\r\n", bos.toString());
}
}
[/code][/quote]
Pois é é bem isso que to falando, se houvessem mais códigos assim, eu acho q facilitaria a adoção. Na primeira mexida no código qdo o teste falha quem está aprendendo logo percebe uma das vantagens e o poder disso.
Aqui eu discordo de você, Sérgio. Acho (minha opinião) que é bem mais fácil escrever código acoplado, sem separação de responsabilidades e pouco coeso sem testes unitários (os de integração não ajudam nisso, aí concordo com você).
Só o fato de você ter que pensar num nome apropriado para o método de teste, testar isoladamente um comportamento do sistema e implementar somente o necessário para que aquele pedaço de código funcione, já te ajudam a evitar o código macarrônico e acoplado.
Só uma pergunta, Sérgio: você já tentou fazer algum pet project usando TDD? (tô perguntando na boa mesmo, só pra saber se você já usou e realmente não achou legal)
De certa forma você está certo. Mas acho que talvez eles não tenham sido muito enfáticos (estou chutando porque não me lembro).
No meu caso só tive contato com o livro do GOF (de 1995) quando foi lançado o CD (datado de 1998 ). Não me lembro o ano que comprei mas foi bem depois do livro ter se tornado clássico. Aliás, foi tão depois, que já havia até edição traduzida para o português e me lembro que comprei o CD porque custava muito mais barato do que a edição nacional. Então no meu caso o Peter Coad foi minha primeira fonte e confesso que até estranhei a insistência dele com este assunto pois dedicou a ele um capítulo inteiro do seu livro Java Design.
Não. Não tenho como dar minha opinião sobre mais esse método de desenvolvimento de software. Agora existe TDD, que obviamente vai depender de testes unitários e existe um sistema qualquer que está sendo feito utilizando a metodologia XXX != TDD que pode ter ou não testes unitários.
Existe algumas classes que gritam por testes unitários. Outras não. Acho testes unitários uma bela ferramenta de testes, mas não é o Santo Gral da programação. Se vc utiliza testes unitários e a luz vermelha está acendendo muito, então procure aprender o porque do erro, o que vc fez errado e como poderia ter evitado isso. Talvez assim seja uma bela ferramenta de aprendizado.
Agora se vc utiliza testes unitários, seu código tem qualidade e vc está feliz e produtivo com isso, então testes unitários é muito bom, para vc.
[quote=saoj]
Não é que as pessoas não entendem os defeitos de herança por causa de testes unitários. Mas sim que testes unitários colaboram para que as pessoas reneguem e não entendam herança. Se vc está começando agora a programar OO, testes unitários poderão lhe dar o ilusório conforto de que vc está fazendo a coisa direito. A mensagem parece ser essa: “Programe como quiser, faça uma arquitetura bem interdependente e enroscada, onde tudo depende de tudo. E se der verdinho parabéns!”[/quote]
Que enorme besteira isso. Herança atrapalha em absolutamente nada para escrever testes unitários, pois você estará verificando uma unidade por vez. O fato de existirem um zilhão de classes filhas não interfere em nada. Sistemas com boa cobertura de testes costuma abusar menos de herança pois ela aumenta em muito o acoplamento da classes do sistema.
Eu pessoalmente acho que herança é um daqueles conceitos de OO que simplesmente não deu certo na prática, é uma idéia falha que só leva a designs furados. O próprio Gosling mesmo disse que teria tirado herança do Java se tivesse achado na época uma solução razoável para se usar apenas composição.
Quase sempre que eu vejo código usando herança é síndrome do programador folgado que prefere usar herança para entubar métodos auxiliares para economizar no teclado. O indivíduo cria classes inchadas sem nenhum desacoplamento claro de qual objeto faz o que.
Herança é forte sintoma de incapacidade de separar as responsabilidades dos objetos de maneira fina. Herança quase sempre é desculpa para fazer POG.
[quote=saoj]A mensagem parece ser essa: “Programe como quiser, faça uma arquitetura bem interdependente e enroscada, onde tudo depende de tudo. E se der verdinho parabéns!”
[/quote]
Você está um pouco equivocado… testes unitários contribuem justamente no sentido contrário de que você afirmou.
Essa analogia as rodinhas de bicicleta implica que existe alguma vantagem em andar sem elas. Numa bicicleta normal, existem diversas vantagens (maior mobilidade, flexibilidade nas manobras, nao fazer papel de maneh quando vc ja passou dos 16 anos, etc). Eu nao vejo absolutamente nenhuma vantagem em escrever codigo fora do TDD alem do bom e velho “so pra dizer que eh macho”. Cite algumas?
Joshua Bloch usou bastante herança para fazer as collections. Usou interface tb, e da maneira correta é claro, mas fez uso extensivo de herança quando poderia ter optado por composição.
Que eu saiba todo objeto em Java herda de Object, logo herança é um conceito fundamental e essencial da linguagem Java.
Se eu tenho uma classe com 100 métodos e eu quero modificar o comportamento de um método, eu não quero ter que usar composição e delegar 99 métodos. É muita verbosidade em troca de ser puritano, sem falar de classes a mais (FruitImpl, Fruit, Apple). Até mesmo os criadores do Java optaram por herdar de Object ao invés de uma interface Object. Pensem bem como seria a linguagem se tivéssemos uma interface Object e a cada novo objeto tivéssemos obrigatoriamente que implementar hashCode, toString, equals, etc?
De qualquer forma, via composição + interfaces (digitando pra kacete) ou herança (digitando pouco) é possível estender as características de um objeto sem quebrá-lo, isto é, é possível criar novas funcionalidades (novo objeto ou método subescrito) sem distruir o objeto que ficou embaixo (herança) ou dentro (composição).
Mas concordo se vc tem um objeto A, e se por algum motivo justificável vc vai modificar agressivamente o código dos métodos já prontos de A, durante a vida do projeto, então seria bom ter testes unitários para A.
Eu geralmente programo um método uma única vez, faço ele funcionar como eu quero e depois não mexo mais nele, ou se mexo é para fazer um ajuste fino onde qualquer pessoa normal e minimamente atenta não vai fazer cagada. Até onde eu sei, um IF não exige replicação de código, não é POG, nem pecado.
public String myMethod(String p) {
String res = null;
// um monte de código aqui que vc não vai mexer...
if (nova condição) {
// faça uma coisa nova aqui...
} else {
// deixe aqui o código que já estava funcionando...
}
// um monte de código aqui que vc não vai mexer...
return res;
}
A nova funcionalidade pode até não funcionar (claro, pois vc está codificando ela agora), mas o que já funcionava pode e tem que continuar funcionando.
Um caso que pede testes unitários é quando o método myMethod implementa um algoritmo mais ou menos complexo e vc vai modificar agressivamente esse algoritmo (pode até refaze-lo do zero) e não quer quebrar os contratos/output desse algortimo. Ex: BufferUtils, ByteUtils, etc. Nesses casos, achar que vai resolver o problema com um IF do tipo acima é ilusão. Mas essas classes são exceção e não regra.
É claro que o IF a cima só vai ser feito se a possibilidade de hierarquia (via herança ou composição) for descartada. Ninguém deve fazer algo assim: (erro clássico)
public void coma() {
if (sou cachorro?) {
// coma como um cachorro
} else if (sou gato?) {
// coma como um gato
} else {
// coma como um animal
}
}
Será que as pessoas estão com medo de usar herança, com preguiça de usar composição, e estão programando como o exemplo acima e usando testes unitários como muleta? Sinceramente acho que não, mas talvez os conceitos de hierarquia e separação de responsabilidades não estão sendo entendidos como deveriam. É o caso de se ficar vigilante para não adentrar esse caminho, com ou sem testes unitários…
Sim, todo programador Java iniciante sabe disso, mas e daí?
Primeiro, porque você tem um ÚNICA classe com cem, CEM!, métodos? Isso é design ruim, e provavelmente você não está utilizando herança, nem composição, nem orientação a objetos. Provavelmente é comportamento pra ser quebrado em algumas outras classes.
Segundo, quem disse que você TEM que expor todos os métodos da classe que está delegando? A beleza da OO é o encapsulamento, e se você repassa todos os métodos, existe alguma coisa furada.
Terceiro, o que a quantidade de classes influencia na qualidade do código? Se for necessário ter mais classes para melhorar o design, que seja feito.
Quarto, o que FrutaImpl faz? Esse é o problema! Você está querendo fazer composição, mas pensando em herança! Aí é claro, isso é POG! Quer ver um exemplo bom de composição? Digamos que eu queira saber valores nutricionais da fruta, cujo cálculo é “complicado” (na realidade, não tenho a mínima idéia se é isso ou não). Eu criaria uma interface InformacaoNutricional que qualquer fruta pudesse ter como referência (ou seja, composição), e poderia criar mais de uma classe para essa essa interface se houver diferentes estratégias (pattern?) de implementação. A Fruta poderia também ter outras interfaces para outros conceitos. Ou seja, não estou usando uma classe sem sentido como FrutaImpl, onde eu iria socar qualquer coisa lá dentro, estou usando composição de maneira correta.
Isso não acontece de verdade, vide explicação anterior.
Porque complicar o descomplicado? Afinal, como é que eu diferencio um código em que se modifica “agressivamente”, de um outro em que se modifica “calmamente”? Melhor mesmo é parar de discutir besteiras e fazer testes unitários em todos os trechos de código possíveis.
[quote=saoj]Eu geralmente programo um método uma única vez, faço ele funcionar como eu quero e depois não mexo mais nele, ou se mexo é para fazer um ajuste fino onde qualquer pessoa normal e minimamente atenta não vai fazer cagada. Até onde eu sei, um IF não exige replicação de código, não é POG, nem pecado.
public String myMethod(String p) {
String res = null;
// um monte de código aqui que vc não vai mexer...
if (nova condição) {
// faça uma coisa nova aqui...
} else {
// deixe aqui o código que já estava funcionando...
}
// um monte de código aqui que vc não vai mexer...
return res;
}
A nova funcionalidade pode até não funcionar (claro, pois vc está codificando ela agora), mas o que já funcionava pode e tem que continuar funcionando.[/quote]
Primeiro (novamente!), quem disse que o que você quer agora será a mesma coisa que você irá querer amanhã? Quem disse que um “ajuste fino” não vai quebrar outras partes do sistema? Ninguém consegue prever ao certo se determinada modificação vai trazer efeitos colaterais ou não, independente do tamanho da modificação. Isso não existe, é desculpa esfarrapada de programador preguiçoso.
Segundo, aquele if do código é pecado SIM! Quem te disse que todas as partes do sistema que utilizavam o trecho de código antigo, vai continuar usando? Isso só aconteceria se expressão dentro do if estivesse correto, cuja prova só seria possível se fosse feito testes unitários.
Besteira, não existe o conceito de “modificação agressiva”, já explicado anteriormente.
[quote=saoj]É claro que o IF a cima só vai ser feito se a possibilidade de hierarquia (via herança ou composição) for descartada. Ninguém deve fazer algo assim: (erro clássico)
public void coma() {
if (sou cachorro?) {
// coma como um cachorro
} else if (sou gato?) {
// coma como um gato
} else {
// coma como um animal
}
}
[/quote]
Não existe diferenças conceituais entre o primeiro e o segundo exemplo com ifs. Mas você está dizendo que um é bom e o outro não. Por quê?
[quote=saoj]Será que as pessoas estão com medo de usar herança, com preguiça de usar composição, e estão programando como o exemplo acima e usando testes unitários como muleta? Sinceramente acho que não, mas talvez os conceitos de hierarquia e separação de responsabilidades não estão sendo entendidos como deveriam. É o caso de se ficar vigilante para não adentrar esse caminho, com ou sem testes unitários…
[/quote]
Também acho que as pessoas não estão com medo. É questão de inteligência mesmo. Separação de responsabilidades de faz com composição ou com herança. A diferença é que, com composição, a configuração pode ser feita em runtime.
Pra finalizar, não é questão de masculinidade. Fazemos testes unitários por puro pragmatismo.
Só para explicar o FruitImpl, que acho que vc não entendeu:
public interface Object {
public String toString();
public int hashCode();
// outros aqui...
}
public class ObjectImpl implements Object {
public String toString() {
// implementação default de todo objeto..
}
pubilc int hashCode() {
// implementação default de todo objeto...
}
// outras implementações default aqui...
}
public class MyClass implements Object {
private final Object obj; // composição
public MyClass() {
this.obj = new ObjectImpl();
}
public int hashCode() {
return this.obj.hashCode(); // delegando...
}
public String toString() {
// minha implementação, diferente de ObjectImpl...
return "MyClass: " + obj.toString();
}
}
É assim que vcs gostariam que Java fosse? Sem herança, com tudo baseado em composição e interfaces?
Pensei nesse código rapidamente, logo se houver alguma coisa errada por favor corrigia-o, para o bem do debate. Mas corrigia-o com código e não com palavras, pois o código deixa menos espaço para interpretações.
Ok, cem métodos é muito. Foi apenas um número solto, ok? Então pegue o java.util.Map, que tem mais que 10 métodos. Eu (e parece que nem o Joshua Bloch) não quero ter que reescrever e delegar 15 métodos. É tedioso e error-prone. Veja o exemplo bastante concreto de código acima e tire suas próprias conclusões. E sim, composição com interfaces é isso. Java não é Ruby com duck typing. Tem que implementar todos os métodos da interface, se não não compila.
Tem certeza? O primeiro IF é um IF lógico e nada mais. IF’s não são pecado, ou vcs querem tb remover o IF da linguagem? Já o segundo está assassinando claramente o polimorfismo, um erro clássico de quem está começando com programação OO ou de quem está programando muito rápido sem tomar cuidado com a arquitetura.
Trocar polimorfismo por if (obj instanceof Dog) é um erro muito feio que todo programador sincério (sincero + sério) vai admitir que cometeu no passado.
Herança, assim como qualquer outra coisa, tem seus usos mas é amplamente exagerada sem motivo.
Você não precisa de herança -como em java- para ter OO. Linguagens OO baseadas em protótipos basicamente clonam o objeto original. Da mesma forma linguagens como Ruby permitem que reuso de código seja feito através de mixins, você não precisa usar herança para quase nada e um subset de Ruby que não incluísse herança continuaria sendo OO.
Heranças atrapalham testes da mesma maneira que composição atrapalha: quando se cria acomplamento com classes difíceis de serem testadas. O resto escrito neste tópico é FUD
Você não precisa de herança para ter polimorfismo.
Vou tentar trazer o tópico de volta ao assunto original, começando do ponto onde ele começou a desvirtuar…
Sérgio, você pode explicar isso um pouco melhor? Eu uso TDD e testes unitários há alguns anos e não sinto isso. Quando programo eu sempre tento deixar as coisas fáceis de serem testadas, mas não lembro de ter abolido herança para isso.
Programar com testes unitários me faz pensar um pouco diferente. Eu sempre assumo que um método que eu fiz pode ser repensado a qualquer momento e por qualquer pessoa. Novas regras de negócio podem surgir, restrições de performance podem surgir, uma idéia para melhorar o design pode surgir. Neste caso meus testes ajudam a especificar e assegurar a responsabilidade de cada método, já que eu não confio na minha memória depois que alguns dias e algumas centenas de código já passaram na minha mão. E se for outro programador que estiver alterando o código, quero que ele tenha esta mesma segurança que eu.
Já ouvi diversas vezes por aí que herança deve ser evitada pois prejudica os testes unitários…
Colocando dessa maneira não tenho como discordar de vc. Se vc prevê e deseja tanta flexibilidade assim com o seu código e se vc tem certeza que os seus testes unitários vão garantir isso, então tudo bem. Com tanta flexibilidade assim, logo vc vai querer mudar a assinatura do método e terá que refazer também o teste unitário. E quem testa o teste unitário? Eu, pessoalmente, não gosto dessa metodologia de flexibilidade total, muda tudo, refaz tudo, etc. Eu vejo um sistema ou código como coisa séria, principalmente se ele está funcionando. Sair alterando tudo a torto e a direito exige uma ótima razão de ser. As regras do negócio não mudam a torto e a direito. Geralmente elas são evoluidas para acomodar novas funcionalidades e necessidades. Não são refeitas do zero, pelo menos não com essa frequência toda que vc sugere para justificar testes unitários. Apenas minha opinião. Há casos e casos, pessoas e pessoas, projetos e projetos, linguagens e linguagens, etc.
Na verdade, se rever a minha primeira mensagem, verá que eu entrei na discussão não para debater se testes unitários é bom ou ruim. Se vc o utiliza dessa maneira é claro que é bom. TDD é totalmente válido. O que eu discordei, e continuo discordando, é da afirmação de que não seria possível evoluir um sistema adicionando novas funcionalidades sem fazer uso de testes unitários. É sim, plenamente possível, estender sem quebrar um sistema OO minimamente bem-feito, e não deve ser os testes unitários que vão te garantir isso, mas um bom entendimento de herança, separação de responsabilidaes, polimorfismo, lógica básica (IF-THEN-ELSE) e disciplina. Pelo menos em Java…
Justamente porque também vejo código como coisa séria é que prefiro contar com o máximo de testes possível. Não pense no tamanho ou frequencia das mudanças. A questão é que a probabilidade de que um método nunca mude é bem baixa. Então por que não escrever alguns testes para ajudar se/quando essa hora chegar? Não é porque as mudanças são pequenas ou infrequentes que devemos tratá-las como menos importantes. Por isso em boa parte do tempo eu concordo com o Uncle Bob:
Quem afirmou isso? É claro que é possível evoluir um sistema sem testes unitários. Afinal, é assim que a maioria dos sistemas ainda são tratados. Também é óbvio que um sistema bem-feito é mais fácil de se manter. Assim como é óbvio que os testes unitários não resolvem 100% dos problemas na hora de dar manutenção num sistema.
A questão é que um conjunto de testes bem feito ajuda MUITO. E aqui não falo apenas de testes de unidade, embora eles com certeza fazem parte da “safety net” que todo programador deveria tentar construir quando está programando.
[quote=saoj][quote=s4nchez]
Sérgio, você pode explicar isso um pouco melhor? Eu uso TDD e testes unitários há alguns anos e não sinto isso. Quando programo eu sempre tento deixar as coisas fáceis de serem testadas, mas não lembro de ter abolido herança para isso.
[/quote]
Já ouvi diversas vezes por aí que herança deve ser evitada pois prejudica os testes unitários…[/quote]
Eu tambem já ouvi diversas vezes por aí que sair de casa a noite deveria ser evitado pq tem lobisomem na rua.
A resposta é simples: um projeto do zero é dificilimo de se inserir testes unitarios por causa do acoplamento entre as classes ja existentes.
Acho que o Sergio e pessoal do menta passou por isso: alguns comecaram a colocar testes unitarios la, e perceberam que eh uma tarefa quase impossivel. Foi o mesmo quando a gente fez o vraptor1 e quis fazer testes unitarios DEPOIS de ter feito muito codigo. Testes unitarios sao impossiveis quando voce nao tem uma “unidade”.
É normal. Todo mundo passa por isso. Depois que usa testes unitarios num projeto do zero, é paixao a segunda vista!
Repare que todo mundo passou pelo mesmo efeito quando viu OO pela primeira vez: eu detestei. Quando vi que deveria usar interface em vez de heranca, tambem detestei a primeira vez. Pra mim isso é fase: todo programador bom passa pela epoca de odio ao teste unitario assim como odiou quando teve de parar de escrever arquivos de 10 mil linhas de codigo. Vamos sempre avancando.