terça-feira, 29 de março de 2011

Prepare-se para o C# 5 – Parte 2

Demorou mais chegou... rs

No último artigo mostramos que a expressão document = await Fetchasync(urls [i]) do C# 5.0 era realizada assim:

state = State.AfterFetch;
fetchThingy = FetchAsync(urls[i]);
if (fetchThingy.SetContinuation(archiveDocuments))
  return;
AfterFetch: ;
  document = fetchThingy.GetValue();

Nesse modelo, um método assíncrono retorna um Task<T>, e por hora, vamos assumir que o FetchAsync também retorne um Task<Document>. Então, o código atual será realizado assim:

fetchAwaiter = FetchAsync(urls[i]).GetAwaiter();
state = State.AfterFetch;
if (fetchAwaiter.BeginAwait(archiveDocuments))
  return;
AfterFetch: ;
  document = fetchAwaiter.EndAwait();

A chamada a FetchAsync cria e retorna um Task<Document>. Ao chamar este método ele imediatamente retorna um Task<Document> que de alguma forma busca o documento desejado de forma assíncrona. Para fazer alguma coisa quando ele estiver completo buscamos na tarefa por um Awaiter, e ele fornece 2 métodos: BeginAwait, assina a continuação para a tarefa (executado após a finalizada a tarefa) e o EndAwait, que extrai o resultado da tarefa finalizada.

Como esses métodos são implementados na Task (para métodos void) ou Task<T> (para métodos que retornem valor) como fica os métodos que não retornam Task ou Task<T>?

Nesse caso é utilizado a mesma estratégia do LINQ, ou seja, se tivermos:

From c in customers where c.City == "London"

Isso é traduzido para:

Customers.Where(c => c.City == "London")

E uma resolução de overload tenta encontrar o melhor método "Where" checando se a implementação de "Customers" implementa tal método, caso contrário, busca por métodos de extensão.

Com o padrão GetAwaiter/BeginAwait/EndAwait é a mesma coisa, é feita a resolução de overload na transformação da expressão, verificado o método ou a extensão adequeada.

O insight é que assincronia não requer paralelismo, mas o paralelismo exige assincronia, e muitas das ferramentas úteis para o paralelismo podem ser usadas para assincronia sem paralelismo.

Por isso o uso do Task, ele não possui paralelismo inerente e podemos representar unidades de trabalho pendentes que pode ser paralelizada e não requer multithreading, além de, já possui mecanismos de cancelamento entre outras características úteis da TPL (Task Parallel Library).

Voltando ao exemplo da busca de documentos, ele foi deliberadamente planejado para demostrar o pulo do gato, o CPS (Continuation Passing Style), para controlar até mesmo orquestrações simples de 2 tarefas assíncronas de métodos void.

Então, vamos falar um pouco sobre composição de métodos assíncronos.

Suponha que o método ArchiveDocuments retornasse o número total de bytes arquivados:

long ArchiveDocuments(List<Url> urls)
{
  long count = 0;
  for(int i = 0; i < urls.Count; ++i)
  {
    var document = Fetch(urls[i]);
    count += document.Length;
    Archive(document);
  }
  return count;
}

Agora, vamos reescrevê-lo de forma assíncrona usando um CPS:

void ArchiveDocumentsAsync(List<Url> urls, Action<long> continuation)
{
  // de alguma forma executa a busca de forma assíncrona,
  // depois invoca sua continuação
}

Dessa forma, o chamador do ArchiveDocumentsAsync precisaria ser escrito em um CPS de modo que a continuidade possa ser passada. E se o retorno fosse um resultado? Então, seria uma confusão.

No modelo TAP (Task Asynchrony Pattern, nome provisório para essa feature de composição de métodos assíncronos), ao invés disso, diriamos que o tipo que representa o trabalho assíncrono e retorna um valor é um Task<T>. Em C# 5, você poderia simplesmente escrever:

async Task<long> ArchiveDocumentsAsync(List<Url> urls)
{
  long count = 0;
  Task archive = null;
  for(int i = 0; i < urls.Count; ++i)
  {
    var document = await FetchAsync(urls[i]);
    count += document.Length;
    if (archive != null)
      await archive;
    archive = ArchiveAsync(document);
  }
  return count;
}

E o compilador se encarregará de reescrever e gerar algo como:

Task<long> ArchiveDocuments(List<Url> urls)
{
  var taskBuilder = AsyncMethodBuilder<long>.Create();
  State state = State.Start;
  TaskAwaiter<Document> fetchAwaiter = null;
  TaskAwaiter archiveAwaiter = null;
  int i;
  long count = 0;
  Task archive = null;
  Document document;
  Action archiveDocuments = () =>
  {
    switch(state)
    {
      case State.Start: goto Start;
      case State.AfterFetch: goto AfterFetch;
      case State.AfterArchive: goto AfterArchive;
    }
    Start:
    for(i = 0; i < urls.Count; ++i)
    {
      fetchAwaiter = FetchAsync(urls[i]).GetAwaiter();
      state = State.AfterFetch;
      if (fetchAwaiter.BeginAwait(archiveDocuments))
        return;
      AfterFetch:
      document = fetchAwaiter.EndAwait();
      count += document.Length;
      if (archive != null)
      {
        archiveAwaiter = archive.GetAwaiter();
        state = State.AfterArchive;
        if (archiveAwaiter.BeginAwait(archiveDocuments))
          return;
        AfterArchive:
        archiveAwaiter.EndAwait();
      }
      archive = ArchiveAsync(document);
    }
    taskBuilder.SetResult(count);
    return;
  };
  archiveDocuments();
  return taskBuilder.Task;
}

Vamos fazer um teste de mesa aqui para clarear as coisas.

