Práticas recomendadas para C#

Os perigos da violação dos princípios SÓLIDOS no C#

Brannon King

Um número de princípios surgiram à medida em que o processo de gravação de software evoluiu do âmbito teórico para uma disciplina de engenharia verdadeira. E quando eu digo princípio, estou me referindo a um recurso do código de computador que ajuda a manter o valor daquele código. Padrão se refere a um cenário de código comum, seja bom ou ruim.

Por exemplo, você pode valorizar um código de computador que funciona de forma segura em um ambiente multi-threaded. Você pode valorizar um código de computador que não falha quando você modifica o código em outro local. Na verdade, você pode valorizar várias qualidades úteis no seu código de computador, mas encontrar o oposto diariamente.

Alguns princípios de desenvolvimento de software fantásticos têm sido capturados sob o acrônimo SÓLIDO—Reponsabilidade única, Aberta à extensão e fechada para modificação, Substituição de Liskov, Segregação de interface e Injeção de dependência. Você deve ter alguma familiaridade com estes princípios, visto que vou demonstrar uma variedade de padrões específicos do C# que violam estes princípios. Se você não esta familiarizado com os princípios SÓLIDOS, talvez queira revisá-los rapidamente antes de prosseguir. Além disso, eu suponho que você esteja familiarizado com os termos arquiteturais Model e ViewModel.

O acrônimo SÓLIDO e os princípios abrangidos não se originaram comigo. Obrigado, Robert C. Martin, Michael Feathers, Bertrand Meyer, James Coplien e outros por compartilhar sua sabedoria conosco e todos os demais. Muitos outros livros e postagens em blogs têm explorado e refinado estes princípios. Espero ajudar a amplificar a aplicação destes princípios.

Tendo trabalhado e treinado vários engenheiros de software juniores, descobri que tem uma grande lacuna entre os esforços da primeira codificação profissional e o código sustentável. Neste artigo, tentarei diminuir esta lacuna de forma leve. Os exemplos são um pouco bobos com o objetivo de ajudá-lo a reconhecer que você pode aplicar os princípios SÓLIDOS a todas as formas de software.

O ambiente de desenvolvimento profissional proporciona muitos desafios para os ambiciosos engenheiros de software. Na escola você aprendeu a pensar sobre os problemas a partir de uma perspectiva vista de cima. Você usará a abordagem vista de cima para suas tarefas iniciais no mundo cordial do software de dimensões corporativas. Logo você descobrirá que a sua função de nível superior se tornou enorme. Para fazer uma pequena alteração é necessário um conhecimento de trabalho completo de todo o sistema e há pouco para controlá-lo. Orientar os princípios de software (do qual somente um conjunto parcial é mencionado aqui) ajudará a impedir que a estrutura ultrapasse o limite de sua fundação.

O princípio da responsabilidade única

O princípio de responsabilidade única é geralmente definido como: Um objeto deve ter apenas um motivo para mudar; quanto maior o arquivo ou a classe, mais difícil será para ele atingir isto. Com esta definição em mente, observe este código:

public IList<IList<Nerd>> ComputeNerdClusters(
  List<Nerd> nerds,
  IPlotter plotter = null) {
  ...
  foreach (var nerd in nerds) {
    ...
    if (plotter != null)
      plotter.Draw(nerd.Location, 
      Brushes.PeachPuff, radius: 10);
    ...
  }
  ...
}

O que há de errado com esse código? O software está sendo gravado ou depurado? Pode ser que este código de desenho em particular seja destinado somente para fins de depuração. É bom que ele esteja em um serviço conhecido somente pela interface, mas ele não pertence. O pincel é um bom sinal. Tão bonito e difundido como os bolos de pêssego podem ser, é específico da plataforma. Esta fora do tipo de hierarquia deste modelo computacional. Há várias maneiras de segregar a computação e os utilitários de depuração associados. No mínimo, você pode expor os dados necessários por meio de herança ou eventos. Mantenha os testes e exibições de testes separados.

Aqui está outro exemplo de falha:

class Nerd {
  public int IQ { get; protected set; }
  public double SuspenderTension { get; set; }
  public double Radius { get; protected set; }
  /// <summary>Get books for growing IQ</summary>
  public event Func<Nerd, IBook> InTheMoodForBook;
  /// <summary>Get recommendations for growing Radius</summary>
  public event Func<Nerd, ISweet> InTheMoodForTwink;
  public IList<Nerd> FitNerdsIntoPaddedRoom(
    IList<Nerd> nerds, IList<Point> boundary)
  {
    ...
  }
}

O que há de errado com esse código? Ele mistura o que é chamado de "disciplinas escolares". Você lembra como aprendeu sobre assuntos diferentes em classes diferentes na escola? É importante manter esta separação no código—não porque eles são totalmente desvinculados, mas como uma esforço organizacional. Em geral, não coloque nenhum destes dois itens na mesma classe: matemática, modelos, gramáticas, exibições, física ou adaptadores de plataforma, código específico do cliente e assim por diante.

Você pode ver uma analogia geral para as coisas que você criou na escola com escultura, madeira e metal. Elas precisam de medidas, análise, instrução e assim por diante. O exemplo anterior mescla matemática e modelo—FitNerdsIntoPaddedRoom não pertence. Aquele método pode ser movido facilmente para uma classe de utilitários, até mesmo de estatística. Você não deveria ter de criar uma instância de modelos nas suas rotinas de testes de matemática.

Aqui está outro exemplo de responsabilidades múltiplas:

class AvatarBotPath
{
  public IReadOnlyList<ISegment> Segments { get; private set; }
  public double TargetVelocity { get; set; }
  public bool IsReverse { get { return TargetVelocity < 0; } }
  ...
}
public interface ISegment // Elsewhere
{
  Point Start { get; }
  Point End { get; }
  ...
}

O que há de errado aqui? Claramente há duas abstrações diferentes representadas por um único objeto. Uma delas está relacionada ao ato de atravessar uma forma, a outra representa a própria forma geométrica. Isto é comum em código. Você tem a representação e parâmetros específicos de uso separados que vão com aquela representação.

A herança é sua amiga aqui. Você pode mover as propriedades TargetVelocity e IsReverse para um herdeiro e capturá-las em uma interface concisa de IHasTravelInfo. Como alternativa, você pode adicionar uma coleção geral de recursos à forma. Esses que precisam de velocidade consultariam a coleção de recursos para ver se está definida em uma forma em particular. Além disso, você pode usar outro mecanismo de coleção para emparelhar representações com parâmetros de viagem.

O Princípio do Aberto e Fechado

Isto nos transporta para o próximo princípio: aberto à extensão, mas fechado à modificação. Como isto é feito? De preferência não assim:

void DrawNerd(Nerd nerd) {
  if (nerd.IsSelected)
    DrawEllipseAroundNerd(nerd.Position, nerd.Radius);
  if (nerd.Image != null)
    DrawImageOfNerd(nerd.Image, nerd.Position, nerd.Heading);
  if (nerd is IHasBelt) // a rare occurrence
    DrawBelt(((IHasBelt)nerd).Belt);
  // Etc.
}

O que há de errado aqui? Bem, você terá de modificar este método todas as vezes que um cliente precisar que coisas novas sejam exibidas, e eles sempre precisam de coisas novas exibidas. Quase todos os novos recursos de software exigem algum tipo de elemento de interface do usuário. Afinal, foi a falta de alguma coisa na interface existente que solicitou a nova solicitação de recurso. O padrão exibido neste método é um bom sinal, mas você pode mover estas instruções if para os métodos que eles guardam e isto não fará com que o problema desapareça.

Você precisa de um plano melhor, mas como? Como ele se parecerá? Bem, você tem alguns códigos que sabem como desenhar determinadas coisas. Tudo bem. Você apenas precisa de um procedimento geral para corresponder estas coisas com o código para desenhá-los. Essencialmente se resumirá em um padrão como este:

readonly IList<IRenderer> _renderers = new List<IRenderer>();
void Draw(Nerd nerd)
{
  foreach (var renderer in _renderers)
    renderer.DrawIfPossible(_context, nerd);
}

Há outras maneiras de adicionar à lista de processadores. O ponto do código, no entanto, é escrever as classes drawing (ou classes sobre classes drawing) que implementam uma interface bem conhecida. O processo deve ter as percepções para determinar se ele pode ou deve desenhar qualquer coisa na sua entrada. Por exemplo, o código belt-drawing pode mover para seu próprio "belt renderer" que verifica a interface e prossegue, se necessário.

