Andrey on .NET | Порождающие шаблоны: Улучшенный Пул одиночек

Порождающие шаблоны: Улучшенный Пул одиночек

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

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

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 element in this.DataSource) {
            // TODO: Add to listview
        }            
    }

   /* Skipped */
}

Метод 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> iEnum = multiton;
foreach (KeyValuePair<int, DemoClass> obj in iEnum) {
    Console.WriteLine(obj.Key);
    obj.Value.DoSomething();
}

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

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

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

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

Одна мелочь — NullReferenceException не стоит кидать, вы хотели сказать ArgumentNullException (в установщике свойства FactoryMethod).

Спасибо, пропустил что подставилось автоматом по "Null". Там действительно задумывался ArgumentNullException

Вот в упор не вижу смысла всего написанного. Более прямолинейным является встраивание поддержки пула как lifetime manager в тот контейнер, который у вас объекты произвозит. То есть вместо того чтобы писать какой-то отдельный класс, который является и контейнером и lifetime'ом правит, легче отдать контейнерскую часть на аутсорс. Все равно в проектах которые требуют подобного подхода контейнер обычно присуствует.

@ Дмитрий: Можете чуть подробнее вашу идею? Просто у меня сразу куча вопросов возникла. Например:
Пулу же без разницы какой объект отдавать? Или вы про какой-то особый вариант пула?

Потом, что тогда будет гарантировать уникальность экземпляров? Кто помешает завести еще копию контейнера и накидать в него копии объектов с теми же идентификаторами?

Окей, как обычно делается синглтон? Если мы используем Unity, мы просто пишем

container.RegisterType<SomeType>(new ContainerControlledLifetimeManager())
. После этого, любой
container.Resolve<SomeType>()
или же когда делается DI на тип объекта SomeType.

Ну так вот, а что такое Lifetime Manager? Это объект который содержит GetValue() и SetValue() реализации (пример: unity.codeplex.com/.../50871#427249 ). Следовательно для пула, никто не мешает просто переписать реализации. Ну и понятное дело что можно делать sharing этого менеджера между регистрациями.

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

У всех контейнеров есть управление временем жизни, Unity тут не исключение. А работы меньше т.к. уже есть контейнер, следовательно есть инъекция в различных формах, есть возможность делать различные хитрые мэппинги интерфейс-класс, есть в конце концов некие гарантии по потокобезопасности доступа (если правильно отнаследовать LifetimeManager).

Не обязательно нужная такая функциональность. Периодически хватает того же Dictionary. Вот упомянутый в первой части пример с обменом данными с "железом". Там был сторонний класс, обменивающийся данными с  "железкой" в своей сети. Таких до 255. Сама задача – не сложная (считать, показать, записать в файл или БД). Тянуть туда что-то крутое – смысла нет. И таких задач я думаю не мало. Зачем таскать лишнюю функциональность? Но еще раз подчеркну – если уже есть тот же Unity, то да, почему бы не использовать его возможности.

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