Design Pattern - Bridge
Introdução
Dando sequência aos estudos sobre design patterns, chegou a hora de falarmos sobre o padrão BRIDGE. Como seu nome sugere, sua implementação cria conexões entre interfaces e implementações nos auxiliando a criar classes cada vez mais específicas e com pouca ou nenhuma repetição de código mas vamos com calma!
O texto supracitado, foi tirado do site Refactoring GURU e foi a melhor definição que encontrei para esse padrão mas o livro: Padrões de Projetos: Soluções Reutilizáveis de Software Orientados a Objetos, traz um síntese interessante que também quero deixar registrado
Padrão
O padrão bridge é composto geralmente por cinco partes, são elas:
A abstração, responsável pelo controle de alto nível, ela depende do objeto de implementação para que possa desempenhar o sua função.
A implementação (uma interface), que declara a interface comum a todas as implementações concretas. Nossa abstração só pode se comunicar com um objeto de implementação através de métodos declarados aqui.
A abstração pode listar os mesmos métodos que a implementação, mas geralmente a abstração declara alguns comportamentos complexos que dependem de uma ampla variedade de operações primitivas declaradas pela implementação
Implementações concretas que contém as implementações reais da nossa implementação comum
Abstrações refinadas que proveem variantes para o controle lógico. Como a abstração original, as refinadas também podem trabalhar com diferentes implementações através de uma interface comum
Cliente, que é quem vai realmente utilizar nossas estruturas lógicas citadas anteriormente.
Exemplo conceitual
Se você leu o artigo sobre o padrão Singleton, deve imaginar que eu gosto de esportes então para fugir dos exemplos de formas e cores e oferecer uma abordagem diferente, vamos falar sobre basquete!
No basquete temos, em quadra jogando, 5 jogadores em cada equipe. Desses 5 temos posições variadas: armador, pivô, ala, ala-armador e ala-pivô. Cada uma dessas posições apresentam características ímpares e papéis bem definidos para uma boa execução das estratégias da equipe. Mas mesmo dentro de uma posição teremos comportamentos bem diferentes, por exemplo, um pivô pode ser um defensor agressivo, mas podemos ter um outro pivô que seja um defensor mais posicional, um ala que seja rápido e outro que seja lento...
Vamos pensar um pouco como isso se daria em um código, teríamos uma classe base chamada Jogador
e então iríamos implementar um ArmadorRápido
, PivoLento
, AlaAtaqueLento
...
public class Jogador
{
public string Nome { get; set; }
public int Pontos { get; set; }
}
public class JogadorRapido : Jogador
{
public void Correr()
{
Console.WriteLine("Correu rápido!");
}
}
public class JogadorLento : Jogador
{
public void Correr()
{
Console.WriteLine("Correu lento...");
}
}
public class JogadorRapidoDefensor : JogadorRapido
{
public void Defender()
{
Console.WriteLine("Defendeu com velocidade!");
}
}
public class JogadorRapidoAtacante : JogadorRapido
{
public void ChutarBola()
{
Console.WriteLine("Chutou a bola com velocidade!");
}
}
public class JogadorLentoDefensor : JogadorLento
{
public void Defender()
{
Console.WriteLine("Defendeu com resistência!");
}
}
public class JogadorLentoAtacante : JogadorLento
{
public void ChutarBola()
{
Console.WriteLine("Chutou a bola com força!");
}
}
// E assim por diante...
Ou seja para cada característica nova que eu fosse adicionar ao meus jogadores, exigiria a implementação de novas classes de maneira exponencial, sendo necessário inserir esta característica em todos os demais... Uma verdadeira bola de neve!
Como o padrão bridge pretende resolver essa situação?! De uma maneira até que simples, inserindo em Jogador
uma interface com nome IEstiloDeJogo
. Dessa forma ao criarmos novos jeitos de jogo, poderíamos apenas construir classes que implementem IEstiloDeJogo
e então usar essas classes para construir nosso jogador.
Vamos dar uma olhada rápida em como isso se daria
public interface IVelocidade
{
void Correr();
}
public class VelocidadeRapida : IVelocidade
{
public void Correr()
{
Console.WriteLine("Correu rápido!");
}
}
public class VelocidadeLenta : IVelocidade
{
public void Correr()
{
Console.WriteLine("Correu lento...");
}
}
public interface IPosicao
{
void Jogar();
}
public class PosicaoDefensor : IPosicao
{
public void Jogar()
{
Console.WriteLine("Defendeu!");
}
}
public class PosicaoAtacante : IPosicao
{
public void Jogar()
{
Console.WriteLine("Chutou a bola!");
}
}
public class Jogador
{
public string Nome { get; set; }
public int Pontos { get; set; }
public IVelocidade Velocidade { get; set; }
public IPosicao Posicao { get; set; }
public void Jogar()
{
Velocidade.Correr();
Posicao.Jogar();
}
}
Deste modo consigo criar jogadores de todas as posições com estilos de jogos diferentes, sem precisar criar uma classe para cada tipo de jogo que quero fazer, herdando de jogador e de mais outras classes... Desse modo tornamos o código mais flexível a possíveis mudanças, facilitamos o trabalho de manutenção e proporcionamos o desacoplamento entre classes e interfaces.
Exemplo Prático
Por mais que eu tenha gostado do exemplo envolvendo jogadores de basquete, queria um exemplo um pouco mais real para adicionarmos ao nosso repositório.
// Interface for payment
public interface IPayment
{
void MakePayment(decimal amount);
void CancelPayment();
}
// Concrete class for credit card payment
public class CreditCardPayment : IPayment
{
public void MakePayment(decimal amount)
{
Console.WriteLine($"Payment made with credit card for {amount}");
}
public void CancelPayment()
{
Console.WriteLine("Payment cancelled");
}
}
// Concrete class for PayPal payment
public class PayPalPayment : IPayment
{
public void MakePayment(decimal amount)
{
Console.WriteLine($"Payment made with PayPal for {amount}");
}
public void CancelPayment()
{
Console.WriteLine("Payment cancelled");
}
}
// Interface for payment plan
public interface IPaymentPlan
{
decimal CalculateFee(decimal amount);
bool CheckLimit(decimal amount);
}
// Concrete class for basic payment plan
public class BasicPaymentPlan : IPaymentPlan
{
public decimal CalculateFee(decimal amount)
{
return amount * 0.05m;
}
public bool CheckLimit(decimal amount)
{
return amount <= 1000m;
}
}
// Concrete class for premium payment plan
public class PremiumPaymentPlan : IPaymentPlan
{
public decimal CalculateFee(decimal amount)
{
return amount * 0.03m;
}
public bool CheckLimit(decimal amount)
{
return amount <= 5000m;
}
}
// Class for payment
public class Payment
{
public IPayment PaymentType { get; set; }
public IPaymentPlan PaymentPlan { get; set; }
public Payment(IPayment paymentType, IPaymentPlan paymentPlan)
{
PaymentType = paymentType;
PaymentPlan = paymentPlan;
}
}
class Program
{
static void Main(string[] args)
{
// Create a credit card payment
IPayment creditCardPayment = new CreditCardPayment();
// Create a basic payment plan
IPaymentPlan basicPaymentPlan = new BasicPaymentPlan();
// Create a payment object with the credit card payment and basic payment plan
Payment payment = new Payment(creditCardPayment, basicPaymentPlan);
// Make a payment of $500
decimal amount = 500m;
if (payment.PaymentPlan.CheckLimit(amount))
{
decimal fee = payment.PaymentPlan.CalculateFee(amount);
Console.WriteLine($"Fee: {fee}");
payment.PaymentType.MakePayment(amount);
}
else
{
Console.WriteLine("Payment exceeds limit");
}
// Create a PayPal payment
IPayment payPalPayment = new PayPalPayment();
// Create a premium payment plan
IPaymentPlan premiumPaymentPlan = new PremiumPaymentPlan();
// Create a payment object with the PayPal payment and premium payment plan
Payment payment2 = new Payment(payPalPayment, premiumPaymentPlan);
// Make a payment of $2000
decimal amount2 = 2000m;
if (payment2.PaymentPlan.CheckLimit(amount2))
{
decimal fee2 = payment2.PaymentPlan.CalculateFee(amount2);
Console.WriteLine($"Fee: {fee2}");
payment2.PaymentType.MakePayment(amount2);
}
else
{
Console.WriteLine("Payment exceeds limit");
}
}
}
Resultado desse código pode ser visto aqui:
Fee: 25.00
Payment made with credit card for 500
Fee: 60.00
Payment made with PayPal for 2000
Vamos confrontar nosso código com a estrutura de código com o design pattern bridge:
Abstração: Classe
Payment
Implementação: Interfaces
IPayment
eIPaymentPlan
Implementações concretas:
CreditCardPayment
,PayPalPayment
,BasicPaymentPlan
,PremiumPaymentPlan
Abstrações refinadas:
Payment
Relação com o padrão Adapter
Olhando superficialmente para o padrão bridge podemos ter a sensação de que ele se tornaria um coringa, tornando o padrão adapter obsoleto. Contudo é importante notar que os padrões em questão são aplicados em momento diferentes, o padrão adapter é um recurso para que possamos alinhar códigos já existentes a novas demandas de negócio, por sua vez o padrão bridge é utilizado desde o início do desenvolvimento para permitir que abstrações e implementações possam variar independentemente.
Conclusão
Vimos que o padrão Bridge pode representar um ganho significativo de legibilidade e flexibilidade para o seu código. Para além disso, nota-se que este padrão nos ajuda a manter a atender o princípios aberto/fechado, permitindo que novas abstrações e implementações sejam introduzidas ao código independentes uma das outras, e o princípio de responsabilidade única mantendo cada implementação especializada.
Repositório GIT
Materiais de estudo
Estamos chegando ao fim do nosso artigo e queria deixar alguns materiais que usei para estudar esse padrão de software:
Padrões de Projetos: Soluções Reutilizáveis de Software Orientados a Objetos
Subscribe to my newsletter
Read articles from Nicolas de Souza directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by