Artigo

Design Patterns - Flyweight

Rodrigo Branas
Rodrigo Branas
15 jan 2024·3 min de leitura
#design-patterns#flyweight

Em alguns tipos de programa, principalmente em jogos, e necessario instanciar uma grande quantidade de objetos similares, como por exemplo elementos de um mapa, e isso pode acabar ocupando bastante memória.

O padrao Flyweight permite o reuso de determinadas características de um objeto, separando características unicas de outras que são repetitivas, evitando que elas ocupem memória desnecessaria.

O Problema Inicial

No exemplo abaixo, temos uma classe Account que tem uma relação de composição com a classe Transaction, que representam as transacoes de debito e credito contendo o valor, a moeda, o meio de pagamento e a bandeira do cartao.

Pensando em Domain-Driven Design, Account forma um Aggregate com Transaction, agindo como Aggregate Root.

class Transaction {
  constructor(
    readonly type: string,
    readonly amount: number,
    readonly currency: string,
    readonly paymentMethod: string,
    readonly brand: string
  ) {}
}

class Account {
  private transactions: Transaction[];

  constructor() {
    this.transactions = [];
  }

  credit(amount: number, currency: string, paymentMethod: string, brand: string) {
    this.transactions.push(new Transaction("credit", amount, currency, paymentMethod, brand));
  }

  debit(amount: number, currency: string, paymentMethod: string, brand: string) {
    this.transactions.push(new Transaction("debit", amount, currency, paymentMethod, brand));
  }
}

Supondo uma grande quantidade de transacoes, nesse caso criei 10 milhões de transacoes aleatorias, temos aproximadamente 1gb de memória utilizada, podendo variar já que o valor de cada transacao e aleatorio, ocupando uma quantidade de memória diferente. Na minha maquina o tempo de execução ficou na faixa de 1.5 segundos.

console.time("performance");
const account = new Account();
let i = 0;
while (true) {
  if (i === 10000000) break;
  if (i % 2 === 0) {
    account.credit(Math.random() * 1000, "BRL", "creditCard", "Visa");
  } else {
    account.debit(Math.random() * 1000, "USD", "debitCard", "Mastercard");
  }
  i++;
}

const formatMemoryUsage = (data: number) => `${Math.round((data / 1024 / 1024) * 100) / 100} MB`;
console.log(formatMemoryUsage(process.memoryUsage().heapUsed));
console.timeEnd("performance");

A Solução

Esse padrao absorve a similaridade entre diferentes objetos criando uma cache, chamada de FlyweightFactory, que faz o reuso das instancias, permitindo reduzir muito a quantidade de memória utilizada quando instanciamos um volume muito grande de objetos similares.

No exemplo abaixo separamos as características unicas e repetitivas da transacao, derivando uma nova classe chamada TransactionInformation, um objeto Flyweight compartilhado, armazenando e retornando a mesma instância caso ela tenha exatamente os mesmos parametros.

class TransactionInformation {
  constructor(
    readonly type: string,
    readonly currency: string,
    readonly paymentMethod: string,
    readonly brand: string
  ) {}
}

class Transaction {
  transactionInformation: TransactionInformation;
  amount: number;

  constructor(
    type: string,
    amount: number,
    currency: string,
    paymentMethod: string,
    brand: string
  ) {
    this.transactionInformation = TransactionInformationFlyweightFactory.getTransactionInformation(
      type,
      currency,
      paymentMethod,
      brand
    );
    this.amount = amount;
  }
}

class TransactionInformationFlyweightFactory {
  static transactionInformations: {
    [transactionInformation: string]: TransactionInformation;
  } = {};

  static getTransactionInformation(
    type: string,
    currency: string,
    paymentMethod: string,
    brand: string
  ): TransactionInformation {
    const transactionInformation = new TransactionInformation(type, currency, paymentMethod, brand);
    const index = `${type}/${currency}/${paymentMethod}/${brand}`;
    if (!TransactionInformationFlyweightFactory.transactionInformations[index]) {
      TransactionInformationFlyweightFactory.transactionInformations[index] =
        transactionInformation;
    }
    return TransactionInformationFlyweightFactory.transactionInformations[index];
  }
}

class Account {
  transactions: Transaction[];

  constructor() {
    this.transactions = [];
  }

  credit(amount: number, currency: string, paymentMethod: string, brand: string) {
    this.transactions.push(new Transaction("credit", amount, currency, paymentMethod, brand));
  }

  debit(amount: number, currency: string, paymentMethod: string, brand: string) {
    this.transactions.push(new Transaction("debit", amount, currency, paymentMethod, brand));
  }
}

Com isso, foi possível reduzir a memória utilizada de 1gb para 750mb, mas essa redução vai depender do volume de dados unicos e repetitivos, quanto mais repetido, maior e o ganho em utilizar esse padrao. Sobre performance, nos meus testes não ocorreu aumento no tempo de execução, ele seguiu nos mesmos 1.5 segundos.

Consideracoes de Performance

Durante a criação dos exemplos eu tentei tres formas diferentes de indexacao, uma criando um JSON, o que aumentou em quase 10 vezes o tempo de execução e ainda aumentou o consumo de memória. Outra criando um hash utilizando o algoritmo SHA1, o que acabou também aumentando o tempo de execução quase na mesma proporcao do JSON. Por conta disso, utilizei uma simples concatenacao de String, que foi mais rápido e eficiente.

Repare que foi utilizado o padrao Singleton para garantir que as instancias sejam unicas.

Uma coisa que devemos ter em mente e que criar uma cache, indexar as informacoes e depois busca-las pode consumir muitos ciclos de CPU, por Isso é fundamental fazer testes de performance para avaliar se realmente as vantagens compensam as desvantagens.

Exemplo do Mundo Real: String Pool

Nas linguagens Java, C#, JavaScript, entre outras, existe um String Pool, que funciona de forma similar ao padrao Flyweight, onde e possível reusar as instancias do tipo String. Isso pode ser observado pelo exemplo abaixo, escrito em Java:

String a = "Java";
String b = "Java";
System.out.println(a == b); // true

Ainda que tenhamos criado variáveis diferentes, ao comparar as suas referencias elas apontam para o mesmo objeto. Claro, isso so funciona porque a String e imutavel, ou seja, para modificar uma String e necessario criar uma nova. Dessa forma e possível reusar as instancias sem correr o risco de causar um efeito colateral, por isso, se for utilizar o padrao Flyweight, faca com que os dados repetitivos dos objetos compartilhados sejam imutaveis.

Conclusão

Esse e um padrao que muitas vezes utilizamos sem perceber, claro que o conceito do padrao envolve não somente uma cache, mas uma separacao entre a parte única e repetitiva do objeto, o design dele esta justamente nesse conceito.

Continue aprendendo

Já conhece nossas formações?

Os desenvolvedores que vão se manter no futuro são os que dominam a Inteligência Artificial e são arquitetos de software. Aprenda como ser esse tipo de desenvolvedor.

Cookies e privacidade

Utilizamos cookies para melhorar sua experiência, analisar o tráfego do site e personalizar conteúdo. Você pode aceitar todos, rejeitar ou personalizar suas preferências.