Você pode precisar separar o método CanDraw do método Draw, mas isso não violará o princípio de Aberto e Fechado ou OCP. O código que está usando os processadores não deve alterar se você adicionar um novo processador. É simples assim. Você deve também conseguir adicionar o novo processador na ordem correta. Enquanto estou usando o processamento como um exemplo, isto também se aplica à manipulação de entrada, processamento de dados e armazenamento de dados. Este princípio tem várias aplicações através de todos os tipos de software. O padrão é mais difícil para emular no Windows Presentation Foundation (WPF), mas é possível. Veja a Figura 1 para uma possível opção.

Figura 1 Exemplo de mesclagem de processadores do Windows Presentation Foundation em uma classe

public abstract class RenderDefinition : ViewModelBase
{
  public abstract DataTemplate Template { get; }
  public abstract Style TemplateStyle { get; }
  public abstract bool SourceContains(object o); // For selectors
  public abstract IEnumerable Source { get; }
}
public void LoadItemsControlFromRenderers(
    ItemsControl control,
    IEnumerable<RenderDefinition> defs) {
  control.ItemTemplateSelector = new DefTemplateSelector(defs);
  control.ItemContainerStyleSelector = new DefStyleSelector(defs);
  var compositeCollection = new CompositeCollection();
  foreach (var renderDefinition in defs)
  {
    var container = new CollectionContainer
    {
      Collection = renderDefinition.Source
    };
    compositeCollection.Add(container);
  }
  control.ItemsSource = compositeCollection;
}

Aqui está outro exemplo de falta:

class Nerd
{
  public void WriteName(string name)
  {
    var pocketProtector = new PocketProtector();
    WriteNameOnPaper(pocketProtector.Pen, name);
  }
  private void WriteNameOnPaper(Pen pen, string text)
  {
    ...
  }
}

O que há de errado aqui? Os problemas com este código são vastos e diversos. O principal problema que quero salientar é que não tem forma de substituir a criação da instância PocketProtector. Um código como este torna difícil escrever herdeiros. Você tem algumas opções para lidar com este cenário. Você pode executar o código para:

  • Tornar o método WriteName virtual. Isto também exigiria que você tornasse o WriteNameOnPaper protegido para alcançar o objetivo de criar um protetor de bolso modificado.
  • Tornar o método WriteNameOnPaper público, porém isso manterá o método WriteName quebrado em seus herdeiros. Esta não é uma boa opção a menos que você se livre do WriteName, nesse caso a opção devolve na passagem uma instância do PocketProtector para o método.
  • Adicione um método virtual protegido adicional cujo único fim é construir o PocketProtector.
  • Forneça à classe um tipo T genérico que é um tipo de PocketProtector e construa-o com algum tipo de objeto provedor. Então você terá a mesma necessidade para injetar a fábrica do objeto.
  • Transmita uma instância do PocketProtector para esta classe no seu construtor ou através de uma propriedade pública, ao invés de construí-la dentro da classe.

A última opção listada é geralmente o plano melhor, supondo que você possa reutilizar o PocketProtector. O método de criação virtual é também uma opção boa e fácil.

Você deve considerar quais métodos tornar virtual para acomodar o OCP. Essa decisão é geralmente deixada até o último minuto: "Farei os métodos virtuais quando precisar chamá-los de um herdeiro que não tenho no momento." Outros podem escolher tornar todos os métodos virtuais, esperando que isso permitirá aos extensores contornar qualquer supervisão no código inicial.

Ambas as abordagens estão incorretas. Elas exemplificam uma incapacidade de se comprometer com uma interface aberta. Ter muitos métodos virtuais limita a sua capacidade de alterar o código mais tarde. Uma falta de métodos que você possa substituir limita a extensibilidade e reutilização do código. Isso limita sua utilidade e tempo de vida.

Aqui está outro exemplo comum de violações do OCP:

class Nerd
{
  public void DanceTheDisco()
  {
    if (this is ChildOfNerd)
            throw new CoordinationException("Can't");
    ...
  }
}
class ChildOfNerd : Nerd { ... }

