Design Patterns - Prototype

Design Patterns - Prototype

Criar objetos pode ser sempre um desafio de performance, a solução pode ser copiar!

Uma das primeiras coisas que aprendemos na programação, seja qual for a linguagem, é como copiar valor de uma variável para outra. Uma atividade extremamente comum mas que rende situações interessantes no nosso dia a dia.

Definição

É um padrão criacional que permite a criação de novos objetos copiando um objeto já existente, isso pode ser útil quando a criação de um objeto é custosa ao sistema, seja por sua complexidade inerente ou pelo alto consumo de recursos envolvidos.

Exemplo conceitual

Quando estudei pela primeira vez este padrão de design, confesso que tive dificuldade para entender sua aplicação. Num primeiro momento me pareceu um tanto quanto desnecessário, criar toda uma estrutura para realizar uma simples cópia. Mas foi aí que pensei em um possível uso na minha rotina.

Imagine que você possua uma entidade Perfil cujo uma de suas propriedades seja a classe Configuração, e a cada vez que você for criar um novo Perfil, precisamos ir na tabela de configurações e obter as configurações mais recentes... Agora imagine, toda vez que precisar criar um novo objeto, será feito uma consulta em banco!! Se esses dados são os mesmos de outros objetos que já estamos manipulando, por quê não usá-los?! É aí que o prototype brilha, padronizando o processo de cópia desses objetos.

Shallow Copy e Deep Copy

Há, pelo menos inicialmente, duas situações para pensarmos sobre as cópias de objetos, as cópias parciais (shallow copy) e as cópias profundas (Deep Copy). A diferença está na forma como os valores são passados de um objeto ao outro.

Quando estudei programação pela primeira vez, a linguagem C era dominante na instituição (até hoje é ela) e o conceito de passagem de valor e referência fazia parte das primeiras aulas, já que é extremamente comum nesta linguagem, como sei que não é o caso de todos, vou explicar um pouco sobre.

Passagem de Valor vs Referência

Passagem de valor é literalmente passar um valor, passamos o conteúdo de uma variável para outra, como no exemplo que segue, onde passamos o valor que está em a para b. Com isso temos duas alocações na memória, dois endereços de memória diferentes.

int a = 10;
int b = a;

Parece óbvio que se queremos copiar algo, queremos copiar o seu valor, mas na programação certos tipos são sempre passados como referência, é o caso dos vetores em C.

int a[4] = [0,1,2,3];
int b[4] = a;

Neste caso é feito uma passagem por referência, isso significa que passamos para b apenas o endereço de memória de a. Isso significa que toda e qualquer modificação neste vector será refletida tanto em a quanto em b.

Mas o que isso tem a ver com as cópias?!

Em diversas linguagens, Java e C#, por exemplo, os objetos são passados como referência e isso entrega comportamentos peculiares. Ao realizarmos a cópia de um objeto que possui outros objetos, estes objetos internos serão copiados como referência para o seu destino, portanto compartilharam o mesmo estado em memória, tendo seus valores sempre iguais entre suas cópias, portanto uma cópia parcial (Shallow Copy).

Qual devo usar?!

O ganho do uso de cópias parciais é a eficiência do seu código, que usará menos recursos do que se realizasse uma cópia profunda, instanciando um novo objeto interno e copiando os valores presentes.

Mas qual o melhor a ser usado? Dependerá do seu projeto e necessidades, o padrão Prototype, conferirá a você a padronização desses processos, para que sempre que for necessário copiar um objeto, não seja necessário abrir a classe e verificar se há mais objetos internos, se esses objetos internos não possuem outros objetos internos...

Aplicando o Prototype

A primeira coisa que iremos precisar é uma interface do tipo Prototype, que terá os métodos ShallowCopy e DeepCopy.

public interface IPrototype<T>
{
    T ShallowCopy();
    T DeepCopy();    
}

Agora poderemos implementar a nossa classe abstrata que herdará de IPrototype, aqui já poderíamos implementar a interface se assim quiséssemos mas no caso prefiro usar uma classe intermediária.

Criarei também a classe Weapon para ser nossa classe interna, ela eu já opto por implementar diretamente e portanto não marco suas propriedades como abstract.

public abstract class Enemy : IPrototype<Enemy>
{
    public string Name;
    public string Description;    
    public Weapon Weapon;
    public abstract Enemy ShallowCopy();
    public abstract Enemy DeepCopy();
}

public class Weapon : IPrototype<Weapon>
{
    public string Type;
    public int Damage;

    public Weapon(string type, int damage)
    {
        this.Type = type;
        this.Damage = damage;
    }

    public Weapon ShallowCopy()
    {
        return (Weapon)this.MemberwiseClone();
    }

