Classes Mutáveis vs Objetos Imutáveis


É possível ter a comodidade em utilizar classes mutáveis para construir os objetos aos poucos, configurando propriedades e opções de execução além do uso dos construtores, porém com a vantagem de obter instâncias imutáveis no final do processo.

E eu não estou falando do padrão Builder. Aliás você não deveria precisar construir uma classe para criar uma instância de outra classe…

Unsplash image
Photo by Djim Loic on Unsplash

Introdução

Os seres na natureza não são criados num único instante.

Um ser dentro do ventre de sua progenitora é “construído” aos poucos, parte por parte.

Imagine todos os órgãos crescendo em um desenvolvimento constante.

Mas mesmo que esse ser ainda não tenha nascido, ele já existe.

Ele ocupa espaço.

Ele interage com o mundo através do corpo de sua mãe…

Porém ele ainda não é uma entidade 100% concluída ou estável.

O mesmo pode acontecer com um objeto no software.

O objeto pode não ter sido totalmente formado, mas ele já existe quando obtemos uma nova instância a partir do construtor de sua classe.

O objeto já existe mas pode não estar pronto para uso.

Faz sentido?

Mutável vs Imutável

Na internet muito tem se falado sobre utilizar objetos imutáveis considerando seus prós e contras.

Eu mesmo sou um defensor de objetos imutáveis.

Porém há casos onde os objetos, apesar de já ter “nascido”, ainda não estão 100% pronto para uso.

Você pode estar pensando: “se podemos ter um objeto mutável, e se isso parece até estar de acordo com a natureza e de como o mundo realmente funciona, por quê eu iria querer ter um objeto imutável afinal?”

Objetos imutáveis são muito mais previsíveis, seguros e simples de usar. Isso é um fato.

No entanto, também haverá casos onde nem todos os objetos em um software poderão ser imutáveis. Vide widgets de formulários Desktop, por exemplo. Os Edits, Memos, Grids… todos são mutáveis.

Apesar da imutabilidade ser o ideal a seguir, ela não é uma verdade absoluta.

Então, se mutabilidade e imutabilidade tem seus prós e contras, e eventualmente iremos trabalhar com ambos, como decidir entre um e outro quando isso já não for pré-determinado?

Eu diria para utilizarmos classes mutáveis em conjunto com objetos imutáveis.

Na Prática

Uma classe é a construtora de seus objetos — a progenitora.

Um objeto deveria ser uma entidade 100% pronta para uso.

Entretanto, assim como uma criatura ainda está sendo formada no ventre, um objeto ainda pode estar sendo construído.

Mas o programador pode determinar quando essa construção termina em um momento especial: quando a instância objeto se transforma em instância de interface.

Se você não tiver nenhum método na interface que promova a mutabilidade do objeto, você poderá construir seus objetos aos poucos utilizando a mutabilidade dos métodos da classe, porém com a vantagem de ter uma instância final imutável que só implementa métodos imutáveis da interface.

É como ter o melhor dos dois mundos.

Imagine uma classe que faz um processamento de Faturas — TInvoiceMaker.

Tal classe necessitaria de uma “configuração especial” sobre como fazer o processamento.

Essa configuração poderia depender de muitos fatores, como período do ano, tipos de contrato, tipos de produtos, promoções, se é apenas um teste ou se está executando em produção.

Estamos apenas especulando as opções.

Primeiro, iniciamos com a definição de uma interface que TInvoiceMaker deverá implementar:

IDataMaker = interface
  procedure Make;
end;

Veja que não há métodos que promovem a mutabilidade, ou seja, não há [Setters].

Uma instância IDataMaker seria passada como argumento para algum objeto/rotina “big process” que recebe um “maker”, não importando quem:

BigProcess(const aMaker: IDataMaker);

Internamente aMaker.Make será chamado.

Modelo Imutável

Então, vamos pensar como poderia ser essa implementação no “modelo imutável”:

TInvoiceMaker = class(TInterfacedObject, IDataMaker)
public
   constructor Create(aPeriod: TPeriod; 
     aContracts: TContractTypes;
     aProducts: TProductTypes;  
     aPromotions: TPromotionArray); reintroduce;
   procedure Make;
end;

O construtor da classe deverá ter todas as dependências injetadas no construtor.

Esse é um modelo sólido. Não há setters. Após o objeto ter sido construído, ele estará 100% pronto para uso.

Esse seria um modelo de implementação ideal.

A classe é pequena; não há muitos argumentos no construtor; fácil de instanciar; imutável.

Agora imagine essa classe seja utilizada em muitos lugares no código e que, na maioria das vezes, seus argumentos serão sempre os mesmos.

Mesmo que haja poucos argumentos no construtor, a repetição deles torna o código verboso e chato para o programador.

Se os contratos são os mesmos, os produtos os mesmos, promoções… por quê não definir valores padrão para tais atributos?

Então implementamos mais um [construtor secundário] que irá chamar o primário:

TInvoiceMaker = class(TInterfacedObject, IDataMaker)
public
   constructor Create(aPeriod: TPeriod; 
     aContracts: TContractTypes;
     aProducts: TProductTypes;
     aPromotions: TPromotionArray); reintroduce; overload;
   constructor Create(aPeriod: TPeriod); overload;
   procedure Make;
end;

O construtor secundário irá passar valores padrão para os argumentos aContracts, aProducts, aPromotions. Apenas o período deverá sempre ser informado, por exemplo.

E, novamente, temos um design limpo e sólido.

Mas sabemos como é a vida de um desenvolvedor: os requisitos mudam; modelos mudam; não há tempo suficiente.