O que acontece quando a lista está vazia? Nós criamos um contrutor de tarefas, um delegate que retorna void e invocamos ele de forma assíncrona. Ele inicializa a variável ”count” com 0, executa a label start, pula o loop, diz para o helper “você tem um resultado” e retorna. Finaliza o delegate. O taskbuilder é solicitado para uma tarefa, e uma vez que ele sabe que a tarefa foi concluida, retorna a tarefa concluida que simplesmente representa o número 0.

E caso existam vários documentos de arquivos? Novamente criamos um taskbuilder e um delegate que serão invocados assíncronamente. No primeiro loop começamos uma busca assíncrona, assinamos o delegate como sua continuação e retornamos do delegate. O taskbuilder contrói uma tarefa que representa “estou trabalhando de forma assíncrona no corpo do ArchiveDocumentsAsync” e retorna essa tarefa. Ao ser concluída, invoca sua continuação e o delegate inicia de novo “do ponto onde ele parou” graças a máquina de estado. Tudo continua exatamente como antes, porém, direferente da versão void, o Task<long> retornado para o ArchiveDocumentsAsync sinaliza que finalizou (invocando sua continuação) e o delegate diz ao task builder para definir o resultado.

NOTA:Tal qual o LINQ, o TAP pode ser extensível por qualquer tipo que tenha um GetAwaiter, que retorne um tipo que tenha BeginAwait, EndAwait e assim por diante, para que possa ser usado nas expressões “await”. Contudo, métodos marcados para serem assíncronos devem retornar void, Task ou Task<T>.

Existem situações onde sintaxe com tokens como “where” do LINQ são mais naturais e outras onde são a sintaxe “fluent” .Where(c => ...), no TAP haverá métodos com nomes como WhenAll ou WhenAny para compor e orquestrar tarefas assíncronas:

List<List<Url>> groupsOfUrls = whatever;
Task<long[]> allResults = Task.WhenAll(
      from urls in groupsOfUrls
      select ArchiveDocumentsAsync(urls));
long[] results = await allResults;

O que isto faz? Bem, o ArchiveDocumentsAsync retorna um Task<long>, então a query retorna um IEnumerable<Task<long>>. WhenAll pega a sequência de tarefas e produz uma nova tarefa, que de forma assíncrona aguarda cada um deles, preenche um array com o resultado e invoca sua continuação com o resultado quanto ele estiver disponível.

Da mesma forma o WhenAny pega uma sequência de tarefas e cria uma tarefa que invoca sua continuação com o primeiro resultado quando qualquer uma das tarefas estiverem finalizadas.

Existirão outras combinações de tarefas e helpers relacionados. Veja mais exemplos no CTP e repare que como não é possível modificar um Task existente os combinadores foram provisóriamente adicionados na classe TaskEx,mas, serão movidos para o Task até a versão final.

Até a próxima!


Artigos Recomendados:

>>   Prepare-se para o C# 5 – Parte 1
>>   Reflection de Alta de Performance
>>   Extension Methods = Manutenibilidade


segunda-feira, 14 de fevereiro de 2011

Nós temos Arquiteto de Software! E arquitetura?

O termo arquiteto de software está virando uma commodity dentro das software houses desse meu Brasil.

Esse papel tão importante se apresenta como um diferencial nos serviços prestados aos olhos do cliente e um imenso custo aos olhos da empresa, que sem hesitar, desprezam a idéia de encarar de forma correta o processo de desenvolvimento de software.

Mas, afinal, o que é arquitetura de software?


Bom, a arquitetura de software trata o interesse de 3 visões:

  • Usuário: como os usuários usarão o sistema?
  • Negócio: como manter a aplicação flexível e gerenciável?
  • Sistema: quais os requisitos de performance, internacionalização, segurança e configuração? Quais tendências impactam no design agora ou após a distribuição da aplicação?

O processo de definir estruturas de solução fornece e avalia alternativas de projeto atuando na organização geral de controle, definindo a atribuição de funcionalidade a componentes de projeto, buscando extrair alto desempenho e escalabilidade, sem esquecer claro, de atender requisitos técnicos e operacionais.

O arquiteto de software é o profissional que:

  • Possui grande compreensão (arriscamos a incluir também “ou facilidade de compreensão”) do negócio e das tecnologias pertinentes para trabalhar na modelagem da solução;
  • Possui entendimento dos apectos técnicos para o desenvolvimento de sistemas para trabalhar na análise de viabilidade;
  • Conhece bem as técnicas de levantamento, modelagem e desenvolvimento para trabalhar em provas de conceito, simulações e experimentos;
  • Entende as estratégias de negócio da instituição onde atua para analisar tendências tecnológicas;

O que é um software bem arquitetado?


Alguns podem dizer que é aquele que faz o que o(s) usuário(s) quer(em). Mas, para avaliarmos se um software está bem projetado temos que identificar os atributos que desejaríamos encontrar nele.

O software bem arquitetado fornece:



E como construímos um software bem arquitetado?


Nosso trabalho começa junto da etapa de levantamento de requisitos buscando identificar:

  • Peculiaridades de comportamento;
  • Informações do domínio da aplicação;
  • Restrições sobre a operação;

A lista abaixo pode ser utilizada como um check-list inicial do que levantar de requisito não-funcional:

  • Usabilidade
    • Facilidade de aprender
    • Facilidade de usar
  • Manutenção
    • Reparo
    • Evolução
  • Confiabilidade
    • Disponibilidade
    • Taxa de falhas
  • Desempenho
    • Tempo
    • Espaço
  • Reusabilidade
    • Da aplicação
    • Dos subsistemas
    • Dos objetos
    • Dos módulos
  • Portabilidade
  • Segurança
    • Disponibilidade
    • Integridade
    • Confidencialidade
    • Operacional

