Elemar DEV

Tecnologia e desenvolvimento .net

FluentCodeMetrics – Parte 1– Motivações, Estrutura e primeira especificação (Acoplamento Eferente [Ce])

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”

O que é (será) FluentCodeMetrics?!

A idéia é prover:

  • interface fluente (de código) para coleta de indicadores;
  • parser para uma DSL de consultas;
  • uma GUI;
  • utilitário de linha de comando;
  • quem sabe, teremos uma extensão para o Visual Studio.

Meu desejo é prover uma alternativa FOSS, de qualidade, para o excelente NDepend.

Sobre o processo de desenvolvimento

Nesse projeto, vamos adotar BDD (for real). O processo de desenvolvimento será fundamentalmente o indicado no post anterior.

image

Passo 1 – Entendendo o problema

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.

Algumas decisões fundamentais

Para considerar durante todo o projeto:

  • a interface fluente deverá ser compatível com LINQ;
  • a interface fluente deverá fundamentar uma DSL (Domain-specific Language);
  • todas as consultas em GUI (e linha de comando) deverão ser executadas, internamente, via DSL;

Passo 2 – Começando o projeto

Vamos começar devagar. Criei uma solução com três projetos:

image

  1. Core  – Será a “casa” da interface fluente para coleta de indicadores;
  2. Specs – Onde escrevo todos os testes de aceitação. Usarei SpecFlow e Gherkin para escrever as features;
  3. Tests – Onde escrevo os testes de unidade.

Feito o primeiro commit,  vamos preparar o build.

image

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!

Passo 3 – Primeira especificação

Casa arrumada, vamos começar os trabalhos. Eis que surge nosso primeira especificação de feature (usando Gherkin) [no github].

image

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:

image

Como não poderia deixar de ser, o teste é inconclusivo.

Hora de escrever os Step Definitions.

Passo 4 – Walking Skeleton e Failing Acceptance Test

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:

image

Perfeito!

Perceba, agora que completamos o teste de aceitação, iniciamos um ciclo padrão TDD.

image

 

Passo 5 – Cumprindo a especificação – Reconhecendo a referência para o tipo base

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,

image

 

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:

image

Repare como a especificação deixa mais claro quanto trabalho ainda precisamos fazer.

Passo 6 – Cumprindo a especificação – Reconhecendo as referências em atributos

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:

image

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"; }
    }
}

image

Pronto!

BDD oferece feedback contínuo da qualidade de nosso código e, também, da qualidade de nossas especificações.

Passo 7 – Cumprindo a especificação – Reconhecendo as referências em propriedades (não automáticas)

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();
}

Passo 8 – Cumprindo a especificação – Reconhecendo as referências em retorno de métodos

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:

image

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:

image

 

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
}

Passo 9 – Cumprindo a especificação – Reconhecendo as referências nos tipos dos parâmetros em métodos

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();
}

Passo 10 – Cumprindo a especificação – Reconhecendo as referências nos tipos dos parâmetros em construtores

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();
}

Parando …

Neste momento, nossa feature está quase concluída. Observe:

image

Além disso, temos alguns bons testes de unidade. Veja:

image

Falta apenas suporte para o lançamento de Exceptions. Mas, a coisa aqui complica um pouco. Estou pensando em usar Mono.Cecil.

Concluindo…

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.

2 Comentários em “FluentCodeMetrics – Parte 1– Motivações, Estrutura e primeira especificação (Acoplamento Eferente [Ce])

  1. wnoizumi
    02/05/2012

    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

  2. Pingback: FluentCodeMetrics – Parte 2 – Refinando a métrica Ce « Elemar DEV

Deixe uma resposta

Preencha os seus dados abaixo ou clique em um ícone para log in:

WordPress.com Logo

Você está comentando usando sua conta WordPress.com. Sair / Mudar )

Imagem do Twitter

Você está comentando usando sua conta Twitter. Sair / Mudar )

Foto do Facebook

Você está comentando usando sua conta Facebook. Sair / Mudar )

Conectando a %s

Informação

Publicado às 30/04/2012 por em Post e marcado , , .

Estatísticas

  • 430,861 hits
%d bloggers like this: