Порождающие шаблоны: Пул одиночек (Multiton)

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

Пул одиночек (Multiton / Registry of singletons).

Тип

Порождающий шаблон проектирования (Creational).

Описание

MultitonШаблон "Пул одиночек" позволяет создать определенное число своих экземпляров и предоставляет точку доступа для работы с ними. При этом каждый экземпляр связан с уникальным идентификатором.

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

Данный шаблон можно рассматривать как объединение идей Одиночки и Пула объектов. Исходя их этого можно определить его свойства:

  1. Шаблон может использоваться как с жестко заданным списком экземпляров, так и с созданием по требованию.
  2. Если список фиксированный, то возможно создание всех экземпляров при старте программы или обращению к любому из них.
  3. Возможны два варианта реакции на запрос экземпляра с неизвестным идентификатором: отказ или создание нового.
  4. Минусом шаблона является возможность появления большого числа зависимых от него частей приложения. Однако, как и в случае с Одиночкой, это можно смягчить используя Внедрение зависимостей (Dependency injection).

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

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

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

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

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

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

1. Самая простая реализация на .NET4

Идентификаторами будут выступать значения типа string. Ограничений по списку экземпляров не будет. Это значит, что на любой запрос будет возвращен существующий объект или создан новый. В качестве контейнера объектов возьмем ConcurrentDictionary, входящий в .NET 4. От обычного Dictionary этот вариант отличается потокобезопасностью.

Давайте посмотрим исходный код:

/// <summary>Thread-safe .NET4 multiton.</summary>
public sealed class Multiton
{
    /// <summary>Container for multiton instances.</summary>
    private static readonly ConcurrentDictionary<string, Multiton> _instances
        = new ConcurrentDictionary<string, Multiton>();

    /// <summary>Initializes a new instance of the 
    /// <see cref="Multiton&lt;TKey&gt;"/> class.</summary>
    /// <param name="key">The key of the instance.</param>
    private Multiton(string key) { /* SKIPPED */ }

    /// <summary>Gets the instance associated with the specified key .</summary>
    /// <param name="key">The key of the instance to get.</param>
    /// <returns>The instance for the key.</returns>
    public static Multiton GetInstance(string key)
    {
        return Multiton._instances.GetOrAdd(key, (x) => new Multiton(x));
    }
}

Все очень просто:

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

Остается добавить в класс нужную функциональность и использовать его в деле.

Multiton obj1 = Multiton.GetInstance("instance-id-1");
obj1.DoSomething();

Но что если нужно контролировать по каким идентификаторам создаются экземпляры? В данном варианте это возможно сделать через исключения в конструкторе.

Собственно, для понимания сути Пула одиночек этого достаточно и можно переходить к изучению других шаблонов. Но если вам интересно как можно улучшить данную реализацию, то продолжим.

2. Улучшенная реализация

Давайте доработаем предыдущий вариант. Что изменится?

  1. Сделаем так, чтобы реализация не завесила от типа идентификаторов экземпляра. Оставим этот выбор программисту, который будет с ней работать. Для этого используем generic-тип (TKey).
  2. Добавим контроль над созданием экземпляров. Такая возможность хоть и была в прошлом варианте, но исключения при создании экземпляра это не всегда удобно и даже не всегда необходимо. Разрешим вызывающему коду самому определять дальнейшие действия при невозможности выдать ему объект. Для этого потребуется аналог метода GetOrAdd(), но с учетом возможности отказа в порождении. Поэтому для контейнера теперь используем класс Dictionary.
  3. Кроме того, добавим параметризованный фабричный метод FactoryMethod(). Теперь он будет отвечать за создание экземпляра класса. Если для указанного идентификатора его создать нельзя, то будет возвращено значение по умолчанию (null).
  4. Разрешим пользователю удалять единичные экземпляры (метод Remove()) или выполнить полную очистку (метод Clear()).
  5. Добавим метод GetExistingInstance(), который возвращает только существующие экземпляры.

Перейдем к исходному коду:

/// <summary>Thread-safe multiton.</summary>
public sealed class Multiton<TKey>
{
    /// <summary>Container for multiton instances.</summary>
    private static readonly Dictionary<TKey, Multiton<TKey>> _instances
        = new Dictionary<TKey, Multiton<TKey>>();

