Posts Marcados .NET

Azure DevOps

Oi pessoal! Iniciando a sequência de artigos sobre tecnologias e plataformas citadas aqui, vou começar pelo básico: configuração de ambiente DevOps no Azure.

Embora DevOps esteja em adoção crescente há vários anos, sua adoção em larga escala só se tornou possível com o foco da Microsoft no Azure DevOps. A migração do antigo TFS para o GIT, a integração de todas as ferramentas em https://dev.azure.com/[suaempresa] e  a automatização via pipelines de CI (Continuous Integration) e CD (Contiunous Deploy) tornou todo o processo simples de ser implementado.

Bom, vamos assumir que estamos começando do zero e montar um ambiente de DevOps no Azure e mostrar todos os passos necessários. O único requisito é ter uma assinatura Azure válida para se registrar os artefatos.

O primeiro passo é criar a empresa no DevOps. Isto pode ser feito em https://dev.azure.com, bastando seguir os tutoriais. O primeiro artefato que temos que criar é o projeto inicial. O termo projeto aqui pode gerar confusão, pois há a tendência de se tratar um projeto como projeto Visual Studio. Mas, na nossa experiência, é mais produtivo tratar um projeto DevOps como sendo todo o contexto de um cliente. Assim, todas as solutions, projetos visual studio, artefatos etc. relativos a um cliente normalmente são armazenados em um único projeto DevOps. Abaixo a tela de gestão do projeto no Azure.

Dentro de um projeto podemos ter múltiplos repositórios GIT. Usualmente é criado um repositório para cada projeto do tipo bibliotecas, serviços ou sistemas. Já vi algumas defesas de se ter um repositório único para tudo (nós até já chegamos a usar este formato por algum tempo), mas isto complica a geração dos pipelines de CI. Assim, preferimos usar repositórios separados mesmo. Como é típico em um repositório GIT, temos o branch master, que armazena a versão de produção e branchs de desenvolvimento, usados para gerar as versões para testes e homologação. Em organizações que precisam de um compliance mais forte, pode ser criada uma política que impeça o commit direto no branch master, exigindo um pull request do desenvolvimento para ele. Este pull request pode também exigir aprovadores, o que garante um alto controle do que segue para o master. Claro que isto tem um impacto negativo em produtividade. Como na White Fox o objetivo é agilidade, nós não temos estes controles ativados.

Outro ponto importante para configurar no projeto é um feed de artefatos (último elemento na barra lateral esquerda). Este feed tem como finalidade a publicação dos pacotes Nuget das bibliotecas utilizadas naquele projeto DevOps. Embora o feed seja para uso interno (ele exige autenticação), dá para se configurar acesso para outros projetos ou mesmo para outras empresas. Na White Fox, estamos migrando todos os nossos pacotes Nugets de um servidor público para um feed de projeto, simplificando nossa infra e facilitando o processo de CI/CD.

Com o feed de artefatos criado, o próximo passo é criar os pipelines de CI para cada projeto utilizado. Ao se criar um pipeline, temos duas opções: utilizar pipelines clássicos ou baseados em arquivos yaml. Em geral, o pipeline clássico atende os cenários típicos, enquanto que os baseados em arquivos yaml permitem uma maior flexibilidade e customização das tarefas. A figura abaixo mostra a tela de criação. Inicialmente se escolhe o repositório de origem e aí basta seguir o assistente que vai gerar o arquivo yaml. Para escolher criar um pipeline clássico, basta usar o link no final da tela.

Usualmente nossos pipelines de CI fazem as seguintes tarefas: 1 – baixar artefatos nuget; 2 – compilar projetos; 3 – executar testes unitários; 4 – publicar artefato no próprio pipeline. Abaixo um pipeline clássico web e um de arquivo yaml.

Um alerta: apesar da Microsoft já ter recentemente permitido o desenvolvimento de Azure Functions em .NET Core, ainda não é possível fazer um pipeline para deploy deste tipo de Function. Se alguém tiver este cenário, é necessário por enquanto ainda fazer uma publicação manual – o que não é nada complexo, dá pra fazer direto do Visual Studio. Creio que em breve este tipo de pipeline também estará disponível.

Após termos os pipelines de CI funcionando, o último passo é criar os pipelines de release – CD. Usualmente o pipeline de release é criado baseado em um trigger de pipeline de CI bem sucedido. O pipeline de CD simplesmente baixa os artefatos e, caso seja uma aplicação web, publica direto no Web App; caso seja um Nuget, publica direto no feed de artefatos. Abaixo exemplos de pipelines Nuget e web app. Abaixo um pipeline web.

Finalizando os pipelines de release, voi-la, temos todo o nosso ambiente operacional em DevOps CI/CD. Um commit no master irá iniciar o pipeline de CI, que irá compilar, testar e deixar pronto pra publicação. Quando o CI finalizar, o pipeline de CD irá pegar os artefatos e automaticamente publicar no servidor Nuget ou direto no Web App.

Um último item que gostaria de apontar é sobre notificações. O DevOps por padrão já envia e-mails no sucesso ou falha do CI para quem fez o commit. É tranquilo também configurar o envio de e-mails para um grupo no caso de sucesso ou falha – dá até pra criar automaticamente workitems em falhas, se for desejado. No nosso caso, como somos usuários do Microsoft Teams, nós preferimos receber as notificações por ele.

Existe um bot público chamado Azure Pipelines – basta buscar em aplicativos no Teams. Ao instalar, ele permita que façamos assinaturas dos pipelines e publiquemos notificações em canais específicos. Assim, toda vez que um Release é feito com sucesso, todos nós da White Fox recebemos uma notificação no Teams automaticamente.

É isto pessoal. Uma vez configurado, o Azure DevOps funciona impressionantemente bem. Todos os nossos artefatos principais já estão em CI/CD e isto tem poupado muito tempo nosso em testes e deploys. Recomendo a qualquer empresa de software que passe a usar o Azure DevOps com CI/CD, se ainda não o faz!

No próximo artigo vou falar um pouco de services .net core usando NoSql. Até breve!

, , , , , , , ,

Deixe um comentário

Casa de Ferreiro…

Olá pessoal! Como estão de quarentena, com quase 5 meses de isolamento? Neste tempo muita gente aproveitou para aprender novas habilidades como cozinhar, pintar etc. No meu caso, resolvi empregar o tempo em algo que estávamos precisando fazer há muito tempo. Como prega o ditado, “casa de ferreiro, espeto de pau”, os sistemas utilizados internamente na White Fox já tinham passado da hora de serem atualizados. Ainda tínhamos sistemas em tecnologia antiga, hospedados em formatos que não recomendaríamos para ninguém.

Então, de março para cá, gastamos muitas horas para atualizar 100% do nosso parque tecnológico. E não só atualizar, aproveitamos para colocar tudo no estado-da-arte da que existe hoje. Como já citei várias vezes, estar atualizado é parte fundamental de nosso trabalho. Mas, principalmente por falta de tempo, a gente foca em empregar novas tecnologias e práticas nos nossos clientes e deixa de lado os nossos sistemas internos. Bom, não mais! Depois destes meses, estamos novamente orgulhosos de nossos sistemas! Eles estão utilizando o que há de melhor e mais moderno, tanto em boas práticas quanto em tecnologia. Neste artigo, vou fazer uma sinopse de como ficou a estrutura completa e, nas próximas semanas, detalhar melhor algumas das tecnologias e plataformas empregadas.

Para começar, fizemos uma refatoração e uma separação de camadas, isolando todas as nossas regras de negócios em APIs REST. Estas foram hospedadas em um API Gateway com o uso o Microsoft Azure Api Gateway – que é fantástico, com certeza teremos um artigo só para ele. O Api Gateway age como fachada para a cada API, gerenciando o acesso através de assinaturas e implementando as regras que se fazem necessárias. A figura abaixo mostra o uso do nosso API em uma semana normal.

Uso do White Fox Api Gateway

Aproveitamos que queríamos ter um caso de estudo de banco de dados NoSql e convertemos todo nosso sistema de autenticação e segurança para usarmos um banco de dados deste tipo. Fizemos testes com o Cosmos Db mas acabamos utilizando mesmo o Azure TableStorage padrão. O resultando ficou muito bom, detalho isto em artigo futuro.