O que há de errado aqui? O Nerd tem uma referência difícil para seu tipo filho. O que é doloroso de ver e um erro comum infeliz para desenvolvedores juniores. Você pode ver ele violando o OCP. Você deve modificar várias classes para melhorar ou refatorar o ChildOfNerd.

As classes base não devem nunca fazer referência diretamente aos seus herdeiros. A funcionalidade de herdeiro não é então mais consistente entre os herdeiros. Uma ótima maneira de evitar este conflito é colocar os herdeiros de uma classe em projetos separados. Desta maneira, a estrutura da árvore de referência do projeto impedirá este cenário indesejado.

Este problema não está limitado aos relacionamentos pai-filho. Também existe com colegas de classe. Suponha que você tenha algo assim:

class NerdsInAnArc
{
  public bool Intersects(NerdsInAnLine line)
  {
    ...
  }
  ...
}

Os arcos e linhas são geralmente colegas em uma hierarquia de objetos. Eles não devem saber de nenhum detalhe íntimo não herdado um do outro, visto que estes detalhes geralmente são necessários para algoritmos com intersecção ideal. Mantenha-se livre para modificar um sem ter de modificar o outro. Isto traz novamente uma violação da responsabilidade única. Você está armazenando arcos ou analisando-os? Colocar as operações de análise em sua própria classe de utilitários.

Se você precisar desta capacidade entre colegas em particular, então você precisará introduzir uma interface apropriada. Siga esta regra para evitar a confusão entre entidades: Você deve usar a palavra-chave "is" com uma abstração ao invés de uma classe concreta. Você pode possivelmente criar uma interface IIntersectable ou INerdsInAPattern para o exemplo, embora você continuaria a defender algumas outras classes de utilitários de intersecção para analisar dados expostos naquela interface.

O princípio da substituição de Liskov

O princípio de substituição de Liskov define algumas instruções para manter a substituição do herdeiro. Transferir um herdeiro de objetos em lugar da classe base não deve quebrar nenhuma funcionalidade existente no método chamado. Você deve ser capaz de substituir todas as implementações de uma determinada interface entre si.

O C# não permite modificar tipos de parâmetros ou tipos de retorno ao substituir métodos (mesmo se o tipo de retorno é um herdeiro do tipo de retorno na classe base). No entanto, ele não luta contra as violações de substituições mais comuns: contravariância de argumentos de método (os substituidores devem ter o mesmo ou tipos de base de métodos pai) e a covariância de tipos de retorno (tipos de retorno ao substituir métodos devem ser os mesmos ou um herdeiro de tipos de retorno na classe de base). No entanto, é comum tentar contornar esta limitação:

class Nerd : Mammal {
  public double Diopter { get; protected set; }
  public Nerd(int vertebrae, double diopter)
    : base(vertebrae) { Diopter = diopter; }
  protected Nerd(Nerd toBeCloned)
    : base (toBeCloned) { Diopter = toBeCloned.Diopter; }
  // Would prefer to return Nerd instead:
  // public override Mammal Clone() { return new Nerd(this); }
  public new Nerd Clone() { return new Nerd(this); }
}

O que há de errado aqui? O comportamento do objeto modifica quando chamado com uma referência de abstração. O novo método clone não é virtual e no entanto não é executando ao usar uma referência Mammal. A palavra-chave nova no contexto de declaração do método é supostamente um recurso. Se você não controlar a classe base, então, como você pode garantir uma execução correta?

O C# tem algumas alternativas viáveis, embora elas continuem desagradáveis de alguma forma. Você pode usar uma interface genérica (algo como IComparable<T>) para explicitamente implementar cada herdeiro. No entanto, você ainda precisa de um método virtual que realize a operação de clone real. Você precisa disso para que seu clone corresponda ao tipo derivado. O C# também oferece suporte ao padrão Liskov na contravariância dos tipos de retorno e na covariância dos argumentos do método ao usar eventos, mas isso não ajudará você a alterar a interface exposta por meio da herança de classe.

A julgar a partir daquele código, você pode pensar que o C# inclui o tipo de retorno na superfície do método que o resolvedor de método de classe está usando. Isto está incorreto, você não pode ter várias substituições com tipos de retorno diferentes, mas o mesmo nome e tipos de entrada. As restrições do método também são ignoradas para a resolução do método. Figura 2 mostra um exemplo de um código sinteticamente correto que não será compilado devido à ambiguidade do método.

