Design Pattern

Padrão de Projeto Flyweight em PHP com exemplo

Capa do artigo Padrão de Projeto Flyweight em PHP com exemplo
Leitura com foco

Entenda o contexto.
Leve para a prática.

Neste artigo
  1. Qual problema o Flyweight resolve?
  2. Exemplo do padrão Flyweight em PHP
  3. Testando o exemplo
  4. Quanto de memória o Flyweight economiza?
  5. Quando usar o padrão Flyweight?
  6. Cuidados ao usar o Flyweight
  7. Flyweight e Singleton: qual a diferença?

O Padrão de Projeto Flyweight reduz o consumo de memória compartilhando, entre muitos objetos, a parte dos dados que é igual em todos eles. Cada objeto guarda só o que é dele. O que se repete fica em um objeto único, criado uma vez e reaproveitado.

O nome vem do boxe: flyweight é a categoria peso-mosca, uma das mais leves. A ideia é deixar cada objeto o mais leve possível.

Diagrama de classe UML do padrão de projeto Flyweight
Diagrama de classe UML do padrão de projeto Flyweight

Observe no diagrama os participantes do padrão:

  • Flyweight (MarkerStyle): o objeto compartilhado. Guarda os dados que se repetem e recebe o restante por parâmetro.
  • FlyweightFactory (MarkerStyleFactory): cria e guarda os flyweights, e devolve sempre a mesma instância para os mesmos dados.
  • Contexto (Marker): guarda os dados próprios de cada objeto e a referência para o flyweight.
  • Client (DeliveryMap): pede os flyweights à fábrica, em vez de criá-los com new.

Qual problema o Flyweight resolve?

Pense em um mapa de entregas com milhares de marcadores. Cada marcador tem uma posição (latitude e longitude) e um estilo (ícone e cor). A posição é diferente em cada um, mas o estilo se repete: todos os caminhões são azuis, todas as lojas são verdes.

A solução direta guarda tudo em cada marcador:

<?php

$markers = [];

for ($i = 0; $i < 10000; $i++) {
    // Cada marcador cria o seu próprio estilo, mesmo sendo igual aos outros.
    $markers[] = new Marker(-23.55, -46.63, new MarkerStyle('truck', 'blue'));
}

São 10.000 marcadores e 10.000 objetos de estilo, embora só exista um estilo diferente. O Flyweight separa os dados do objeto em dois grupos:

Tipo de estadoNo exemploOnde fica
Intrínseco: igual em muitos objetosÍcone e corNo MarkerStyle, compartilhado
Extrínseco: próprio de cada objetoLatitude e longitudeNo Marker
Os dois tipos de estado do padrão Flyweight.

Exemplo do padrão Flyweight em PHP

Vamos para o código. O MarkerStyle é o flyweight. Ele guarda o ícone e a cor e recebe a posição por parâmetro na hora de desenhar:

<?php

declare(strict_types=1);

namespace Growthdev\DesignPatterns\Structural\Flyweight;

final class MarkerStyle
{
    private readonly string $icon;
    private readonly string $color;

    public function __construct(string $icon, string $color)
    {
        $this->icon = $icon;
        $this->color = $color;
    }

    public function render(float $latitude, float $longitude): string
    {
        return sprintf('%s (%s) at %.4f, %.4f', $this->icon, $this->color, $latitude, $longitude);
    }
}

Repare no readonly. Como o mesmo objeto é usado por milhares de marcadores, ele não pode ser alterado depois de criado.

A fábrica é quem garante o compartilhamento. Ela guarda os estilos já criados e só cria um novo quando a combinação de ícone e cor ainda não existe:

<?php

declare(strict_types=1);

namespace Growthdev\DesignPatterns\Structural\Flyweight;

final class MarkerStyleFactory
{
    /** @var array<string, MarkerStyle> */
    private array $styles = [];

    public function get(string $icon, string $color): MarkerStyle
    {
        $key = $icon . ':' . $color;

        if (!isset($this->styles[$key])) {
            $this->styles[$key] = new MarkerStyle($icon, $color);
        }

        return $this->styles[$key];
    }

    public function count(): int
    {
        return count($this->styles);
    }
}

O Marker guarda a posição e a referência para o estilo compartilhado:

<?php

declare(strict_types=1);

namespace Growthdev\DesignPatterns\Structural\Flyweight;

final class Marker
{
    private float $latitude;
    private float $longitude;
    private MarkerStyle $style;

    public function __construct(float $latitude, float $longitude, MarkerStyle $style)
    {
        $this->latitude = $latitude;
        $this->longitude = $longitude;
        $this->style = $style;
    }

    public function render(): string
    {
        return $this->style->render($this->latitude, $this->longitude);
    }
}

E o DeliveryMap é o cliente. Ele sempre pede o estilo à fábrica:

<?php

declare(strict_types=1);

namespace Growthdev\DesignPatterns\Structural\Flyweight;

final class DeliveryMap
{
    private MarkerStyleFactory $styles;

    /** @var Marker[] */
    private array $markers = [];

    public function __construct(MarkerStyleFactory $styles)
    {
        $this->styles = $styles;
    }

    public function addMarker(float $latitude, float $longitude, string $icon, string $color): void
    {
        $this->markers[] = new Marker($latitude, $longitude, $this->styles->get($icon, $color));
    }

    public function count(): int
    {
        return count($this->markers);
    }

    /** @return string[] */
    public function render(): array
    {
        return array_map(static fn (Marker $marker): string => $marker->render(), $this->markers);
    }
}

Testando o exemplo

Os testes conferem as duas promessas do padrão: o mesmo estilo devolve a mesma instância, e 10.000 marcadores criam só dois objetos de estilo.

<?php

declare(strict_types=1);

namespace Growthdev\DesignPatterns\Tests\Structural\Flyweight;

use Growthdev\DesignPatterns\Structural\Flyweight\DeliveryMap;
use Growthdev\DesignPatterns\Structural\Flyweight\MarkerStyleFactory;
use PHPUnit\Framework\TestCase;

final class FlyweightTest extends TestCase
{
    public function testExpectSameInstanceForSameStyle(): void
    {
        $factory = new MarkerStyleFactory();

        $this->assertSame($factory->get('truck', 'blue'), $factory->get('truck', 'blue'));
        $this->assertSame(1, $factory->count());
    }

    public function testExpectDifferentInstancesForDifferentStyles(): void
    {
        $factory = new MarkerStyleFactory();

        $this->assertNotSame($factory->get('truck', 'blue'), $factory->get('store', 'green'));
        $this->assertSame(2, $factory->count());
    }

    public function testExpectThousandsOfMarkersToShareFewStyles(): void
    {
        $factory = new MarkerStyleFactory();
        $map = new DeliveryMap($factory);

        for ($i = 0; $i < 10000; $i++) {
            if ($i % 2 === 0) {
                $map->addMarker(-23.55 + $i / 100000, -46.63, 'truck', 'blue');
            } else {
                $map->addMarker(-23.55, -46.63 + $i / 100000, 'store', 'green');
            }
        }

        $this->assertSame(10000, $map->count());
        $this->assertSame(2, $factory->count());
    }

    public function testExpectEachMarkerToKeepItsOwnPosition(): void
    {
        $map = new DeliveryMap(new MarkerStyleFactory());
        $map->addMarker(-23.5505, -46.6333, 'truck', 'blue');
        $map->addMarker(-22.9068, -43.1729, 'truck', 'blue');

        $this->assertSame(
            [
                'truck (blue) at -23.5505, -46.6333',
                'truck (blue) at -22.9068, -43.1729',
            ],
            $map->render()
        );
    }
}

O último teste mostra que compartilhar o estilo não mistura os marcadores: cada um continua com a sua posição.

Quanto de memória o Flyweight economiza?

Medi a memória usada para criar os marcadores deste exemplo, primeiro com um MarkerStyle novo para cada marcador e depois com a fábrica:

MarcadoresUm estilo por marcadorCom Flyweight
10.0002.226 KB1.317 KB
100.00021.279 KB12.443 KB
Memória usada pelos marcadores, medida com memory_get_usage() no PHP 8.3.6, com dois estilos diferentes.

A redução ficou perto de 41%. Os números mudam conforme a versão do PHP e o tamanho dos objetos, então meça no seu caso antes de decidir.