    /// <summary>Initializes a new instance of the 
    /// <see cref="Multiton&lt;TKey&gt;"/> class.</summary>
    /// <param name="key">The key of the instance.</param>
    private Multiton(TKey key) { /* SKIPPED */ }

    /// <summary>Gets or create the instance associated with the specified key .</summary>
    /// <param name="key">The key of the instance to get or create.</param>
    /// <returns>The instance for the key.</returns>
    public static Multiton<TKey> GetInstance(TKey key)
    {
        Multiton<TKey> instance = null;
        if (Multiton<TKey>._instances.TryGetValue(key, out instance)) {
            return instance;
        }

        // add new value
        lock (Multiton<TKey>._instances) {
            if (Multiton<TKey>._instances.TryGetValue(key, out instance)) {
                return instance;
            }

            instance = Multiton<TKey>.FactoryMethod(key);
            if (instance != null) {
                Multiton<TKey>._instances.Add(key, instance);
            }
        }

        return instance;
    }

    /// <summary>Gets the instance associated with the specified key.</summary>
    /// <param name="key">The key of the instance to get.</param>
    /// <param name="instance">When this method returns, contains the instance associated 
    /// with the specified key, if the key is found; otherwise, the default value for the 
    /// type of the value parameter. </param>
    /// <returns>true if the Multiton contains an instance with the specified key;
    /// otherwise, false</returns>
    public static bool GetExistingInstance(TKey key, out Multiton<TKey> instance)
    {
        return Multiton<TKey>._instances.TryGetValue(key, out instance);
    }

    /// <summary>Removes all instances from the multiton.</summary>
    public static void Clear()
    {
        Multiton<TKey>._instances.Clear();
    }

    /// <summary>Removes the instance with the specified key from the multiton.</summary>
    /// <param name="key">The key of the instance to remove. 
    /// If the multiton does not contain an instance with the specified key, 
    /// no exception is thrown.</param>
    public static void Remove(TKey key)
    {
        Multiton<TKey>._instances.Remove(key);
    }

    /// <summary>Creates an instance of the Multiton&lt;TKey&gt; class 
    /// using the private constructor .</summary>
    /// <param name="key">The key of the instance to create.</param>
    /// <returns>The instance for the key or null, if the operation failed.</returns>
    private static Multiton<TKey> FactoryMethod(TKey key)
    {
        return new Multiton<TKey>(key);
    }
}

Может возникнуть вопрос: "зачем еще один вызов TryGetValue() в начале блока lock"? Ответ простой: в момент ожидания другой поток может создать требуемый экземпляр. Повторный вызов гарантирует, что не будет лишнего порождения объектов с одинаковым идентификатором.

Полученный класс можно использовать как основу при разработке собственной реализации. Нужно изменить код конструктора и фабричного метода, а так же добавить необходимые свойства и методы. Его использование может выглядеть так:

var obj1 = Multiton<int>.GetInstance(1);
var obj2 = Multiton<int>.GetInstance(1);
obj2.DoSomething();

Multiton<int> obj3;
Multiton<int>.GetExistingInstance(2, out obj3);

В данном примере obj1 и obj2 будут содержать ссылку на один и тот же объект. Переменная obj3 будет равна null, т.к. экземпляр с идентификатором 2 еще не создан.

Продолжим улучшать реализацию шаблона "Пул одиночек" в следующей части.

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

Андрей 29.11.2015 3:01:06

Сделаем так, чтобы реализация не завесила от типа идентификаторов экземпляра. Оставим оставим этот выбор программисту, который будет с ней работать. Для этого используем generic-тип (TKey).
Исправьте опечатку два раза слово оставим.  Не код, но все же..)

Андрей Спасибо за внимательность

И все таки я не допонял, "зачем еще один вызов TryGetValue() в начале блока lock"? Почему не убрать верхний вызов TryGetValue. Тогда останется толъко один в блоке lock, что должно гарантироватъ потокобезопасность. И вообще практика и теория говорят о том, что в блоках lock предподчительнее использовать "какой нибудь левый object" а не реальную коллекцию, что в данном случае все равно, поскоку _instances is static and readonly

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

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