Novembro de 2016

Volume 31 - Número 11

.NET Framework - Descartáveis Ocultos

De Artak Mkrtchyan | Novembro de 2016

Os tipos descartáveis são ótimos já que eles deixam você liberar recursos de uma maneira determinística. No entanto, existem situações em que os desenvolvedores trabalham com tipos descartáveis sem nem ao menos perceber. O uso de padrões de design de criação é um exemplo de uma situação em que o uso de um tipo descartável pode não ser óbvio, o que pode levar a um objeto não ser descartado. Este artigo mostrará maneiras de lidar com o problema. Começarei pela revisão dos padrões de design de criação.

Padrões de design de criação

Um ótimo benefício dos padrões de design de criação é que eles se abstraem das implementações reais e “conversam” na linguagem da interface. Eles lidam com mecanismos de criação de objetos para criar objetos adequados à solução. Em comparação à criação do objeto básico, os padrões de design de criação aprimoram diversos aspectos do processo de criação do objeto. Veja dois benefícios bem conhecidos dos padrões de design de criação:

  • Abstração: Abstraem o tipo de objeto que está sendo criado, de forma que o chamador não saiba o que o objeto real sendo retornado é, pois só estão cientes da interface.
  • Elementos internos de criação: Eles encapsulam o conhecimento sobre a criação de instância de tipo específico.

Em seguida, farei uma breve visão geral de dois padrões de design de criação bem conhecidos.

O padrão de design Método de Fábrica O padrão de design Método de Fábrica é um dos meus favoritos e eu o utilizo muito em meu trabalho diário. Esse padrão usa os métodos de fábrica para lidar com o problema da criação de objetos sem especificar a classe exata do objeto criado. Em vez de chamar um construtor de classes diretamente, você chama um método de fábrica para criar o objeto. O método de fábrica retorna uma abstração (interface ou uma classe base) que as classes filhas implementam. A Figura 1 mostra o diagrama UML (Unified Modeling Language) para este padrão.

Na Figura 1, o ConcreteProduct é um tipo específico da abstração/interface IProduct. Da mesma forma, o ConcreteCreator é uma implementação específica da interface ICreator.

Padrão de Design Método de Fábrica
Figura 1 Padrão de Design Método de Fábrica

O cliente desse padrão usa uma instância do ICreator e chamará seu método Create para obter uma nova instância de IProduct, sem saber qual produto real foi retornado.

O padrão de Design Fábrica Abstrata A meta do padrão de design Fábrica Abstrata é fornecer uma interface para a criação de famílias de objetos relacionados ou dependentes sem especificar implementações concretas.

Isso protege o código do cliente da confusão da criação de objetos ao fazer o cliente solicitar que o objeto de fábrica crie um objeto do tipo abstrato desejado e retorne um ponteiro abstrato para o objeto de volta ao cliente. Em particular, isso significa que o código do cliente não tem conhecimento sobre o tipo concreto. Ele lida somente com um tipo abstrato.

A adição de suporte para novos tipos concretos é manipulada pela criação de novos tipos de fábrica e pela modificação do código do cliente para usar um tipo de fábrica diferente conforme necessário. Na maioria dos casos, essa é uma alteração de código de uma linha. Isso obviamente simplifica a manipulação de alterações, já que o código do cliente não precisa ser alterado para acomodar o novo tipo de fábrica. A Figura 2 mostra o diagrama UML para o padrão de design Fábrica Abstrata.

Padrão de Design Fábrica Abstrata
Figura 2 Padrão de Design Fábrica Abstrata

Da perspectiva de um cliente, o uso da Fábrica Abstrata é representado pelo seguinte trecho de código:

IAbstractFactory factory = new ConcreteFactory1();
IProductA product = factory.CreateProductA();

O cliente está livre para modificar a implementação da fábrica real para controlar o tipo de produto criado nos bastidores e isso não terá absolutamente qualquer impacto no código.

Este código é apenas uma amostra. Em um código adequadamente estruturado, a própria instanciação de fábrica provavelmente seria abstraída, com um padrão de método de fábrica como exemplo.

O problema

Em ambos os exemplos de padrão de design, houve uma fábrica envolvida. Uma fábrica é o método/procedimento real, que retorna uma referência de tipo construído por meio de uma abstração em resposta à chamada do cliente.

Tecnicamente, você pode usar uma fábrica para criar um objeto em qualquer lugar onde existir uma abstração, conforme mostrado na Figura 3.

Exemplo de Abstração Simples e seu Uso
Figura 3 Exemplo de Abstração Simples e seu Uso

A fábrica lida com a opção entre as diferentes implementações disponíveis tendo como base os fatores envolvidos.

