Wzór wagi muszej - Flyweight pattern

Zrzut ekranu pakietu Writer LibreOffice.
Edytory tekstu, takie jak LibreOffice Writer , często używają wzorca wagi muchowej.

W programowaniu komputerowym The muszej projektowanie oprogramowania wzór odnosi się do obiektu , który minimalizuje pamięci użytkowania poprzez dzielenie niektórych swoich danych z innymi podobnymi obiektami. Wzór wagi muchowej jest jednym z dwudziestu trzech dobrze znanych wzorców projektowych GoF . Wzorce te promują elastyczne, obiektowe projektowanie oprogramowania, które jest łatwiejsze do wdrożenia, zmiany, testowania i ponownego użycia.

W innych kontekstach idea współdzielenia struktur danych nazywana jest hash consing .

Termin ten został po raz pierwszy ukuty, a pomysł został szeroko zbadany przez Paula Caldera i Marka Lintona w 1990 roku, aby wydajnie obsługiwać informacje o glifach w edytorze dokumentów WYSIWYG . Podobne techniki były już stosowane w innych systemach, jednak już w 1988 roku.

Przegląd

Wzór wagi muchowej jest przydatny w przypadku dużej liczby obiektów z prostymi powtarzającymi się elementami, które w przypadku indywidualnego przechowywania zużywałyby dużą ilość pamięci. Powszechne jest przechowywanie współdzielonych danych w zewnętrznych strukturach danych i tymczasowe przekazywanie ich do obiektów, gdy są one używane.

Klasycznym przykładem są struktury danych używane do przedstawiania znaków w edytorze tekstu . Naiwnie każdy znak w dokumencie może mieć obiekt glifów zawierający kontur czcionki, metryki czcionki i inne dane dotyczące formatowania. Jednak wymagałoby to użycia setek lub tysięcy bajtów pamięci dla każdego znaku. Zamiast tego każdy znak może mieć odniesienie do obiektu glifu współdzielonego przez każde wystąpienie tego samego znaku w dokumencie. W ten sposób tylko pozycja każdego znaku musi być przechowywana wewnętrznie.

W rezultacie, obiekty typu flyweight mogą:

  • przechowywać stan wewnętrzny, który jest niezmienny, niezależny od kontekstu i udostępniany (na przykład kod znaku „A” w danym zestawie znaków)
  • zapewnić interfejs do przekazywania w stanie zewnętrznym, który jest wariantem, zależnym od kontekstu i nie może być współdzielony (na przykład pozycja znaku „A” w dokumencie tekstowym)

Klienci mogą ponownie wykorzystywać Flyweightobiekty i w razie potrzeby przechodzić w stan zewnętrzny, zmniejszając liczbę fizycznie tworzonych obiektów.

Struktura

Przykładowy diagram klas i sekwencji UML dla wzorca projektowego Flyweight.

Powyższy diagram klas UML pokazuje:

  • Clientklasy, która używa wzoru muszej
  • FlyweightFactoryklasy, która tworzy i dzieli Flyweightobiekty
  • Flyweight interfejs , który odbywa się w stanie zewnątrzpochodną i przeprowadza operację
  • Flyweight1klasa, która implementuje Flyweighti zapisuje stan wewnętrzny

Diagram sekwencji przedstawia następujące interakcje w czasie wykonywania :

  1. ClientObiekt nazywa getFlyweight(key)sprawie FlyweightFactory, która zwraca Flyweight1obiekt.
  2. Po wywołaniu operation(extrinsicState)na zwróconego Flyweight1obiektu, Clientponownie zwraca getFlyweight(key)na FlyweightFactory.
  3. FlyweightFactoryZwraca już istniejącego Flyweight1obiektu.

Szczegóły dotyczące wdrożenia

Istnieje wiele sposobów na zaimplementowanie wzoru wagi muchowej. Jednym z przykładów jest zmienność: czy obiekty przechowujące zewnętrzny stan masy muchowej mogą się zmienić.

