flyweightПаттерн Приспособленец (Flyweight) – структурный шаблон проектирования, который позволяет использовать разделяемые объекты сразу в нескольких контекстах. Данный паттерн используется преимущественно для оптимизации работы с памятью.

В качестве стандартного применения данного паттерна можно привести следующий пример. Текст состоит из отдельных символов. Каждый символ может встречаться на одной странице текста много раз. Однако в компьютерной программе было бы слишком накладно выделять память для каждого отдельного символа в тексте. Гораздо проще было бы определить полный набор символов, например, в виде таблицы из 128 знаков (алфавитно-цифровые символы в разных регистрах, знаки препинания и т.д.). А в тексте применить этот набор общих разделяемых символов, вместо сотен и тысяч объектов, которые могли бы использоваться в тексте. И как следствие подобного подхода будет уменьшение количества используемых объектов и уменьшение используемой памяти.

Паттерн Приспособленец следует применять при соблюдении всех следующих условий:

  • Когда приложение использует большое количество однообразных объектов, из-за чего происходит выделение большого количества памяти
  • Когда часть состояния объекта, которое является изменяемым, можно вынести во вне. Вынесение внешнего состояния позволяет заменить множество объектов небольшой группой общих разделяемых объектов.

Ключевым моментом здесь является разделение состояния на внутренне и внешнее. Внутреннее состояние не зависит от контекста. В примере с символами внутреннее состояние описывается кодом символа из таблицы кодировки. Так как внутреннее состояние не зависит от контекста, то оно может быть разделяемым и поэтому выносится в разделяемые объекты.

Внешнее состояние зависит от контекста, является изменчивым. В применении к символам внешнее состояние может представлять положение символа на странице. То есть код символа может быть использован многими символами, тогда как положение на странице будет для каждого символа индивидуально.

При создании приспособленца внешнее состояние выносится. В приспособленце остается только внутреннее состояние. То есть в примере с символами приспособленец будет хранить код символа.

Формальное определение паттерна на C#:

class FlyweightFactory
{
    Hashtable flyweights = new Hashtable();
    public FlyweightFactory()
    {
        flyweights.Add("X", new ConcreteFlyweight());
        flyweights.Add("Y", new ConcreteFlyweight());
        flyweights.Add("Z", new ConcreteFlyweight());
    }
    public Flyweight GetFlyweight(string key)
    {
        if (!flyweights.ContainsKey(key))
            flyweights.Add(key, new ConcreteFlyweight());
        return flyweights[key] as Flyweight;
    }
}
 
abstract class Flyweight
{
    public abstract void Operation(int extrinsicState);
}
 
class ConcreteFlyweight : Flyweight
{
    int intrinsicState;
    public override void Operation(int extrinsicState)
    {
    }
}
 
class UnsharedConcreteFlyweight : Flyweight
{
    int allState;
    public override void Operation(int extrinsicState)
    {
        allState = extrinsicState;
    }
}
 
class Client
{
    void Main()
    {
        int extrinsicstate = 22;
 
        FlyweightFactory f = new FlyweightFactory();
 
        Flyweight fx = f.GetFlyweight("X");
        fx.Operation(--extrinsicstate);
 
        Flyweight fy = f.GetFlyweight("Y");
        fy.Operation(--extrinsicstate);
 
        Flyweight fd = f.GetFlyweight("D");
        fd.Operation(--extrinsicstate);
 
        UnsharedConcreteFlyweight uf = new UnsharedConcreteFlyweight();
 
        uf.Operation(--extrinsicstate);
    }
}

Участники

  • Flyweight: определяет интерфейс, через который приспособленцы-разделяемые объекты могут получать внешнее состояние или воздействовать на него
  • ConcreteFlyweight: конкретный класс разделяемого приспособленца. Реализует интерфейс, объявленный в типе Flyweight, и при необходимости добавляет внутреннее состояние. Причем любое сохраняемое им состояние должно быть внутренним, не зависящим от контекста
  • UnsharedConcreteFlyweight: еще одна конкретная реализация интерфейса, определенного в типе Flyweight, только теперь объекты этого класса являются неразделяемыми
  • FlyweightFactory: фабрика приспособленцев – создает объекты разделяемых приспособленцев. Так как приспособленцы разделяются, то клиент не должен создавать их напрямую. Все созданные объекты хранятся в пуле. В примере выше для определения пула используется объект Hashtable, но это не обязательно. Можно применять и другие классы коллекций. Однако в зависимости от сложности структуры, хранящей разделяемые объекты, особенно если у нас большое количество приспособленцев, то может увеличиваться время на поиск нужного приспособленца – наверное это один из немногих недостатков данного паттерна.Если запрошенного приспособленца не оказалось в пуле, то фабрика создает его.
  • Client: использует объекты приспособленцев. Может хранить внешнее состояние и передавать его в качестве аргументов в методы приспособленцев