E os complementando estão os atributos do projeto que guiam a concepção de solução para que o produto final satisfaça aos requisitos identificados. Esses atributos são:



Em uma arquitetura podemos identificar os componentes (subsistemas), conectores, mecanismos de interação, topologia e restrições semânticas (por exemplo, a camada de interface só pode acessar dados através da camada de negócios).

O estilo arquitetural, ou o conjunto de padrões, fornecem um conjunto de requisitos não-funcionais e atributos de projeto que permitem distinguir uma arquitetura de outra e prever as características desejadas no sistema.

Os estilos de arquiteturas mais comuns são:

:: CAMADAS

Estruturar o sistema num conjunto de camadas onde cada uma agrupa um conjunto de tarefas num determinado nível de abstração.

vantagem
  • flexibilidade
desvantagem
  • alto grau de dependência
  • performance

:: OBJETOS

Permite a organização do programa em tipos de dados abstratos, que interagem através de troca de mensagens, podem ser modificados, reutilizados, distribuídos e executados sequencialmente ou em paralelo.

vantagem
  • manutenibilidade
  • performance
desvantagem
  • acoplamento
  • reuso


Atualmente, a combinação de estilos, por exemplo, camadas e objetos, guiam o desenvolvimento de software. A imagem abaixo ilustra a recomendação da Microsoft para a criação de sistemas de informação:



Os atributos que guiam o Microsoft Architecture Guide 2.0 são:
  • Separação de conceitos: divida as funcionalidades da aplicação (alta coesão) e minimizar pontos de interação (baixo acoplamento);
  • Responsabiliade única: cada componente deve ser responsável por apenas uma funcionalidade;
  • DRY (Don’t Repeat Yourself): específique suas intenções em apenas um lugar;
  • YAGNI (You Ain’t Gonna Neet It): projete apenas o necessário;
  • LoD (Least Knowledge): um componente ou objeto não deveria conhecer os detalhes internos de implementação

Esperamos que esses conceitos acadêmicos com uma visão prática possam ajudá-lo a vencer os desafios do dia-a-dia.

Na próxima falaremos sobre Arquiteturas de Domínios Específicos (DSSA) e Arquiteturas de Interface com o Usuário.

Até a próxima!



Artigos Recomendados:

>>   Pensando em ERGONOMIA e USABILIDADE
>>   Reflection de Alta de Performance
>>   Boas práticas para gerência de requisitos, de acordo com os modelos MPS.BR e CMMI
>>   Extension Methods = Manutenibilidade
>>   Alinhamento Estratégico de TI – Parte 1

 

terça-feira, 25 de janeiro de 2011

Prepare-se para o C# 5 – Parte 1

É fantástico trabalhar com uma plataforma que evolua junto com a complexidade que enfrentamos em nosso dia-a-dia.

Olhando a evolução da linguagem vemos que paramos (ao menos deveriamos ter parado) de escrever certos tipos de código. Por exemplo, com a adição do iterator blocks e do anonymous methods no C# 2.0, o compilador pode armazenar a continuação de “o que vem depois” e encapsular em uma classe os métodos que acessam varíaveis locais.