Obiekty niezmienne można łatwo udostępniać, ale wymagają one tworzenia nowych obiektów zewnętrznych za każdym razem, gdy nastąpi zmiana stanu. W przeciwieństwie do tego, mutowalne obiekty mogą współdzielić stan. Mutowalność umożliwia lepsze ponowne wykorzystanie obiektów poprzez buforowanie i ponowną inicjalizację starych, nieużywanych obiektów. Udostępnianie jest zwykle nieopłacalne, gdy stan jest bardzo zmienny.

Inne podstawowe problemy obejmują pobieranie (w jaki sposób klient końcowy uzyskuje dostęp do wagi), buforowanie i współbieżność .

Wyszukiwanie

Fabrycznie interfejs do tworzenia lub ponownego użycia przedmiotów muszej jest często fasadą dla złożonego systemu bazowego. Na przykład interfejs fabryczny jest powszechnie implementowany jako singleton, aby zapewnić globalny dostęp do tworzenia wag.

Ogólnie rzecz biorąc, algorytm pobierania zaczyna się od żądania nowego obiektu przez interfejs fabryczny.

Żądanie jest zazwyczaj przekazywane do odpowiedniej pamięci podręcznej w zależności od rodzaju obiektu. Jeśli żądanie jest spełnione przez obiekt w pamięci podręcznej, może zostać ponownie zainicjowany i zwrócony. W przeciwnym razie tworzony jest nowy obiekt. Jeśli obiekt jest podzielony na wiele zewnętrznych podkomponentów, zostaną one połączone, zanim obiekt zostanie zwrócony.

Buforowanie

Istnieją dwa sposoby buforowania obiektów wagi muchowej: utrzymywane i nieutrzymywane pamięci podręczne.

Obiekty o bardzo zmiennym stanie mogą być buforowane za pomocą struktury FIFO . Ta struktura utrzymuje nieużywane obiekty w pamięci podręcznej, bez konieczności przeszukiwania pamięci podręcznej.

W przeciwieństwie do tego, nieobsługiwane pamięci podręczne mają mniejszy narzut z góry: obiekty pamięci podręcznej są inicjowane zbiorczo w czasie kompilacji lub uruchamiania. Gdy obiekty zapełnią pamięć podręczną, algorytm pobierania obiektów może wiązać się z większym obciążeniem niż operacje push/pop obsługiwanej pamięci podręcznej.

Pobierając zewnętrzne obiekty o niezmiennym stanie, należy po prostu przeszukać pamięć podręczną w celu znalezienia obiektu o pożądanym stanie. Jeśli nie zostanie znaleziony taki obiekt, należy zainicjować taki obiekt o tym stanie. Podczas pobierania zewnętrznych obiektów ze stanem zmiennym, pamięć podręczna musi zostać przeszukana pod kątem nieużywanego obiektu, aby ponownie zainicjować, jeśli nie zostanie znaleziony używany obiekt. Jeśli nie ma dostępnego nieużywanego obiektu, należy utworzyć wystąpienie nowego obiektu i dodać go do pamięci podręcznej.

Dla każdej unikalnej podklasy obiektu zewnętrznego można użyć oddzielnych pamięci podręcznych. Wiele pamięci podręcznych można zoptymalizować oddzielnie, przypisując do każdej pamięci podręcznej unikalny algorytm wyszukiwania. Ten system buforowania obiektów może być hermetyzowany za pomocą wzorca łańcucha odpowiedzialności , który promuje luźne sprzężenie między komponentami.

Konkurencja

Szczególną uwagę należy wziąć pod uwagę, gdy obiekty typu flyweight są tworzone na wielu wątkach. Jeśli lista wartości jest skończona i znana z góry, wagi muchowe można utworzyć z wyprzedzeniem i pobrać z kontenera w wielu wątkach bez rywalizacji. Jeśli instancje wagi muchowej są tworzone w wielu wątkach, istnieją dwie opcje:

  1. Spraw, aby instancja wagi muchowej była jednowątkowa, wprowadzając w ten sposób rywalizację i zapewniając jedną instancję na wartość.
  2. Zezwalaj współbieżnym wątkom na tworzenie wielu instancji wagi muchowej, eliminując w ten sposób rywalizację i umożliwiając wiele instancji na wartość.