Figura 2 Superfície do método ambígua

interface INerd {
  public int Smartness { get; set; }
}
static class Program
{
  public static string RecallSomeDigitsOfPi<T>(
    this IList<T> nerdSmartnesses) where T : int
  {
    var smartest = nerdSmartnesses.Max();
    return Math.PI.ToString("F" + Math.Min(14, smartest));
  }
  public static string RecallSomeDigitsOfPi<T>(
    this IList<T> nerds) where T : INerd
  {
    var smartest = nerds.OrderByDescending(n => n.Smartness).First();
    return Math.PI.ToString("F" + Math.Min(14, smartest.Smartness));
  }
  static void Main(string[] args)
  {
    IList<int> list = new List<int> { 2, 3, 4 };
    var digits = list.RecallSomeDigitsOfPi();
    Console.WriteLine("Digits: " + digits);
  }
}

O código na Figura 3 mostra como a capacidade de substituir pode estar quebrada. Considere seus herdeiros. Um deles pode modificar o campo isMoonWalking aleatoriamente. Se isso acontecesse, a classe base executaria o risco de perder uma seção de limpeza crítica. O campo isMoonWalking deve ser privado. Se os herdeiros precisam saber, deve ter uma propriedade com um getter protegido que fornece acesso, mas não modificação.

Figura 3 Um exemplo de como a capacidade de substituir pode estar quebrada

class GrooveControl: Control {
  protected bool isMoonWalking;
  protected override void OnMouseDown(MouseButtonEventArgs e) {
    isMoonWalking = CaptureMouse();
    base.OnMouseDown(e);
  }
  protected override void OnMouseUp(MouseButtonEventArgs e) {
    base.OnMouseUp(e);
    if (isMoonWalking) {
      ReleaseMouseCapture();
      isMoonWalking = false;
    }
  }
}

De forma inteligente e ocasional programadores pretensiosos avançarão mais adiante. Sele os manipuladores de mouse (ou qualquer outro método que depende do estado privado ou modifica-o) e permita que os herdeiros usem eventos ou outros métodos virtuais que não são métodos que devem ser chamados. O padrão para exigir uma chamada base é admissível, mas não ideal. Todos nós esquecemos de chamar métodos base esperados em determinadas ocasiões. Não permita que os herdeiros quebrem o estado encapsulado.

A substituição de Liskov também exige que os herdeiros não emitam novos tipos de exceção (embora os herdeiros de exceções que já emitiram na classe base estejam bem). O C# não tem como reforçar isto.

O princípio da Segregação de interface

Cada interface deve ter um fim específico. Você não deve ser forçado a implementar uma interface quando seu objeto não compartilha aquele fim. Por extrapolação, quanto maior a interface, mais provável que ela inclua métodos que nem todos os implementadores possam alcançar. Esta é a essência do princípio de Segregação de interface. Considere uma interface casada comum e antiga do Microsoft .NET Framework:

public interface ICollection<T> : IEnumerable<T> {
  void Add(T item);
  void Clear();
  bool Contains(T item);
  void CopyTo(T[] array, int arrayIndex);
  bool Remove(T item);
}
public interface IList<T> : ICollection<T> {
  T this[int index] { get; set; }
  int IndexOf(T item);
  void Insert(int index, T item);
  void RemoveAt(int index);
}

As interfaces ainda são de alguma forma úteis, mas há uma suposição implícita que se você está usando estas interfaces, você deseja modificar as coleções. Muitas vezes, quem quer que crie estas coleções de dados deseja impedir qualquer um de modificar os dados. É na verdade muito útil separar interfaces em fontes e consumidores.

Muitos repositórios de dados gostariam de compartilhar uma interface comum, não gravável e indexável. Considere o software de análise de dados ou de pesquisa de dados. Eles geralmente leem em um arquivo de log grande ou em uma tabela de banco de dados para análise. Modificar os dados que nunca foram parte da agenda.