Um detalhe do PHP: quando você atribui a mesma string a várias variáveis, o motor não copia o valor enquanto ninguém o altera, como explica o manual do PHP. Por isso, neste exemplo o ganho vem de deixar de criar milhares de objetos repetidos.

Quando usar o padrão Flyweight?

  • Quando a aplicação cria uma quantidade muito grande de objetos parecidos e a memória virou um problema que você mediu.
  • Quando boa parte dos dados desses objetos se repete e pode ser separada do que é próprio de cada um.
  • Quando os objetos compartilhados podem ser imutáveis.

Cuidados ao usar o Flyweight

  • O flyweight precisa ser imutável. Se um marcador pudesse alterar o estilo compartilhado, alteraria o de todos os outros.
  • O código fica mais complicado: aparece uma fábrica, e parte dos dados passa a viajar por parâmetro. Sem um problema real de memória, não compensa.
  • Em PHP para web, cada requisição começa do zero e raramente cria milhares de objetos. O padrão faz mais sentido em importações, scripts de linha de comando, filas e processos que ficam rodando.
  • A fábrica segura os objetos enquanto ela existir. Em processos longos, isso mantém na memória estilos que talvez ninguém use mais.

Flyweight e Singleton: qual a diferença?

Os dois padrões evitam criar objetos repetidos, e por isso são confundidos. A diferença está em quantas instâncias existem e no que elas podem guardar:

PadrãoQuantas instânciasO que a instância guarda
FlyweightUma para cada combinação de dados (aqui, uma por ícone e cor)Só dados que não mudam, usados por muitos objetos
SingletonUma única para a classe inteiraPode guardar dados que mudam
Diferença entre Flyweight e Singleton.

Esse é mais um padrão estrutural do livro GoF. Para conhecer os outros, veja o artigo Resumo dos Padrões de Projetos.

O código completo deste exemplo, com os testes, está no meu GitHub, na pasta src/Structural/Flyweight do repositório growthdev-repo/design-patterns.

Veja também: o resumo dos 23 padrões de projeto GoF e os outros padrões estruturais já publicados: Adapter, Bridge, Composite, Decorator, Facade, Proxy.

Confiança Sempre!!!

Fontes:

  • [1] GAMMA, Erich et al. Padrões de Projeto: Soluções reutilizáveis de software orientado a objetos.
  • [2] FREEMAN, Eric; ROBSON, Elisabeth; BATES, Bert; SIERRA, Kathy. Head First Design Patterns.
  • [3] PHP Manual. Reference Counting Basics.
Sobre quem escreve

Walmir Silva

Olá! Sou Walmir, engenheiro de software com MBA em Engenharia de Software e o cérebro por trás do GrowthCode e autor do livro "Além do Código".

Se você acha que programação é apenas sobre escrever código, prepare-se para expandir seus horizontes. Aqui, nós vamos além do código e exploramos as interseções fascinantes entre tecnologia, negócios, artes e filosofia.

Você está em busca de crescimento na carreira? Quer se destacar em um mercado competitivo? Almeja uma vida mais rica em conhecimento e realização? Então você chegou ao lugar certo. No GrowthCode, oferecemos insights profundos, estratégias comprovadas e um toque de sabedoria filosófica para catalisar seu crescimento pessoal e profissional.

Ver todos os artigos →

Continue a conversa

Compartilhe sua perspectiva

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Encontre sua próxima resposta

O que você quer aprender?

Preferências de cookies

Os cookies opcionais começam desligados. Você pode mudar a escolha a qualquer momento por esta tela.

NecessáriosSempre ativos

Guardam a sua escolha de cookies e mantêm o site funcionando. Não são usados para publicidade.

Contam visitas e mostram como o site é usado: Google Analytics e Microsoft Clarity, carregados pelo Google Tag Manager. Como esse contêiner também carrega tags de publicidade, as estatísticas só são ativadas junto com Publicidade e marketing.

Exibem anúncios do Google AdSense e medem campanhas e e-mails: pixels da Meta e do TikTok e o Mautic.

Permite que o Google escolha anúncios com base nos seus interesses. Desligado, os anúncios aparecem sem personalização. Depende de Publicidade e marketing.

Também é possível bloquear cookies nas configurações do navegador. Detalhes na Política de privacidade.