Aby umożliwić bezpieczne udostępnianie między klientami i wątkami, obiekty flyweight można przekształcić w obiekty o niezmiennej wartości , w których dwa wystąpienia są uważane za równe, jeśli ich wartości są równe.

Ten przykład z C# 9 używa rekordów do utworzenia obiektu wartości reprezentującego smaki kawy:

public record CoffeeFlavours(string flavour);

Przykład w C#

W tym przykładzie FlyweightPointertworzy statyczny element członkowski używany dla każdej instancji MyObjectklasy.

// Defines Flyweight object that repeats itself.
public class Flyweight
{
    public string CompanyName { get; set; }
    public string CompanyLocation { get; set; }
    public string CompanyWebsite { get; set; }
    // Bulky data
    public byte[] CompanyLogo { get; set; }
}

public static class FlyweightPointer
{
    public static readonly Flyweight Company = new Flyweight
    {
        CompanyName = "Abc",
        CompanyLocation = "XYZ",
        CompanyWebsite = "www.example.com"
        // Load CompanyLogo here
    };
}

public class MyObject
{
    public string Name { get; set; }
    public string Company => FlyweightPointer.Company.CompanyName;
}

Przykład w Pythonie

Atrybuty mogą być definiowane na poziomie klasy, a nie tylko dla instancji w Pythonie, ponieważ klasy są obiektami pierwszej klasy w języku — co oznacza, że ​​nie ma ograniczeń co do ich użycia, ponieważ są takie same jak każdy inny obiekt. Instancje klas w nowym stylu przechowują dane instancji w specjalnym słowniku atrybutów instance.__dict__. Domyślnie, dostępne atrybuty są najpierw wyszukiwane w this __dict__, a następnie wracają do atrybutów klasy instancji. W ten sposób klasa może efektywnie być rodzajem kontenera Flyweight dla swoich instancji.

Chociaż klasy Pythona są domyślnie mutowalne, niezmienność można emulować, nadpisując __setattr__metodę klasy tak, aby nie zezwalała na zmiany jakichkolwiek atrybutów Flyweight.

# Instances of CheeseBrand will be the Flyweights
class CheeseBrand:
    def __init__(self, brand: str, cost: float) -> None:
        self.brand = brand
        self.cost = cost
        self._immutable = True  # Disables future attributions

    def __setattr__(self, name, value):
        if getattr(self, "_immutable", False):  # Allow initial attribution
            raise RuntimeError("This object is immutable")
        else:
            super().__setattr__(name, value)

class CheeseShop:
    menu = {}  # Shared container to access the Flyweights

    def __init__(self) -> None:
        self.orders = {}  # per-instance container with private attributes

    def stock_cheese(self, brand: str, cost: float) -> None:
        cheese = CheeseBrand(brand, cost)
        self.menu[brand] = cheese  # Shared Flyweight

    def sell_cheese(self, brand: str, units: int) -> None:
        self.orders.setdefault(brand, 0)
        self.orders[brand] += units  # Instance attribute

    def total_units_sold(self):
        return sum(self.orders.values())

    def total_income(self):
        income = 0
        for brand, units in self.orders.items():
            income += self.menu[brand].cost * units
        return income

shop1 = CheeseShop()
shop2 = CheeseShop()

shop1.stock_cheese("white", 1.25)
shop1.stock_cheese("blue", 3.75)
# Now every CheeseShop have 'white' and 'blue' on the inventory
# The SAME 'white' and 'blue' CheeseBrand

shop1.sell_cheese("blue", 3)  # Both can sell
shop2.sell_cheese("blue", 8)  # But the units sold are stored per-instance