Admito que a interface IEnumerable foi destinada a ser a interface somente para leitura, mínima. Com a adição dos métodos de extensão LINQ, ela começou a cumprir aquele destino. A Microsoft também reconheceu a lacuna das interfaces de coleções indexáveis. A empresa tem abordado isto na versão 4.5 do .NET Framework com a adição do IReadOnlyList<T>, agora implementado por várias coleções de estrutura.

Você lembrará destas coisas boas na interface ICollection antiga:

public interface ICollection : IEnumerable {
  ...
  object SyncRoot { get; }
  bool IsSynchronized { get; }
  ...
}

Em outras palavras, antes que você possa iterar a coleção, você deve primeiro potencialmente bloquear no SyncRoot. Um número de herdeiros até implementaram estes itens em particular explicitamente somente para ajudar a ocultar sua vergonha de ter que implementá-los. A expectativa em cenários multi-threaded se manifestou para que você bloqueie na coleção em qualquer local que você usá-la (ao invés de usar o SyncRoot).

A maioria de vocês deseja encapsular suas coleções para que possam ser acessadas de uma maneira segura no ambiente multi-thread. Em vez de usar o foreach, você deve encapsular os repositórios de dados multi-threaded e somente expor um método ForEach que usa em vez disso um representante. Felizmente, as novas classes de coleção como as coleções atuais no .NET Framework 4 ou as coleções imutáveis agora disponíveis para o .NET Framework 4.5 (por meio do NuGet) têm eliminado muito desta loucura.

A abstração .NET Stream compartilha as mesmas falhas de ser um pouco grande demais, incluindo elementos graváveis e legíveis e sinais de sincronização. No entanto, ela possui propriedades para determinar a capacidade de ser gravável: CanRead, CanWrite, CanSeek e assim por diante. Compare if (stream.CanWrite) com if (stream is IWritableStream). Para aqueles entre vocês que estão criando fluxos que são graváveis, o último é certamente apreciado.

Agora, veja o código da Figura 4.

Figura 4 Um exemplo de inicialização e limpeza desnecessárias

// Up a level in the project hierarchy
public interface INerdService {
  Type[] Dependencies { get; }
  void Initialize(IEnumerable<INerdService> dependencies);
  void Cleanup();
}
public class SocialIntroductionsService: INerdService
{
  public Type[] Dependencies { get { return Type.EmptyTypes; } }
  public void Initialize(IEnumerable<INerdService> dependencies)
  { ... }
  public void Cleanup() { ... }
  ...
}

Qual é o problema aqui? A inicialização e limpeza do serviço deve vir por meio de um dos fantásticos contêineres de inversão de controle (IoC) disponíveis comumente para o .NET Framework, ao invés de ser reinventado. Por causa do exemplo, ninguém se preocupa com a inicialização e limpeza além do gerenciador de serviços/­contêiner/boostrapper, seja qual for o código que carrega estes serviços. Este é o código que importa. Você não quer que mais ninguém chame a Limpeza antecipadamente. O C# tem um mecanismo chamado implementação explícita para ajudar com isso. Você pode implantar o serviço mais corretamente desta maneira:

public class SocialIntroductionsService: INerdService
{
  Type[] INerdService.Dependencies { 
    get { return Type.EmptyTypes; } }
  void INerdService.Initialize(IEnumerable<INerdService> dependencies)
  { ... }
  void INerdService.Cleanup() {       ... }
  ...
}

Geralmente, você deseja projetar suas interfaces com algum outro objetivo além da abstração pura de uma única classe concreta. Isto fornece a você os meios para organizar e ampliar. No entanto, há pelo menos duas exceções notáveis.

Primeiro, as interfaces tendem a se modificar com menos frequência do que suas implementações concretas. Você pode usar isto para sua vantagem. Coloque as interfaces em um assembly separado. Deixe que os consumidores façam referência somente ao assembly da interface. Ajuda na velocidade de compilação. Ajuda a evitar que você coloque propriedades na interface que são diferentes (porque tipos de propriedades inapropriadas não estão disponíveis com uma hierarquia de projeto apropriada). Se abstrações e interferências correspondentes estão no mesmo arquivo, algo deu errado. As Interfaces se enquadram na hierarquia do projeto como pais de suas implementações e colegas dos serviços (ou abstrações dos serviços) que os usam.

Segundo, por definição, as interfaces não têm nenhuma dependência. Portanto, elas são propícias para facilitar os testes de unidade por meio de simulação de objetos/estruturas de proxy. Isto me conduz ao próximo e último princípio.

