C# -  Padrões Estruturais Gof - Bridge


Neste artigo vou apresentar com o padrão estrutural Gof Bridge.

O padrão estrutural Bridge desacopla uma abstração de sua implementação para que as duas possam variar de forma independente.

Para isso ele atua de forma a dividir a lógica de negócio (uma classe maior) em hierarquias de classe separadas que podem ser desenvolvidas independentemente. Assim, o objetivo principal do padrão Bridge é separar a interface do objeto de sua implementação.

O padrão Bridge permite que a abstração e a implementação sejam desenvolvidas de forma independente e o código do cliente pode acessar apenas a parte da abstração sem se preocupar com a parte da implementação. Este padrão separa a hierarquia de abstração e a hierarquia de implementação em duas camadas diferentes para que a mudança em uma hierarquia não afete o desenvolvimento ou a funcionalidade de outra hierarquia.

Como exemplo de aplicação do Bridge temos o seguinte cenário :

Suponha que temos em um projeto um requisito para salvar ou excluir um objeto na persistência. Aqui, podemos salvar o objeto em um sistema de arquivos ou em um banco de dados.



A seguir, temos dois implementadores que são usados para aplicar a persistência :

  1. O FilePersistenceImplementor que é usado para salvar o objeto em um arquivo,
  2. O DatabasePersistenceImplementor é usado para salvar o objeto em um banco de dados.

Portanto, podemos usar qualquer um dos implementadores acima para salvar o objeto. 

De acordo com o padrão Bridge, a abstração e a implementação devem estar em uma camada separada.  Aqui, a persistência é a camada de abstração e a implementação da persistência é a camada de implementação.

Neste cenário se adicionarmos uma nova implementação ou se removermos qualquer implementação, isso não afetará a camada de abstração. 

Esta é a vantagem do padrão Bridge : A camada de persistência abstrata usará qualquer um dos implementadores para salvar ou excluir um objeto e o cliente usará apenas a abstração para salvar ou excluir o objeto.

Temos assim o desacoplamento da abstração de sua implementação e ambas podem evoluir de forma independente.

Diagrama UML

A seguir temos o diagrama UML do padrão Bridge segundo o Gof:

Os participantes são:

1- Abstraction - É uma classe abstrata e contém membros que definem um objeto de negócio abstrato e sua funcionalidade.

Ela contém uma referência a um objeto do tipo Bridge (representado aqui por Implementor). Ela também pode atuar como a classe base para outras abstrações.
Esta é uma interface que é usada pelo cliente para atingir sua funcionalidade/solicitação.

2- RefinedAbstraction - É uma classe que herda da classe Abstraction ou estende a interface definida pela em Abstraction.


3- Implementor(Bridger) - É uma interface que atua como uma ponte (Bridge) entre a classe de abstração e as classes do implementador e também torna a funcionalidade da classe do implementador independente da classe de abstração,e também define a interface para a implementação das classes concretas

O Adapter implementa a interface Target e é composto com o Adpatee. Ela é responsável pela comunicação entre o Client e a Adaptee.

4- ConcreteImplementorA e ConcreteImplementorB - São classes que implementam Implementor ou seja a interface Bridge e também fornecem os detalhes de implementação para a classe Abstraction associada.

Podemos verificar no diagrama as duas hierarquia de classes que são definidas : a abstração e a implementação, de forma desacoplada  e que podem evoluir de forma independente.

Usos do padrão Bridge

Podemos usar o padrão Bridge quando :

  1. Quando queremos ocultar os detalhes de implementação do cliente
  2. Quando queremos evitar o forte acoplamento entre a abstração e a sua implementação
  3. Quando queremos alterar a implementação sem compilar a abstração
  4. Quando queremos compartilhar uma implementação entre vários objetos e isso deve ser escondido do cliente

Vantagens e desvantagens do padrão Bridge

