Independentemente de qual tecnologia estamos utilizando, uma das grandes perguntas que nos fazemos ao criar serviços, é o que realmente devemos disponibilizar em cada um deles, que ao meu ver, vai muito além os parâmetros e do resultado que cada operação recebe e/ou retorna. Existe uma série de aspectos que devemos nos atentar ao projetar ou construir um conjunto de serviços, que muitas vezes atenderão as aplicações que rodam dentro dessa mesma companhia.
O primeiro aspecto que temos que verificar é a questão da granularidade dos serviços, que é utilizada para mensurar a profundidade de abstração que foi aplicado. A granularidade pode ser dividida em duas partes, sendo: granularidade fina (fine-grained) e granularidade grossa (coarse-grained), onde granularidade fina determina que precisamos de muitos “grãos”, enquanto na granularidade grossa, teremos poucos “grãos”, bem maiores.
O que eu quero mostrar com o parágrafo acima, é que com a granularidade fina, teremos serviços com poucas operações, mas dividiremos essas operações por vários serviços. Já com a granularidade grossa, isso se inverte, ou seja, teremos poucos serviços, mas cada um deles conterá uma porção bem maior de operações. Cada uma das técnicas tem suas vantagens e desvantagens, e ao meu ver, quando temos uma granularidade fina, temos pequenos “blocos” de funcionalidades bem específicas e muitas vezes independentes, e que ficam bem mais fáceis de serem atualizadas, distribuídas e gerenciadas, mas isso pode se tornar complexo demais para aqueles que consomem os serviços, já que terão que compor e sincronizar suas operações, para que atinja um determinado objetivo. Por outro lado, a granularidade grossa pode tornar os serviços mais auto-suficientes, mas o problema disso é que cada serviço poderá, acidentalmente, fazer muito mais trabalho do que ele realmente deveria, e que muitas vezes precisará recorrer à outros serviços, para que, também, atinjam o seu objetivo, aumentando assim o acoplamento.
Acoplamento é um outro problema que pode ocorrer, que nada mais é do que a – forte – dependência que um serviço tem de outro. Isso infringe um dos princípios do SOA, que diz que os serviços precisam ser autônomos, ou seja, não depender de outros serviços. Mas em algumas situações isso pode ser benéfico, principalmente em um ambiente de composição. Imagine que você queira criar um serviço para resolver um problema maior, com uma complexidade muito grande. Ao invés de todos os consumidores acessarem esses serviços e ficar sob responsabilidade de cada um organizar isso, você poderá criar um serviço para compor essas tarefas que, como dissemos acima, precisarão recorrer à outros serviços.
Esses conceitos que vimos acima não são novidades. Eles já são (ou deveriam ser) aplicados na programação orientada à objetos, exatamente para termos os mesmos benefícios. Essas características não são exclusividades da computação, pois podemos adotar esses mesmos princípios no nosso dia-à-dia.
Outro grande ponto a ser considerado na construção de serviços, é a criação de serviços com interfaces CRUD (Create, Read, Update e Delete). A proposta destes tipos de serviços é permitir, na maioria das vezes, a manipulação de registros dentro de uma determinada base de dados. Nestes casos, o serviço será apenas uma espécie de wrapper para os dados, não fazendo nada além do que as operações básicas que todo banco de dados possui (INSERT, SELECT, UPDATE e DELETE).
Quando você trabalha com uma aplicação data-centric, onde a toda a regra se concentra em manipular a base de dados, talvez esses tipos de serviços sejam úteis. Considere aqui o uso do ADO.NET Data Services, que evitará conhecer e criar toda a estrutura necessária para expor via WCF.
Mas o ideal é não ter isso em mente ao construir serviços. Interfaces (contratos) CRUD induzem à granularidade fina, onde cada serviço será responsável por manipular uma determinada entidade/tabela. Como vimos acima, a granularidade fina em si não é o problema. A questão aqui é que muitas vezes, um cadastro de um cliente não consiste apenas em um INSERT na base de dados, ao contrário, vai muito além disso.
Imagine um novo cliente que deseja abrir uma conta bancária em um determinado banco, com um cartão de crédito vinculado. O processo de cadastro do cliente consistirá em:
-
Validar os dados, como idade, renda, endereço, etc.;
-
Consultar outras instituições financeiras para se certificar de que ele é um bom pagador;
-
Definir o limite que ele terá no cheque especial;
-
Inserir o cliente na base de dados;
-
Criar a conta corrente para este cliente;
-
Efetuar o lançamento da taxa de cadastro/abertura na conta corrente recém criada;
-
Comunicar com o serviço de cartões de crédito, para que ele gere um novo cartão para este cliente;
-
Notificar outros departamentos do banco de que uma nova conta foi aberta, para oferecimento de novos produtos.
Como podemos perceber, o cadastro de um novo cliente não consiste apenas em adicioná-lo na base de dados. Há muito mais do que isso. Se modelarmos nossos serviços orientado à dados, eu teria um serviço que manipula os clientes, outro serviço que manipula a conta corrente, outro de notificação e por aí vai. Quanto mais entidades/tabelas você tiver envolvidas em uma mesma tarefa, mais complicado ficará para gerenciar tudo isso, principalmente do ponto de vista daquele que consumirá esses serviços.
O consumo de serviço é um processo caro para se fazer à todo momento, e neste cenário, para efetuar o cadastro de um cliente, eu precisarei chamar, no mínimo, quatro serviços e tudo o que eu precisarei passar para eles, é exatamente os dados do cliente que eu desejo avaliar/cadastrar. Outro ponto importante é com relação ao fluxo de informações. Neste caso, fica sempre sob responsabilidade do cliente que consome esses serviços, configurar a ordem de chamadas, e em um ambiente onde múltiplas aplicações podem incluir clientes, eventualmente uma delas poderá alterar essa ordem, fazendo com que o processo fique em um estado inválido, comprometendo assim a veracidade e consistência das informações. Nada impedirá que uma pessoa maliciosa invoque apenas o serviço de cadastro de cliente diretamente, sem passar pelas políticas de validação necessárias.
Quando temos várias “sub-tarefas” que se juntam para algo maior, em muitos casos queremos garantir a atomicidade, que garantirá que todos os passos sejam efetuados com sucesso, ou tudo falhará. O que garante a atomicidade são as transações, e transações distribuídas são caras, e todas as chamadas para esses serviços devem estar envolvidas dentro dessa transação, que será, também, coordenada pelo cliente, que será o responsável por avaliar se tudo deu certo. Se sim, ele efetivará (Commit), do contrário, irá desfazer (Rollback).
Um segundo cenário que também ilustra isso: você possui clientes e cada um deles possui um flag que determina a situação dele dentro da sua empresa: Ativo, Bloqueado, EmProcessoJuridico, etc. Da mesma forma que vimos antes, alterar situação dele vai muito além de um simples comando de UPDATE na base de dados. Quando eu mover um determinado cliente para a situação de EmProcessoJuridico, eu terei que inserir um item no histórico deste cliente, desativar algumas opções que o mesmo tem site, e alterar a sua situação na tabela do banco de dados. Se movê-lo para Bloqueado, terei que inserir um item no seu histórico, efetuar um lockdown em todas as contas de acesso desse cliente no site, notificar o gerente responsável e, finalmente, efetuar o UPDATE da coluna onde armazeno a situação atual na tabela de clientes.
Como podemos perceber, interfaces CRUD definem os serviços como sendo data-centric, que modelando dessa forma, nós perderemos o contexto de negócio que será executado pelo cliente, tornando bem mais complicado de se entender o processo como um todo. Ao modelar os serviços, o ideal seria pensar em task-centric, ou seja, o serviço fornecerá operações que englobam grande parte do processo, não sendo o consumidor o responsável por isso. Nos dois cenários que vimos acima, teríamos um serviço de cliente, e que me forneceria uma operação para criar um novo cliente dentro do banco (IncluirNovoCliente), outra operação para bloquear o cliente (Bloquear), outro para criar um processo contra este cliente (AbrirProcessoJuridico), que o moverá para a situação EmProcessoJuridico, e assim por diante.
Note que as operações que serão expostas pelo serviço expressarão claramente o negócio, fazendo internamente tudo o que for necessário para atingir o respectivo objetivo. O interessante é que neste caso, não temos o overhead de chamar N serviços, problemas de fluxo e, principalmente, evitando transações distrubuídas.
Claro que poderá haver situações em que, mesmo que você utilize o modelo task-centric, os teus serviços estarão, coincidentemente, alinhados à interfaces CRUD, mas o importante é que isso apenas seja uma coincidência e não uma regra. Isso muitas vezes acontece quando as regras não estão tão aparentes. Por exemplo, se você mantém um cadastro de cidades e permite a alteração delas, provavelmente você poderá ter: 1 – Valinhos e 2 – Campinas. Os clientes cadastrados na sua base de dados e que são de Valinhos guardam o número 1, e os de Campinas, o número 2. Se agora, você diz que Valinhos será o 2 e Campinas o 1, você corromperá todos os clientes que fazem uso dessas cidades. Poderia haver aqui uma regra que, ao efetuar a alteração da cidade (swap), você atualizasse todos os clientes relacionados.
É importante dizer que serviços task-centric, em algum momento, precisarão efetuar operações de CRUD dentro da base de dados. Com isso, podemos criar duas categorias de serviços: Task Service e Entity Service. Task Services são os tipos de serviços que vimos acima, que serão responsáveis por orquestrar toda a regra de validação, fluxo, manipulação e persistência das informações. É neste ponto que entra em cena os Entity Services, que são responsáveis por gerenciar a vida/estado de uma entidade específica, incluindo seus respectivos relacionamentos, tendo esses serviços, uma interface semelhante à interface CRUD.
Os Entity Services levam o nome de uma entidade, como por exemplo: Cliente, ContaCorrente, PoliticasDeValidacao, etc., não fazendo nada além do que o nome diz, ou seja, disponibilizará apenas as informações referentes à respectiva entidade, não conhecendo nada sobre negócios. Já os nomes dos Task Services são voltados para o negócio em si: AdministracaoDeClientes, GestorDeCredito, CobrancaDeTitulos, etc. E ainda, os Task Services poderão utilizar um ou vários Entity Services para executar uma determinada tarefa, atentanto-se sempre aos conceitos que vimos acima, como é o caso do baixo/alto acoplamento e a granularidade.
Tudo o que foi falado aqui poderá, em alguns cenários, não ser a melhor opção, mas utilizar esse tipo de visão para a construção de serviços, ajudará a ter e criar uma representação muito mais consolidada de sua estrutura, e que será relativamente fácil de gerenciar, mas que refletirá, virtualmente, o seu negócio.
Estou lendo o livro Principles of Service Design do Thomas Erl que ilustra bem o que você falou, incluindo comparações onde serviços fine-grained podem ser mais eficientes de um ponto de vista, pois podem pedir e retornar dados mais específicos ao caso, porém exigir mais chamadas e aumentar a latência. Já serviços coarse-grained são mais genéricos, auxiliando o reuso, porém podem retornar informações em excesso por não serem especializados. Tudo depende do caso!
Parabéns pelo artigo!
Boas Danilo,
Vi mesmo em alguns lugares, pessoas que mencionavam este autor. Vou procurar por este livro. Obrigado!
Post muito bom… inclusive, na hora certa pra mim, que estou começando a elaborar melhor a estrutura de um sistema que está em andamento.
Agora vou levar em conta manter uma baixa granulidade e criar mais algumas interfaces para desacoplar um pouco as coisas.
Israel, fugindo um pouco do assunto, mas nem tanto, você poderia me falar como seria a melhor maneira de tratar a seguinte situação?
Seguinte, estou trabalhando com repositório genérico Repositorio<TEntity> e hoje, quando estava migrando minhas classes([i]o acesso a dados e a verificação do plano de senhas era feita em parciais da entidade, por exemplo, ‘Cliente : Entidade’, tinha o método Salvar, que era disponibilizado de Entidade… esse método salvar verificava as permissões de acesso e fazia o que tinha que fazer para salvar. Nas parciais eu adicionava também, métodos como ‘BloquearCliente’, que realizava as operações necessárias para tal[/i]) me deparei com a questão: "Trabalhando com repositorio, onde devo verificar se o usuário tem permissão para fazer tal coisa?". Pensei talvez em criar uma Interface "IVerificadorAcesso" que descreveria um [b]serviço[/b] que trabalha com o que for relacionado a acesso de usuário e tudo mais. E no repositório eu chamaria "IVerificadorAcesso.VerificarPermissao(parametros bla bla bla)", mas não sei se é o mais aconselhavel…
Enfim, a minha dúvida maior, tanto na parte de arquitetura quanto na parte de código, é como tudo trabalharia junto… o acesso a dados, os serviços e as entidades. Meus serviços devem ter uma instância dos repositórios que é(são) usado(s) por ele? Posso fazer algumas queryes que cruzam muitas tabelas nos serviços?
Tenho que manter granulidade baixa em algumas partes, já que o futuro sistema de indústria deverá aproveitar grande parte do que esse sistema atual de gestão comercial tem.
obrigado pelas dicas.. abraço
Boas Davi,
Você se refere as regras de validação de um usuário, para saber se ele pode ou não invocar um método?
Quase isso… essa validação não é separada por método mas quase que por entidade… por exemplo, verifica se o usuário pode criar, alterar, excluir ou consultar uma venda.
Todos os cadastros precisam deste controle, então eu teria que criar um método de serviço que chamaria o acesso a dados e adicionasse apenas uma linha para chamar o metodo VerificarPermissao do meu provedor de acesso. Com certeza alguns desses métodos realmente precisariam de algo na camada de serviços, mas há alguns que definitivamente não precisam, como ListarGruposCliente(), do repositório. Este método tem acesso restrito mas não precisa de nenhum processamento adicional.
Boas Davi,
Na maioria das vezes, a autenticação e autorização são parte da infraestrutura, ou seja, você pode resolver isso de outros jeitos, como a configuração declarativa (via atributos), para conceder ou negar acesso a um método específico, e deixar o .NET Framework se encarregar disso.
Nada impede também de você criar uma layer de segurança dentro do seu domínio, que permite você refinar como deseja efetuar essa validação.