Princípio de Inversão de Dependência (DIP)

1. Conceito
O que é DIP?
O princípio afirma:

Módulos de alto nível não devem depender de módulos de baixo nível. Ambos devem confiar em abstrações.
As abstrações não devem depender de detalhes. Os detalhes devem depender da abstração.
Por exemplo, o código a seguir não está em conformidade com os princípios acima:

public class HighLevelModule
{
    
    
  private readonly LowLevelModule _lowLowelModule;
 
  public HighLevelModule()
  {
    
    
    _lowLevelModule = new LowLevelModule();   
  }

  public void Call()
  {
    
    
    _lowLevelModule.Initiate();
    _lowLevelModule.Send();
  }
}

public class LowLevelModule
{
    
    
  public void Initiate()
  {
    
    
    //do initiation before sending
  }
  
  public void Send()
  {
    
    
    //perform sending operation
  }
}

No código acima, HighLevelModule depende diretamente de LowLevelModule e não segue o primeiro ponto do DIP. Por que isso é tão importante? A relação direta e fortemente acoplada entre os dois dificulta a criação de testes de unidade para o HighLevelModule isoladamente do LowLevelModule. Você precisa testar HighLevelModule e LowLevelModule ao mesmo tempo, porque eles estão fortemente acoplados.

Observe que ainda é possível isolar HighLevelModule para teste de unidade usando uma estrutura de teste que executa a interceptação .NET CLR, como TypeMock Isolator. Usando esta estrutura, é possível alterar o comportamento do teste LowLevelModule. No entanto, não recomendo essa prática por dois motivos. Primeiro, o uso da interceptação CLR no teste viola a realidade do código: a dependência do HighLevelModuleon do LowLevelModule. Nos piores cenários, os testes produzem resultados falsos positivos. Em segundo lugar, essa prática pode nos impedir de aprender a escrever um código limpo e testável.

Como aplicamos o DIP?
O primeiro ponto do DIP sugere que apliquemos duas coisas ao nosso código:

Inversão de Dependência de Abstração
ou Inversão de Controle
Primeiro, o LowLevelModule precisa ser abstraído, e o HighLevelModule dependerá da abstração. Os diferentes métodos de abstração são discutidos na próxima seção. Para os exemplos abaixo, usarei interfaces para abstração. Uma interface IOperation é usada para abstrair LowLevelModule.

public interface IOperation
{
    
    
  void Initiate();
  void Send();
}

public class LowLevelModule: IOperation
{
    
    
  public void Initiate()
  {
    
    
    //do initiation before sending
  }
  
  public void Send()
  {
    
    
    //perform sending operation
  }
}

Em segundo lugar, como HighLevelModule dependerá apenas da abstração IOperation, não podemos mais usar o novo LowLevelModule() dentro da classe HighLevelModule. LowLevelModule requer que HighLevelModule seja injetado na classe a partir do contexto do chamador. A dependência LowLevelModule precisa ser revertida. É daí que vêm os termos "inversão de dependência" e "inversão de controle".

LowLevelModule precisa passar a implementação de abstração ou comportamento de fora, e o processo de HighLevelModule movendo-o de dentro da classe para fora é chamado de inversão. Discuto diferentes abordagens para a inversão de dependência na Seção 3. No exemplo a seguir, será usada a injeção de dependência por meio do construtor.

public class HighLevelModule
{
    
    
  private readonly IOperation _operation;

  public HighLevelModule(IOperation operation)
  {
    
    
    _operation = operation;
  }

   public void Call()
  {
    
    
    _operation.Initiate();
    _operation.Send();
  }
}

Desacoplamos HighLevelModule e LowLevelModule um do outro e agora ambos dependem da IOperation abstrata. O comportamento do método Send pode ser controlado de fora da classe, fazendo qualquer escolha de implementação, como LowLevelModule

No entanto, ainda não está feito. O código ainda não atende ao segundo ponto do DIP. As abstrações não devem depender de detalhes ou implementação. Na verdade, o método Initiate IOperation é um detalhe de implementação de LowLevelModule, que é usado para LowLevelModule se preparar antes de executar a operação Send.