Рассмотрим пример. Допустим, мы проектируем программу для моделирования города. Город состоит из отдельных домов, поэтому нам надо создать объекты этих домов. Однако домов в городе может быть множество: сотни, тысячи. Они могут иметь разный вид, отличаться по различным признакам. Однако, как правило, многие дома делаются по стандартным проектам. И фактически мы можем выделить несколько типов домов, например, пятиэтажные кирпичные хрущевки, многоэтажные панельные высотки и так далее.

Используя некоторый анализ, мы можем выделить внутренне состояния домов и внешнее. К внутреннему состоянию, например, может относиться количество этажей, материал (кирпичи, панели и т.д.), или те показатели, которые определены его шаблоном, планом проектирования. К внешнему состоянию может относиться положение дома на географической карте, то есть его координаты, цвет дома, и так далее, то есть такие показатели, которые для каждого отдельного дома могут быть относительно индивидуальны.

В этом случае реализация строительства домов на C# с применением паттерна Flyweight могла бы выглядеть следующим образом:

class Program
{
    static void Main(string[] args)
    {
        double longitude = 37.61;
        double latitude = 55.74;
 
        HouseFactory houseFactory = new HouseFactory();
        for (int i = 0; i < 5;i++)
        {
            House panelHouse = houseFactory.GetHouse("Panel");
            if (panelHouse != null)
                panelHouse.Build(longitude, latitude);
            longitude += 0.1;
            latitude += 0.1;
        }
 
        for (int i = 0; i < 5; i++)
        {
            House brickHouse = houseFactory.GetHouse("Brick");
            if (brickHouse != null)
                brickHouse.Build(longitude, latitude);
            longitude += 0.1;
            latitude += 0.1;
        }
 
        Console.Read();
    }
}
 
abstract class House
{
    protected int stages; // количество этажей
 
    public abstract void Build(double longitude, double latitude);
}
 
class PanelHouse : House
{
    public PanelHouse()
    {
        stages = 16;
    }
 
    public override void Build(double longitude, double latitude)
    {
        Console.WriteLine("Построен панельный дом из 16 этажей; координаты: {0} широты и {1} долготы",
            latitude, longitude);
    }
}
class BrickHouse : House
{
    public BrickHouse()
    {
        stages = 5;
    }
 
    public override void Build(double longitude, double latitude)
    {
        Console.WriteLine("Построен кирпичный дом из 5 этажей; координаты: {0} широты и {1} долготы",
            latitude, longitude);
    }
}
 
class HouseFactory
{
    Dictionary<string, House> houses = new Dictionary<string, House>();
    public HouseFactory()
    {
        houses.Add("Panel", new PanelHouse());
        houses.Add("Brick", new BrickHouse());
    }
 
    public House GetHouse(string key)
    {
        if (houses.ContainsKey(key))
            return  houses[key];
        else
            return null;
    }
}

В качестве интерфейса приспособленца выступает абстрактный класс House, который определяет переменную stages – количество этажей, поскольку количество этажей относится к внутреннему состоянию, которое присуще всем домам. И также определяется метод Build(), который в качестве параметра принимает широту и долготу расположения дома – внешнее состояние.

Конкретные классы разделяемых приспособленцев – PanelHouse и BrickHouse отвечают за построение конкретных типов домов. Поскольку архитектурный план проектирования может точно задавать количество этажей для определенного типа дома, то в данном случае количество этажей устанавливается в конструкторе.

Фабрика HouseFactory создает два объекта дома для каждого конкретного приспособленца и возвращает их в методе GetHouse() в зависимости от параметра.

В роли клиента выступает класс Program, который задает начальные широту и долготу – внешнее состояние домов и использует фабрику для создания домов. Причем в реальности мы будем оперировать всего лишь двумя объектами, которые будут храниться в словаре в HouseFactory.