E se, agora, o desenvolvedor precisar informar o período e, também, os contratos? Não há um overload de construtor para informar apenas esses dois parâmetros.

Seguindo o método acima, o desenvolvedor tem duas opções: a) criar um novo construtor ou b) utilizar o construtor principal passando todos os parâmetros — o que eu vejo acontecer bastante em códigos de terceiros onde tais parâmetros são dispostos, por exemplo, assim foo(true, true, '', '', false, '', 1), que é difícil de ler/entender.

Há os argumentos com valores padrão no Pascal. Mas eles não ajudariam muito se quisermos passar apenas 1 dos valores “no meio”. Nesses casos você terá que digitar os valores padrão até chegar na posição do argumento que você deseja informar.

Voltando ao problema, o mais “correto”, seguindo o modelo imutável, seria implementar um novo construtor:

TInvoiceMaker = class(TInterfacedObject, IDataMaker)
public
   constructor Create(aPeriod: TPeriod; 
     aContracts: TContractTypes;
     aProducts: TProductTypes;  
     aPromotions: TPromotionArray); reintroduce; overload;
   constructor Create(aPeriod: TPeriod; 
     aContracts: TContractTypes); overload;
   constructor Create(aPeriod: TPeriod); overload;
   procedure Make;
end;

Infelizmente essa abordagem pode não ser estável no longo prazo, dependendo das mudanças.

Se os argumentos ou a combinação deles aumentarem, teria de haver muitos outros construtores com inúmeras combinações.

Esse é uma abordagem que funciona com classes estáveis com poucas mudanças, não sendo o ideal para classes que tem a tendência de mudar muito.

Modelo Mutável

Então, vamos pensar como poderia ser essa implementação no “modelo mutável”:

TInvoiceMaker = class(TInterfacedObject, IDataMaker)
private
   fPeriod: TPeriod;
   fContracts: TContractTypes;
   fProducts: TProductTypes;
   fPromotions: TPromotionArray;
public
   constructor Create(aPeriod: TPeriod); reintroduce;
   procedure Make;
   property Contracts: TContractTypes 
     read fContracts write fContracts;
   property Products: TProductTypes 
     read fProducts write fProducts;
   property Promotions: TPromotionArray 
     read aPromotions write fPromotions;
end;

A classe tem um construtor primário que solicita o essencial: um período.

Todos os outros atributos que podem ter um valor padrão, poderiam ser implementados como propriedades.

A vantagem é óbvia: não precisamos construir inúmeros construtores com inúmeras combinações. Além disso, podemos adicionar mais propriedades a qualquer momento, com tanto que cada uma tenha um valor padrão que será inicializado no construtor primário.

Esse também é um um modelo limpo e (de certa forma) sólido.

Muitos desenvolvedores acham que a classe deve ter apenas os mesmos métodos que a interface que implementam. Ledo engano.

Há setters através das propriedades, porém cada uma delas terá um valor padrão definido no construtor. Então, o desenvolvedor não é obrigado a passar cada uma das propriedades.

Após o objeto ter sido construído, ele estará 100% pronto para uso.

Mas, como manter a imutabilidade utilizando essa classe?

Na verdade, a instância será virtualmente imutável quando você transformá-la em instância de interface.

Lembre-se que precisamos de uma instância IDataMaker para passar a uma rotina:

/// imutable version
procedure Execute(aPeriod: TPeriod);
var
  maker: IDataMaker;  // interface
begin
  maker := TInvoiceMaker.Create(
    aPeriod, TDefContracts.Create, 
    TDefProducts.Create, []);
  BigProcess(maker);
end;

/// mutable version
procedure Execute(aPeriod: TPeriod);
var
  maker: TInvoiceMaker; // class
begin
  maker := TInvoiceMaker.Create(aPeriod);
  // changing default TDefProducts instance
  maker.Products := TOtherProducts.Create;
  BigProcess(maker);
end;

Você pode ainda utilizar uma construção sem ter que definir uma variável, utilizando WITH and REF:

/// mutable version using WITH
procedure Execute(aPeriod: TPeriod);
begin
  with TInvoiceMaker.Create(aPeriod) do
  begin
    // changing default TDefProducts instance
    Products := TOtherProducts.Create;
    BigProcess(Ref);  // obj.Ref
  end;
end;

Conclusão

A rotina BigProcess só sabe o que é um IDataMaker com apenas o único método Maker. É irrelevante para o processo se aMaker é mutável com propriedades ou totalmente imutável, implementando somente o único método da interface.

Dessa forma você teria classes mutáveis com outras propriedades e métodos gerando instâncias de objetos mutáveis.

Porém, após as instâncias serem convertidas para instâncias de interface, você teria instâncias virtualmente imutáveis, provenientes de classes mutáveis.

Combinar classes mutáveis com objetos imutáveis pode ser uma escolha poderosa no design de sua aplicação, trazendo a flexibilidade da construção de instâncias mutáveis com a segurança das instâncias imutáveis, após serem convertidas.

Até logo.

Posts Relacionados

  • Memória Segura Utilizando Instâncias de Interfaces

  • Implementando Interfaces Utilizando Diferente Assinaturas de Métodos

  • Usando Paths ao invés de Diretivas de Compilação

  • Trabalhando com Exceções em Requisições HTTP

  • Tipo object Continua Vivo

  • Array de Objetos

  • Variáveis Locais Deveriam ter Nomes Curtos

  • Como Dividir e Organizar o Código em Formulários com Muitos Widgets

  • Pascal Deveria ser Modernizado?

  • Records - Antiga Nova Tecnologia