O que faço é removê-lo da abstração IOperation e tratá-lo como parte dos detalhes de implementação do LowLevelModule. Posso incluir a ação Iniciar no construtor LowLevelModule. Isso torna a operação um método privado, limitando o acesso à classe.

public interface IOperation
{
    
    
  void Send();
}

public class LowLevelModule: IOperation
{
    
    
  public LowLevelModule()
  {
    
    
    Initiate();
  }

  private void Initiate()
  {
    
    
    //do initiation before sending
  }
  
  public void Send()
  {
    
    
    //perform sending operation
  }
}

public class HighLevelModule
{
    
    
  private readonly IOperation _operation;

  public HighLevelModule(IOperation operation)
  {
    
    
    _operation = operation;
  }

  public void Call()
  {
    
    
    _operation.Send();
  }
}

2. Métodos de abstração
A primeira atividade na implementação do DIP é aplicar a abstração a várias partes do código. No mundo C#, existem várias maneiras de fazer isso:

Usando interfaces
Usando classes abstratas
Usando delegados
Em primeiro lugar, uma interface é usada apenas para fornecer abstração e uma classe abstrata também pode ser usada para fornecer alguns detalhes de implementação compartilhados. Finalmente, um delegado fornece uma abstração para uma função ou método específico.

Como observação, é uma prática comum marcar um método como virtual para que possa ser ridicularizado ao escrever testes de unidade para a classe de chamada. No entanto, isso não é o mesmo que aplicar uma abstração. Marcar um método como virtual apenas o torna substituível, para que o método possa ser simulado, o que é útil para fins de teste.

Minha preferência é usar uma interface para fins abstratos. abstract Use classes somente quando os detalhes de implementação forem compartilhados entre duas ou mais classes. Mesmo assim, também garantirei que a interface da classe abstrata seja a implementação abstrata real. Na Seção 1, dei exemplos de interfaces para aplicativos abstratos usando . Nesta seção, darei outros exemplos usando classes abstratas e delegados.

Usando uma classe abstrata
Usando o exemplo da Seção 1, só preciso alterar a interface de IOperation para uma classe abstrata, OperationBase.

public abstract class OperationBase
{
    
    
  public abstract void Send();
}

public class LowLevelModule: OperationBase
{
    
    
  public LowLevelModule()
  {
    
    
    Initiate();
  }

  private void Initiate()
  {
    
    
    //do initiation before sending
  }
  
  public void Send()
  {
    
    
    //perform sending operation
  }
}

public class HighLevelModule
{
    
    
  private readonly OperationBase _operation;

  public HighLevelModule(OperationBase operation)
  {
    
    
    _operation = operation;
  }

  public void Call()
  {
    
    
    _operation.Send();
  }
}

O código acima é equivalente a usar interface. Geralmente, eu só uso classes abstratas se elas compartilharem detalhes de implementação. Por exemplo, se um HighLevelModule pudesse usar um LowLevelModule ou AnotherLowLevelModule, e ambas as classes tivessem detalhes de implementação compartilhados, eu usaria uma classe abstrata como a classe base para ambas. A classe base implementará IOperation, que é a abstração real.

public interface IOperation
{
    
    
  void Send();
}

public abstract class OperationBase: IOperation
{
    
    
  public OperationBase()
  {
    
    
    Initiate();
  }

  private void Initiate()
  {
    
    
    //do initiation before sending, also shared implementation in this example
  }

  public abstract void Send();
}

public class LowLevelModule: OperationBase
{
    
    
  public void Send()
  {
    
    
    //perform sending operation
  }
}

public class AnotherLowLevelModule: OperationBase
{
    
    
  public void Send()
  {
    
    
    //perform another sending operation
  }
}

public class HighLevelModule
{
    
    
  private readonly IOperation _operation;

  public HighLevelModule(IOperation operation)
  {
    
    
    _operation = operation;
  }

  public void Call()
  {
    
    
    _operation.Send();
  }
}