As vantagens em usar o padrão são:

  1. Estimula o uso de um código fracamente acoplado - Como o uso do padrão desacopla a abstração de seu motivo de implementação, a implementação pode ser alterada sem afetar o código do cliente e o código do cliente não precisa ser compilado quando a implementação mudar;
     
  2. Aumenta a capacidade de manutenção e reutilização do código e reduz a duplicação de código;
     
  3. Ajuda a promover o princípio Aberto-Fechado - Pois novas abstrações e implementações podem ser desenvolvidas de forma independente;
     
  4. Facilita a Extensibilidade - Pois a abstração e a implementação podem ser estendidas de forma independente;

Vale lembra que aplicando o adapter geralmente se usa o principio da responsabilidade única na separação da conversão da interface e o principio OCP/Aberto-Fechado - introduzido novos tipos de adaptadores sem quebrar o código.

Como desvantagens do padrão Bridger podemos destacar que a utilização do padrão pode aumentar a complexidade do código pela criação de uma hierarquia de classes complexa. 

Além disso você tem que considerar que o código que está sendo usado para abstração e implementação deve ser separável, e, a funcionalidade separada deve ser capaz de funcionar de forma independente e isso pode levar a uma complexidade no código.

Aplicando o padrão Bridge

Veremos agora um exemplo prático de aplicação do padrão Bridge.

Vamos supor que você precisa processar o salário dos funcionários de uma empresa, e, você precisa salvar os dados nos formatos XML ou JSON.  O processo de salvar em um formato específico será decidido em tempo de execução, e, o cliente pode incluir outros formatos para geração do arquivo como formato texto, pdf , etc.... Além disso o processamento do salário também poderá variar no futuro:
 

Vemos aqui o cenário típico para aplicar o padrão Bridge,  e assim vamos definir uma abstração para calcular o salário
e gerar os dados nos formatos XML e JSON, e definir a implementação para efetivamente gerar os dados no formato XML e JSON.

Usando o padrão Bridge vamos separar a abstração da implementação de forma que elas possam evoluir de forma independente.  Assim se eu precisar modificar o cálculo do salário eu não preciso alterar a implementação, e, seu precisar alterar a implementação. incluindo um novo formato para gerar o arquivo. eu não preciso mexer na abstração, e com isso temos um código desacoplado.

Vamos então criar um projeto Console usando o .NET 5.0 e a linguagem C# e a seguir vamos criar a classe Funcionario que representa o nosso modelo de domínio:

    public class Funcionario
    {
        public int Id { get; set; }
        public string Nome { get; set; }
        public decimal SalarioBase { get; set; }
        public decimal Incentivo { get; set; }
        public decimal SalarioTotal { get; set; }
    }

A seguir vamos criar a interface IGeraArquivo  que representa o Implementor ou seja é o nosso Bridge, onde vamos definir um contrato para gravar o arquivo no formato especificado para o funcionário:

using Bridge1.Domain;

public interface IGeraArquivo
{
    void GravaArquivo(Funcionario funcionario);
}

A seguir vamos criar as classes concretas GeraJson e GeraXML que representam ConcreteImplementor e implementam a interface IGeraArquivo e grava os dados do salário do funcionário nos formatos XML e JSON:

1- GeraXML()

   using Bridge1.Domain;
   using System;
   using System.IO;
   using System.Xml.Serialization;

    public class GeraXML : IGeraArquivo
    {
        private string nomeArquivo = "SalarioFuncionario.xml";
        private XmlSerializer xmlSerializer = new XmlSerializer(typeof(Funcionario));
        private FileStream fileStream;

        public void GravaArquivo(Funcionario funcionario)
        {
            fileStream = new FileStream(nomeArquivo, FileMode.OpenOrCreate);

            xmlSerializer.Serialize(fileStream, funcionario);

            Console.WriteLine($"Salário para o Funcionário : {funcionario.Nome} " +
                $"gerado com sucesso em : {nomeArquivo} \n");
        }
    }

2- GeraJSON()

using Bridge1.Domain;
using Bridge1.Implementor;
using System;
using System.IO;
using System.Text.Json;

    public class GeraJson : IGeraArquivo
    {
        private string nomeArquivo = "SalarioFuncionario.json";

        public void GravaArquivo(Funcionario funcionario)
        {
            var funcionarioSerializado = JsonSerializer.Serialize(funcionario);

            File.WriteAllText(nomeArquivo, funcionarioSerializado);

            Console.WriteLine($"Salário para o Funcionário : {funcionario.Nome} " +
                $"gerado com sucesso em : {nomeArquivo} \n");
        }
    }