    public Weapon DeepCopy()
    {
        return new Weapon(Type, Damage);
    }
}

Note que os métodos herdados de IPrototype são marcados como abstract para que na próxima herança seja codificado.

Agora podemos finalmente implementar nossa classe produto, a classe Troll.

public class Troll : Enemy
{
    public override Enemy ShallowCopy()
    {
        return (Enemy)this.MemberwiseClone();
    } 

    public override Enemy DeepCopy()
    {
        Enemy enemy = ShallowCopy();
        enemy.Weapon = new Weapon(Weapon.Type, Weapon.Damage);

        return enemy;
    }

    public override string ToString()
    {
        var Str = new StringBuilder();

        Str.AppendLine($"Name: {this.Name}");
        Str.AppendLine($"Description: {this.Description}");
        Str.AppendLine($"Weapon: {Weapon.Type} ({Weapon.Damage})");

        return Str.ToString();
    }
}

Podemos ver que a classe Troll implementa nossos dois tipos de cópia e já na sua implementação entendemos que por mais que seus valores sejam parecidos, haverá um comportamento diferente entre os dois.

Executando nossas classes

Enemy Boss = new Troll
{
    Name = "Boss",
    Description = "Tordoaldo is an evil troll, with a sharp intelligence and hidden strength, who lives in a forest cave adorned with treasures from his adventures.",
    Weapon = new Weapon("Sword", 500)
};

Enemy Boss_ShallowCopy = Boss.ShallowCopy();
Enemy Boss_DeepCopy = Boss.DeepCopy();

System.Console.WriteLine("First Run \n");
ConsoleInfo();

Boss.Name = "BossWithNewName";
Boss.Weapon.Type = "Other Sword";  

System.Console.WriteLine("Second Run \n");
ConsoleInfo();

void ConsoleInfo()
{
    Console.WriteLine("Boss");
    Console.WriteLine(Boss.ToString());
    Console.WriteLine("Boss Shallow Copy");
    Console.WriteLine(Boss_ShallowCopy.ToString());
    Console.WriteLine("Boss Deep Copy");
    Console.WriteLine(Boss_DeepCopy.ToString());
}

Passo a Passo

Step 1

Aqui realizamos as duas cópias e podemos ver que todas as propriedades são idênticas como deveria ser.

Step 2

Após isso atualizamos o nome do nosso "Boss" original, e trocamos o nome da sua arma e o que vemos na saída é interessante!

Como podemos ver nosso "Boss" aparece com seu nome novo, sem nenhum problema. Mas nosso "Boss Shallow Copy" também teve sua arma alterada! Isso se dá justamente pela propriedade que comentamos anteriormente, como realizamos uma cópia parcial, a classe interna Weapon compartilha o mesmo espaço na memória, assim, toda alteração realizada ali refletirá nos dois objetos, "Boss" e "Boss Shallow Copy"

Sobre seu uso

Quando usar?!

O livro sobre padrões do Erich Gamma, enumera três casos de uso para este modelo:

  • Quando classes que serão instanciadas são especificadas em tempo de execução, por carga dinâmica por exemplo.

  • Para evitar o acoplamento do nosso código, evitando a construção de uma hierarquia de classes factories paralela à hierarquia de classes do produto.

  • Quando as instâncias de uma classe puderem ter uma dentre poucas combinações diferentes de estados. Pode ser mais vantajoso instalar um número correspondente de protótipos e cloná-los, ao invés de instanciar a classe manualmente, cada vez com um estado apropriado.

Vantagens

Ainda utilizando o livro supracitado, temos alguns benefícios:

  • Acrescentar e remover produtos em tempo de execução. É um padrão mais flexível que outros padrões de criação, permitindo registrar novos produtos ao instanciar um novo prototype no cliente, deste modo o cliente pode instalar e remover protótipos em tempo de execução.

  • Especificar novos objetos pela variação de valores. Ao utilizarmos o padrão prototype podemos reduzir consideravelmente o número de classes envolvidas.

  • Especificar novos objetos pela variação da estrutura. Este design permite que criemos objetos com partes e subpartes, desde que o nosso objeto composto implemente um clone profundo.

Desvantagem

A principal desvantagem é que cada subclasse do Prototype deve implementar a operação Clone, o que pode ser difícil. Casos onde as classes possuem referências circulares, ou que não suportam a operação de cópia, são exemplos dessa dificuldade.

Repositório GIT

Design Patterns

Materiais de Estudo

Estamos chegando ao fim do nosso artigo e queria deixar alguns materiais que usei para estudar esse padrão de software:

Refactoring Guru

Dofactory

Padrões de Projetos: Soluções Reutilizáveis de Software Orientados a Objetos