Cada uma das APIs foi documentada em swagger usando o swaggerhub – que é completamente integrado ao Azure Api Gateway. Todas as APIs foram implantadas com o uso do Microsoft DevOPs, com Continuous Integration (CI) e Continuous Delivery (CD). Assim, após o commit, todos os testes unitários são automaticamente executados e, quando aprovados, atualizados diretamente no API Gateway, ficando disponível para aplicações web, móveis ou outras.

The API gateway pattern versus the direct client-to-microservice ...
Aplicaçoes usando API Gateway

O uso de APIs Rest nos sistemas Windows e Web ficou bem simples com o uso do Unchase Open API Connected Service (que pode ser instalado no Visual Studio). Ele se usa das especificações swagger para gerar um proxy local, similar à uma referência WCF.  O primeiro sistema que atualizamos foi o sistema interno de gestão, com o uso de um site Angular 10. Um ponto que tivemos cuidado de implementar no sistema web foi a utilização de chamadas de APIs de ação de forma 100% assíncrona. Isto melhora o tempo de resposta para o usuário e a escalabilidade do sistema. Mais sobre isto em artigos futuros

Dashboard White Fox Web

O segundo sistema que atualizamos foi o nosso aplicativo Windows. Ele é utilizado por alguns desenvolvedores que preferem ter algo sempre ativo no desktop. Com o uso do Unchase, também foi super simples migrá-lo para utilizar o Api Gatway.

Agora a “jóia da coroa” deste período foi a Lana. Já éramos usuários do Microsoft Teams há um bom tempo, e tínhamos ouvido que o Microsoft Bot Framework 4 estava valendo a pena, especialmente integrado ao Teams. Assim, mergulhamos a fundo e o resultado foi excelente. Construímos um bot, chamado Lana, para o Teams da empresa, que age como interface para todas as nossas APIs. E por ser um bot, comunicamos em linguagem natural escrita de maneira rápida e intuitiva. O resultado ficou tão bom que poucos ainda usam o sistema web ou aplicação Windows. Com certeza farei um artigo sobre a Lana.

Tudo tem lados positivos e negativos e esta pandemia não foi diferente. Sem ela não teríamos todo este tempo disponível para atualizar e avançar nosso conhecimento sobre as mais recentes plataformas e tecnologias. Podemos afirmar com certeza que agora em casa de ferreiro, temos um espeto de aço inoxidável da melhor qualidade! Nos próximos artigos detalho um pouco mais cada um destes componentes. Até breve!

, , , , ,

2 Comentários

ORM e Concorrência

Olá pessoal! Há algum tempo atrás sofremos um pouco na manutenção de um dos grandes sistemas da White Fox. Quero compartilhar a experiência para que outros não tenham que passar pelas mesmas dificuldades!

Só relembrando, conforme meu antigo post sobre a camada de domínio, nós utilizamos ORM (nHibernate ou Microsoft Entity Framework) para fazer o mapeamento de entidades para o banco de dados, de modo a abstrair os mecanismos de persistência. Isto tem funcionado muito bem para nós – utilizamos ORM em praticamente todos nossos sistemas de maior porte há mais de 10 anos com sucesso. Claro que por mais que tentemos compartimentalizar, ao longo do tempo os domínios acabam crescendo. Hoje temos sistemas com domínios de mais de 300 objetos e milhares de linhas de código de regras de negócio.

Pois bem, neste sistema em particular, começamos a perceber há algum tempo, problemas de dados somente no ambiente de produção. Valores que aparentemente ficavam errados do nada. E, obviamente, nenhum destes problemas aparecia em homologação ou durante a execução de testes unitários. Claro que, como todo acidente, nenhum erro mais grave tem uma única causa. Neste caso em específico, também temos uma sequência de eventos que levaram à falha. Olhando agora, são até que óbvios, mas gastamos um bom tempo no diagnóstico e solução.

O primeiro componente começou com uma prática usual de ORM. Quase todos possuem uma proteção para evitar que a persistência ocorra em um registro que foi alterado por outrem. Por exemplo, se o ORM carrega um objeto em memória, faz nele alguma alteração e ao salvar, detecta que o registro no banco não é o mesmo de quando o objeto foi carregado, ele gera uma exceção. Porém, este mecanismo deixa tudo lento, já que para implementá-lo, o ORM acaba tendo que fazer uma query a cada UPDATE. E como usamos transações, os LOCK do banco de dados acabam garantido a atomicidade da operação, assim esta proteção acaba ficando redundante. No nosso caso ela sempre é desligada.

O segundo componente é a criação de campos para contadores ou totalizadores. Sim, claro, isto é algo que se deve evitar, especialmente como atributo de uma entidade de domínio. Por exemplo, colocar o total de uma nota fiscal como atributo ao invés de calcular o total através da soma de seus elementos. Usualmente evitamos isto, mas, em algumas entidades, calcular o valor toda hora pode ser complicado, seja porque existe alguma regra de negócio muito complexa envolvida ou um número muito grande de elementos para compor o total. Então, em algum momento, alguém resolve que manter o totalizador oferece uma melhor razão custo/benefício. Obviamente que se usam transações para manter os totais e se criam testes unitários para garantir que os totalizadores funcionam em todos os cenários.

O último componente do problema é a concorrência. Dificilmente teste unitários são criados para simular uma carga de múltiplos usuários simultâneos, pela complexidade de se simular este tipo ambiente. Assim, no teste unitário, quase não há concorrência. Porém, em produção os nossos sistemas são usados por centenas de usuários simultâneos. Apesar dito, tipicamente um usuário faz transações em uma única grande “entidade” por vez (p. ex., vendendo um produto). Assim, mesmo vários usuários em paralelo dificilmente mexem na mesma entidade, simultaneamente, ao mesmo tempo.

Mas as exceções é que fazem a coisa desandar. Se desligamos a proteção de dados alterados, usarmos totalizadores em entidade e usuários alteram esta mesma entidade quase ao mesmo tempo, temos o nosso problema acontecendo! Para detalhar, vejam a figura a seguir. Imaginem 2 processos executando em paralelo, em tempos muito próximos um do outro.

image

Em um momento 1, ambos carregam a mesma entidade em memória, uma delas com um atributo totalizador. Como neste momento ainda não houve nenhuma transação, ambos conseguem carrega-la simultaneamente, e ambas possuem o mesmo valor para o atributo. No momento 2, ambos fazem algum processamento em que vão atualizar o campo totalizador. No momento 3, o primeiro processo abre uma transação, salva os objetos e faz o COMMIT. O segundo processo tenta fazer o mesmo, porém como o primeiro fez o LOCK, ele é bloqueado e fica em espera. Se não cair por timeout, quando o primeiro processo acabar, ele é liberado, começa sua transação, salva os seus objetos e faz o seu COMMIT. Como não há proteção para alteração, ele não vai perceber que o objeto foi alterado pelo primeiro processo e vai completar a ação achando que tudo correu bem.

Mas percebam que o segundo processo partiu objetos com totalizadores incorretos! Como o ele carregou os objetos no mesmo momento que o primeiro processo, ele não vai contemplar as alterações feitas por ele e vai salvar um total incorreto. Um exemplo, ambos carregam um atributo com um valor total de 10, ambos somam 1, o primeiro salva 11, o segundo também vai salvar 11, incorretamente!!!

Uma vez diagnosticado, o problema também não é simples de resolver. Não há solução trivial com o uso de ORM. Soluções como ativar a proteção de alteração, usar LOCK pessimista ou usar singletons, possuem pontos extremamente negativos e foram rejeitadas por nós. No final, a solução que adotamos foi, para campos totalizadores, ignorar totalmente o ORM e ir direto ao banco. Fizemos isto com o uso de um repositório especialmente projetado para este fim e com o uso de stored procedures para garantir que as alterações sejam feitas com as proteções adequadas. Esta solução conseguiu até mesmo melhorar o desempenho da aplicação, pois evitamos a manipulação de totais pelo ORM e transferimos todo o trabalho para o banco. O ponto negativo é que o sistema fica bem mais complexo de manter e uma porção das regras de negócio saiu do domínio e foi para o banco. Mas de todos os males possíveis, este foi o que achamos de menor impacto.

