Andrey on .NET | Структурные шаблоны: Приспособленец (Flyweight)

Структурные шаблоны: Приспособленец (Flyweight)

Название шаблона

Приспособленец (Flyweight).

Тип

Структурный шаблон проектирования (Structural).

Описание

Flyweight patternПриспособленец это экземпляр объекта, который выдает себя за группу самостоятельных экземпляров.

Шаблон применяется если:

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

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

Рассмотрим, уже упоминавшийся в описании шаблона Компоновщик, пример с выводом карты. Обратим внимание на такие объекты как "элемент дороги" и "дерево". Они могут иметь несколько вариантов (например лиственное, хвойное дерево, куст и т.д.). Но при этом все экземпляры конкретного варианта отличаются только координатами положения на карте. Вынеся эти контекстные данные во вне, возможно заменить все экземпляры на один. При этом будет ощутимая экономия памяти, т.к. картинка с изображением дерева будет загружена только в одном экземпляре.

Наличие различных вариантов внутреннего состояния приводит к необходимости создания Пула приспособленцев. При запросе нового экземпляра от клиента, он ищет подходящий вариант среди ранее созданных. Если такого не находится, то порождается новый. Для примера выше, Пул приспособленцев создавал бы и содержал различные варианты объекта "дерево", различающиеся выводимой картинкой: хвойное, лиственное, куст и т.д. Аналогично для "элемента дороги": прямые участки разного направления, повороты, перекрестки и т.д.

Таким образом, можно отметить следующих участников шаблона:

  1. Приспособленец (Flyweight) –  объект-приспособленец.
  2. Пул или Фабрика приспособленцев (Flyweight Pool / Factory) – объект, создающий и управляющий экземплярами Приспособленцев.
  3. Неразделяемый объект (Unshared Flyweight) – экземпляр, существующий вне рамок шаблона.

Особенности применения шаблона

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

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

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

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

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

Структурные шаблоны и их отличия

Приспособленец

Не ставит целью изменение интерфейса объекта. Но это может потребоваться для получения обратно данных из вынесенной части состояния.

Позволяет уменьшить число экземпляров объекта в приложении и тем самым сэкономить его ресурсы. Выносит контекстно-зависимую часть состояния объекта вовне, заменяя несколько его экземпляров одним.
Адаптер Изменяет интерфейс объекта не изменяя его функциональности. Может адаптировать несколько объектов к одному интерфейсу. Позволяет повторно использовать уже существующий код. Содержит или наследует адаптируемый объект.
Фасад Объединяет группу объектов под одним специализированным интерфейсом. Упрощает работу с группой объектов, вносит новый уровень абстракции. Содержит или ссылается на объекты, необходимые для реализации специализированного интерфейса.
Мост Разделяет объект на абстракцию и реализацию. Используется для иерархии объектов. Позволяет отдельно изменять (наследовать) абстракцию и реализацию, повышая гибкость системы. Содержит объект(реализацию), который предоставляет методы для заданной абстракций и ее уточнений (наследников).
Декоратор Расширяет возможности объекта, изменяет его поведение. Поддерживает интерфейс декорируемого объекта, но может добавлять новые методы и свойства. Дает возможность динамически менять функциональность объекта. Является альтернативой наследованию (в том числе множественному). Содержит декорируемый объект. Возможна цепочка объектов, вызываемых последовательно.
Прокси Прозрачно замещает объект и управляет доступом к нему. Не изменяет интерфейс или поведение. Упрощает и оптимизирует работу с объектом. Может добавлять свою функциональность, скрывая ее от клиента. Содержит объект или ссылку на него, может управлять существованием замещенного объекта.
Компоновщик Предоставляет единый интерфейс для взаимодействия с составными объектами и их частями. Упрощает работу клиента, позволяет легко добавлять новые варианты составных объектов и их частей. Включается в виде интерфейса в составные объекты и их части.

Схожие шаблоны и их отличия

Приспособленец Главное отличие: указанные далее шаблоны, в отличии от Приспособленца, не подразумевают вынесение части состояния (данных) объекта во внешнюю среду
Пул объектов Ничего не знает о сущности хранимых объектов и выдает экземпляры из хранилища в порядке очереди. При возврате в Пул состояние экземпляра сбрасывается. Пул приспособленцев может выбрать подходящий для конкретного запроса экземпляр или создать его. Благодаря выносу данных не нужно сбрасывать состояние экземпляра.
Пул одиночек Запрещает клиенту самостоятельно порождать экземпляры объекта. Приспособленец не накладывает такое ограничение.
Одиночка Не скрывает уникальности объекта и запрещает клиенту создавать дополнительные экземпляры. Приспособленец старается скрыть уникальность объекта и не запрещает самостоятельно создавать экземпляры.

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

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

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

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

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

Интерфейс 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);
    }
}

Комментарии (3) -

Хорошая статья

В разделе Описание  есть ряд слов "выдается себя за группу", думаю что правильно будет "выдаЕТ себя за группу", без "ся".

А в целом статья отличная, освежил свои знания)

Alex Поправил. Спасибо.

Pingbacks and trackbacks (1)+

Добавить комментарий