Usando delegados
Você pode usar delegados para abstrair métodos ou funções individuais. O delegado genérico Func ou Action pode ser usado para essa finalidade.

public class Caller
{
    
     
  public void CallerMethod()
  {
    
    
    var module = new HighLevelModule(Send);
    ...
  }

  public void Send()
  {
    
    
    //this is the method injected into HighLevelModule
  }
}

public class HighLevelModule
{
    
    
  private readonly Action _sendOperation;

  public HighLevelModule(Action sendOperation)
  {
    
    
    _sendOperation = sendOperation;
  }

  public void Call()
  {
    
    
    _sendOperation();
  }
}

Como alternativa, você pode criar seu próprio delegado e dar a ele um nome significativo.

public delegate void SendOperation();

public class Caller
{
    
     
  public void CallerMethod()
  {
    
    
    var module = new HighLevelModule(Send);
    ...
  }

  public void Send()
  {
    
    
    //this is the method injected into HighLevelModule
  }
}

public class HighLevelModule
{
    
    
  private readonly SendOperation _sendOperation;

  public HighLevelModule(SendOperation sendOperation)
  {
    
    
    _sendOperation = sendOperation;
  }

  public void Call()
  {
    
    
    _sendOperation();
  }
}

A vantagem de usar delegados genéricos é que não precisamos criar ou implementar um tipo como interfaces e classes para dependências. Podemos usar qualquer método ou função do contexto do chamador ou de qualquer outro lugar.

3. Abordagem de inversão de dependência
Na primeira seção, usei a injeção de dependência do construtor como uma abordagem de inversão de dependência. Nesta seção, discuto várias abordagens para a abordagem de inversão de dependência.

Aqui está uma lista de métodos de inversão de dependência:

Usando Injeção de Dependência
Usando Estado Global
Usando Indireção
A seguir, explicarei cada abordagem.

1. Usando injeção de dependência
Usando injeção de dependência (DI) é onde as dependências são injetadas diretamente em uma classe por meio de seus membros públicos. As dependências podem ser injetadas no construtor de uma classe (injeção de construtor), propriedades de coleção (injeção de setter), métodos (injeção de método), eventos, propriedades indexadas, campos e basicamente qualquer membro de uma classe pública. Eu geralmente não recomendo usar campos porque em programação orientada a objetos não é recomendado expor campos como uma boa prática, pois o mesmo pode ser feito usando propriedades. A injeção de dependência usando propriedades indexadas também é um caso raro, então não vou explicar mais.

Injeção de construtor
Eu uso principalmente injeção de construtor. O uso de injeção de construtor também pode tirar proveito de certos recursos em contêineres IoC, como autowiring ou descoberta de tipo. Discutirei os contêineres IoC posteriormente na Seção 5. Aqui está um exemplo de injeção de construção:

public class HighLevelModule
{
    
    
  private readonly IOperation _operation;

  public HighLevelModule(IOperation operation)
  {
    
    
    _operation = operation;
  }

  public void Call()
  {
    
    
    _operation.Send();
  }
}

Setter Injection
e Method Injection são usados ​​para injetar dependências depois que o objeto é construído. Isso pode ser visto como uma desvantagem quando usado com contêineres IoC (discutido na Seção 5). No entanto, se você não usar um contêiner IoC, eles obterão a mesma funcionalidade da injeção de construtor. Outro benefício do Setter ou Method Injection é que ele permite que você altere as dependências no tempo de execução, eles podem ser usados ​​para complementar a injeção do construtor. Aqui está um exemplo de injeção de Setter, que permite injetar dependências uma de cada vez:

public class HighLevelModule
{
    
    
  public IOperation Operation {
    
     get; set; }

  public void Call()
  {
    
    
    Operation.Send();
  }
}

Injeção de método
Usando a injeção de método, você pode definir várias dependências ao mesmo tempo. Aqui está um exemplo de injeção de método:

public class HighLevelModule
{
    
    
  private readonly IOperation _operationOne;
  private readonly IOperation _operationTwo;

