C# - Criando objetos : escolhendo a melhor opção
 |
Veja as opções para criar objetos
na linguagem C# e escolha a melhor opção. |
O mundo da programação é rico e vasto em
recursos e opções e quase sempre você terá diversas maneiras de alcançar o mesmo
resultado. Com o tempo e a experiência acabamos escolhendo a opção que
geralmente trás o melhor resultado com o menor custo.


Pensando nisso o que seria mais fácil do
que criar um objeto ?
Na verdade, mesmo essa simples tarefa,
pode ser feita de pelo menos 5 formas diferentes. Então veremos cada uma delas e
vamos analisar os prós e contras de cada uma e tentar escolher a opção mais
adequada. Claro que isso pode variar para cada projeto.
1- Criando um objeto com a palavra-chave new
Essa talvez seja a maneira mais simples
de criar um objeto. Usar a palavra new.
public class
PedidoService
{
private readonly IEmailService _emailService;
public PedidoService()
{
_emailService =
new EmailService();
}
} |
Lembrando que no C# 10 temos uma nova
sintaxe para usar o new:
EmailService emailService = new();
|
Vejamos a seguir os prós e contras desta
abordagem:
Pros:
- Esta é a maneira mais fácil de
instanciar um objeto. É tão fácil quanto criar uma variável ou chamar um
método estático;
- Não há restrições sobre onde criar
um objeto com a palavra-chave new. Um objeto pode ser instanciado de
qualquer parte do aplicativo, como métodos de instância, métodos estáticos,
construtores, blocos catch etc;
- Usar a palavra-chave new é um
processo barato em termos de desempenho para objetos que não possuem uma
lógica pesada nos construtores ou objetos não muito grandes;
Contras:
- A palavra-chave new é como uma
cola. Essa maneira de criar objetos cria um acoplamento estreito entre
classes que não pode ser quebrado em tempo de execução. Abusar do new
complica a reutilização do código e os testes de unidade. Quanto mais a
palavra-chave new for usada, mais monolítica se torna a base de código;
- Não há uma maneira simples de
alterar o tempo de vida do objeto. O tempo de vida de um objeto instanciado
pela palavra-chave new será controlado pelo objeto externo. O objeto
EmailService será criado após a criação do
PedidoService. Assim, o objeto EmailService
se torna lixo assim que o objeto OrderService se torna lixo. Para
mudar o tempo de vida de um objeto EmailService para, digamos, um
singleton, você precisa modificar o código da classe PedidoService;
- A alocação de um objeto será mais
lenta se houver a necessidade de executar uma lógica de execução longa do
construtor do objeto;
- A alocação de um objeto na pilha de
objetos grandes será mais lenta do que a alocação de um objeto pequeno,
porque o runtime precisará procurar o local apropriado na área
fragmentada da memória;
- Como uma instância pode ser criada
em qualquer lugar no código, é fácil acabar com dependências implícitas. Uma
dependência é chamada de implícita se não estiver definida no construtor,
mas for criada em algum lugar dentro dos métodos da classe. Para responder à
pergunta sobre quais dependências uma classe usa, os desenvolvedores
precisam analisar toda a implementação dessa classe.+
2- Delegar a criação do objeto ao contêiner DI
Nesta opção a responsabilidade de criar
o objeto pode ser delegada a um componente separado, o contêiner de injeção de
dependência.
public class PedidoService
{
private readonly IEmailService _emailService;
public PedidoService(IEmailService emailService)
{
_emailService = emailService;
}
} |
Prós:
- É muito mais fácil entender quais
dependências o objeto está usando, pois todas são especificadas no
construtor. Os desenvolvedores só precisam olhar para o construtor, e ficará
claro quais dependências o objeto está usando, sem ter que analisar o resto
da implementação da classe. O contêiner DI incentiva o uso de dependências
explícitas;
- O uso de um contêiner DI permite
que os desenvolvedores alternem facilmente o tempo de vida do objeto (singleton,
por solicitação, etc.), o que pode ser feito na raiz da composição de um
aplicativo sem precisar modificar o restante do código do aplicativo;
- O contêiner DI permite que os
desenvolvedores alternem rapidamente o código do aplicativo entre diferentes
implementações da mesma interface;
- O contêiner DI, além de cumprir sua
principal responsabilidade de construir um gráfico de objetos, fornece
recursos úteis adicionais, como interceptação;
Contras:
- A criação de objetos é mais lenta
em comparação com o uso direto da palavra-chave new, porque os contêineres
de DI usam Reflection e outras técnicas;
- Se as dependências forem
configuradas incorretamente na raiz da composição do aplicativo, não haverá
erros de compilação, mas apenas travamentos de tempo de execução;
- O fluxo em um aplicativo é mais
difícil de rastrear. Apenas observando as interfaces nos construtores de
classe, é difícil adivinhar qual implementação está em uso no momento sem
ter que ler a configuração na raiz da composição;
- Os desenvolvedores precisam
aprender a usar o contêiner DI lendo a documentação;
3 - Criando um objeto usando Reflection
A próxima abordagem para instanciar um
objeto é o mecanismo Reflection:
namespace Teste
{
public class Demo
{
public string Valor { get; set; } = "001";
}
public void Create()
{
Type codeType = Assembly.GetExecutingAssembly().GetType("Teste.Demo");
//Cria a instância
object codeInstance = Activator.CreateInstance(codeType);
PropertyInfo codePropertyInfo = codeType.GetProperty("Valor");
string codigo = (string)codePropertyInfo.GetValue(codeInstance,
null);
//...
}
} |
Prós:
- A reflexão permite vinculação
tardia. O mecanismo de reflexão permite que os desenvolvedores criem
instâncias de classes que não existem em tempo de compilação, por exemplo,
de assemblies carregados dinamicamente.
- A reflexão permite que os
desenvolvedores implementem algum algoritmo genérico. Por exemplo, os
desenvolvedores que usam reflexão podem implementar um método que cria uma
cópia profunda de um objeto de qualquer complexidade.
Contras:
- A Reflection permite que os
desenvolvedores ignorem o encapsulamento, por isso é fácil quebrar
invariantes de classe por engano;
- Em Algoritmos de reflexão
implementados incorretamente, erros de digitação em nomes de tipo não serão
detectados em tempo de compilação, mas falharão em tempo de execução;
- A criação de objetos é mais lenta
do que usar a palavra-chave new porque os metadados do assembly devem ser
verificados para localizar as informações de tipo;
4
- Obter o objeto a partir de um Pool de Objetos
Nesta opção ao invés de criar um objeto,
o objeto já pré-criado pode ser carregado de um pool de objetos. Obviamente, o
objeto deve ser criado antecipadamente com uma palavra-chave new ou usando
Reflection, mas isso só precisa ser feito uma vez.
int arraySize =
100000;
//Pega um array a partir do pool
int[] arr = ArrayPool<int>.Shared.Rent(arraySize);
//Uso o array
//Retorna o array para pool
ArrayPool<int>.Shared.Return(arr);
|
Prós:
- Carregar um objeto do pool é muito
mais rápido do que criar um objeto do zero, portanto, o desempenho do
aplicativo é aprimorado.
- O uso de um pool de objetos reduz a
pressão sobre o coletor de lixo porque o objeto é enviado de volta ao pool
de objetos em vez de se tornar lixo após o uso. Novamente, isso melhora o
desempenho do aplicativo.
Contras:
- Isso é importante lembrar de enviar
o objeto de volta ao pool de objetos quando não for mais necessário. Este é
um passo importante adicional que os desenvolvedores podem facilmente
esquecer de tomar.
- O agrupamento de objetos não é a
melhor opção quando os desenvolvedores precisam manter muitos objetos
grandes.
- Os limites de memória podem ser
alcançados.
5- Criando um objeto pela
sua desserialização
Nesta última maneira de criar um objeto
podemos fazer a sua desserialização de um binário, JSON ou outros formatos. É
claro que o objeto deve ser serializado previamente e armazenado na memória ou
no armazenamento externo de arquivos.
Nota: A serialização é o
processo de armazenar um objeto , incluindo todos os atributos públicos e
privados para um stream. A desserialização é o processo inverso.
[Serializable]
public class Pessoa
{
public string Nome { get; set; }
public string Email { get; set; }
}
public Pessoa CreatePessoa()
{
using FileStream fs = new
FileStream("SerializedPessoa.dat", FileMode.Open);
BinaryFormatter formatter = new BinaryFormatter();
Person pessoa = (Pessoa)formatter.Deserialize(fs);
return pessoa;
}
|
Prós:
- O objeto que é serializado e
armazenado no armazenamento externo sobreviverá à reinicialização do
aplicativo. Às vezes, objetos grandes que raramente mudam são muito mais
baratos para construir uma vez, armazená-los em formato binário e
desserializá-los de volta para a memória quando necessário;
- A serialização binária é mais
rápida que a serialização JSON ou XML;
- A serialização binária pode
serializar e desserializar um gráfico de objeto que possui dependências
circulares;
Contras:
- A serialização binária requer que o
objeto seja marcado com o atributo [Serializable];
- A serialização binária não armazena
dados em um formato legível em comparação com a serialização JSON ou XML.
- Essa abordagem requer a presença e
manutenção de armazenamento adicional para os arquivos serializados.
E após essas apresentações, qual á
melhora abordagem ?
Ora, ora, não existe abordagem melhor ou
pior. Tudo depende do seu caso específico.
A única maneira de encontrar uma solução
que resolva seu problema da maneira mais eficiente é pesar seus requisitos em
relação aos prós e contras de cada abordagem disponível até encontrar a melhor
opção.
E estamos conversados.

"Porque foi do
agrado do Pai que toda a plenitude nele habitasse,
E que, havendo por ele feito a paz pelo sangue da sua cruz, por meio dele
reconciliasse consigo mesmo todas as coisas, tanto as que estão na
terra, como as que estão nos céus."
Colossenses 1:19,20

Referências:
José Carlos Macoratti