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!

👀
Bridge é um padrão de projeto estrutural que permite que você divida uma classe grande ou um conjunto de classes intimamente ligadas em duas hierarquias separadas — abstração e implementação — que podem ser desenvolvidas independentemente umas das outras.

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

💡
A intenção deste padrão é desacoplar uma abstração da sua implementação, de modo que as duas possam variar independentemente.

Padrão

O padrão bridge é composto geralmente por cinco partes, são elas:

  1. 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.

  2. 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

  3. Implementações concretas que contém as implementações reais da nossa implementação comum

  4. 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

  5. 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:

  1. Abstração: Classe Payment

  2. Implementação: Interfaces IPayment e IPaymentPlan

  3. Implementações concretas: CreditCardPayment, PayPalPayment, BasicPaymentPlan, PremiumPaymentPlan

  4. 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

Design Patterns

Materiais de estudo

Estamos chegando ao fim do nosso artigo e queria deixar alguns materiais que usei para estudar esse padrão de software:

Refactoring Guru

Padrões de Projetos: Soluções Reutilizáveis de Software Orientados a Objetos

0
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

Nicolas de Souza
Nicolas de Souza