Com o LINQ e o query comprehensions (C# 3.0) fica a cargo do compilador construir o modelo de objetos correto para construir as chamadas, árvores de expressão e simplifica muito a escrita de código que ordena, filtra, associa, agrupa e sumariza dados.

Hoje em dia vivemos a integração de sistemas. Desenvolvemos aplicativos .NET para rodar dentro de uma rede social em Python. Nossos aplicativos precisam tanto interoperar com sistemas legados quanto sistemas SAP. E para simplificar isso foi criado o dynamic type (C# 4.0), onde fica a cargo do compilador descobrir como gerar o código em tempo de compilação que faça a análise do Dynamic Language Runtime em tempo de execução.

Também estamos utilizando muito algoritmos de programação assíncronos. E a tendência, com sistemas baseados em nuvem, é popularizar esse tipo de algoritmo.

Com o C# 5.0 você será capaz de pegar esse código síncrono:

void ArchiveDocuments(List urls)
{
  for(int i = 0; i < urls.Count; ++i)     Archive(Fetch(urls[i]));
}

E implementando o FetchAsync e ArchiveAsync transformá-lo em:

async void ArchiveDocuments(List urls)
{
  Task archive = null;
  for(int i = 0; i < urls.Count; ++i)
  {
    var document = await FetchAsync(urls[i]);
    if (archive != null) await archive;
    archive = ArchiveAsync(document);
  }
}

O compilador do C# 5.0 irá gerar o código da máquina de estado, os lambdas, as continuações e checar se a tarefa foi concluída para você. Show!

Esses recursos foram apresentados no último PDC pelo Anders Hejlsberg e atualmente está sendo desenvolvido o CTP do protótipo do compilador do C# 5.0.

Se você se animou com a notícia e quer saber mais visite Asynchronous Programming for C#.

QUE TAL LER A PARTE 2?



Veja também:

>>  Reflection de Alta de Performance
>>  Dynamic CRUD no .NET 4.0
>>  Programação Dinâmica no C# 4.0
>>  Extension Methods = Manutenibilidade

quinta-feira, 6 de janeiro de 2011

Pensando em ERGONOMIA e USABILIDADE

Você já leu um livro onde as páginas não estejam numeradas, sem índice ou resumo? Ou ainda, se sentiu p@#! da vida por procurar uma rua que não tenha placa indicando o nome?

A ergonomia e a usabilidade são constituídos de pequenos detalhes que são notados apenas quando não estão presentes, e estes detalhes ou qualidades, fazem com que os usuários se sintam confiantes e satisfeitos por atingirem seus objetos com menos esforços, em menos tempo e com menos erros.

Programas de software (e até mesmo bibliotecas de código - API) e suas interfaces são ferramentas cognitivas, capazes de modelar representações, abstrair dados e produzir informações, facilitando a percepção, o raciocínio, a memorização e a tomada de decisão para cada tipo de usuário, inteligência, estilo cognitivo e personalidade.

A engenharia de usabilidade surge como resposta a necessidade de desenvolver programas de software interativo com usabilidade.

Enquanto a engenharia de software se preocupa com o núcleo funcional, a estrutura de dados, os algoritmos, os recursos computacionais, a engenharia de usabilidade se preocupa com a lógica de funcionamento, apresentações, estruturas de diálogos e lógica de operação.

Para desenvolvermos uma interface agradável, intuitiva, eficiente e fácil de operar temos que projetar a:

  • Condução: a inteface deve aconselhar, orientar, informar e conduzir o usuário na interação com o sistema;
  • Carga de Trabalho: elementos da interface que têm papel importante na redução da carga cognitiva e perceptiva do usuário e no aumento da eficiência do diálogo;
  • Adaptabilidade: a interface fornece diversas maneiras de realizar uma tarefa, deixando ao usuário a liberdade de escolher e dominar uma delas no curso de aprendizado;
  • Gestão de Erros: mecanismos que permitem evitar ou reduzir a ocorrência de erros e que favoreçam sua correção caracteriza a interface segura;

Durante toda etapa de desenvolvimento das interfaces deverão avaliadas a ergonomia e a usabilidade da solução proposta buscando identificar:

  • Barreiras: o usuário esbarra sucessivas vezes e não aprende a transpor sem ajuda externa um aspecto da interface fazendo com que ele desista de utilizar a função do sistema temporária ou definitivamente;
  • Obstáculos: o usuário esbarra algumas vezes, mas aprende a transpor um aspecto da interface, porém, perde desempenho nas próximas realizações da tarefa;
  • Ruídos: aspecto da interface que mesmo não sendo barreira ou obstáculo causa uma diminuição de seu desempenho na tarefa.

O aspecto não determinístico do projeto de interfaces sugere que o ciclo de desenvolvimento seja essencialmente evolutivo, iterativo e centrado no usuário (ver DESIGN CENTRADO NO USUÁRIO).

A figura abaixo ilustra esse ciclo:



Os projetistas devem identificar e definir com cuidado os requisitos de usabilidade e, durante o desenvolvimento, certificar-se de que são corretos, assegurar que o protótipo esteja indo na direção correta e que satisfaz tais requisitos antes de ser liberada para implementação ou para o mercado.

Essas abordagens e métodos são iniciativas recentes e efetivas para o desenvolvimento da engenharia de usabilidade e certamente o ajudará nos desafios diários.




Gostou? Siga-nos e compartilhe suas experiências e opiniões deixando seu comentário.



Posts Relacionados

Design centrado no usuário

 

segunda-feira, 15 de novembro de 2010

Design centrado no usuário

Atualmente o design tem recebido uma atenção especial, visto que, além de ser o conjunto que é percebido pelo usuário, ele também pode significar redução de custos de treinamentos e manuais.

Pesquisando na internet sobre o assunto encontramos uma video aula de um curso de iPhone da Stanford sobre "How to Build an iPhone App that Doesn’t Suck! (In 10 Easy Steps)" do Steve Marmon (@marmon). Ele menciona 10 passos para se desenvolver aplicativos que não sejam “um lixo”. Porém, estes passos podem ser aplicados para quase qualquer projeto. Os passos comentados, estão a seguir:


1. Decida o que construir (Decide what to build)

Muitas vezes pensamos em uma solução levando em conta apenas necessidades secundárias, ou seja, aquelas que nos guiam a atender desejos e não o real problema que estamos nos propondo a resolver. Dessa forma, corremos o risco de não solucionar nenhum problema, ou ainda, de desenvolvermos uma solução ineficaz.

Para não cair nessa armadilha, procure enteder qual problema estamos nos propondo a resolver, pra quem estamos projetando e os tenhamos em mente ao desenvolver a solução. Focar em um grupo específico aumenta a chance de sucesso, pois o feedback desses usuários será de útil para melhorarmos o produto e conquistarmos os clientes.

Se o seu software for um produto interno, envolva os usuários finais (ou pessoas chave que representem e entendam as necessidades desses usuários), agora, caso seja um produto para o mercado, ou seja, você não tem como envolver o usuário, procure fazer pesquisas (faixa etária, classe social, nível de escolaridade, etc) para identificar bem as necessidades e guiar o desenvolvimento da interface do produto.

Conheça as personas.


2. Visite a app store (Visit the app store)

No mundo globalizado um produto inovador pode ser a composição de idéias brilhantes de produtos similares ou não, portanto, observe como aplicativos similares resolveram problemas que você está se propondo a resolver.

Eles já devem ter tido feedback de usuários e alterado o produto em função disso, e você, estará "recebendo" estes feedbacks gratuitamente.

Se existirem concorrentes, faça um benckmarking e compare as vantes e desvantagens em relação ao seu produto.


3. Explore as possibilidades de soluções (Explore possible solutions)

A primeira solução não é a única e tampouco a melhor!

Trabalhe com as limitações e aproveite os padrões para conseguir, com mais algumas tentativas, encontrar a solução ideal.


4. Rabisque (Sketch)

Para ter uma boa idéia, tenha várias, procure designs alternativos para o mesmo problema, faça várias propostas de solução!

Com 10 propostas, por exemplo, sua chance de ter algo realmente bom é muito maior. É assim que a Apple funciona. É assim que a IDEO funciona.

Rabisque e comunique suas idéias.


5. Construa um protótipo de papel (Build a paper prototype)

Um vício de longa data da área de desenvolvimento de softare e sair para a programação ou o design sem antes criar um protótipo, e quando ele é feito, a única preocupação é com relação aos campos que tem que ter.

Hoje há mais itens a ser levado em consideração, e aproveite que nesta fase, o custo de alteração é muito baixo. Pense, quanto tempo você gasta para alterar uma página no protótipo? E para reprogramar uma tela? Apresente o protótipo para pessoas chaves, analise suas reações, colha feedbacks e veja se o caminho que você está tomando é o correto.

"Fail early to succeed sooner".

6. Abra o omnigraffle (Fire up omnigraffle)

Aqui ele fala do omnigraffe para o caso específico do iPhone, porém, a mensagem é para que use uma ferramenta que permita criar um protótipo (Sketchflow?). Este protótipo será esteticamente parecido com a versão final, mas sem precisar gastar tempo programando.

A idéia é a mesma da etapa anterior, permitir alterações sem elevar o custo.

7. Faça tudo denovo (Do it all again)

Já ouviu a frase "A melhor época de começar uma faculdade é quando se termina"? Isso quer dizer que, quando terminamos os estudos é que descobrimos o que precisavamos aprender.

Sendo assim, como todo design centrado no usuário, a parte mais importante do processo é iterar. Veja onde está com problemas, volte, questione se a solução dada é a melhor para aquele caso e pense em várias alternativas de correção para o problema encontrado.

Prototipe antes de alterar, teste e siga assim até que o produto esteja estável.

8. Agora você pode programar (Okay, you can code finally)

Agora pode partir para a programação do seu produto.

Use padrões de design e programação como MVC, que possibilitem alterações futuras sem muito trabalho, desvinculando o código da parte visual/apresentação.

9. Abra seu aplicativo para beta testers (Beta test your app)

Utilize os testes para levantar os bugs que passaram desapercebidos pelos testes em protótipos e pelos programadores, como bugs relativos à satisfação dos usuários.

Um exemplo, no iPhone, pode ser a rolagem da tela estar muito "lerda". Os programadores vão achar que a tela está excelente, porque está rolando, porém os usuários, após algum tempo de uso, se sentirão incomodados com isso.

O Tweetie foi o primeiro app de twitter a conseguir uma rolagem rápida, e teve grande destaque por conta disso.

10. Lance (Release)

Se você fez tudo direito, este será um momento feliz e de boas noites de sono, senão, prepare-se para os problemas que virão (e-mails de bugs, reclamações, comentários negativos sobre o aplicativo, etc.).

Bem vindo ao mundo moderno do design!

Compartilhe suas idéias conosco e até a próxima!

 

terça-feira, 9 de novembro de 2010

Reflection de Alta de Performance

Pesquisando no Google vemos alguns bons artigos explicando que Reflection é lento. Está certo, alguns métodos do Reflection, como GetXXX (do MethodInfo, PropertyInfo, FieldInfo, etc.), são cerca de 100 vezes mais lento que acessar diretamente uma propriedade ou executar um método.

O quanto o Reflection é lento?

Vamos criar um pequeno exemplo para analisarmos a performance do Reflection. Esse exemplo terá uma classe simples que contém apenas 1 método:

namespace CustomNamespace
    public interface ICustom
    {
        void CallMe(int x);
    }

    public class CustomClass : ICustom
    {
        public void CallMe(int x)
        {
            x = x + 10;
            return;
        }
    }
}

Simples, não? Bom, vamos criar uma classe para consumir e checar a performance em situações de extress. Vamos criar objetos do tipo CustomClass em um loop e executar o método CallMe com valores randomicos de duas formas: 1. Sem Reflection, 2. Com Reflection

Para o teste com Reflection, vamos criar uma classe (ReflectionCalculator) com alguns métodos:

public partial class ReflectionCalculator
{
private Type dummyType;
public Type DummyType
{
get
{
dummyType = dummyType ??
CurrentAssembly.GetType("CustomNamespace.CustomClass",
true,
true);
                return dummyType;
}
}

private MethodInfo method;
public MethodInfo Method
{
get
{
this.method = this.method ??
this.DummyType.GetMethod("CallMe", BindingFlags.Instance |
BindingFlags.Public, null, new Type[] { typeof(int) },
null);
return this.method;
}
}

private object dummyObject;
public object DummyObject
{
get
{
if (this.dummyObject == null)
{
dummyObject = Activator.CreateInstance(this.DummyType);
}
return dummyObject;
}
}
       
public void ReflectionBasedCall(int value)
{
this.Method.Invoke(this.DummyObject, new object[] { value });
}

public void NormalCall(int value)
{
this.MyClass = this.MyClass ?? new CustomClass();
this.MyClass.CallMe(20);
        }
}

Então, se olhar para as duas chamadas ReflectionBasedCall e NormalCall verá que ambos fazem a mesma coisa. A abordagem de reflexão deve primeiro obter um Type usando Assembly.GetType (que evetualmente vasculha toda a hierarquia do objeto inteiro) e depois o GetMethod no tipo encontrado. Portanto quanto maior o tamanho do Assembly mais lento será. Por fim, usamos o Activator.CreateInstance para criar a instância do objeto.

Agora, e se executassemos os métodos 1.000.000 de vezes usando esse código e imprimissemos o tempo em segundos, ele irá se parecer com:

static void Main(string[] args)
{
var watcher = new System.Diagnostics.Stopwatch();
var calculator = new ReflectionCalculator();

watcher.Start();
for (int i = 0; i < 1000000; i++)
{
calculator.NormalCall(i);
}
watcher.Stop();

Console.WriteLine("Time Elapsed for Normal call : {0}",
watcher.Elapsed.TotalSeconds);
watcher.Reset();

watcher.Start();
for (int i = 0; i < 1000000; i++)
{
calculator.ReflectionBasedCall(i);
}
watcher.Stop();
Console.WriteLine("Time Elapsed for Reflection call : {0}",
watcher.Elapsed.TotalSeconds);

Console.ReadLine();
}

Abaixo, a saída da execução:

 

Reflection + Lambda Expression = Dynamic Delegates


Na versão 3.0 do Microsoft .Net Framework, juntamente com o Linq, foi introduzido o Lambda Expression, que é uma função anonima que pode conter expressões e funções e pode ser usada para criar Delegates ou tipos de árvores de expressão.

Criar delegates em runtime é atualmente a melhor maneira de fazer o código Reflection ser executado muito rápido. A idéia básica é criar delegates de cada MethodInfo que precisamos executar e eventualmente fazer cache dele em memória.

Para isso, precisamos construir um Action<> (no caso de método VOID) ou Func<> (no caso de método que retorne algo) para cada método:

public static Action<object, T> CallMethod(this MethodInfo methodInfo)
{
if (!methodInfo.IsPublic) return null;

var returnParameter = Expression.Parameter(typeof(object), "method");
var valueArgument = Expression.Parameter(typeof(T), "argument");

var setterCall = Expression.Call(Expression.ConvertChecked(returnParameter,
methodInfo.DeclaringType),
                             methodInfo,
                             Expression.Convert(valueArgument, typeof(T)));
return Expression.Lambda<Action<object, T>>(setterCall, returnParameter,
valueArgument).Compile();
}

A expressão lambda acima cria um delegate do tipo Action onde “object” representa a instância da classe no qual executaremos o método e “T” representa o tipo do argumento do método. O código acima cria uma expressão: 

(x, y) = return x.[method](y)


Com isso, podemos guardar na memória a referência construída dinamicamente:


private MethodInfo methodInfo;
public MethodInfo CachedMethodInfo
{
get
{
this.methodInfo = this.methodInfo ?? this.GetMethodInfo();
return this.methodInfo;
}
}

private MethodInfo GetMethodInfo()
{
Type myClass = this.CurrentAssembly.GetType("CustomNamespace.CustomClass",
true,
true);
return myClass.GetMethod("CallMe", BindingFlags.Instance |
BindingFlags.Public, null, new Type[] { typeof(int) }, null);
}

private Action<object, int> _methodcallDelegate = null;
public Action<object, int> MethodCallDelegate
{
get
{
this._methodcallDelegate = this._methodcallDelegate ??
this.CachedMethodInfo.CallMethod<int>();

return this._methodcallDelegate;
}
}

Vamos executar o código de teste mais uma vez para ver o quão eficiente é essa técnica:


Veja que a diferença em relação a uma abordagem hard-coded caiu drasticamente expandindo as possibilidades de criação de algoritmos genéricos e de alta performance.

Você tem dicas e truques? Compartilhe! Envie para social@apolineo.com.br que colocaremos no blog.

Fique a vontade também de enviar suas sugestões e críticas.

Download: Código Fonte

Até a próxima!

domingo, 3 de outubro de 2010

Boas práticas para gerência de requisitos, de acordo com os modelos MPS.BR e CMMI

A atividade de gerência de requisitos tem por objetivo mapear os requisitos identificados na fase inicial de um projeto e rastreá-los até os artefatos finais, permitindo a validação da consistência entre os mesmos e as requisições originalmente vindas dos solicitantes, conforme definido pelo modelo CMMI [SEI, 2006].

No entanto, [SOMMERVILLE, 2007] ressalta o fato de que os requisitos de sistema mudam constantemente, em virtude do amadurecimento na compreensão das pessoas envolvidas acerca do que desejam que o software faça ou ainda de modificações no hardware, software ou ambiente organizacional do sistema.

Deste modo, um controle efetivo dos requisitos vindos do usuário e de suas mudanças, assume papel maior que simplesmente atender a especificação de uma área de processo e sim de garantir a adequação do produto final ao seu meio externo.

A gerência de requisitos representa assim, a figura de ligação entre as áreas de planejamento de projeto, solução técnica e gerência de configuração. Um reflexo disso é o fato de que este é um dos processos que não aceitam qualquer exclusão no modelo MPS.BR [SOFTEX, 2008].

Sendo fundamental a ambos os modelos de referência, esta área de processo recebe enfoques semelhantes em cada um deles. Porém ambos apresentam apenas listas de necessidades que devem ser atendidas e não uma proposta de artefatos para sua realização.

Deste modo, empresas que tenham interesse em melhorar seus processos precisam buscar referências externas a respeito de como proceder, gerando margem para implementações custosas e pouco eficientes daquilo que deveria ser uma otimização de processos.

Este artigo tem por objetivo apresentar uma contextualização entre o processo de gerência de requisitos e as necessidades impostas pelos modelos CMMI e MPS.BR, definindo um conjunto mínimo de artefatos para cumprimento das mesmas, que possam ser utilizados por empresas de pequeno ou médio porte.

São descartadas neste artigo as necessidades referentes à área de processo de definição de requisitos – formalizada em [SEI, 2006] - uma vez que a mesma não é o
foco deste estudo.

Modelos de Referência


Dentre os diversos modelos propostos ao redor do mundo, foram selecionados os modelos de referência CMMI e MPS.BR, sendo o primeiro selecionado por sua aceitação mundial e representatividade de mercado, enquanto o segundo representa um movimento que propõe uma aproximação ao cenário das empresas brasileiras.

O modelo de referência para melhoria de software brasileiro (MR-MPS) indica ainda uma equivalência [SOFTEX, 2008] entre seus processos e as áreas de processo definidas no modelo CMMI, conforme ilustra a abaixo.



CMMI e a Gerência de Requisitos

Dentro do modelo CMMI, a gerência de requisitos é uma área de processo componente do Nível 2 de Maturidade, conhecido como “Gerenciado”. Neste nível, os processos da empresa devem ser executados dentro de um padrão e por pessoas habilitadas, de acordo com controles definidos [SEI, 2006].

A gerência de requisitos tem aqui o papel de garantir que todos os artefatos produzidos sejam coesos com a necessidade do cliente, por meio da identificação dos requisitos que originaram cada implementação. É vista também como ponto de partida para o planejamento de atividades e entregas, uma vez que a área de gerência de configuração pode definir seus pacotes em virtude de agrupamentos de requisitos.

Outro aspecto relevante é a possibilidade de rastrear o impacto na solução de falhas no software ou nas solicitações de mudanças vindas dos usuários.

MPS.BR e a Gerência de Requisitos

Diferentemente do CMMI, dentro do modelo MPS.BR, o processo de gerência de requisitos é visto como parte do nível mais baixo de maturidade possível, o Nível G, conhecido como “Parcialmente Gerenciado” [SOFTEX, 2007].

Este enfoque coloca um maior destaque nas tarefas que deste processo, colocando-o num papel de ligação entre artefatos e requisitos, objetivando que a rastreabilidade com relação às necessidades originalmente mapeadas seja mantida. Trata-se então de uma diferença sutil, que demanda atenção extra às tarefas que o
compõe.

Com relação à avaliação de impacto em solicitações de mudanças ou correções de falhas, a definição é similar ao proposto pelo CMMI.

A Questão da Rastreabilidade


Uma das exigências comuns ao nível G do MPS.BR e ao nível 2 do CMMI é a rastreabilidade entre um requisito e todos os artefatos envolvidos na implementação
decorrente dele, conforme apresentado em [SEI, 2006] e [SOFTEX, 2007].

Esta prática permite uma maior efetividade na gerência de configuração, que visa justamente garantir que cada versão do software permita isolar uma versão compatível dos componentes envolvidos em sua realização, chamada baseline. Realizar
esta tarefa sem uma referência sólida a quais telas ou componentes são afetados por
determinado requisito, dificultaria a tarefa de isolar a versão, inviabilizando a geração de baselines.

No entanto, este rastreio se complica quando consideramos que um requisito pode ser atendido por uma tela inteira ou por uma simples caixa de texto. [MEDEIROS, 2006]

Levando em conta que um documento de caso de uso pode ser utilizado para refletir uma tela ou um único método, desde que este seja suficientemente relevante, tem-se uma opção se solução: mapear casos de uso versus requisitos funcionais.

Por meio desta prática é possível identificar no detalhe os artefatos que serão afetados cada vez que uma mudança é realizada em um caso de uso específico, bem como prover uma análise de impacto mais eficaz antes de planejar uma tarefa.

A elaboração de uma planilha descrevendo esta relação, em conjunto à existência de um documento padronizado para a elicitação de requisitos, onde seja possível não apenas mapear as necessidades, mas também a interdependência entre requisitos (sejam
eles funcionais ou não) permite atender a todas as necessidades especificadas pelos modelos CMMI e MPS.BR, conforme descrito em seus documentos de referência.

Artefatos para a Gerência de Requisitos


Levando em conta os aspectos discutidos anteriormente, propõe-se que a gerência de requisitos seja atendida por meio da adoção de artefatos que reflitam tarefas consideradas padrão de mercado, no que diz às atividades realizadas pelos envolvidos
em sua realização.

Os documentos propostos são: Documento de Visão, Matriz de Rastreabilidade e Documento de Especificação de Caso de Uso, compatíveis com o Rational Unified Process [RATIONAL, 2009], processo de engenharia de software que se baseia na definição de disciplinas para atribuição de tarefas e responsabilidades no desenvolvimento do software.

Visando atender os modelos de referência observados, é necessário que os artefatos possuam sessões específicas para mapeamento das relações de dependência com os diversos requisitos elicitados, conforme definido. Cada um destes artefatos acompanha todo o ciclo de vida do desenvolvimento do software, permeando suas diferentes fases. A seguir, cada um deles será observado em mais detalhes, bem como a relação entre artefatos, usos e fases do ciclo de vida.

Por meio deles, as cinco práticas específicas exigidas pelo CMMI são totalmente atendidas, bem como os cinco resultados esperados propostos melo MPS.BR.

Documento de Visão


Um documento relacionando as necessidades do usuário, seu entendimento por parte do analista de requisitos e o conjunto de requisitos (funcionais ou não) propostos para atender cada uma das necessidades mapeadas. Este documento é validado pelos usuários e pode ser utilizado para definição do escopo inicial do projeto.

Fases do Ciclo de Desenvolvimento em que é empregado e atividades realizadas:
  • Pré-Venda: Análise do Problema, Definição de Requisitos de Negócio e Requisitos de Usuário, Definição de Requisitos Funcionais e Requisitos Não Funcionais;
  • Análise e Design: Relação entre: Requisitos de Usuário e Requisitos de Negócio, Requisitos de Negócio e Requisitos Não Funcionais, Requisitos de Usuário e Requisitos Funcionais.

Práticas Específicas do CMMI atendidas [SEI, 2006]:
  • SP 1.1. Obter um entendimento dos requisitos;
  • SP 1.2. Obter aprovação dos requisitos.

Resultados Esperados do MPS.BR atendidos [SOFTEX, 2007]:
  • GRE 1. O entendimento dos requisitos é obtido junto aos fornecedores de requisitos;
  • GRE 2. Os requisitos de software são aprovados utilizando critérios
    objetivos.

Matriz de Rastreabilidade


Documento responsável por estabelecer uma relação bidirecional entre os requisitos originais e os componentes do produto final (software). Caso um requisito não possua nenhum caso de uso equivalente mapeado na planilha, uma inconsistência de projeto é efetivamente localizada.

Fases do Ciclo de Desenvolvimento em que é empregado e atividades realizadas:
  • Análise e Design: Relação entre Casos de Uso e Requisitos Funcionais;
  • Validação e Homologação: Possibilita o Rastreio e comparação entre uma implementação e a necessidade que a originou.

Práticas Específicas do CMMI atendidas [SEI, 2006]:
  • SP 1.3. Gerenciar as mudanças de requisitos;
  • SP 1.4. Manter rastreabilidade bidirecional entre os requisitos;
  • SP 1.5. Identificar inconsistências entre projeto e requisitos.

Resultados Esperados do MPS.BR atendidos [SOFTEX, 2007]:
  • GRE 3. A rastreabilidade bidirecional entre os requisitos e os produtos de trabalho é estabelecida e mantida;
  • GRE 4. Revisões em planos e produtos de trabalho do projeto são realizadas visando identificar e corrigir inconsistências em relação aos requisitos;
  • GRE 5. Mudanças nos requisitos são gerenciadas ao longo do projeto.

Documento de Especificação de Caso de Uso


Além do tradicional Diagrama de Casos de Uso, cabe elaborar um documento detalhando cada caso de uso, de acordo com sua especificidade [MEDEIROS, 2006]. Neste documento é realizada a especificação da implementação que será realizada pelos desenvolvedores, já com o foco nos artefatos que serão produzidos.

Embora esteja muito mais relacionado à área de processo de definição de requisitos do que à gerência propriamente dita, sua realização é essencial à construção da Matriz de Rastreabilidade, que vincula os casos de uso a seus requisitos.

Fases do Ciclo de Desenvolvimento em que é empregado e atividades realizadas:
  • Análise de Design: Detalha a solução para uma necessidade traduzida em requisito funcional e permite a validação da solução proposta pelo Analista de Requisitos ou de Negócio.

Práticas Específicas do CMMI atendidas [SEI, 2006]:
  • SP 1.4. Manter rastreabilidade bidirecional entre os requisitos.

Resultados Esperados do MPS.BR atendidos [SOFTEX, 2007]:
  • GRE 3. A rastreabilidade bidirecional entre os requisitos e os produtos de trabalho é estabelecida e mantida.

Conclusão


Por meio da utilização de três artefatos de confecção relativamente simples – Documento de Visão, Matriz de Rastreabilidade e Documento de Especificação de Caso de Uso - é possível atender a todas as exigências estabelecidas à área de processo de Gerência de Requisitos, de acordo com os modelos de referência analisados.

Porém, o nível de interdependência entre os documentos evidencia a necessidade de um processo eficiente para a definição dos requisitos, uma vez que uma falha nesta atividade resultará em um conjunto inconsistente de requisitos no documento de visão, refletidos em casos de uso que não traduzem a real expectativa do cliente.

Estes casos de uso e requisitos serão relacionados na matriz de rastreabilidade e futuramente, quando forem recebidas solicitações de mudanças em determinados pontos do sistema, a tarefa de localizar o ponto de origem do problema e os demais casos de uso relacionados a este mesmo requisito terá sua complexidade incrementada.

Outra questão que se torna evidente é a margem de falha humana, pois ao discutir a elaboração dos referidos artefatos sem a proposta de um pacote especializado de ferramentas surge a possibilidade de que determinado campo não seja preenchido, mesmo que de forma não intencional. Deste modo, cabe a cada empresa avaliar a relação entre custo e benefício da adoção de meios automatizados para a construção e relacionamento dos documentos.

É interessante ressaltar também que o modelo MPS.BR prega compatibilidade com o CMMI e, no que diz respeito à gerência de requisitos, seus resultados esperados correspondem diretamente às práticas específicas impostas pelo modelo norteamericano, de modo que os artefatos propostos poderiam ser elaborados considerando apenas o CMMI como critério de satisfação.

No anexo você encontra modelos de documentos ilustrando os conceitos abordados: Gerencia_Requisitos_slides.pdf

Referências Bibliográficas
[SEI, 2006] CMMI Product Team. CMMI for Developmment, Version 1.2. Pittsburg, EUA: Carnegie Mellon University – Software Egineering Institute, 2006.

[SOMMERVILLE, 2007] Sommerville, Ian. Engenharia de Software, 8ª Edição. São Paulo: Pearson Addison Wesley, 2007.

[SOFTEX, 2008] FAQ Implementação MR-MPS - Dúvidas sobre o Guia Geral e Guia de Implementação. http://www.softex.br/mpsBr/_faq/faqImplementacao.asp.
Recuperado em 01/12/2008.

[SOFTEX, 2007] Associação para Promoção da Excelência do Software Brasileiro - SOFTEX. MPS.BR – Guia Geral, Versão 1.2, junho 2007.

[MEDEIROS, 2006] Medeiros, Ernani Sales de. Desenvolvimento de Software com
UML 2.0: definitivo. São Paulo: Pearson Makron Books, 2006.

[RATIONAL, 2009] Technical resources and best practices for the Rational® software
platform. http://www.ibm.com/developerworks/rational. Recuperado em 03/02/2009.

Autor: Ângelo Amaral
Sobre: Pós-graduado em Engenharia de Software pelo ITA com especialização em modelos de construção de aplicações comerciais utilizando motores de regras de negócio, possui experiência na implantação de scrum e coordenação de projetos e sistemas de varejo.
Contato: angelo.amaral@gmail.com