Moral da história é: conheça seu ORM, evite a todo custo campos totalizadores e, se tiver que usá-los, não se esqueça que poderá ter graves problemas em um ambiente com concorrência.

Este e outros eventos tem nos feito repensar o uso de ORM como um absoluto. Acho que já está na hora de refazer minha série sobre produtividade, atualizando-a com as tecnologias e práticas que temos adotado nos últimos anos. Tudo muda e TI muda ainda mais rápido. Acho que até que conseguimos ter uma relativa estabilidade nos nossos ambientes por muito tempo. Mas obviamente a evolução é necessária e tem hora que mudar paradigmas é essencial para mantermos nossa produtividade. Mais sobre isto em breve!

Até a próxima!

, , ,

1 comentário

Arquitetura de Microservices utilizando o Microsoft SQL Service Broker – Parte 3 – Final

Olá! Antes de iniciar a última parte desta série, uma notícia boa: a White Fox se tornou parceiro Microsoft Silver em Application Development e Gold em Devices and Deployment. Além do reconhecimento, a parceria com a Microsoft é importante para dar uma visibilidade maior para a White Fox e nos permitir acesso a muito mais recursos para nosso processo de desenvolvimento. Obrigado a todos que nos ajudaram durante a certificação! Em 2016 a White Fox deve focar mais no Microsoft Azure, sempre buscando trazer o melhor da tecnologia para nossos clientes com a melhor relação custo/benefício.

No último post, mostramos a estrutura montada no SQL Service Broker para suportar nossa arquitetura de Microservices. Nesta última parte vamos mostrar as estruturas em .NET que suportam o Thin Client (cliente) e os executores. Como estamos montando uma arquitetura expansível, criamos então uma classe estática, utilizando um container Microsoft Unity, que chamamos de ServiceBus, para registrar cada cliente e executor. O código dela pode ser visto abaixo.

public static class ServiceBus {
	private static IUnityContainer iocCcontainer;
	private static bool isInitialized;

	public static void InitializeSql(string connectionStringName) {
		isInitialized = true;
		iocCcontainer = new UnityContainer();
		iocCcontainer.RegisterInstance(typeof(IStorage), new SqlStorage(connectionStringName));
	}

	public static T GetService<T>() where T: class, IService {
		if (!isInitialized) throw new ServiceBusNotInitializedException();
		return iocCcontainer.Resolve<T>();
	}

	public static void RegisterService<T>(Type type) where T : class, IService {
		if (!isInitialized) throw new ServiceBusNotInitializedException();
		iocCcontainer.RegisterType(typeof(T), type);
	}

	public static void RegisterComponent<T>(T instance) where T : class, IComponent {
		if (!isInitialized) throw new ServiceBusNotInitializedException();
		iocCcontainer.RegisterInstance(typeof(T), instance);
	}
}

As interfaces IService são os serviços de mensageria, tanto do cliente quanto do executor. As interfaces de IComponent são para classes que são responsáveis por implementar as regras de negócio do executor. Abaixo um exemplo de uso do ServiceBus, onde o ResultMessagesReceiver (que implmenta IReceiver) é o responsável por processar as mensagens de retorno recebidas e o ClienteService (que implementa IClientService) é o Thin Client.

 ServiceBus.InitializeSql("ServiceBroker");
 ServiceBus.RegisterComponent<IReceiver>(new ResultMessagesReceiver());
 ServiceBus.RegisterService<IClientService>(typeof(ClientService));

Criamos também um ServiceBase, que é usado por executores e clientes para implementar as rotinas principais de processamento de mensagens. O código está abaixo. É praticamente um loop onde as mensagens são lidas do SQL Server, dentro de uma transação, e processadas. Em caso de qualquer problema, a transação pode ser desfeita e a mensagem permanece lá. É importante notar que caso aconteçam muitos rollbacks em sequência, o SQL Server desativa a Queue (isto nos causou alguns problemas de debug!). Neste caso, a Queue deve ser reativada antes que outras mensagens possam ser lidas.

protected void ProcessMessages(int maxMessages) {
	var counter = 1;
	do {
		if (++counter > maxMessages) break;
		storage.BeginTransaction();
		try {
			var message = storage.ReadMessage(endPoint);
			if (message == null) {
				storage.Commit();
				break;
			}
			if (string.IsNullOrEmpty(message.Handle)) {
				storage.Commit();
				break;
			}
			if (initiator && message.MessageType == EndMessageType) {
				storage.EndConversation(message.Handle);
				storage.Commit();                        
				continue;
			}
			if (message.MessageType == ErrorMessageType) {
				// todo: logar o erro
				storage.EndConversation(message.Handle);
				storage.Commit();                        
				continue;
			}
			message.Date = DateTime.Now;
			if (!ProcessMessage(message)) {
				storage.Rollback();
				break;
			}
			if (!initiator) storage.EndConversation(message.Handle);
			storage.Commit();                    
		}
		catch (Exception) {
			storage.Rollback();
			throw;
		}                               
	} while (true); 
}

Abaixo está a parte relevante do código que implementa o storage do SQL Server. Optamos por simplesmente encapsular chamadas a stored procedures que ficam no banco de dados utilizado para as filas de mensagens. A seguir estão também as 3 procedures de leitura, envio e final de conversação.

 public string SendMessage(EndPointConfiguration endPoint, string data, string conversationHandle = null) {
	var parameters = new Parameters()
		.Add("initiatorService", endPoint.Initiator).Add("targetService", endPoint.Target)
		.Add("contract", endPoint.Contract).Add("messageType", endPoint.MessageType)
		.Add("data", data);
			
	if (!string.IsNullOrEmpty(conversationHandle))
		parameters.Add("handle", conversationHandle);
	StoredProcedureFacility.ExecuteScalar<string>(connectionStringName, "SendMessage", parameters)	
}

public Message ReadMessage(EndPointConfiguration endPoint) {
	var root = StoredProcedureFacility.GetXml(connectionStringName, "ReadMessage", Parameter.Create("queueName", endPoint.QueueName));
	return new Deserializer<Message>(root)
		.Property(m => m.MessageType, "mt")
		.Property(m => m.Contents, "data")
		.Property(m => m.Date, "dt")
		.Property(m => m.Handle, "ch")
		.Instance();
}

public void EndConversation(string handle) {
	StoredProcedureFacility.ExecuteNoResults(connectionStringName, "EndConversation", Parameter.Create("handle", handle));
}

Stored procedures:

CREATE PROCEDURE [dbo].[SendMessage] (
	  @initiatorService sysname,
	  @targetService varchar(255),
	  @contract varchar(255),
	  @messageType varchar(255),
      @data varchar(MAX) = NULL,
	  @handle varchar(255) = null
)
AS 
BEGIN
	if @handle is null 
	begin   declare @id uniqueidentifier   BEGIN DIALOG CONVERSATION @id
            FROM SERVICE @initiatorService
            TO SERVICE @targetService
            ON CONTRACT @contract
            WITH ENCRYPTION = OFF, LIFETIME = 7200;
		set @handle = cast(@id as varchar(255));
	  end;
	  send on conversation @handle message type @messageType (@data)                  
      select @handle;	  
END
GO

CREATE procedure [dbo].[ReadMessage](@queueName varchar(255))
as 
begin
declare @ch varchar(255)
declare @mt varchar(255)
declare @data varchar(max);
declare @dt DateTime;

set nocount on

declare @Sql nvarchar(max) = N'RECEIVE TOP(1) @h = conversation_handle, @messageTypeName = message_type_name, @Packet = message_body, @date = message_enqueue_time FROM ' + @queueName + ';'

EXECUTE sp_executesql @Sql, N'@h UNIQUEIDENTIFIER OUTPUT, @messageTypeName varchar(255) OUTPUT, @Packet VARCHAR(max) OUTPUT, @date datetime OUTPUT'
		,@h = @ch OUTPUT
        ,@messageTypeName = @mt OUTPUT
        ,@Packet = @data OUTPUT
		,@date = @dt output;

select * from (select @ch ch, @mt mt, @data [data], @dt [dt]) M for xml auto

end
GO

CREATE procedure [dbo].[EndConversation](@handle varchar(255)) as
begin
;end conversation @handle;
end