O princípio de inversão de dependência

Inversão de dependência significa depender de abstrações ao invés de tipos concretos. Há várias sobreposições entre este princípio e os outros já discutidos. Muitos dos exemplos anteriores incluem uma falha para depender de abstrações.

Neste livro, "Domain Driven Design" (Addison-Wesley Professional, 2003), Eric Evans descreve algumas classificações de objetos que são úteis na discussão da Inversão de dependência. Para resumir o livro, é útil classificar o seu objeto em um destes três grupos: valores, entidades ou serviços.

Os valores se referem a objetos sem dependências que são geralmente transitórios e imutáveis. Eles geralmente não são abstratos e você pode criar instâncias deles conforme sua vontade. No entanto, não há nada de errado em abstraí-los especialmente se você pode obter todos os benefícios das abstrações. Alguns valores podem evoluir para entidades com o tempo. As entidades são os Models e ViewModels de sua empresa. São construídos de tipos de valores e outras entidades. É útil ter abstrações para estes itens, especialmente se você tem um ViewModel que representa diversas variantes diferentes de um Model ou vice-versa. Os serviços são as classes que contêm, organizam, prestam serviço e usam as entidades.

Com esta classificação em mente, a Inversão de dependência trata principalmente de serviços e objetos que precisam deles. Métodos específicos do serviço devem sempre ser capturados em uma interface. Sempre que você precisar acessar aquele serviço, você o acessa por meio da interface. Não use um tipo de serviço concreto no seu código em nenhum lugar, exceto onde o serviço foi construído.

Os serviços geralmente dependem de outros serviços. Alguns ViewModels dependem de serviços, especialmente serviços do tipo fábrica e contêiner. No entanto, os serviços são geralmente difíceis de criar instâncias para testar porque você precisa da árvore de serviço completo. Abstrair sua essência em uma interface. Depois todas as referências aos serviços devem ser feitas através daquela interface para que possam ser facilmente simuladas para fins de testes.

Você pode criar abstrações em qualquer nível no código. Quando você se encontrar pensando, "Uau, vai ser dolorido para A fornecer suporte para a interface de B e B fornecer suporte para a interface de A," este é o momento perfeito para introduzir uma nova abstração no centro. Torne as interfaces utilizáveis e confie nelas.

Os padrões adaptador e mediador podem ajudá-lo a estar em conformidade com a interface preferida. Parece que abstrações extras trazem código extra, mas geralmente isto não é verdade. A tomada de medidas parciais em direção a interoperabilidade ajuda você a organizar o código que teria de existir para A e B para falar um com outro.

Anos atrás, eu li que um desenvolvedor deveria "sempre reutilizar o código". Parecia tão simples naquela época. Eu não poderia acreditar que um simples mantra poderia penetrar o espaguete sobre a minha tela. Com o tempo, no entanto, eu aprendi. Veja o código aqui:  

private readonly IRamenContainer _ramenContainer; // A dependency
public bool Recharge()
{
  if (_ramenContainer != null)
  {
    var toBeConsumed = _ramenContainer.Prepare();
    return Consume(toBeConsumed);
  }
  return false;
}

Você vê qualquer código repetido? Há a leitura dupla no _ramenContainer. Tecnicamente falando, o compilador eliminará isto com uma otimização chamada "eliminação da subexpressão comum". Para discussão, suponha que você estava executando uma situação multi-threaded e o compilador na verdade repetiu as leituras do campo da classe no método. Você correria o risco de que sua variável de classe seja alterada para nulo antes mesmo de ser usada.

Como resolver isso? Introduza uma referência local acima da instrução if. Esta reorganização exige que você adicione um novo item no escopo externo ou acima dele. O princípio é o mesmo na organização de seu projeto! Quando você reutiliza código ou abstrações, você eventualmente chega a um escopo útil na hierarquia de seu projeto. Deixe que as dependências acionem a hierarquia de referência entre projetos.

Agora, olhe para este código:

public IList<Nerd> RestoreNerds(string filename)
{
  if (File.Exists(filename))
  {
    var serializer = new XmlSerializer(typeof(List<Nerd>));
    using (var reader = new XmlTextReader(filename))
      return (List<Nerd>)serializer.Deserialize(reader);
  }
  return null;
}