De acordo com o princípio de Inversão de Dependência:

  • Os módulos de alto nível não devem ser dependentes de módulos de baixo nível. Ambos devem depender das abstrações.
  • As abstrações não devem depender de detalhes. Os detalhes devem depender das abstrações.

Isso, tecnicamente falando, significa que em todos os níveis de uma cadeia de dependência, a dependência deve ser substituída por uma abstração. Além disso, a criação dessas abstrações pode e, na maioria dos casos, deve ser manipulada por meio de fábricas.

Tudo isso enfatiza a importância das fábricas na codificação diária. Entretanto, na verdade elas estão ocultando o problema: os tipos descartáveis. Antes de chegarmos a esses detalhes, primeiro falarei sobre a interface IDisposable e sobre o padrão de design Dispose.

Padrão de Design Dispose

Todos os programas adquirem recursos como memória, indicadores de arquivo e conexões de banco de dados durante sua execução. Os desenvolvedores têm de ser cuidadosos ao usar tais recursos, já que os recursos devem ser liberados depois de adquiridos e usados.

O CLR (Common Language Runtime) oferece suporte para o gerenciamento automático de memória por meio do GC (garbage collector, coletor de lixo). Você não precisa limpar explicitamente a Memória Gerenciada porque o GC fará isso automaticamente. Infelizmente, há outros tipos de recursos (conhecidos como Recursos Não Gerenciados) que ainda precisam ser explicitamente liberados. O GC não foi projetado para lidar com esses tipos de recursos e, portanto, é responsabilidade do desenvolvedor liberá-los.

Ainda assim, o CLR ajuda os desenvolvedores a lidar com os recursos não gerenciados. O tipo System.Object define um método virtual público, chamado Finalize, que é chamado pelo GC antes que a memória do objeto seja recuperada. O método Finalize normalmente é conhecido como finalizador. Você pode substituir o método para limpar recursos não gerenciados adicionais usados pelo objeto.

Esse mecanismo, entretanto, tem algumas desvantagens devido a determinados aspectos da execução do GC.

O finalizador é chamado quando o GC detecta que um objeto está qualificado para a coleta. Isso acontece em um período indeterminado depois que o objeto não é mais necessário.

Quando o GC precisa chamar o finalizador, ele precisa adiar a coleta de memória real para a próxima rodada de coleta de lixo. Isso adia ainda mais a coleta de memória do objeto. É aqui que entra a interface System.IDisposable. O Microsoft .NET Framework oferece a interface IDisposable que você precisa implementar para fornecer ao desenvolvedor um mecanismo para a liberação manual de recursos não gerenciados. Os tipos que implementam essa interface são chamados de tipos descartáveis. A interface IDisposable define somente um método sem parâmetros, chamado Dispose. Dispose deve ser chamado para liberar imediatamente qualquer recurso não gerenciado que ele referencia assim que o objeto não for mais necessário.

Talvez você pergunte: “por que devo chamar Dispose quando sei que o GC eventualmente lidará com isso para mim?” A resposta exige um outro artigo, separado, que também aborda aspectos do impacto da execução do GC no desempenho. Isso está além do escopo deste artigo e, portanto, vou prosseguir.

Existem determinadas regras a serem seguidas ao decidir se um tipo deve ser descartável. A regra geral é esta: Se um objeto de um determinado tipo for referenciar um recurso não gerenciado ou outro objeto descartável, então ele também deverá ser descartável.

O padrão Dispose define uma implementação específica para a interface IDisposable. Ele exige a implementação de dois métodos Dispose: um público sem parâmetros (definido pela interface IDisposable) e o outro um virtual protegido com um único parâmetro booliano. Obviamente, se o tipo tiver de ser selado, o virtual protegido deverá ser substituído por um privado.

Figura 4 Implementação do Padrão de Design Dispose

public class DisposableType : IDisposable {
  ~DisposableType() {
    this.Dispose(false);
  }
  public void Dispose() {
    this.Dispose(true);
    GC.SuppressFinalize(this);
  }
  protected virtual void Dispose(bool disposing) {
    if (disposing) {
      // Dispose of all the managed resources here
    }
    // Dispose of all the unmanaged resources here
  }
}

O parâmetro booliano indica a forma na qual o método de descarte será chamado. O método público chama o protegido com um valor de parâmetro “true”. Da mesma forma, as sobrecargas do método Dispose(bool) na hierarquia de classe deverão chamar base.Dispose(true).

A implementação do padrão Dispose também exige que o método Finalize seja sobrecarregado. Isso é feito para cobrir os cenários onde um desenvolvedor esquece de chamar o método Dispose depois que um objeto não é mais necessário. Como o finalizador está sendo chamado pelo GC, os recursos gerenciados referenciados já podem (ou vão) ser criados e, portanto, você só deverá lidar com a liberação de recursos não gerenciados quando o método Dispose(bool) for chamado a partir do finalizador.