assert shop1.total_units_sold() == 3
assert shop1.total_income() == 3.75 * 3

assert shop2.total_units_sold() == 8
assert shop2.total_income() == 3.75 * 8

Przykład w C++

Biblioteka szablonów standardowych języka C++ udostępnia kilka kontenerów, które umożliwiają mapowanie unikatowych obiektów na klucz. Użycie kontenerów pomaga jeszcze bardziej zmniejszyć zużycie pamięci, eliminując potrzebę tworzenia obiektów tymczasowych.

#include <iostream>
#include <map>
#include <string>

// Instances of Tenant will be the Flyweights
class Tenant {
public:
    Tenant(const std::string& name = "") : m_name(name) {}

    std::string name() const {
        return m_name;
    }
private:
    std::string m_name;
};

// Registry acts as a factory and cache for Tenant flyweight objects
class Registry {
public:
    Registry() : tenants() {}

    Tenant& findByName(const std::string& name) {
        if (tenants.count(name) != 0) return tenants[name];
        Tenant newTenant{name};
        tenants[name] = newTenant;
        return tenants[name];
    }
private:
    std::map<std::string,Tenant> tenants;
};

// Apartment maps a unique tenant to their room number.
class Apartment {
public:
    Apartment() : m_occupants(), m_registry() {}

    void addOccupant(const std::string& name, int room) {
        m_occupants[room] = &m_registry.findByName(name);
    }

    void tenants() {
        for (auto i : m_occupants) {
            const int room = i.first;
            const auto tenant = i.second;
            std::cout << tenant->name() << " occupies room " << room << std::endl;
        }
    }
private:
    std::map<int,Tenant*> m_occupants;
    Registry m_registry;
};

int main() {
    Apartment apartment;
    apartment.addOccupant("David", 1);
    apartment.addOccupant("Sarah", 3);
    apartment.addOccupant("George", 2);
    apartment.addOccupant("Lisa", 12);
    apartment.addOccupant("Michael", 10);
    apartment.tenants();

    return 0;
}

Przykład w PHP

<?php

class CoffeeFlavour  {

    private string $name;
    private static array $CACHE = [];
    
    private function __construct(string $name){
        $this->name = $name;
    }
    
    public static function intern(string $name) : \WeakReference {
        if(!isset(self::$CACHE[$name])){
            self::$CACHE[$name] =  new self($name);
        }
        return \WeakReference::create(self::$CACHE[$name]);
    }
    
    public static function flavoursInCache() : int {
        return count(self::$CACHE);
    }
    
    public function __toString() : string {
        return $this->name;
    }
    
}

class Order {
    
    public static function of(string $flavourName, int $tableNumber) : callable {
        $flavour = CoffeeFlavour::intern($flavourName)->get();
        return fn() => print("Serving $flavour to table $tableNumber ".PHP_EOL);
    }
    
}

class CoffeeShop {
    
    private array $orders = [];

    public function takeOrder(string $flavour, int $tableNumber) {
        $this->orders[] = Order::of($flavour, $tableNumber);
    }

    public function service() {
        array_walk($this->orders, fn($v) => $v());
    }
}

$shop = new CoffeeShop();
$shop->takeOrder("Cappuccino", 2);
$shop->takeOrder("Frappe", 1);
$shop->takeOrder("Espresso", 1);
$shop->takeOrder("Frappe", 897);
$shop->takeOrder("Cappuccino", 97);
$shop->takeOrder("Frappe", 3);
$shop->takeOrder("Espresso", 3);
$shop->takeOrder("Cappuccino", 3);
$shop->takeOrder("Espresso", 96);
$shop->takeOrder("Frappe", 552);
$shop->takeOrder("Cappuccino", 121);
$shop->takeOrder("Espresso", 121);
$shop->service();
print("CoffeeFlavor objects in cache: ". CoffeeFlavour::flavoursInCache());

Zobacz też

Bibliografia