  public void SetOperations(IOperation operationOne, IOperation operationTwo)
  {
    
    
    _operationOne = operationOne;
    _operationTwo = operationTwo;
  }

  public void Call()
  {
    
    
    _operationOne.Send();
    _operationTwo.Send();
  }
}

Ao usar injeção de método, as dependências passadas como parâmetros são preservadas na classe, por exemplo, como campos ou propriedades, para uso posterior. Quando você passa alguma classe ou interface em um método e só usa no método, isso não conta como injeção de método.

Uso de eventos
O uso de eventos na injeção de tipo de delegado é restrito e aplicável apenas se modelos de assinatura e notificação forem necessários, e os delegados não devem retornar nenhum valor ou apenas nulos. O chamador assinará um delegado para a classe que implementa o evento e pode haver vários assinantes. A injeção de eventos pode ser realizada após a construção do objeto. Injetar eventos por meio de construtores não é comum. Abaixo está um exemplo de injeção de evento.

public class Caller
{
    
     
  public void CallerMethod()
  {
    
    
    var module = new HighLevelModule();
    module.SendEvent += Send ;

    ...
  }

  public void Send()
  {
    
    
    //this is the method injected into HighLevelModule
  }
}

public class HighLevelModule
{
    
    
  public event Action SendEvent = delegate {
    
    };

  public void Call()
  {
    
    
    SendEvent();
  }
}

Meu mantra em geral sempre foi usar a injeção de construtor, que também nos permite usar um contêiner IoC posteriormente se não houver nada que o obrigue a usar Setter ou Method Injection.

2. Usando o estado global
As dependências podem ser recuperadas do estado global dentro da classe sem precisar ser injetadas diretamente na classe. As dependências podem ser injetadas no estado global e acessadas de dentro da classe.

  public class Helper
  {
    
    
    public static IOperation GlobalStateOperation {
    
     get; set;}
  }

  public class HighLevelModule
  {
    
    
    public void Call()
    {
    
    
       Helper.GlobalStateOperation.Send();
    }
  }
   
  public class Caller
  {
    
    
    public void CallerMethod()
    {
    
    
      Helper.GlobalStateOperation = new LowLevelModule();

      var highLevelModule = new HighLevelModule();
      highLevelModule.Call();
    }
  }  
}

O estado global pode ser representado como propriedades, métodos ou até mesmo campos. Um ponto importante é que o valor subjacente tem setters e getters públicos. Setters e getters podem assumir a forma de métodos em vez de propriedades.

Se o estado global tiver apenas getters (por exemplo, singletons), as dependências não serão invertidas. Usar o estado global para inverter dependências não é recomendado, pois torna as dependências menos óbvias e as oculta dentro das classes.

3. Use indireto
Se estiver usando indireto, você não passará dependências diretamente para a classe. Em vez disso, passe um objeto que crie ou passe uma implementação abstrata para você. Isso também significa que você cria outra dependência para essa classe. O tipo de objeto que você passa para a classe pode ser:

Objetos de registro/contêiner
Objetos de fábrica
Você pode escolher se deseja passar objetos diretamente (injeção de dependência) ou usar o estado global.

Objetos de registro/contêiner
Se estiver usando um registro (comumente conhecido como padrão de localizador de serviço), o registro pode ser consultado para retornar uma implementação de uma abstração (como uma interface). No entanto, primeiro você precisará registrar a implementação de fora da classe. Você também pode usar um contêiner para agrupar um registro, como fazem muitas estruturas de contêiner IoC. Os contêineres geralmente têm outros tipos de recursos de descoberta ou autowiring, portanto, ao registrar uma interface de contêiner e sua implementação, você não precisa especificar dependências na implementação de classes. Quando consultado por uma interface, o contêiner poderá retornar a instância de classe de implementação resolvendo primeiro todas as suas dependências. Obviamente, você precisará registrar todas as dependências primeiro.

Nos primeiros dias das estruturas de contêiner IoC, o contêiner era frequentemente implementado como um estado global ou um Singleton em vez de passá-lo para a classe explicitamente, então agora é considerado um antipadrão. Aqui está um exemplo usando um objeto do tipo container:

public interface IOperation
{
    
    
  void Send();
}

public class LowLevelModule: IOperation
{
    
    
  public LowLevelModule()
  {
    
    
    Initiate();
  }

  private void Initiate()
  {
    
    
    //do initiation before sending
  }
  
  public void Send()
  {
    
    
    //perform sending operation
  }
}

public class HighLevelModule
{
    
    
  private readonly Container _container;

  public HighLevelModule(Container container)
  {
    
    
    _container = container;
  }

  public void Call()
  {
    
    
    IOperation operation = _container.Resolvel<IOperation>();
    operation.Send();
  }
}

public class Caller
{
    
    
  public void UsingContainerObject()
  {
    
    
     //registry the LowLevelModule as implementation of IOperation
     var register  = new Registry();
     registry.For<IOperation>.Use<LowLevelModule>();

     //wrap-up registry in a container
     var container = new Container(registry);
      
     //inject the container into HighLevelModule
     var highLevelModule = new HighLevelModule(container);
     highLevelModule.Call();     
  }
}

Você pode até tornar o HighLevelModule dependente da abstração do contêiner, mas essa etapa não é obrigatória.

Além disso, provavelmente não é uma boa ideia usar contêineres ou registros em qualquer lugar de sua classe, pois isso torna as dependências da classe menos óbvias.


A diferença entre usar um registrador/container e um objeto fábrica com um objeto fábrica é que com um registrador/container a classe de implementação precisa ser registrada antes de ser consultada, enquanto com uma fábrica isso não é necessário, pois a instanciação ocorre dentro do hardcode de implementação Objetos de fábrica não precisam ter "fábrica" ​​como parte de seu nome. Pode ser apenas uma classe normal retornando uma abstração (por exemplo, uma interface).

Além disso, como a instanciação de LowLevelModule é codificada permanentemente na implementação de fábrica, a fábrica de dependência de HighLevelModule não causa inversão de dependência de LowLevelModule. Para inverter as dependências, o HighLevelModule precisa depender da abstração da fábrica e o objeto da fábrica precisa implementar essa abstração. Aqui está um exemplo usando um objeto de fábrica:

public interface IOperation
{
    
    
  void Send();
}

public class LowLevelModule: IOperation
{
    
    
  public LowLevelModule()
  {
    
    
    Initiate();
  }

  private void Initiate()
  {
    
    
    //do initiation before sending
  }
  
  public void Send()
  {
    
    
    //perform sending operation
  }
}

public interface IModuleFactory
{
    
    
   IOperation CreateModule();
}

public class ModuleFactory: IModuleFactory
{
    
    
  public IOperation CreateModule()
  {
    
    
      //LowLevelModule is the implementation of the IOperation, 
      //and it is hardcoded in the factory. 
      return new LowLevelModule();
  }
}

public class HighLevelModule
{
    
    
  private readonly IModuleFactory _moduleFactory;

  public HighLevelModule(IModuleFactory moduleFactory)
  {
    
    
    _moduleFactory = moduleFactory;
  }

  public void Call()
  {
    
    
    IOperation operation = _moduleFactory.CreateModule();
    operation.Send();
  }
}

public class Caller
{
    
    
  public void CallerMethod()
  {
    
    
     //create the factory as the implementation of abstract factory
     IModuleFactory moduleFactory = new ModuleFactory();
      
     //inject the factory into HighLevelModule
     var highLevelModule = new HighLevelModule(moduleFactory);   
     highLevelModule.Call();  
  }
}

Meu conselho é usar a indireta com moderação. O padrão Service Locator é considerado um antipadrão hoje. No entanto, pode haver momentos em que você precise usar um objeto de fábrica para criar dependências para você. Meu ideal é evitar o uso indireto, a menos que seja necessário.

Além de implementações de abstrações (interfaces, classes abstratas ou delegados), geralmente também podemos injetar dependências em tipos primitivos, como boolean, int, double, string ou apenas uma classe contendo apenas propriedades.

Guess you like

Origin blog.csdn.net/u014249305/article/details/110355504