Voltando ao tópico principal, o problema começa quando você lida com objetos descartáveis quando usados com padrões de design de criação.

Imagine um cenário em que um dos tipos concretos que implementam a abstração também implementa a interface IDisposable. Vamos supor que seja a ConcreteImplementation2 em meu exemplo, como mostrado na Figura 5.

Abstração com uma Implementação de IDisposable
figura 5 Abstração com uma Implementação de IDisposable

Observe que a interface IAbstraction propriamente dita não herda de IDisposable.

Abora examine o código do cliente onde a abstração será usada. Como a interface IAbstraction não foi alterada, o cliente não se preocupará com qualquer possível alteração nos bastidores. Naturalmente, o cliente não saberá que recebeu um objeto do qual agora é responsável pelo descarte. A realidade é que uma instância de IDisposable não é realmente esperada ali e, na maioria dos casos, aqueles objetos nunca são explicitamente descartados pelo código do cliente.

A esperança é que a implementação real da ConcreteImplementation2 implemente o padrão de design Dispose, o que nem sempre é o caso.

Agora é óbvio que o mecanismo mais simples para manipular um caso onde a instância IAbstraction retornada também implementa a interface IDisposable deveria ser para introduzir um check-in explícito no código do cliente, como mostrado aqui:

IAbstraction abstraction = factory.Create();
try {
  // Operations with abstraction go here
}
finally {
  if (abstraction is IDisposable)
    (abstraction as IDisposable).Dispose();
}

Isso, entretanto, logo se tornará um procedimento tedioso.

Infelizmente, um bloco using não pode ser usado com IAbstraction, já que não estende IDisposable explicitamente. Dessa forma, acabei com uma classe auxiliar, que envolve a lógica no bloco finally e permite que você use o bloco using também. A Figura 6 mostra o código completo da classe e também fornece um uso de exemplo.

Figura 6 O Tipo PotentialDisposable e seu Uso

public sealed class PotentialDisposable<T> : IDisposable where T : class {
  private readonly T instance;
  public T Instance { get { return this.instance; } }
  public PotentialDisposable(T instance) {
    if (instance == null) {
      throw new ArgumentNullException("instance");
    }
    this.instance = instance;
  }
  public void Dispose() {
    IDisposable disposableInstance = this.Instance as IDisposable;
    if (disposableInstance != null) {
      disposableInstance.Dispose();
    }
  }
}
The client code:
IAbstraction abstraction = factory.Create();
using (PotentialDisposable<IAbstraction> wrappedInstance =
  new PotentialDisposable<IAbstraction>(abstraction)) {
    // Operations with abstraction wrapedInstance.Instance go here
}

Como você pode ver na parte “O código do cliente” da Figura 6, o uso da classe PotentialDisposable<T> reduziu o código do cliente a apenas algumas linhas com um bloco using.

Você poderia argumentar que poderia simplesmente atualizar a interface IAbstraction e transformá-la em IDisposable. Essa pode ser a solução preferencial em algumas situações, mas não em outras.

Em uma situação em que tiver a interface IAbstraction e fizer sentido para IAbstraction estender IDisposable, você deverá fazer isso. Na verdade, um bom exemplo disso seria a classe abstrata System.IO.Stream. A classe implementa a interface IDisposable, mas não tem uma lógica real definida. O motivo é que os autores da classe sabiam que a maioria das classes filhas teria algum tipo de membro descartável.

Outra situação: Quando você faz sua própria interface IAbstraction, mas não faz sentido que ela estenda IDisposable já que a maioria das implementações dela não é descartável. Pense em uma interface ICustomCollection como exemplo. Você tem várias implementações na memória e, de repente, precisa adicionar uma implementação apoiada por um banco de dados que será a única implementação descartável.

A situação final seria quando você não possuísse uma interface IAbstraction, portanto, você não tem controle sobre ela. Considere um exemplo de ICollection, apoiada por um banco de dados.

Conclusão

A abstração obtida sendo ou não obtida por meio de um método de fábrica, é importante manter os descartes em mente ao gravar seu código do cliente. O uso dessa classe auxiliar simples é uma maneira de garantir que seu código seja o mais eficiente possível ao lidar com os objetos descartáveis.


Artak Mkrtchyan é engenheiro de software sênior e vive em Redmond, Washington. Codificar para ele é tão prazeroso quanto pescar.

Agradecemos aos seguintes especialistas técnicos da Microsoft pela revisão deste artigo: Paul Brambilla
Paul Brambilla é um desenvolvedor de software sênior da Microsoft especializado em serviços de nuvem e em infraestrutura fundamental.