Finalmente, a seguir está o exemplo de um Thin Client, com um envio de mensagem e a rotina que faz o processamento das mensagens de retorno. Para cada tipo de ação criada, deve haver um tratamento específico. O construtor recebe, por injection (o Unity cuida disto) o storage e a classe que irá tratar as regras de negócio.

public class ClientService: ServiceBase, IClientService {	 
    private readonly IReceiver receiver;

    public ClientService(IStorage storage, IReceiver receiver)
		: base(storage, true, Configuration.Requester.Service, Configuration.Executer.Service, Configuration.Contract, 
			Configuration.Requester.Message, Configuration.Requester.Queue) {
		this.receiver = receiver;
	} 
	
    public void RequestAction1() {
      SendMessage(new MessageContent { Action = Actions.Action1});
    }

    public override bool ProcessMessage(Message message) {
       var ser = new DataContractJsonSerializer(typeof (MessageContent));        MessageContent content;        using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(message.Contents))) {
		content = (MessageContent) ser.ReadObject(ms);        }        if (content == null) return true;
	try {      switch (content.Action) {
		case Actions.Action1:     return receiver.ReceiveMessage(content, message.Date);
		default:     return receiver.Error(content.Action, ErrorCodes.NotImplemented, null);
		}
	}
	catch (Exception ex) {    return receiver.Error(content.Action, ErrorCodes.Exception, ex);
    }
}

O executor não é diferente, como pode ser visto abaixo. Ele somente recebe a mensagem, faz algum tipo de ação de negócio específica e retorna os dados para o solicitante. Claro que isto é um código simplificado, já que em um código de produção é importante tratar todos os tipos de erros possíveis, evitando rollbacks da fila.

public override bool ProcessMessage(Message message) {
	var ser = new DataContractJsonSerializer(typeof(MessageContent));
	MessageContent message;
	using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(message.Contents))) {
		message = (MessageContent)ser.ReadObject(ms);
	}
	if (message == null) return true;
	var returnMessage = new MessageContent {Action = message.Action};
	try {
		switch (message.Action) {
			case Action.Action1:
				returnMessage.Data = MyBusinessRulesManager.ExecuteSomething(message).ToString();
				break;
		}
	}
	catch (Exception ex) {
		returnMessage.Error = ex.Message;
	}
	SendMessage(returnMessage, message.Handle);
	return true;
}

Bom, espero ter conseguido passar uma visão geral da arquitetura de Microservices que estamos utilizando. Claro que temos muitos outros cenários que fazem com que a complexidade desta arquitetura seja bem maior. Por exemplo, temos situações onde temos mensagens geradas em horários específicos ou que só podem ser executadas em horas úteis. Isto faz com que o Executor tenha toda uma lógica para armazenar as mensagens com agendamento de execução específica em outras filas. Mas tudo isto é feito com o fundamento que mostrei nesta série. Como sempre, fiquem à vontade para entrar em contato para tirar dúvidas ou conversar mais sobre este assunto. Até a próxima!

, , , , ,

Deixe um comentário

Arquitetura de Microservices utilizando o Microsoft SQL Service Broker – Parte 2

Oi pessoal! Este post é continuação da série sobre a implementação de um barramento de microservices usando o SQL Server Service Broker (veja a primeira parte aqui).

O primeira passo é montar a infraestrutura no SQL Server. O barramento de microservices é baseado em filas, assim é necessário escolher um banco de dados para a criação das mesmas. Em tese, qualquer banco pode ser usado, mas recomendo a utilização de um banco criado especificamente para esta finalidade, para facilitar as tarefas de infra como backup, monitoração etc.

No nosso caso, criamos um banco novo, chamado, por exemplo, “MyMicroservicesBus”. Após a criação do banco, deve ser feito o comando abaixo, para que o Service Broker seja ativado no mesmo.

ALTER DATABASE MyMicroservicesBus SET ENABLE_BROKER

O próximo passo é criar as filas e contratos. Para quem não é muito familiar com os conceitos do SQL Server Broker, ver referência aqui. Recomendo também o livro “Pro Sql Server 2008 Service Broker”, bem completo. São necessárias duas filas, uma para servir de canal de comunicação da “Thin API” para o Executor (ExecuterQueue) e outra para que ele possa enviar as respostas de volta (InitiatorQueue). Para cada fila é necessário definir um contrato, que vai servir também para o versionamento das mensagens e um Service, para o roteamento de mensagens. Por último, não esquecer de dar as permissões de RECEIVE para os usuários apropriados. Um script completo é mostrado a seguir.