Реализация шаблона в общем виде

  • определяем следующие параметры:
    • какая информация о состоянии объекта может быть вынесена;
    • вписывается ли выносимая информация в абстракцию нового хранилища;
    • насколько это уменьшит число экземпляров в приложении;
  • выносим часть состояния и разрабатываем схему передачи ее данных объекту при вызовах;
  • разрабатываем Пул приспособленцев и механизм удаления неиспользуемых экземпляров;
  • клиент получает экземпляры объектов от Пула, однако при крайней необходимости может порождать их самостоятельно.

Пример реализации

За основу возьмем пример, который был приведен в описании шаблона Компоновщик.

Если посмотреть внимательно, то можно заметить, что многие элементы карты, например деревья или участки дорог, отличаются только координатами. Поэтому если вынести эти данные из класса, то все его экземпляры можно будет заменить одним. Кроме того, они вполне вписываются в абстракцию своего нового места хранения (в классы составных объектов).

Интерфейс IMapComponent, описывающий компонент карты, будет выглядеть как в исходном примере. Однако, учитывая что координаты объекта вынесены, параметры x и y метода Draw() теперь задают точку для отображения объекта, а не положение родительского компонента.

public interface IMapComponent
{
    IMapComponent Parent { get; set; }
 
    string Title { get; set; }
 
    void Draw(int x, int y);
 
    IMapComponent FindChild(string name);
}

А вот его реализация MapComponent немного изменится, т.к. “потеряет” поля для хранения координат.

public abstract class MapComponent : IMapComponent
{
    public IMapComponent Parent { get; set; }
 
    public string Title { get; set; }
 
    public abstract void Draw(int x, int y);
 
    public virtual IMapComponent FindChild(string name)
    {
        return (this.Title == name) ? this : null;
    }
}

Перейдем к составному объекту, который содержит компоненты. Именно он будет содержать вынесенные данные. Поэтому метод AddComponent() должен теперь содержать не только ссылку на добавляемый экземпляр, но и его координаты. Кроме того, для сохранения координат и связи их с объектом, создадим внутренний класс ComponentContainer. Соответственно, изменится пересчет положения в методе Draw():

public interface IMapComposite : IMapComponent
{
    void AddComponent(IMapComponent component, int x, int y);
}
 
public class MapComposite : MapComponent, IMapComposite
{
    private class ComponentContainer
    {
        public int X { get; set; }
        public int Y { get; set; }
        public IMapComponent Component { get; set; }
    }
 
    private List<ComponentContainer> _components = new List<ComponentContainer>();
 
    public void AddComponent(IMapComponent component, int x, int y)
    {
        ComponentContainer newContainer = new ComponentContainer() {
            X = x,
            Y = y,
            Component = component
        };
 
        this._components.Add(newContainer);
        component.Parent = this;
    }
 
    public override void Draw(int x, int y)
    {
        Console.WriteLine(this.Title);
 
        foreach (ComponentContainer container in this._components) {
            container.Component.Draw(x + container.X, y + container.Y);
        }
    }
 
    public override IMapComponent FindChild(string name)
    {
        if (this.Title == name) {
            return this;
        }
 
        foreach (ComponentContainer container in this._components) {
            IMapComponent found = container.Component.FindChild(name);
 
            if (found != null) {
                return found;
            }
        }
 
        return null;
    }
}

Перейдем к реализации компонент. Создадим два Приспособленца, представляющих объекты “дерево” и “участок дороги”. Кроме того, для сравнения добавим реализацию объекта “дом”.

public class MapTreeFlyweight : MapComponent
{
    public override void Draw(int x, int y)
    {
        Console.WriteLine("{0} Tree at {1}:{2}", this.Title, x, y);
    }
}
 
public class MapRoadFlyweight : MapComponent
{
    public override void Draw(int x, int y)
    {
        Console.WriteLine("{0} Road at {1}:{2}", this.Title, x, y);
    }
}
 
public class MapHouse : MapComponent
{
    public override void Draw(int x, int y)
    {
        Console.WriteLine("{0} House at {1}:{2}", this.Title, x, y);
    }
}

