Por meio deste exemplo, conheceremos uma solução em Java útil para a criação e atribuição de objetos que tenham interdependência entre si. Essas dependências representam o relacionamento dos objetos em um sistema, relacionamento este que pode se tornar bastante complexo para ser realizado manualmente pelo programador.
Nestes casos, a injeção de dependências com CDI pode ser empregada para automatizar esta tarefa.
A injeção de dependências é um padrão de desenvolvimento idealizado para facilitar a implementação de sistemas orientados a objetos. Assim como outros padrões de projeto, surgiu de um problema recorrente no desenvolvimento de software, mas antes de entrarmos na explicação acerca deste padrão, é importante relembrar rapidamente alguns conceitos relacionados à própria Orientação a Objetos e a alguns padrões de projeto que também são pré-requisitos para o entendimento da solução apresentada neste artigo, um projeto de um Jogo de Batalha Naval.
A orientação a objetos permite que um sistema seja modelado de acordo com entidades do mundo real por meio de classes, objetos, atributos e métodos.
Neste contexto, um objeto é composto basicamente por métodos, que representam comportamentos, e atributos, que representam propriedades.
Vale lembrar também que objetos são instâncias materializadas a partir de uma classe, as quais podem ser vistas como “modelos”. Essa característica permite modelar um sistema o mais próximo possível da realidade negocial.
Por exemplo, é possível abstrair uma classe para representar um carro. Assim, os modelos de diversas marcas podem ser vistos como instâncias de um carro, onde cada uma tem suas características como a potência do motor, quantidade de portas, bem como são capazes de desenvolver comportamentos específicos, como acelerar, ligar a ignição, parar, etc.
Os comportamentos de um objeto devem refletir funções que existem no mundo real. Seguindo esta abordagem, cada objeto deve contemplar funções relacionadas a tarefas que possam ser descritas com apenas um verbo (andar, verificar, checar e etc.) para facilitar o entendimento do código, bem como a manutenção e/ou evolução do mesmo.
Aplicando esta metodologia durante o desenvolvimento, uma classe pode ser quebrada em outras, reduzindo e dividindo a solução em pedaços mais compreensíveis. Por exemplo, suponha que o carro do exemplo anterior seja um objeto que possua o método acelerar e ligar o farol.
Neste caso, é claro que ambas as funções estão disponíveis em um carro. No entanto, estes comportamentos tornam-se mais intuitivos se for possível distinguir que acelerar é uma função relacionada com o motor do carro e ligar o farol é uma função do sistema elétrico do veículo. Esta constatação deve conduzir o implementador a refatorar a classe inicial, resultando em mais dois objetos que compõem o carro: O Sistema Elétrico e o Motor, os quais devem receber os comportamentos citados.
A reorganização do código é uma técnica chamada de refatoração (refactoring), empregada para melhorar a organização das classes de um sistema orientado a objetos. Classes, métodos e atributos tendem a se modificar constantemente, gerando novos objetos e reduzindo outros, à medida que o problema é melhor compreendido.
Esta dinâmica é importante para um projeto OO, pois ajuda a manter o relacionamento do sistema com a realidade. A modelagem deve ser atualizada constantemente mesmo após a construção do software, para que continue sempre refletindo nomes atuais, ou seja, se o negócio muda a ponto de mudar o nome de uma entidade previamente modelada e tratada pelo sistema, essa entidade deve ser ajustada e renomeada para contemplar a atualização do negócio, ainda que seja uma mera mudança de nomenclatura.
Esta recomendação é necessária para que implementadores e usuários conversem sempre utilizando o mesmo vocabulário, facilitando o entendimento de ambas as partes. Em contrapartida, a refatoração pode causar instabilidade no sistema e a melhor forma de rastrear os erros decorrentes é pela implementação de testes automatizados.
De maneira sucinta, a modelagem de classes orientada a objetos é uma tarefa realizada após a identificação dos requisitos e consiste na criação de um modelo de domínio, inclusão dos comportamentos necessários e definição das mensagens a serem trocadas entre os objetos envolvidos.
Não obstante, durante a fase de projeto e implementação existem outros desafios a serem enfrentados pelo desenvolvedor, como a decisão de como e onde colocar determinado método, ou qual objeto será o proprietário de certa informação. Estes questionamentos muitas vezes não são triviais, contudo, os padrões GRASP auxiliam o projetista de software ou o programador a atribuir corretamente as competências de cada objeto.
A aderência ao princípio da alta coesão requer que cada classe, atributo ou método contenha exclusivamente responsabilidades relacionadas, ou seja, cada elemento deve executar o mínimo de funções possíveis, evitando a criação de objetos ou métodos multifacetados (que desempenham múltiplas funções).
O excesso de responsabilidades em um método ou classe dificulta a compreensão, prejudica o reuso, além de desencorajar qualquer tipo de mudança no código.
O padrão Especialista na Informação (Information Expert) é outro princípio GRASP e possui complementariedade com o padrão de Alta Coesão, pois mesmo quando as responsabilidades são corretamente divididas em pequenos blocos, haverá também a necessidade de decidir qual é o melhor local para colocá-los.
Esse padrão orienta que um novo elemento (método ou atributo) a ser incluído deve ser colocado em um local onde seja possível preencher a maioria das informações necessárias a ele. Como exemplo, considere o diagrama de classes da Figura 1.
Figura 1. Relacionamento muitos-para-muitos entre as classes Pedido e Produto
Neste cenário é preciso implementar a contagem do total de unidades vendidas de um certo produto em cada pedido realizado.
Tendo somente a descrição apresentada é possível inferir que o novo método pode ser implementado em qualquer uma das classes citadas, contudo, ao observar as indicações de navegabilidade na Figura 1, nota-se que a classe Produto não tem acesso à classe Pedido (fato constatado pelo “X” na seta), ao passo que a classe Pedido possui navegabilidade para a classe Produto.
Desta forma, o novo método não pode ser colocado na classe Produto porque ela não é capaz de preencher todas as informações que o método necessita para fazer o cálculo do total.
Nesse exemplo, uma das classes não foi capaz de suprir todas as informações necessárias, contudo, haverá situações mais complexas em que mais classes serão capazes de preencher todos os requisitos de um método. Nestes casos, deverá se optar pelo local em que as informações possam ser obtid ...