O GRASP é um ==conjunto de princípios de design orientado a objetos que sugerem como atribuir responsabilidades a classes==.
Consistem em nove princípios que ajudam a desenhar o modelo estático e dinâmico do sistema de forma eficaz.
Aplicação
O GRASP se aplica a objetos de software, ajudando a identificar classes adequadas para responsabilidades específicas.
Quando nenhuma classe existente se encaixa, busca-se inspiração no Modelo do Domínio para criar novas classes.
-
Contexto GRASP: No contexto dos princípios GRASP, ao designar responsabilidades a classes, às vezes você se depara com uma situação onde as classes existentes no seu design não são adequadas ou não existem classes que naturalmente se encaixem para a responsabilidade em questão.
-
Modelo de Domínio: Nesse ponto, você se volta para o “Modelo do Domínio”. O Modelo do Domínio é uma representação do mundo real relacionado ao seu sistema. Ele contém os conceitos, termos e relacionamentos que são relevantes para o seu domínio de problema. Basicamente, é a sua compreensão abstrata do problema que você está tentando resolver.
-
Inspiração para Novas Classes: Se você precisa de uma nova classe para assumir uma responsabilidade, mas não tem uma classe existente que se encaixe bem, o Modelo do Domínio se torna uma fonte de inspiração. Você revisita o modelo para identificar conceitos ou entidades que ainda não foram representados no seu design de software. A partir disessa análise, você pode criar novas classes que refletem esses conceitos ou entidades, garantindo que as responsabilidades sejam atribuídas de maneira lógica e coerente com o domínio do problema.
-
Exemplo Prático: Imagine que você está desenvolvendo um software para um hotel e até agora tem classes como
Quarto,ClienteeReserva. Ao implementar a funcionalidade de serviços de quarto, você percebe que não tem uma classe existente que se encaixe bem para gerenciar esses serviços. Então, você volta ao seu Modelo do Domínio e percebe que “Serviço de Quarto” é um conceito relevante que ainda não foi representado em seu design. Assim, você cria uma nova classeServicoDeQuartopara lidar com essas responsabilidades específicas.
Essa abordagem assegura que as classes no seu design de software estejam alinhadas com os conceitos reais do domínio do problema, facilitando um design mais intuitivo e fácil de manter.
Princípios
Claro, vamos complementar a explicação do princípio Information Expert (Perito) com um exemplo prático:
1. Information Expert (Perito)
Conceito: Este princípio sugere que responsabilidades devem ser atribuídas à classe que possui as informações necessárias para cumpri-las. Em outras palavras, a classe que tem o conhecimento necessário para realizar uma ação deve ser responsável por essa ação.
Exemplo Prático: Imagine que estamos desenvolvendo um sistema para uma livraria. Temos várias classes, como Livro, Pedido, e Cliente. Suponha que precisamos de uma funcionalidade para calcular o preço total de um pedido, considerando descontos e impostos.
De acordo com o princípio do Perito, a classe Pedido seria a melhor candidata para assumir essa responsabilidade. Isso porque ela tem acesso direto a todos os itens do pedido (ou seja, os livros comprados) e, potencialmente, as informações sobre descontos e impostos aplicáveis. Portanto, seria natural e eficiente que a Pedido tivesse um método como calcularPrecoTotal().
Contraindicações
Cuidado com a Coesão e o Acoplamento: Embora o princípio do Perito seja útil para atribuir responsabilidades com base no conhecimento, é importante estar atento para não criar classes que façam "demais" e se tornem pouco coesas ou excessivamente acopladas.
Por exemplo, se estendêssemos a responsabilidade da classe Pedido para também gerenciar pagamentos (processar pagamentos, validar cartões de crédito, etc.), isso poderia levar a uma classe com responsabilidades demais e potencialmente um alto grau de acoplamento com sistemas externos de pagamento. Neste caso, seria mais apropriado aplicar outros padrões GRASP, como Low Coupling e High Cohesion, e potencialmente criar uma classe separada, como GerenciadorDePagamentos, para lidar com a lógica de pagamento.
Portanto, ao aplicar o princípio do Perito, deve-se sempre considerar o equilíbrio entre o conhecimento necessário para uma tarefa e a manutenção de classes coesas e desacopladas.
Este exemplo ilustra como o princípio do Perito pode ser aplicado para atribuir responsabilidades de forma lógica, ao mesmo tempo em que destaca a importância de evitar a sobrecarga de uma única classe com múltiplas responsabilidades, o que poderia comprometer a coesão e o acoplamento do design.
2. Creator
Conceito: Este princípio sugere que a responsabilidade de criar instâncias de uma classe deve ser dada à classe que contém, agrega ou tem os dados necessários para inicializar a nova instância.
Exemplo Prático: Considere um sistema de gerenciamento de cursos em uma escola. Temos classes como Curso, Estudante e Matricula. Suponhamos que precisamos criar instâncias de Matricula que associe um Estudante a um Curso.
De acordo com o princípio do Creator, uma boa candidata para assumir a responsabilidade de criar instâncias de Matricula seria a classe Curso. Isso se deve ao fato de que cada curso terá um conjunto de matrículas associadas a ele. Além disso, o Curso provavelmente terá as informações necessárias para inicializar uma Matricula (por exemplo, informações sobre o curso e o estudante). Portanto, seria natural e lógico que Curso tivesse um método como adicionarEstudante(Estudante estudante) que cria e gerencia uma nova Matricula.
Contraindicações
Complexidade na Criação: Embora o princípio do Creator seja eficaz para atribuir responsabilidades de criação, deve-se considerar a complexidade envolvida na criação de objetos. Se a criação do objeto for especialmente complexa, envolvendo várias etapas de configuração ou dependências externas, pode ser mais apropriado delegar essa responsabilidade a uma classe separada especializada na criação de objetos, como uma Fábrica de Objetos ou Fábrica Abstrata.
Por exemplo, se a criação de uma Matricula envolvesse processos complexos, como validações extensas, configurações específicas ou interações com outros sistemas (como um sistema de cobrança), seria mais sensato ter uma fábrica dedicada (MatriculaFactory) para encapsular toda essa complexidade. Isso manteria a classe Curso mais simples e focada em suas responsabilidades principais.
Este exemplo adicional mostra como o princípio do Creator pode ser aplicado em um contexto real, destacando a importância de considerar a complexidade envolvida na criação de objetos e quando é apropriado delegar essa responsabilidade a uma classe ou padrão de design especializado.
3. Controller
Conceito: O princípio do Controller sugere que deve haver um intermediário entre a interface do usuário (IU) e a lógica de negócio do sistema, controlando as operações do sistema. Esse intermediário, o “controlador”, pode representar o sistema como um todo, um dispositivo específico ou um sub-sistema importante.
Exemplo Prático: Imagine um sistema de comércio eletrônico com várias funcionalidades, como gerenciamento de produtos, processamento de pedidos e atendimento ao cliente. Uma abordagem para implementar o padrão Controller seria criar um controlador para cada um desses aspectos significativos do sistema.
Por exemplo, poderíamos ter um PedidoController para gerenciar as operações relacionadas a pedidos. Este controlador teria métodos para adicionar itens ao carrinho, processar pagamentos e rastrear o status do pedido. Ao separar essas responsabilidades em um controlador específico, garantimos que a interface do usuário se comunique com a lógica de negócios de maneira organizada e controlada.
Contraindicações
Excesso de Responsabilidades: Um problema comum com controladores é quando eles assumem muitas responsabilidades, o que pode levar a uma fraca coesão e alto acoplamento. Isso acontece quando um controlador começa a lidar com uma ampla gama de operações do sistema que talvez não estejam relacionadas entre si.
Para evitar isso, é recomendável usar múltiplos controladores, cada um focado em uma área específica do sistema. Por exemplo, além do PedidoController, poderíamos ter um ProdutoController para gerenciamento de produtos e um AtendimentoClienteController para lidar com questões de suporte e atendimento ao cliente. Essa separação ajuda a manter uma alta coesão e baixo acoplamento, facilitando a manutenção e expansão do sistema.
Outra abordagem é delegar responsabilidades específicas para classes ou componentes auxiliares, mantendo o controlador focado em coordenar as operações e fluxos de trabalho, em vez de implementar detalhes específicos da lógica de negócios.
Este exemplo ilustra como aplicar o princípio do Controller de forma eficaz, garantindo que cada controlador mantenha um foco claro e evitando sobrecarregá-los com responsabilidades excessivas. Ao seguir essa abordagem, o design do software torna-se mais modular, flexível e fácil de gerenciar.
4. Low Coupling (Baixo Acoplamento)
Conceito: O princípio de Baixo Acoplamento visa minimizar a dependência direta entre diferentes classes, promovendo a modularidade e facilitando a manutenção e a escalabilidade do sistema.
Exemplo Prático: Considere um sistema de gerenciamento de biblioteca. Neste sistema, temos várias classes como Livro, Usuario, e Emprestimo. Uma abordagem com alto acoplamento seria fazer com que a classe Livro conheça e interaja diretamente com Usuario e Emprestimo para gerenciar o processo de empréstimo. Isso criaria uma forte dependência entre essas classes, tornando o sistema rígido e difícil de manter.
Para aplicar o princípio de Baixo Acoplamento, podemos introduzir uma classe intermediária, como GestorDeEmprestimos, que gerencia os empréstimos. A classe Livro não interage mais diretamente com Usuario ou Emprestimo, mas apenas com GestorDeEmprestimos. Isso reduz as dependências diretas entre as classes, facilitando mudanças futuras no sistema, como a implementação de novas regras de empréstimo ou a introdução de novos tipos de usuários.
Contraindicações
Acoplamento a Elementos Estáveis: É importante ressaltar que nem todo acoplamento é prejudicial. O acoplamento a elementos estáveis e bem definidos, como bibliotecas padrão ou componentes do sistema que raramente mudam, é normal e aceitável. O que deve ser evitado é o alto acoplamento a elementos instáveis ou em constante evolução, pois isso pode levar a um sistema frágil, onde mudanças em uma parte podem afetar inesperadamente outras partes.
5. High Cohesion (Alta Coesão)
Conceito: Alta Coesão refere-se à ideia de que uma classe deve ter responsabilidades bem focadas e limitadas, de forma que todas as funcionalidades da classe estejam fortemente relacionadas entre si. Isso torna a classe mais compreensível e facilita a manutenção.
Exemplo Prático: Imagine um sistema de gerenciamento de conteúdo (CMS). Neste sistema, temos várias classes responsáveis por diferentes aspectos, como EditorDeTexto, GerenciadorDeImagens e PublicadorDeConteudo.
Uma classe com baixa coesão seria uma classe GerenciadorDeConteudo que tenta lidar com a edição de texto, gerenciamento de imagens e publicação de conteúdo. Essa classe seria sobrecarregada com responsabilidades diversas e pouco relacionadas, tornando-a complexa e difícil de manter.
Para alcançar alta coesão, podemos dividir essa classe em classes menores e mais focadas: EditorDeTexto lida exclusivamente com a edição de textos, GerenciadorDeImagens é responsável pelo gerenciamento de imagens, e PublicadorDeConteudo cuida apenas da publicação do conteúdo final. Cada uma dessas classes tem uma responsabilidade clara e bem definida, o que aumenta a coesão e facilita a manutenção e a expansão do sistema.
Contraindicações
Baixa Coesão em Casos Específicos: Em certos cenários, pode ser aceitável ter classes com baixa coesão. Por exemplo, em sistemas distribuídos, pode ser preferível ter um objeto de interface com baixa coesão que agrega várias operações para melhorar o desempenho, reduzindo o número de chamadas de rede necessárias. Da mesma forma, em certos casos, pode-se optar por concentrar funcionalidades relacionadas a um aspecto específico (como interações com um banco de dados) em uma única classe para centralizar e simplificar a manutenção, mesmo que isso resulte em menor coesão.
Este exemplo ilustra a importância de manter classes com alta coesão para criar um design de software mais limpo e manejável, ao mesmo tempo em que reconhece que, em situações específicas, pode ser necessário equilibrar coesão com outros fatores como desempenho e praticidade de manutenção.
Vamos aprofundar a compreensão do princípio Polymorphism (Polimorfismo) com um exemplo prático:
6. Polymorphism (Polimorfismo)
Conceito: O princípio do Polimorfismo encoraja a designação de responsabilidades para interfaces ou classes abstratas para lidar com comportamentos que variam conforme o tipo do objeto. Isso permite que diferentes implementações sejam fornecidas por classes concretas que implementam a mesma interface ou estendem uma classe abstrata.
Exemplo Prático: Imagine um sistema de simulação de veículos onde diferentes tipos de veículos têm diferentes modos de movimentação. Temos classes como Carro, Barco e Aviao, cada uma com seu próprio método de movimento.
Sem polimorfismo, teríamos que escrever código condicional para lidar com cada tipo de veículo separadamente. Com o polimorfismo, podemos definir uma interface Veiculo com um método abstrato mover(). As classes Carro, Barco e Aviao implementam esta interface e fornecem suas próprias implementações do método mover.
interface Veiculo {
void mover();
}
class Carro implements Veiculo {
public void mover() {
System.out.println("Carro se movendo na estrada.");
}
}
class Barco implements Veiculo {
public void mover() {
System.out.println("Barco navegando no mar.");
}
}
class Aviao implements Veiculo {
public void mover() {
System.out.println("Avião voando no céu.");
}
}Dessa forma, podemos usar um objeto Veiculo polimorficamente sem conhecer o tipo específico de veículo, e o comportamento correto será executado com base no tipo real do objeto.
Contraindicações
Evite Excesso de Proteção: Embora o polimorfismo seja uma ferramenta poderosa para lidar com variações de comportamento, é importante evitar a proteção contra todas as possíveis variações futuras. Isso pode resultar em um design excessivamente complexo e difícil de entender. Em vez disso, foque em variações previsíveis e relevantes para o domínio do problema. O uso criterioso do polimorfismo resulta em um sistema mais flexível e extensível, sem a complexidade desnecessária.
Este exemplo mostra como o polimorfismo pode ser aplicado para criar um design de software flexível e extensível, destacando a importância de aplicá-lo de forma equilibrada para lidar com variações relevantes e previsíveis.
7. Pure Fabrication (Artifício)
Conceito: O princípio de Artifício (Pure Fabrication) envolve criar classes que não têm uma representação direta no domínio do problema, mas são utilizadas para manter uma alta coesão e baixo acoplamento no design do software. Estas classes são fabricadas exclusivamente para atender a necessidades técnicas ou de design, como a separação de preocupações ou a facilitação da manutenção.
Exemplo Prático: Considere um sistema de e-commerce que requer uma integração complexa com diferentes gateways de pagamento. Em vez de incorporar a lógica de pagamento diretamente em classes de domínio, como Pedido ou Cliente, podemos criar uma classe de Artifício chamada ProcessadorDePagamento. Esta classe é responsável por toda a comunicação e processamento relacionados aos pagamentos, interagindo com diferentes gateways de pagamento e lidando com detalhes técnicos específicos de cada um.
class ProcessadorDePagamento {
public void processarPagamento(Pedido pedido) {
// Implementação da lógica de pagamento
}
}Com essa abordagem, a complexidade do processamento de pagamento é isolada em uma classe dedicada, mantendo as classes de domínio focadas em suas responsabilidades principais e reduzindo o acoplamento entre diferentes partes do sistema.
Contraindicações
Evite Abuso de Artifícios: Embora o uso de Artifício possa ser muito útil para manter um design limpo e modular, é importante não abusar dessa prática. Criar muitas classes artificiais sem justificativa clara pode levar a um sistema com acoplamento excessivo e uma arquitetura que se afasta dos princípios da orientação a objetos. Cada classe de Artifício deve ter um propósito bem definido e ser justificada pela necessidade de manter a coesão e reduzir o acoplamento.
Este exemplo demonstra como o princípio de Pure Fabrication pode ser aplicado para abstrair complexidades técnicas e manter um design de software coeso e desacoplado, enfatizando a importância de usar esse princípio com moderação para evitar a criação de uma arquitetura excessivamente artificial e desconectada do domínio do problema.
8. Indirection
Conceito: O princípio de Indirection foca na redução do acoplamento direto entre componentes ao atribuir a tarefa de mediação a um objeto intermediário. Isso facilita a manutenção e promove a flexibilidade do design do software.
Exemplo Prático: Imagine um sistema de mensagens em um aplicativo de redes sociais, onde usuários podem enviar mensagens uns aos outros. Sem o uso de Indirection, cada usuário teria que conhecer e interagir diretamente com os outros usuários para enviar mensagens, criando um alto acoplamento entre as instâncias de usuário.
Para aplicar o princípio de Indirection, podemos introduzir um objeto intermediário chamado GerenciadorDeMensagens. Este objeto atua como um mediador, recebendo mensagens de um usuário e encaminhando-as para o destinatário apropriado.
class Usuario {
private GerenciadorDeMensagens gerenciador;
public void enviarMensagem(String mensagem, Usuario destinatario) {
gerenciador.enviarMensagem(this, destinatario, mensagem);
}
}
class GerenciadorDeMensagens {
public void enviarMensagem(Usuario remetente, Usuario destinatario, String mensagem) {
// Lógica de encaminhamento da mensagem
}
}Com esse design, os objetos Usuario não interagem diretamente uns com os outros para enviar mensagens, mas sim com o GerenciadorDeMensagens, reduzindo o acoplamento e centralizando a lógica de encaminhamento de mensagens.
Este exemplo mostra como o princípio de Indirection pode ser utilizado para desacoplar componentes em um sistema, criando uma arquitetura mais flexível e fácil de manter. Ao introduzir um objeto intermediário para mediar interações, reduz-se a dependência direta entre os componentes do sistema.
9. Protected Variations
Conceito: O princípio de Protected Variations tem como objetivo proteger elementos do sistema contra variações em outros elementos, encapsulando a variação instável dentro de componentes estáveis.
Isso é feito através da criação de interfaces ou abstrações que isolam os pontos de variação, garantindo que mudanças em um componente não afetem diretamente outros componentes do sistema.
Exemplo Prático: Imagine um sistema de processamento de pagamentos em uma loja online que precisa lidar com diferentes gateways de pagamento (como PayPal, Stripe, etc.). Em vez de codificar a lógica de cada gateway de pagamento diretamente nas classes de negócio, podemos encapsular cada gateway em sua própria classe e definir uma interface comum GatewayDePagamento.
interface GatewayDePagamento {
void processarPagamento(Pagamento pagamento);
}
class PayPalGateway implements GatewayDePagamento {
public void processarPagamento(Pagamento pagamento) {
// Lógica específica do PayPal
}
}
class StripeGateway implements GatewayDePagamento {
public void processarPagamento(Pagamento pagamento) {
// Lógica específica do Stripe
}
}Assim, o sistema pode interagir com diferentes gateways de pagamento através da interface GatewayDePagamento, sem se preocupar com os detalhes específicos de cada um. Isso protege o sistema de variações nas implementações dos gateways de pagamento e facilita a adição de novos gateways no futuro.
Este exemplo demonstra como o princípio de Protected Variations pode ser aplicado para criar uma arquitetura de software mais robusta e adaptável a mudanças. Ao encapsular as variações e dependências em interfaces ou abstrações, reduz-se o impacto das mudanças em outros componentes do sistema, promovendo maior estabilidade e flexibilidade.
Protected Variations vs. Polimorfismo
Você está correto ao notar a semelhança entre o princípio Protected Variations e o polimorfismo. Ambos compartilham um tema comum de abstração e flexibilidade, mas com focos ligeiramente diferentes. Vamos explorar como eles se relacionam e diferem:
-
Polimorfismo: O princípio do polimorfismo é mais focado em permitir que diferentes classes implementem a mesma interface ou herdem da mesma classe abstrata, mas com comportamentos distintos. O objetivo principal é permitir que um único ponto no código possa trabalhar com diferentes tipos de objetos, cada um realizando a mesma ação de maneira única. Isso é central para alcançar a flexibilidade e extensibilidade em design orientado a objetos.
-
Protected Variations: Este princípio, embora use técnicas semelhantes como interfaces e abstrações, é mais focado em proteger partes do sistema contra mudanças em outras partes. Ele enfatiza a criação de uma "barreira" ou "capa" ao redor dos pontos de variação, de modo que as mudanças sejam localizadas e não propaguem efeitos colaterais indesejados através do sistema.
Em resumo, enquanto ambos os princípios utilizam abstrações e interfaces, o polimorfismo é mais voltado para a flexibilidade de comportamento, permitindo que diferentes objetos sejam tratados de forma uniforme. Já o Protected Variations se concentra em isolar e proteger o sistema contra as mudanças, mantendo a estabilidade do design geral. É uma distinção sutil, mas importante, na aplicação desses princípios de design de software.
Considerações Gerais sobre o aplicação dos princípios GRASP
- Proteja os pontos de variação identificados. Seja criterioso ao proteger pontos de evolução especulativos.
- Cada princípio GRASP deve ser aplicado com consideração do contexto específico e em conjunto com outros princípios para alcançar um design equilibrado.