Для полной реализации шаблона, осталось создать Абстрактную фабрику, которая будет включать в себя Пул приспособленцев. Для корректной работы Пула дополнительно потребуется реализовать шаблон Одиночка. Это обеспечит доступ с созданным экземплярам из любой точки приложения. В качестве хранилища экземпляров используем ConcurrentDictionary. Так же добавим перечисления Trees и Roads, показывающие какие варианты объектов поддерживаются.

public class MapComponentFactory
{
    private static readonly Lazy<MapComponentFactory> _instance
        = new Lazy<MapComponentFactory>(() => new MapComponentFactory());
 
    public enum Trees { Oak, Spruce, Pine, Birch, Aspen };
    public enum Roads { Direct, TurnLeft, TurnRight }
 
    private ConcurrentDictionary<Trees, IMapComponent> _trees
        = new ConcurrentDictionary<Trees, IMapComponent>();
 
    private ConcurrentDictionary<Roads, IMapComponent> _roads
        = new ConcurrentDictionary<Roads, IMapComponent>();
 
    private MapComponentFactory() { }
 
    public static MapComponentFactory Instance
    {
        get { return MapComponentFactory._instance.Value; }
    }
 
    public IMapComponent CreateTree(Trees treeType)
    {
        return this._trees.GetOrAdd(treeType,
            (key) => { return new MapTreeFlyweight() { Title = key.ToString() }; }
            );
    }
 
    public IMapComponent CreateRoad(Roads roadType)
    {
        return this._roads.GetOrAdd(roadType,
            (key) => { return new MapRoadFlyweight() { Title = key.ToString() }; }
            );
    }
 
    public IMapComponent CreateHouse(string title)
    {
        return new MapHouse() { Title = title };
    }
}

Посмотрим на работу шаблона в действии. Создадим простой участок карты и выведем на экран его самого и его части. Обратите внимание на следующие моменты:

  • код клиентской части, т.е. методы DrawArea() и Execute(), практически не изменились по сравнению с исходным примером;
  • объекты-Приспособленцы создаются только в том количестве, сколько их вариантов используется;
  • не обязательно все компоненты должны быть Приспособленцами;
  • принцип создания клиентом обычных объектов и Приспособленцев не отличается.
public static class Demo
{
    public static IMapComponent BuildCity(MapFactory mapFactory)
    {
        IMapComposite road1 = new MapComposite() { Title = "Main Street" };
        road1.AddComponent(mapFactory.CreateRoad(MapFactory.Roads.Direct), 0, 2);
        road1.AddComponent(mapFactory.CreateRoad(MapFactory.Roads.Direct), 1, 2);
        road1.AddComponent(mapFactory.CreateRoad(MapFactory.Roads.TurnRight), 2, 2);
        road1.AddComponent(mapFactory.CreateRoad(MapFactory.Roads.Direct), 2, 1);
        road1.AddComponent(mapFactory.CreateRoad(MapFactory.Roads.Direct), 2, 0);
 
        IMapComposite district1 = new MapComposite() { Title = "District 1" };
        district1.AddComponent(mapFactory.CreateHouse("House 1"), 1, 3);
        district1.AddComponent(mapFactory.CreateHouse("House 2"), 3, 1);
        district1.AddComponent(road1, 0, 0);
 
        IMapComposite park1 = new MapComposite() { Title = "City Park" };
        park1.AddComponent(mapFactory.CreateTree(MapFactory.Trees.Oak), 0, 0);
        park1.AddComponent(mapFactory.CreateTree(MapFactory.Trees.Aspen), 1, 0);
        park1.AddComponent(mapFactory.CreateTree(MapFactory.Trees.Aspen), 1, 1);
        park1.AddComponent(mapFactory.CreateTree(MapFactory.Trees.Aspen), 0, 1);
        district1.AddComponent(park1, 0, 0);
 
        IMapComposite city = new MapComposite() { Title = "New city" };
        city.AddComponent(district1, 0, 0);
 
        return city;
    }
 
    public static void DrawArea(IMapComponent component)
    {
        if (component == null) {
            return;
        }
 
        Console.WriteLine("Drawing ...");
        component.Draw(0, 0);
        Console.WriteLine("==============n");
    }
 
    public static void Execute()
    {
        IMapComponent city = BuildCity(MapFactory.Instance);
        DrawArea(city);
 
        IMapComponent road = city.FindChild("Main Street");
        DrawArea(road);
 
        IMapComponent house = city.FindChild("City Park");
        DrawArea(house);
    }
}