-- messages
create message type [http://mydomain.net/services/myservice/requestMessageV1] Validation = None
GO

create message type [http://mydomain.net/services/myservice/responseMessageV1] Validation = None
go

-- contracts
create contract [http://mydomain.net/services/myservice/contractV1]
(
	[http://mydomain.net/services/myservice/requestMessageV1] sent by initiator,
	[http://mydomain.net/services/myservice/responseMessageV1] sent by target
)
go

-- QUEUES
create queue MyServiceInitiatorQueue with status = on
go
create queue MyServiceExecuterQueue with status = on
go


-- SERVICES
create service MyInitiatorService on queue MyServiceInitiatorQueue ([http://mydomain.net/services/myservice/contractV1])
go
create service MyExecuterService on queue MyServiceExecuterQueue ([http://mydomain.net/services/myservice/contractV1])
go

-- permissions
grant RECEIVE ON MyServiceInitiatorQueue to public
go
grant RECEIVE ON MyServiceExecuterQueue to public
go

Uma vez que as filas estejam definidas, devemos decidir como as mensagens vão ser recebidas peles executores e clientes. Existem várias alternativas para isto, a mais simples seria usar stored procedures para processar mensagens de cada lado. No nosso caso, isto não atende pois temos regras de negócio complexas que devem ser executadas para cada mensagem, que estão implementadas em nosso Domínio e não faria sentido ou seria viável repeti-las em procedures. Assim precisamos de algo que seja capaz de chamar uma regra de negócio dentro do nosso ambiente transacional.

A segunda possibilidade é utilizar o Service Broker External Activator, que é uma ferramenta disponibilizada pela Microsoft para estes cenários. Ele fica “escutando” as filas e se encarrega de chamar um executável externo que, no nosso caso, seria um código em C#. Em nossos testes, funcionou perfeitamente nos nossos ambientes de desenvolvimento e homologação, mas qual a nossa surpresa ao ver que no ambiente de produção ele simplesmente não funcionou. Depois de muito pesquisar, descobrimos que ele não funciona em Clusters de SQL Server, que é exatamente o cenário usado no ambiente de produção do nosso cliente.

A terceira possibilidade, a mais complexa, é utilizar código CLR embutido dentro do SQL Server. Há várias versões do SQL Server é possível compilar uma DLL e registrá-la para uso dentro de um database como se ela fosse uma stored procedure. Esta foi a alternativa escolhida para nosso cenário.

Só recapitulando o procedimento para incluir um código CLR no SQL Server: primeiro, é necessário criar um projeto com uma classe estática e um método estático, que vai ser o ponto de entrada para a chamada no SQL Server, decorado com o atributo Microsoft.SqlServer.Server.StoredProcedure. Depois este assembly deve ser registrado no database e finalmente as queues devem ser modificadas para acionar este método quando uma mensagem chegar. Um exemplo de código CLR e script estão a seguir. Lembrando que caso o assembly não seja assinado com um certificado confiável para o servidor onde está o SQL Server, é necessário baixar o nível de confiança do banco (primeira instrução do script abaixo). Como nossos assemblies não estavam assinados, optamos por fazer isto. Esta decisão deve ser tomada com responsabilidade, pois este comando vai aumentar a vulnerabilidade do banco de dados.

using Microsoft.SqlServer.Server;

namespace MyNamespaceInAssembly {
    public static class StoredProcedures {

        [SqlProcedure]
        public static void ProcessMessages() {
            Domain.Services.ProcessMessages();
        }
    }
}

Script:

-- Allows unsigned Assemblies
ALTER DATABASE ServiceBroker SET TRUSTWORTHY ON
GO

-- Register Assemblies
CREATE ASSEMBLY MyServiceThinClient from 'c:\myservice\ThinClientProcessor.dll' WITH PERMISSION_SET = UNSAFE
go
CREATE ASSEMBLY MyServiceExecuter from 'c:\myservice\ExecuterProcessor.dll' WITH PERMISSION_SET = UNSAFE
go

-- procedures
CREATE PROCEDURE ProcessMessagesThinClient
AS
EXTERNAL NAME MyServiceThinClientAssembly.[MyNamespaceInAssembly.StoredProcedures].ProcessMessages
go
CREATE PROCEDURE ProcessMessagesExecuter
AS
EXTERNAL NAME MyServiceExecuterAssembly.[MyNamespaceInAssembly.StoredProcedures].ProcessMessages
go

-- permissions
grant execute on ProcessMessagesThinClient to public
go
grant execute on ProcessMessagesExecuter to public
go

-- activations
alter queue MyServiceInitiatorQueue with activation( procedure_name = ProcessMessagesThinClient, MAX_QUEUE_READERS = 1, status = on, execute as self)
go

alter queue MyServiceExecuterQueue with activation( procedure_name = ProcessMessagesExecuter, MAX_QUEUE_READERS = 1, status = on, execute as self)
go

Por enquanto é só pessoal. Na parte 3 vou detalhar melhor o código do ProcessMessages, tanto do ThinClient quanto do Executor. Se tiverem dúvidas específicas, podem entrar em contato. Até a próxima!

, , , , ,

Deixe um comentário

Arquitetura de Microservices utilizando o Microsoft SQL Service Broker – Parte 1

Olá pessoal, vou iniciar uma série de posts mais técnicos desta vez. Como mencionei no post anterior, estivemos analisando o uso do Microsoft SQL Server Service Broker como uma alternativa para implementação de Arquitetura de Microservicess. Aproveitando uma necessidade de um de nossos clientes, conseguimos definir e implementar um barramento de microservices com sucesso, que já está em execução em ambiente de produção! Nesta série de posts vou detalhar a arquitetura utilizada, tentando mencionar os desafios que enfrentamos e as soluções adotadas. Espero que sirva de apoio para outros desenvolvedores se aventurando por esta linha.

Inicialmente um pouco de contexto. Em um de nossos clientes, da área financeira, possuímos um grande sistema .NET, em operação há mais de 20 anos. Apesar dele ser relativamente bem estruturado (separação de camadas, mapeamento objeto-relacional com nHibernate, interfaces MVC etc.), ele acumulou, por todo este tempo de evolução, uma infinidade de regras de negócio para cada uma das áreas atendidas. Por ser um sistema central da empresa, praticamente todas as operações passam pelo mesmo e ele é integrado à vários outros, por diversas formas de comunicação. Assim, temos cada vez mais dificuldade para evoluí-lo e mantê-lo, já que sua arquitetura monolítica faz com que testes de integração e homologação sejam extremamente complexos e demorados. É com grande dificuldade que conseguimos manter um ritmo saudável de trocas de uma metodologia ágil, pois o tempo de homologação de usuários quase que inviabiliza nossas janelas semanais.

Outro grande problema de nosso sistema monolítico é a escalabilidade. Com o aumento de demanda, não temos opção a não ser aumentar o número e capacidade de máquinas, pois o sistema tem que ser replicado por inteiro. gerando aumento de custos e complexidade de infra-estrutura.

Este seria um dos típicos problemas resolvidos por um sistema que utiliza uma arquitetura de microservices. Cada área de um sistema de grande porte seria implementada por um serviço independente, que usa um barramento de mensagens para comunicação entre si (ver figura a seguir). Desta forma, a manutenção ou evolução de uma área não afetaria outras, tendo seu próprio ciclo de desenvolvimento independente.

ESB1

Porém, é raro termos o luxo de projetarmos um sistema do zero já incorporando este tipo de arquitetura. Então, nosso primeiro desafio foi achar uma maneira de fazer com que um sistema monolítico como o nosso pudesse utilizar microservices sem que ele tivesse que ser refeito do zero. A solução que encontramos está mostrada na figura a seguir. Inicialmente, isolamos uma área que possa ser implementada como um serviço isolado (A). Depois, quebramos este bloco em duas porções: uma pequena interface de comando e recepção de resultados (“Thin” API); e o módulo que implementa os executores de ações e regras de negócio específicas (B). Entre estes blocos, incluímos o suporte ao envio e recepção de mensagens, para que a API se comunique com o executor através do barramento de microservices (C).

ESB2

Desta forma, conseguimos alguns benefícios da arquitetura de microservices: 1) o executor é completamente isolado do sistema principal, podendo ser evoluído de maneira independente; 2) a execução de atividades passa a ser feita de forma assíncrona, liberando recursos para o sistema e permitindo a escalabilidade horizontal dos executores. Se conseguirmos isolar cada área desta forma poderemos, a longo prazo, quebrar nosso sistema monolítico em vários microservices, chegando bem próximo de um sistema que fosse projetado do zero para esta arquitetura.

Pode-se argumentar que outras arquiteturas poderiam gerar benefícios similares. Uma alternativa, por exemplo, seria fazer uso de componentes intercambiáveis. Embora isto garantisse o isolamento da área de negócio, não resolveria por completo a questão da substituição em produção. Com microservices, podemos parar um dos executores de serviço, ou mesmo todos eles e todo o sistema continua funcionando normalmente (as mensagens simplesmente se acumulam, sendo processadas posteriormente quando o serviço for restabelecido). No caso de componentes, seria muito mais complexo de resolver este cenário, pois há um acoplamento direto entre o mesmo e o sistema; se o componente parar o sistema também para. Com componentes também não teríamos solução trivial para o problema de escalabilidade.

Outra alternativa seria o uso de webservices tradicionais, baseados em SOAP ou outro protocolo. Webservices simplificam o problema da substituição em produção, pois podemos ter um cluster de servidores, com vários em paralelo, e ir substituindo aos poucos, com o uso de versionamento. Eles resolvem também o problema de escalabilidade, pois são de menor porte e podemos aumentar o cluster conforme necessário. O grande problema de webservices é a latência que existe para as chamadas de regras de negócio, já que eles normalmente se encontram em outros servidores. Para ações muito frequentes, em operações comuns do sistema, poderíamos ter uma demora excessiva para a execução, afetando o usuário. E operações assíncronas em sistemas web chamando webservices são extremamente difíceis de serem implementadas.

Analisando estes e outros cenários, a arquitetura de microservices parece ser a mais adequada. Da mesma forma que um webservice, ela separa a camada de negócios, permitindo escalabilidade e isolamento. E é tão rápido quanto um componente, já que a sua “Thin” API reside dentro da aplicação. É claro que o lado negativo é que todas as operações, que antes eram síncronas, passam a ser assíncronas. Este é o maior limitador, já que é necessário reimaginar o comportamento do sistema considerando que as ações não são mais imediatas, mofidifcando a usabilidade do mesmo.

Para a implementação do nosso barramento de serviços, foi considerado o uso vários produtos, open source e comerciais. O RabbitMQ foi uma das alternativas que foram melhor avaliadas. No entanto, no final, a escolha foi o Microsoft SQL Server Service Broker. As razões foram a facilidade de se montar um barramento de serviços simples e o fato dos nossos sistemas já o usarem o SQL Server como DBMS, o que simplificou bastante nossa infraestrutura.

Bom, por hoje é só. Em breve devo detalhar a parte técnica da solução, desde a implementação das filas de mensagens e processadores até o processo de ativação de executores. Até a próxima!

, , , , ,

3 Comentários

Imagina na Copa!

Olá pessoal! Acharam que eu tinha abandonado este blog em definitivo?! Negativo, ainda estou firm e forte por aqui! :-)… Só que vou parar de prometer voltar a escrever em um ritmo normal, já que cada vez que faço isto, acontece algo que me impede totalmente… No último post, falei sobre o aplicativo do Sistema Poliedro que estávamos trabalhando e que posteriormente foi denominado P4Ed (ou simplesmente P+). Então, de lá para cá ficamos (e continuamos!) completamente envolvidos com ele, muitas novas funcionalidades, várias versões, centenas de APIs… A compensação é que vemos o resultado, utilizado por milhares de alunos todos os dias, e nos sentimos orgulhosos de termos contribuído. Os aplicativos são um sucesso e nem tudo ainda foi disponibilizado, as próximas versões irão conter ainda mais funcionalidades e características e farão o P+ ser cada vez mais uma referência absoluta de mercado.

O trabalho neste projeto nos fez confrontar vários “dogmas” internos da White Fox. Como visto em posts anteriores, um dos grandes diferenciais da White Fox é o uso de um framework que permite grande produtividade no desenvolvimento de sistemas e, em especial, de interfaces. Porém, nestes 4 anos de empresa, duas coisas aconteceram: como mencionamos no post anterior, o trabalho do P+ nos fez focar na entrega de produtos SEM interface; nós fomos encarregados de desenvolver APIs e regras de negócio de servidor enquanto que empresas parceiras trabalham em paralelo na confecção de interface. Embora isto nos permitiu desenvolver o P+ em tempo recorde, ele fez com que grande parte do nosso framework fosse totalmente descartada. Daí tivemos que nos reinventar pra conseguir, tendo uma API como produto final, ter a mesma produtividade que estávamos acostumados.

O segundo fato importante destes 4 anos foi uma mudança significativa na característica das interfaces. A web continua importante, mas temos também agora, em pé de igualdade, interfaces de dispositivos móveis (em seus vários tipos) e integrações diversas com outros sistemas e plataformas. As próprias interfaces web, graças a uma evolução cada vez maior de frameworks javacript (como AngularJS, Backbone, Knockout etc.) fizeram com que todo o conceito de desenvolvimento mudasse. Com isto, parte de nosso framework perdeu sua aplicabilidade. Sobre isto, espero fazer um ou mais posts específicos, revisitando o assunto de produtividade.

O bom de se trabalhar com desenvolvimento é que o trabalho nunca é monótono. As mudanças acontecem, e em ritmo rápido. A própria Microsoft, talvez pressionada por plataformas diferentes, tem acelerado bastante o ciclo de vida de suas ferramentas e plataformas. Mal o Visual Studio 2013 foi lançado, no final do ano passado, e já tivemos o Update 1 (e, brevemente, o Update 2). Web API, aplicações MVC e a evolução acelerada da plataforma Azure, com um sem número de facilitadores, módulos e serviços prontos, faz com que tenhamos que repensar toda nossa infraestrutura de código. Só que temos que fazer isto com o avião voando; temos um sem número de sistemas pra manter, que devem continuar funcionando e ao mesmo tempo serem evoluídos para fazer uso de toda esta nova tecnologia.

Junte a isto tudo o desafio de escalabilidade do P+, também mencionado anteriormente. Embora, como arquitetos, nós busquemos fazer sistemas que sejam escaláveis, isto vale até certo ponto. Uma coisa é projetar um sistema para 10 usuários que pode chegar a 10.000 em um ano. Outra, totalmente diferente, é projetar um sistema para 5.000 que pode chegar a 1.000.000 de usuários em pouco tempo. Isto exige um grande planejamento, um trabalho grande de identificação de gargalos e, às vezes, soluções pouco ortodoxas. No P+ temos trabalhado incessantemente para buscar arquiteturas que permitam suportar grandes volumes na Cloud sem ter que reescrever a aplicação a cada aumento. É grande desafio e que, quando não adequadamente tratado, gera situações complicadas de gerenciar. Mais ou menos como suportar uma Copa do Mundo em um ambiente sem a infraestrutura adequada. E nem é só uma questão de recursos, se eles forem mal empregados ou seu uso for mal planejado, vocês podem imaginar o resultado – ou vivê-los, como vamos ter a oportunidade de fazer aqui na cidade maravilhosa, em menos de 1 mês.

Vou ficando por aqui. Tenho vários tópicos mais técnicos rascunhados que espero em algum momento transformar em posts. Até a próxima então!

, , , , , , , , , ,

1 comentário

Autenticação e Segurança

Olá pessoal! Esta semana quero falar de um assunto de infra-estrutura, mas extremamente crítico em nossas aplicações: a parte de autenticação e segurança de usuários. Quando fazemos uma aplicação Web, temos que definir como nossos usuários serão autenticados. Os desenvolvedores felizes são aqueles que utilizam autenticação integrada em suas aplicações e deixam a cargo da rede e do Active Directory (AD) a tarefa de identificar quem está logado. Claro que isto funciona bem em ambiente de Intranet. Na Internet é até possível, mas em geral é uma alternativa que não é utilizada por consumir uma banda excessiva e ser estranho para o usuário, abrindo aquela tela de login Windows cada vez que se acessa um site. Existe ainda a autenticação básica, utilizada em sites HTTPS e que basicamente mapeia o usuário logado para um usuário de rede. Esta alternativa também consome bastante banda e é pouco amigável para o usuário.

Nos cenários comuns, a autenticação é feita diretamente pela aplicação e o usuário se depara com campos de “login/senha” em alguma página da aplicação Web. Nestes casos, o mecanismo mais usado no .NET é o FormsAuthentication, com cookies. Nesta modalidade, após fornecer uma combinação login/senha válida, o sistema cria um cookie que é utilizado para autenticar as requisições subsequentes do usuário. Este cookie é associado à web session do usuário (na verdade, ele serve para “amarrar” a sessão, identificando-a a partir de sua criação) e permanece válido pelo tempo definido de expiração – normalmente 30 minutos. É possível ter FormAuthentication no formato cookieless, mas é menos utilizado por ser menos seguro.

Usar o FormsAuthentication (com cookies) no .NET é algo relativamente simples, todo o trabalho é feito pelo próprio framework. Para a maior parte dos sistemas, isto atende completamente a necessidade de autenticação. Em sistemas um pouco maiores, é possível também incluir uma implementação do security membership provider (ver estes excelentes posts aqui sobre o assunto), e ter a definição de perfis por usuário, resolvendo também as necessidades de autorização. É possível encontrar na web vários exemplos de implementação até com algumas customizações destes providers, como esta aqui fazendo uso de WCF.

Os problemas começam a surgir quando a aplicação cresce ao ponto de ter que ser integrada a outros sistemas e plataformas ou ter que coexistir com outras, ou ainda ter que suportar múltiplas formas de acesso (Windows Forms p. ex.). Nestes casos, temos que ter maneiras alternativas para identificar usuários e até fazer transferências de uma aplicação para outra. Em grandes empresas, surgem necessidades como definir uma política de segurança de expiração de senhas, um banco de dados único para os usuários entre as aplicações e até a necessidade de fazer login integrado ao AD em algumas situações. Aplicações de maior porte podem também necessitar de melhor desempenho na infra-estrutura de segurança, já que isto é utilizado em praticamente todos os requests. Finalmente, em aplicações corporativas é usualmente necessário ter coletas de estatísticas de uso e usuários ativos, para efeito de monitoramento e planejamento de carga. Nestes casos, a implementação padrão de segurança pode não atender completamente e é necessário desenvolver uma própria.

No nosso caso, desenvolvemos uma solução de segurança que já está em uso há mais de 12 anos. É uma solução WCF bastante simples, com um banco de dados de usuários, perfis e aplicações, classificados por empresa. Os serviços expõem mecanismos de autenticação direta por senha e por integração ao AD. A senha não é armazenada, utilizamos um hash gerado pelo próprio .NET. A integração ao AD é algo simples ainda, baseado no ID de rede (só liberamos para algumas poucas aplicações, onde o ambiente é controlado e os riscos baixos), há planos de se utilizar o Federated Services para algo mais avançado. Todo o acesso aos serviços são feitos por um intermédio de um agente de segurança, que implementa as classes de segurança e faz a tradução para os DTOs do WCF. O sistema suporta políticas de segurança, complexidade e expiração de senhas e coleta todas as estatísticas de uso das aplicações.

A solução possui ainda métodos para suportar a geração de tokens de segurança. Este tokens são identificadores (guid) que são passados, via querystring ou qualquer outro mecanismo, de um aplicação para outra. Eles possuem uma expiração rápida e podem ser utilizados apenas uma vez. Cada token está associado à sessão do usuário que o criou e pode ser utilizados para permitir o login rápido em outra aplicação. Com isto, conseguimos fazer a transferência de uma aplicação para outra de maneira transparente para o usuário.

Todos os nossos sistemas fazem uso da mesma solução de segurança, sempre através da utilização do agente. O FormsAuthentication continua sendo usado, a diferença é que a identificação do usuário é feita por nossa solução, não utilizando o membership provider. A única coisa que fazemos na aplicação é implementar classes para encapsular as chamadas ao agente e utilizar a session do ASP.NET para armazenar o usuário ativo e as respectivas permissões. Isto garante um melhor desempenho, evitando que a cada request os dados do usuário e respectivos perfis tenham que ser obtidos via chamadas WCF.

Aplicações Windows também utilizam o agente de segurança. A diferença é que como não temos um SessionID, como na Web, acabamos por simular um criando uma chave guid cada vez que a aplicação é inicializada. Esta chave é utilizada como se fosse o identificador daquela sessão Windows e pode também ser usada para se gerar tokens de autenticação. Assim é possível, por exemplo, clicar em um botão de uma aplicação Windows Form e se abrir um browser, com uma página de uma aplicação Web, já autenticada.

É isto, espero ter dado uma idéia de como lidamos com autenticação e autorização em nossos sistemas. Se alguém tiver alguma dúvida específica, basta entrar em contato. Até a próxima!

, ,

4 Comentários

Como eu Desenvolvo Software – Conclusão

Olá pessoal, este é último post desta série. No post anterior descrevi como construo software até o ponto onde desenvolvemos as telas do sistema que vão de fato gerar valor para o cliente. Estas telas são a própria razão do software sendo construído e são elas que geram o retorno para o custo de desenvolvimento. No fundo, estas telas são a razão de ser do sistema.

A mesma idéia de telas complexas pode ser aplicada para itens como serviços ou aplicações Windows. Um serviço normalmente é feito para atender uma necessidade de integração, seja com outros sistemas, com outras plataformas (mobile, por exemplo) ou para ser parte de um barramento corporativo. Para os serviços, eu sigo praticamente os mesmos passos utilizados em telas complexas. Primeiro identifico o que é desejado do serviço. Aí isto é descrito na forma de interfaces e objetos de transporte – eu uso o excelente Web Service Software Factory, que é um plugin DSL para o Visual Studio 2010. Se a integração é complexa, eu tento simular o máximo antes de efetivamente integrá-lo aos serviços de domínio, para, da mesma forma, identificar todos os aspectos e evitar retrabalho. Finalmente, com todas as interfaces definidas, eu as “preencho” fazendo as implementações acionar os serviços de domínio. Aplicações Windows também seguem estes mesmos passos.

Claro que há situações de exceção. Isto é especialmente válido para requisitos não funcionais como segurança e desempenho. Embora hoje seja relativamente fácil atender a requisitos de segurança com a própria infra-estrutura da plataforma .NET, em algumas situações é necessário que criemos mecanismos específicos para atender um determinado requisito. Um exemplo disto são aplicações multi-tenant, onde é necessário um cuidado especial para que o usuário de um cliente não tenha acesso a dados de outro. Para estes casos, o recomendável é tentar ver a literatura existente e pesquisar outras situações similares e as soluções que foram empregadas.

Outro ponto que gera situações de exceção é a questão de desempenho. Um problema típico é fazer uma aplicação que roda bem em desenvolvimento e testes, mas que, ao ser colocada em produção com um número grande de usuários, falha por completo. Existem vários problemas que podem gerar este cenário e cuja solução às vezes não é simples. Às vezes não é simples nem detectar a causa ou simular o problema em ambiente de testes, portanto boas práticas na hora de desenvolver podem significar menos dor de cabeça no futuro. Agora, se por um lado é ruim pensar em otimização prematura, tentando fazer código pra evitar um problema de desempenho para o qual não se conhece a gravidade ou a frequência de ocorrência; é também ruim não pensar em desempenho durante o desenvolvimento, utilizando práticas que podem gerar problemas potenciais. Aqui o melhor caminho ainda é a experiência, se a aplicação sendo desenvolvida tem estas características, o melhor é ter na equipe alguém que já tenha tido oportunidade de lidar e desenvolver soluções para estes tipos de requisitos.

Acho que deu para dar uma idéia do processo que eu sigo. Mas, além do processo, acho importante reforçar o ponto mais importante que eu tentei passar no decorrer destes post: fazer software é entregar algo que funcione, dentro de custo e prazo que em que o cliente possa ter ROI. A maior parte das vezes esta medida se prolonga por toda a vida útil do software e o ROI continua sendo medido a cada atividade de manutenção e evolução. Se tudo correr conforme previsto, um cliente satisfeito vai ter seus objetivos atendidos com o software que possui e a empresa que o construiu vai ter lucro neste processo.

Eu acredito que fazer software é mais parecido com artesanato do que com uma linha de produção. No fundo, a qualidade do software que vai ser entregue e a questão de se conseguir cumprir prazo e custo em posteriormente, dar manutenção, tem uma relação direta com a qualidade das pessoas que o construíram. Assim, para se tornar um bom desenvolvedor, é necessária uma atualização e melhoria constante no processo de desenvolvimento. Isto não só em aspectos tecnológicos, mas em áreas como habilidade de comunicação, design, capacidade de trabalhar em equipe e até entendimento dos negócios da empresa onde se está inserido e do cliente. Tudo isto está obviamente muito ligado à maturidade e à experiência de quem desenvolve, mas são características que podem ser melhoradas em quaisquer estágios da nossa carreira. Como qualquer tipo de artesanato, fazer bom software significa ter uma boa combinação de talento, conhecimento e experiência. O talento é de cada um e a experiência vem com o tempo. O fator que está mais sob nosso controle é o aspecto técnico, que podemos sempre aprimorar com estudo.

Neste aspecto, apesar da tecnologia evoluir muito rápido, existem conceitos fundamentais e comuns a qualquer tipo de software que permanecem quase imutáveis ao longo do tempo. Assim, coloco a seguir uma relação bibliográfica que abordam estes aspectos mais fundamentais e que eu achei especialmente importantes pra mim:

Code Complete 2 – Acho este livro essencial para quem quer codificar bem. É sobre técnicas para melhorar o código que escrevemos, muito bom mesmo!

Clean Code – Na mesma linha do Code Complete, também muito bom!

The Mythical Man-Month – Este livro foi escrito em 1967, mas é incrível como o que ele apresenta é aplicável até hoje! Muitas vezes vemos conceitos defendidos por profissionais “atuais” que repetem os mesmos erros descritos há mais de 30 anos… Acho que é uma leitura indispensável para qualquer profissional da nossa área (é bem curtinho).

The Object Primer – Embora um pouco antigo, a parte de conceitos sobre programação orientada à objeto é fantástica. A parte de UML hoje nem é tão importante, mas mesmo assim vale – afinal quem não vai esbarrar com um diagrama UML em algum momento?

Refactoring – Os livros do Martin Fowler em geral são todos muito bons. Mas deles, o que eu mais gosto é este. Leitura obrigatória!

The Pragmatic Programmer – É um livro que eu gostei muito, sobre programação baseada em realidade, utilizando conceitos ágeis.

Domain-Driven Design – O assunto está na moda, mas independente de modismos, os conceitos colocados neste livro são muito bons. Não é um livro fácil de ler, mas definitivamente vale a pena.

Domain-Specific Development – Apesar de já desatualizado no que diz respeito ao Visual Studio, a parte inicial deste livro, que descreve o que é e qual o propósito de uma DSL, é muito boa. Não sei como este assunto vai evoluir no futuro, mas este livro influnciou bastante a nossa linha de construção de frameworks. E muitos plugins do Visual Studio 2010 e geradores de código .tt são derivações das idéias criadas por este time.

É isto pessoal. Até a próxima!

, , , , ,

1 comentário

Como eu Desenvolvo Software – Parte 5

Olá pessoal, um ótimo 2011 para todos! Apesar de muita gente estar de férias, o nosso ano começa a todo vapor, com um projeto grande em andamento e toda a manutenção usual. Continuando a série sobre desenvolvimento, fechei o último post com telas de CRUD já disponíveis no sistema.

Apesar de já termos o sistema com muitas telas operacionais até aqui, para mim isto é de pouco valor já que, para o cliente, o produto ainda não traz nenhum benefício de negócio. Ter um sistema que permita cadastrar dados de apoio não traz nenhum valor agregado e, portanto, possui um ROI nulo. Claro que é uma etapa necessária (e nem sempre muito rápida, dependendo do número de dados de apoio), mas é importante que o cliente e a equipe estejam sempre atualizados nesta visão.

Com os CRUDs prontos, podemos voltar às tarefas que geram valor. As telas construídas nestas tarefas normalmente são bem mais complexas do que um CRUD e são suportadas por regras de negócio que também podem ser complexas. É nestas telas e respectivos serviços (que implementam as regras de negócio) que iremos gastar a maior parte do esforço de construção (e posteriormente, de manutenção). A minha abordagem para estas telas é algo bastante top-down. Eu inicio pela definição do layout da tela. Normalmente uso algum produto para desenhar um protótipo e uso-o para conversar com o cliente e tentar identificar se aquilo atende o que ele espera da funcionalidade. As necessidades de negócio são identificadas e classificadas em métodos de classes da camada de serviços. Por exemplo, suponhamos que ao cadastrar uma despesa de aluguel de veículo, haja uma necessidade de se gerar cálculos consolidados por operadora de aluguel. Neste caso poderíamos criar uma área de serviço responsável por estas atividades (uma classe chamada “GerenteAluguelVeiculos”, por exemplo) e um método para aquele serviço (por exemplo, “RegistrarAluguel”). A partir daí isto é incorporado à linguagem e até o cliente vai saber que existe uma serviço de registro de aluguel no sistema que é responsável por gerar os dados consolidados. Durante a análise, estas necessidades são registradas em novas tarefas, que são cadastradas para priorização e implementação.

Esta separação em classes especializadas para serviços é algo que é bastante polemizado em listas de discussão, já que isto gera um modelo bem anêmico. Em uma abordagem DDD clássica, estes métodos de serviço ficariam nas próprias entidades (veículo?, aluguel?). Porém, na minha experiência e com a tecnologia atual, não há ganhos de produtividade em se fazer isto, muito ao contrário, aliás. Além da complicação gerada pela parte subjetiva, que é de cada desenvolvedor, de identificar qual a classe é responsável pelo quê, o posicionamento de regras de negócio em métodos específicos das entidades de domínio tornaria muito mais complicada a gestão dos aspectos não-funcionais das regras, tais como a necessidade ou não de log, aspectos de segurança, aspectos transacionais etc. Isto é uma particularidade do nosso framework, mas com as regras de negócio somente em classes de serviço, esta gestão fica muito mais simples e com isto temos uma maior produtividade escrevendo estas regras – mais sobre isto no posto sobre camada de serviços da série de produtividade.

Continuando em uma abordagem top-down, eu passo à criação da tela em si. Nesta parte temos um grande apoio do framework, mas ainda assim temos que criar artefatos como views e rotinas para preenchimento de dados. As etapas de criação dos componentes das telas, preenchimento dos dados da mesma e execução de regras de negócio ficam bem separadas, graças ao framework – ver mais no post sobre camada de interface. As ações da tela que irão acionar serviços são criadas, mas, inicialmente, sem executar nada (stubs). Eu tento fazer o máximo de comportamento da tela possível antes de iniciar a construção dos serviços. A idéia é tentar ter um protótipo com pelo menos a parte de navegação funcionando e mostrar isto para o cliente antes de continuar. Isto porque, na minha experiência, quando o cliente vê funcionando, pode acontecer dele se lembrar de algo que não foi incluído na análise inicial e pedir modificações. E também porque o esforço gasto nestas telas é tão grande (de longe, a maior parte do sistema) que é importante ter as definições o mais corretas possível antes de fechar para evitar retrabalho. Isto pode fazer até com que às vezes eu utilize mocks para gerar os dados que seriam retornados por serviços, para que a tela possa funcionar antes deles estarem prontos. Quando o serviço é muito simples, pode ser que eles sejam de fato implementados neste ponto. Algo que é muito comum ser feito nesta hora são consultas específicas de repositório. Coisas como “ObterFuncionariosEmViagem” podem ser criadas como métodos do repositório e já implementados. O framework fornece muitos mecanismos para facilitar a geração deste tipo de consulta, desde expressões lambda até uma fluent language para gerar consultas NHibernate e para manipulação de dados. Para isto eu também utilizo muito raramente testes, pois a linguagem de expressão de consultas é muito simples e raramente gera erros ou dúvidas. E por ser compilada, qualquer problema com alterações de entidades de domínio é rapidamente detectada e corrigida. Os casos que exigem testes são consultas complexas que são expressadas em HQL; como estas são baseadas em strings, podem quebrar muito facilmente com alterações em entidades, além de serem difíceis de escrever.

Quando a tela está praticamente pronta, aí sim começo a escrever a parte de serviços. Muita gente acha que a maior parte do esforço está aqui. Mas em geral, a maior parte do esforço está na tela. Escrever serviços, tirando algumas poucas exceções, é algo bastante simples, especialmente com todo o apoio fornecido pelo framework. Claro que existem regras de negócios complexas em muitos sistemas, mas ainda assim, o esforço gasto nelas não se compara com o de construir todas as telas que não são CRUD. O framework tira do desenvolvedor toda a parte de infra-estrutura, então ele não se preocupa com coisas como transações, gerenciamento de exceções, uso ou não de log, identificação do usuário corrente etc…. Ele praticamente escreve só a regra de negócio mesmo. A consolidação de regras de negócio em classes de serviço separadas por “áreas” facilita o reuso e a localização rápida de regras similares. E o uso dos repositórios para consultas mais complexas permite que o serviço seja basicamente direcionado para as alterações da transação.

Para as regras de negócio o ideal é ter pelo menos um teste unitário com o caminho mais usado da mesma. Não uso TDD por achar que, para o meu caso e para os desenvolvedores com quem trabalho, ele diminui produtividade – conheço pessoas que dizem ser o contrário, mas cada caso é um caso. Na criação de regras de negócio, o importante é ser eficaz e eficiente, ou seja, produzir regras que façam o previsto, no menor prazo possível, e que sejam fáceis de entender e manter. Acho que cada um tem um estilo pra atingir este objetivo, o meu definitivamente não é com TDD. Nunca trabalhei com alguém que usasse TDD de maneira consistente e produtiva em todos os casos, mas se alguém funcionar bem assim, nada contra. Um teste ao menos é importante porque regras de negócio são muito sensíveis a mudanças de negócio. Confesso que nem sempre eu mesmo sigo esta regra; muitas vezes a regra é tão simples (menos de 5 linhas é o típico para regras simples) que o esforço para produzir teste fica questionável. Mas acho que no geral faz sentido ter ao menos um teste por regra.

Finalmente, as regras são integradas à interface para que tudo funcione. Eu faço isto de maneira incremental, ou seja, conforme as regras vão ficando prontas eu já vou incorporando-as à tela. Eu gosto mais dos testes finais (integração), ou seja, de ver a tela funcionando e fazendo que ela foi projetada que fazer. No meu caso, é onde eu pego a maior parte dos erros de construção. E, quando acabo este processo, a tela está quase totalmente pronta.

Embora utilizemos uma fase de homologação pelo cliente, eu uso isto mais como um processo de validação/aceitação do que de testes. Acredito que um bom desenvolvedor deva entregar seu software pronto, e pronto significa sem erros. Assim, não acho que o usuário final ou cliente deva participar do processo de depuração. É claro que algumas vezes eles vão encontrar erros ou bugs; mas isto tem que ser exceção, não regra.

Durante todo este processo, o framework é também evoluído. Se for detectado algo que está dando algum excesso de trabalho braçal, ou alguma coisa que poderia ser feita pra aumentar produtividade, isto já é feito durante o desenvolvimento (desde, é claro, que não seja algo muito grande). É claro que isto não pode ser feito por desenvolvedores inexperientes, daí a necessidade que temos de termos somente equipes formadas predominantemente por seniores. Com isto, o objetivo é que nossa produtividade não só seja alta agora, mas que ela continue a aumentar com o tempo.

Em linhas gerais, é este o processo que eu sigo. No próximo e último post eu vou falar um pouco sobre casos de exceção e pontos como construção de serviços e exportação/importação de dados, muito comuns a todos os sistemas. Até breve.

, , , , ,

1 comentário