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

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

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

  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 еще не создан.

3. Реализация в виде контейнера экземпляров

Кроме решения основной задачи, устраним еще один недостаток – зависимость кода, использующего шаблон, от вызовов его статических методов. Такой код сложнее тестировать и сопровождать. Поэтому дадим возможность использовать Пул одиночек как экземпляр обычного класса. При этом почти все его методы перестанут быть статическими. А применение шаблона “Одиночка” гарантирует уникальность этого экземпляра в рамках приложения.

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

Последним штрихом будет поддержка IEnumerable, IEnumerable<KeyValuePair<TKey, TClass>>.

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

/// <summary>Thread-safe .NET4 generic multiton interface.</summary>
/// <typeparam name="TKey">The type of the key.</typeparam>
/// <typeparam name="TClass">The type of the stored class.</typeparam>
public interface IMultiton<TKey, TClass> : IEnumerable, IEnumerable<KeyValuePair<TKey, TClass>>
    where TClass : class
{
    /// <summary>Gets or sets the factory method.</summary>
    Func<TKey, TClass> FactoryMethod { get; set; }
 
    /// <summary>Gets the number of instances contained in the multiton.</summary>
    int Count { get; }
 
    /// <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>
    TClass GetInstance(TKey key);
 
    /// <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>
    bool GetExistingInstance(TKey key, out TClass instance);
 
    /// <summary>Removes all instances from the multiton.</summary>
    void 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>
    void Remove(TKey key);
}

Перейдем к реализации:

/// <summary>Thread-safe .NET4 generic multiton.</summary>
/// <typeparam name="TKey">The type of the key.</typeparam>
/// <typeparam name="TClass">The type of the stored class.</typeparam>
public class Multiton<TKey, TClass> : IMultiton<TKey, TClass>
    where TClass : class
{
    /// <summary>The one and only instance of the Multiton class.</summary>
    private static readonly Lazy<Multiton<TKey, TClass>> _instance
        = new Lazy<Multiton<TKey, TClass>>(() => new Multiton<TKey, TClass>());
 
    /// <summary>The container for TClass instances.</summary>
    private readonly Dictionary<TKey, TClass> _instances = new Dictionary<TKey, TClass>();
 
    /// <summary>Factory method.</summary>
    private Func<TKey, TClass> _factoryMethod = Multiton<TKey, TClass>.DefaultFactoryMethod;
 
    /// <summary>Gets or sets the factory method.</summary>
    public Func<TKey, TClass> FactoryMethod
    {
        get { return this._factoryMethod; }
        set
        {
            if (value == null) {
                throw new ArgumentNullException("FactoryMethod can't be null.");
            }
            this._factoryMethod = value;
        }
    }
 
    /// <summary>Gets the number of instances contained in the multiton.</summary>
    public int Count { get { return this._instances.Count; } }
 
    /// <summary>Gets the multiton instance.</summary>
    public static Multiton<TKey, TClass> MultitonInstance
    {
        get { return Multiton<TKey, TClass>._instance.Value; }
    }
 
    /// <summary>Initializes a new instance
    /// of the <see cref="Multiton&lt;TKey, TClass&gt;"/> class.</summary>
    private Multiton() { }
 
    /// <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 TClass GetInstance(TKey key)
    {
        TClass instance = null;
        if (this._instances.TryGetValue(key, out instance)) {
            return instance;
        }
 
        // add and return a new instance
        lock (this._instances) {
            if (this._instances.TryGetValue(key, out instance)) {
                return instance;
            }
 
            instance = this._factoryMethod(key);
            if (instance != null) {
                this._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 bool GetExistingInstance(TKey key, out TClass instance)
    {
        return this._instances.TryGetValue(key, out instance);
    }
 
    /// <summary>Removes all instances from the multiton.</summary>
    public void Clear()
    {
        this._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 void Remove(TKey key)
    {
        this._instances.Remove(key);
    }
 
    /// <summary>Returns an enumerator that iterates through a collection.</summary>
    /// <returns>An <see cref="T:System.Collections.IEnumerator"/>
    /// object that can be used to iterate through the collection.</returns>
    public IEnumerator GetEnumerator()
    {
        return this._instances.Values.GetEnumerator();
    }
 
    /// <summary>Returns an enumerator that iterates through a collection.</summary>
    /// <returns>An <see cref="T:System.Collections.IEnumerator&lt;KeyValuePair&lt;TKey,
    /// TClass&gt;&gt;"/> object that can be used to iterate through the collection.</returns>
    IEnumerator<KeyValuePair<TKey, TClass>>
    IEnumerable<KeyValuePair<TKey, TClass>>.GetEnumerator()
    {
        return this._instances.GetEnumerator();
    }
 
    /// <summary>Default factory method.
    /// Creates an instance of the TClass using private constructor.</summary>
    /// <param name="key">The key of the instance to create.</param>
    /// <returns>The instance for the key.</returns>
    private static TClass DefaultFactoryMethod(TKey key)
    {
        ConstructorInfo objectCtor = typeof(TClass).GetConstructor(
            BindingFlags.Instance | BindingFlags.NonPublic,
            null, new Type[1] { typeof(TKey) }, null);
 
        if (objectCtor == null) {
            throw new InvalidOperationException(
                "TClass should have private constructor: TClass(TKey key)");
        }
 
        return (TClass)objectCtor.Invoke(new object[] { key });
    }
}

Пример использования

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

Для хранения персональной информации разработаем класс EmployeeInfo.

public class EmployeeInfo { /* Skipped */ }

Информация об одном сотруднике потребует только один экземпляр этого класса в рамках всего приложения. Кроме того, она используется в разных участках кода разных классов. Это дает основание для использования шаблона “Пул одиночек”.

Следующий класс, EmployeeListView, отвечает за вывод списка.

public class EmployeeListView
{
    public IMultiton<int, EmployeeInfo> DataSource { get; set; }
 
    public void Refresh()
    {
        // TODO: Clear listview
        foreach (KeyValuePair<int ,="" employeeinfo=""> element in this.DataSource) {
            // TODO: Add to listview
        }           
    }
 
   /* Skipped */
}</int>

Метод Refresh() считывает данные из DataSource и отображает их. В нем использована поддержка интерфейса IEnumerable для получения всех экземпляров.

Класс EmployeeDetailsView предназначен для вывода подробной информации о выбранном сотруднике. Метод Show() обеспечивает их отображение. При этом следует запрос к Пулу одиночек для получения данных. При этом если данных в нем еще нет, то они будут загружены (создан новый объект).

public class EmployeeDetailsView
{
    public IMultiton<int, EmployeeInfo> DataSource { get; set; }
 
    public void Show(int id)
    {
        EmployeeInfo info = this.DataSource.GetInstance(id);
        if (info == null) { return; }
        // TODO: Show detailed information about employee
    }
}

Класс EmployeeDataSource предназначен для получения данных из базы или другого источника.

public class EmployeeDataSource
{
    private IMultiton<int, EmployeeInfo> _dataTarget;
    public IMultiton<int, EmployeeInfo> DataTarget
    {
        set
        {
            this._dataTarget = value;
            this._dataTarget.FactoryMethod = this.GetEmployeeInfo;
        }
    }
 
    public void GetEmployeesList(int startId, int limit)
    {
        this._dataTarget.Clear();
 
        int currentId = startId;
        int employeesInList = 0;
 
        while ((currentId < this.GetMaxEmployeeId()) || (employeesInList < limit)) {
            EmployeeInfo info = this._dataTarget.GetInstance(currentId);
            currentId++;
 
            if (info != null) { employeesInList++; }
        }
    }
 
    private EmployeeInfo GetEmployeeInfo(int id) { /* Skipped */ }
 
    private int GetMaxEmployeeId() { /* Skipped */ }
}

Обратите внимание на использование интерфейса IMultiton. В противовес вызову статических методов, это позволяет убрать зависимость от конкретной реализации и “волшебного” глобального объекта.

Стоит отметить, что класс EmployeeDataSource подставляет в Multiton свой метод для порождения экземпляров. А так же предоставляет метод GetEmployeesList(), который создает список сотрудников. При этом учитывается, что для некоторых значений currentId может не быть данных. В этом случае переменная info будет равна null.

Остается только инициализировать экземпляр Пула одиночек и передать его в остальные классы.

var employees = Multiton<int, EmployeeInfo>.MultitonInstance;
 
var dataSource = new EmployeeDataSource() { DataTarget = employees };
var employeeListView = new EmployeeListView() { DataSource = employees };
var employeeDetailsView = new EmployeeDetailsView() { DataSource = employees };

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

Вернемся к поддерживаемым интерфейсам. В прошлой реализации, все вызовы методов класса Multiton были статическими. А как известно, такие методы не могут реализовывать интерфейсы. В новом варианте этот недостаток исключён. Значит ничего не мешает сделать поддержку IEnumerable.

Теперь можно перебрать все объекты Пула одиночек:

var multiton = Multiton<int, DemoClass>.MultitonInstance;
 
foreach (DemoClass obj in multiton) {
    obj.DoSomething();
}

Кроме того, кроме самих экземпляров, можно получить и их идентификаторы:

IEnumerable<keyvaluepair><int ,="" democlass="">> iEnum = multiton;
foreach (KeyValuePair<int, DemoClass> obj in iEnum) {
    Console.WriteLine(obj.Key);
    obj.Value.DoSomething();
}</int></keyvaluepair>

Подведем итог и еще раз перечислим достоинства новой реализации:

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