Acima temos as duas classes concretas que geram os arquivos nos formatos especificados.

Agora vamos criar a classe AbstrationGeraArquivo que representa o Abstration e é uma classe abstrata onde temos uma instância de IGeraArquivo e onde é usada esta instancia para gerar o arquivo no formato especificado :

    using Bridge1.Domain;

    public abstract class AbstrationGeraArquivo
    {
        protected IGeraArquivo geraArquivo;

        protected AbstrationGeraArquivo(IGeraArquivo geraArquivo)
        {
            this.geraArquivo = geraArquivo;
        }

        public virtual void GravaArquivo(Funcionario funcionario)
        {
            geraArquivo.GravaArquivo(funcionario);
        }
    }

A seguir crie a classe CalculaSalario que  representa o RefinedAbstration. Esta classe herda de Abstration e recebe no construtor um objeto  IGeraArquivo em tempo de execução calcula o salário do funcionário internamente e executa a operação para persistir os dados do funcionário no formato do arquivo especificado usando a instancia recebido de IGeraArquivo :

using Bridge1.Abstration;
using Bridge1.Domain;
using Bridge1.Implementor;
using System;

    public class CalculaSalario : AbstrationGeraArquivo
    {
        public CalculaSalario(IGeraArquivo geraArquivo) : base(geraArquivo)
        { }

        public void ProcessaSalarioFuncionario(Funcionario funcionario)
        {

            funcionario.SalarioTotal = funcionario.SalarioBase + funcionario.Incentivo;

            Console.WriteLine($"Valor do Salário Total para o funcionario {funcionario.Id} \n" +
                $": R$ {funcionario.SalarioTotal}");

            geraArquivo.GravaArquivo(funcionario);
        }
    }

Na classe Program vamos aplicar a utilização desta implementação:

using Bridge1.ConcreteImplementor;
using Bridge1.Domain;
using Bridge1.RefinedAbstration;
using System;

namespace Bridge1
{
    class Program
    {
        static void Main(string[] args)
        {
            // RedefinedAbstraction recebe via construtor em tempo de execução
            // o tipo especifico de formato para gerar o arquivo
            // e usa a classe base AbstractionGeraArquivo para obter uma instância
            // de IGeraArquivo (Bridge)

            CalculaSalario calculaSalario = new CalculaSalario(new GeraXML());

            //define os dados do funcionário
          
 Funcionario funcionario = new Funcionario
            {
                  Id = 101, Nome = "Macoratti", SalarioBase = 3000,
                  Incentivo = 2000
            };

            //processa o salario
            calculaSalario.ProcessaSalarioFuncionario(funcionario);

            //altera o valor do incentivo em tempo de execução
            funcionario.Incentivo= 2500;

            //altera o formato para gerar o arquivo em tempo de execução
            calculaSalario = new CalculaSalario(new GeraJson());          

            calculaSalario.ProcessaSalarioFuncionario(funcionario);           

            Console.ReadKey();
        }
    }
}

Executando o projeto teremos o resultado:

Note que o padrão Bridge tem quase a mesma estrutura do padrão Adapter.  A diferença é que ele é usado ao projetar novos sistemas em vez do padrão Adapter que é usado com sistemas já existentes.

Assim o padrão Bridge pode ser usado quando uma nova versão de um software ou sistema é lançada, mas a versão mais antiga do software ainda está em execução para seu cliente existente.  Com isso não existe a necessidade de alterar o código do cliente, mas o cliente precisa escolher qual versão deseja usar.


Pegue o código do projeto aqui :
Padrao_Bridge.zip

"Brame o mar e a sua plenitude; o mundo, e os que nele habitam.
Os rios batam as palmas; regozijem-se também as montanhas,
Perante a face do Senhor, porque vem a julgar a terra; com justiça julgará o mundo, e o povo com eqüidade."
Salmos 98:7-9

Referências:


José Carlos Macoratti