Olá pessoal. Tudo certo?!
Inspirado na excelente série sobre indicadores do Leandro Daniel (@leandronet), não encontrando uma opção bacana para coleta de indicadores free, resolvi iniciar esse projeto.
Assim como ocorre com FluentIL, pretendo mostrar todo o histórico de desenvolvimento aqui. Além disso, vou manter todos os fontes no GitHub. Logo, espero colaboradores.
Nesse post, mostro a implementação em “baby-steps”
A idéia é prover:
Meu desejo é prover uma alternativa FOSS, de qualidade, para o excelente NDepend.
Nesse projeto, vamos adotar BDD (for real). O processo de desenvolvimento será fundamentalmente o indicado no post anterior.
Considero válida qualquer iniciativa para obter feedback do meu código. Uma das melhores formas de feedback são indicadores (por favor, leia a série do Leandro para entender meu argumento). O problema é que não encontro soluções de qualidade, free, para essa coleta. Então, resolvi desenvolver uma.
Para considerar durante todo o projeto:
Vamos começar devagar. Criei uma solução com três projetos:
Feito o primeiro commit, vamos preparar o build.
Esse build é fundamentalmente o mesmo usado no StrongChess (projeto meio parado, mas interessante). Ele foi desenvolvido pelo Juan Lopes (@juanplopes) e utiliza MSBuild (estou escrevendo uma série, aqui no blog, para explicar o MSBuild).
Commit para o build – done!
Casa arrumada, vamos começar os trabalhos. Eis que surge nosso primeira especificação de feature (usando Gherkin) [no github].
Embora prefira inglês para escrever código, prefiro o português para escrever especificações.
Como pode ver, descrevo brevemente o que é a métrica que estou coletando. Meu cenário testa diversos “exemplos” [no github] de tipos por analisar. Escrevi esses “exemplos” tentando cobrir, da melhor forma, o que a especificação da funcionalidade descreve sobre a métrica.
Você pode ajudar escrevendo mais “exemplos”. O que acha?! Vejamos a execução da especificação:
Como não poderia deixar de ser, o teste é inconclusivo.
Hora de escrever os Step Definitions.
Hora de escrever nosso teste de aceitação, partindo da especificação. Veja:
using System;
using FluentCodeMetrics.Core;
using SharpTestsEx;
using TechTalk.SpecFlow;
namespace FluentCodeMetrics.Specs
{
[Binding]
public class EfferentCouplingSteps
{
// ReSharper disable InconsistentNaming
private int resultingCe;
private Type workingType;
// ReSharper restore InconsistentNaming
[Given(@"que tenho um (.*)")]
public void DadoQueTenhoUm(string tipo)
{
workingType = Type.GetType(tipo);
workingType.Should().Not.Be.Null();
}
[When(@"inspeciono seu acoplamento eferente")]
public void QuandoInspecionoSeuAcoplamentoEferente()
{
resultingCe = workingType.ComputeCe();
}
[Then(@"obtenho (.*)")]
public void EntaoObtenho(int ce)
{
resultingCe.Should().Be(ce);
}
}
}
Como pode ver, optei por implementar o cálculo do Acoplamento Eferente usando Extension Methods. Veja o “Walking Skeleton”:
using System;
namespace FluentCodeMetrics.Core
{
public static class CeExtensions
{
public static int ComputeCe(this Type that)
{
throw new NotImplementedException();
}
}
}
Lindo! Vejamos nosso teste falhando:
Perfeito!
Perceba, agora que completamos o teste de aceitação, iniciamos um ciclo padrão TDD.
Agora que temos nosso teste de aceitação falhando, é hora de implementações. Entendo que, antes de indicar a contagem de referências eferentes, devo conseguir relacionar que tipos estão sendo usados. Comecemos pelo cenário mais simples:
[Test]
public void GetReferencedTypes_EmptyClass_ReturnsObject()
{
typeof (EmptyClass).GetReferencedTypes()
.Should().Have.SameSequenceAs(
typeof (System.Object)
);
}
Como pode ver, optei por criar um Extension Method que me retornasse a lista de todos os tipos referenciados. Veja a implementação para fazer esse teste passar (classe vazia)
public static IEnumerable<Type>
GetReferencedTypes(this Type that)
{
yield return that.BaseType;
}
Testando,
Ajustando ComputeCe para utilizar esse método,
public static int ComputeCe(this Type that)
{
return that
.GetReferencedTypes()
.Count();
}
Temos parte de nossa especificação funcionando:
Repare como a especificação deixa mais claro quanto trabalho ainda precisamos fazer.
Agora que já reconhecemos a referência para o tipo base, vamos reconhecer as referências em atributos.
Teste:
[Test]
public void GetReferencedTypes_SingleField_ReturnsObjectAndString()
{
typeof(SingleField).GetReferencedTypes()
.Should().Have.SameSequenceAs(
typeof(System.Object), // base type
typeof(System.String) // attribute type
);
}
BTW, achei “SingleAttribute” melhor
Alteração no método:
public static IEnumerable<Type>
GetReferencedTypes(this Type that)
{
var fieldTypes = from field in that.GetFields(BindingFlags.Instance |
BindingFlags.NonPublic |
BindingFlags.Public
)
select field.FieldType;
return new[] {that.BaseType}
.Union(fieldTypes)
.Distinct();
}
Teste passa! Um passo a mais para atender a especificação. Veja:
SURPRESA: A especificação mostrou que nossa classe com propriedades também retornou Ce correto. Mas como?!
Veja:
class SingleProperty
{
public string Foo { get; set; }
}
Como usei uma “auto-property”, o compilador escreveu um atributo por mim. Vamos criar um “exemplo” onde isso não ocorra e ampliar a especificação:
class SingleNonAutoProperty
{
public string Message
{
get { return "Hello World"; }
}
}
Pronto!
BDD oferece feedback contínuo da qualidade de nosso código e, também, da qualidade de nossas especificações.
Teste:
[Test]
public void GetReferencedTypes_SingleNonAutoProperty_ReturnsObjectAndString()
{
typeof(SingleNonAutoProperty).GetReferencedTypes()
.Should().Have.SameSequenceAs(
typeof(System.Object), // base type
typeof(System.String) // attribute type
);
}
Implementação:
public static IEnumerable<Type>
GetReferencedTypes(this Type that)
{
var fieldTypes = from field in that.GetFields(BindingFlags.Instance |
BindingFlags.NonPublic |
BindingFlags.Public
)
select field.FieldType;
var propertyTypes = from property in that.GetProperties(BindingFlags.Instance |
BindingFlags.NonPublic |
BindingFlags.Public
)
select property.PropertyType;
return new[] { that.BaseType }
.Union(fieldTypes)
.Union(propertyTypes)
.Distinct();
}
Teste:
[Test]
public void GetReferencedTypes_IntMethod_ReturnsObjectAndInt32()
{
typeof(IntMethod).GetReferencedTypes()
.Should().Have.SameSequenceAs(
typeof(System.Object), // base type
typeof(System.Int32) // method return type
);
}
Implementação:
public static IEnumerable<Type>
GetReferencedTypes(this Type that)
{
var flags = BindingFlags.Instance |
BindingFlags.NonPublic |
BindingFlags.Public
;
var fieldTypes = from field in that.GetFields(flags)
select field.FieldType;
var propertyTypes = from property in that.GetProperties(flags)
select property.PropertyType;
var methodReturnTypes = from method in that.GetMethods(flags)
where method.ReturnType != typeof(void)
select method.ReturnType;
return new[] { that.BaseType }
.Union(fieldTypes)
.Union(propertyTypes)
.Union(methodReturnTypes)
.Distinct();
}
Executando testes:
Bomba! Meus testes e minha especificação estavam ignorando os métodos implementados em System.Object (classe base). Por isso, agora, todos estão falhando.
O conjunto de testes de unidade “denuncia” eventuais descuidos na especificação.
Primeiro, vamos corrigir a especificação:
Depois, atualizar os testes:
using NUnit.Framework;
using SharpTestsEx;
using FluentCodeMetrics.Core;
using Samples;
using System;
namespace FluentCodeMetrics.Tests
{
[TestFixture]
// ReSharper disable InconsistentNaming
public class CeExtensionsTests
{
[Test]
public void GetReferencedTypes_EmptyClass()
{
typeof(EmptyClass).GetReferencedTypes()
.Should().Have.SameSequenceAs(
typeof(object),
typeof(string),
typeof(bool),
typeof(int),
typeof(Type)
);
}
[Test]
public void GetReferencedTypes_SingleField()
{
typeof(SingleField).GetReferencedTypes()
.Should().Have.SameSequenceAs(
typeof(object),
typeof(DateTime), // field type
typeof(string),
typeof(bool),
typeof(int),
typeof(Type)
);
}
[Test]
public void GetReferencedTypes_SingleNonAutoProperty()
{
typeof(SingleNonAutoProperty).GetReferencedTypes()
.Should().Have.SameSequenceAs(
typeof(object),
typeof(DateTime), // property type
typeof(string),
typeof(bool),
typeof(int),
typeof(Type)
);
}
[Test]
public void GetReferencedTypes_FeeMethod()
{
typeof(FeeMethod).GetReferencedTypes()
.Should().Have.SameSequenceAs(
typeof(object),
typeof(Fee), // returning type
typeof(string),
typeof(bool),
typeof(int),
typeof(Type)
);
}
}
// ReSharper restore InconsistentNaming
}
Teste:
[Test]
public void GetReferencedTypes_SingleArgVoidMethod()
{
typeof(SingleArgVoidMethod).GetReferencedTypes()
.Should().Have.SameSequenceAs(
typeof(object),
typeof(Fee), // argument type
typeof(string),
typeof(bool),
typeof(int),
typeof(Type)
);
}
Implementação:
public static IEnumerable<Type>
GetReferencedTypes(this Type that)
{
var flags = BindingFlags.Instance |
BindingFlags.NonPublic |
BindingFlags.Public
;
var fieldTypes = from field in that.GetFields(flags)
select field.FieldType;
var propertyTypes = from property in that.GetProperties(flags)
select property.PropertyType;
var methodReturnTypes = from method in that.GetMethods(flags)
where method.ReturnType != typeof(void)
select method.ReturnType;
var methodParameterTypes = from method in that.GetMethods(flags)
from parameter in method.GetParameters()
select parameter.ParameterType;
return new[] { that.BaseType }
.Union(methodParameterTypes)
.Union(fieldTypes)
.Union(propertyTypes)
.Union(methodReturnTypes)
.Distinct();
}
Teste:
[Test]
public void GetReferencedTypes_SingleArgCtor()
{
typeof(SingleArgCtor).GetReferencedTypes()
.Should().Have.SameSequenceAs(
typeof(object),
typeof(Fee), // argument type
typeof(string),
typeof(bool),
typeof(int),
typeof(Type)
);
}
Implementação:
public static IEnumerable<Type>
GetReferencedTypes(this Type that)
{
var flags = BindingFlags.Instance |
BindingFlags.NonPublic |
BindingFlags.Public
;
var fieldTypes = from field in that.GetFields(flags)
select field.FieldType;
var propertyTypes = from property in that.GetProperties(flags)
select property.PropertyType;
var methodReturnTypes = from method in that.GetMethods(flags)
where method.ReturnType != typeof(void)
select method.ReturnType;
var methodParameterTypes = from method in that.GetMethods(flags)
from parameter in method.GetParameters()
select parameter.ParameterType;
var ctorParameterTypes = from ctor in that.GetConstructors(flags)
from parameter in ctor.GetParameters()
select parameter.ParameterType;
return new[] { that.BaseType }
.Union(ctorParameterTypes)
.Union(methodParameterTypes)
.Union(fieldTypes)
.Union(propertyTypes)
.Union(methodReturnTypes)
.Distinct();
}
Neste momento, nossa feature está quase concluída. Observe:
Além disso, temos alguns bons testes de unidade. Veja:
Falta apenas suporte para o lançamento de Exceptions. Mas, a coisa aqui complica um pouco. Estou pensando em usar Mono.Cecil.
A escrita da feature ajudou a explicitar melhor o que desejamos fazer. Além disso, o arquivo em Gherkin ajuda bastante na compreensão do que estamos fazendo.
No próximo post, devo concluir essa feature. Além disso, vou fazer algum refactoring. Mas, …
Era isso.
Excelente iniciativa!! A única observação que tenho é que as métricas de acoplamento foram feitas originalmente para pacotes e não para tipos (http://www.objectmentor.com/resources/articles/stability.pdf), inclusive o NDepend aplica essas métricas no assembly (http://www.ndepend.com/Metrics.aspx#EfferentCoupling).
Mas sua abordagemQ é muito interessante. Acredito que com algumas alterações é possível que a mesma métrica seja aplicada em diferentes níveis de abstração (classes e pacotes).
Não tenho muita prática com reflexão, mas participei de algumas pesquisas acadêmicas sobre métricas arquiteturais. Caso eu possa contribuir de alguma forma, estou a disposição!
abraço
Pingback: FluentCodeMetrics – Parte 2 – Refinando a métrica Ce « Elemar DEV