Ele é dependente de abstrações?

Não, não é. Ele começa com uma referência estática do sistema de arquivos. Está usando um desserializador codificado com referências tipo codificadas. Espera-se que o tratamento de exceção ocorra fora da classe. É impossível testar este código sem o código de armazenamento que acompanha.

Geralmente, você moveria isto em duas abstrações: uma para o formato do armazenamento e outra para o meio de armazenamento. Alguns exemplos de formatos de armazenamento incluem XML, JSON e dados binários Protobuf. Os meios de armazenamento incluem arquivos diretos em um disco e banco de dados. Uma terceira abstração é também característica neste tipo de sistema: algum tipo de memento de mudança raro representando o objeto a ser armazenado.

Considere este exemplo:

class MonsterCardCollection
{
  private readonly IMsSqlDatabase _storage;
  public MonsterCardCollection(IMsSqlDatabase storage)
  {
    _storage = storage;
  }
  ...
}

Você consegue ver algo de errado com estas dependências? A pista esta no nome da dependência. É específico da plataforma. O serviço não é específico da plataforma (ou pelo menos está tentando evitar uma dependência de plataforma ao usar um mecanismo de armazenamento externo). Esta é uma situação em que você precisa empregar o padrão adaptador.

Quando as dependências são específicas da plataforma, os dependentes terminarão com seus próprios códigos específicos da plataforma. Você pode evitar isso com uma camada adicional. A camada adicional ajudará você a organizar os projetos de tal maneira que a implementação específica da plataforma exista em seu próprio projeto especial (com todas as suas referências específicas da plataforma). Você somente precisará fazer referência ao projeto que contém todos os códigos específicos da plataforma no início do projeto do aplicativo. Os invólucros de plataforma tendem a ser grandes; não os duplique mais do que necessário.

A Inversão de dependência une o todo o conjunto de princípios discutido neste artigo. Ele usa abstrações limpas, intencionais que você pode preencher com implementações concretas que não quebram o estado do serviço subjacente. Esse é o objetivo.

Realmente, os princípios SÓLIDOS estão geralmente sobrepondo-se em seus efeitos sobre o código de computador sustentável. O mundo enorme do código intermediário (o que significa descompilado de forma fácil) é fantástico em sua capacidade de revelar a extensão completa para a qual é possível ampliar qualquer objeto. Um número de projetos da biblioteca .NET desaparecem com o tempo. Isto não ocorre porque a ideia foi falha; eles apenas não puderam ampliar com segurança as necessidades variadas e antecipadas do futuro. Fique orgulhoso de seu código. Aplique os princípios SÓLIDOS e você verá o tempo de vida de seu código aumentar.

Brannon B. King tem trabalhado como um desenvolvedor de software em período integral por 12 anos, oito deles dedicados profundamente ao C# e ao .NET Framework. Seu trabalho mais recente foi com a Autonomous Solutions Inc. (ASI) próximo a Logan, Utah (asirobots.com). A ASI é a única com capacidade de promover um amor contagioso do C#; a equipe da ASI adota com paixão o uso da linguagem de forma completa e a extensão do .NET Framework aos seus limites. Entre em contato com ele pelo email countprimes@gmail.com.

Agradecemos aos seguintes especialistas técnicos pela revisão deste artigo: Max Barfuss (ASI) e Brian Pepin (Microsoft)
Brian Pepin tem trabalhado como um engenheiro de software na Microsoft Corporation desde 1994, com foco principalmente nas APIs do desenvolvedor e ferramentas. Ele tem trabalhado no Visual Basic, Java, .NET Framework, Windows Forms, WPF, Silverlight e no designer do Windows 8 XAML no Visual Studio. Atualmente ele trabalha na equipe do Xbox com foco nos componentes do sistema operacional do Xbox e gosta de passar seu tempo livre na área de Seattle com sua esposa Danna e seu filho Cole.
Max Barfuss é um profissional de fabricação de software dedicado à crença de que hábitos bons de código, design e comunicação são coisas que diferenciam grandes engenheiros de software do restante. Ele tem dezesseis anos de experiência em desenvolvimento de software, incluindo onze no campo do .NET.