Продолжим разработку вариантов реализации Пула одиночек, начатую в прошлой части. Поставим задачу сделать простым использование шаблона с уже существующими классами. Кроме того, добавим несколько новых возможностей.
Для достижения указанной цели можно использовать наследование, как в случае с Одиночкой. Однако существующий класс изначально никак не связан с Пулом одиночек. Зачем тогда добавлять лишние связи? Более логичным будет реализовать шаблон как контейнер объектов заданного типа.
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<TKey, TClass>"/> 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<KeyValuePair<TKey,
/// TClass>>"/> 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();
}
Подведем итог и еще раз перечислим достоинства новой реализации:
- возможность наделить любой класс функциональностью Пула одиночек. Единственное отличие в невозможности запретить создание экземпляров этого класса вне шаблона;
- использование интерфейса и экземпляра класса вместо статических методов уменьшает зависимость кода приложения от реализации шаблона;
- разделение функциональности между реализацией шаблона и используемым класса, возможность их независимого повторного использования;
- поддержка реализации интерфейсов для обеспечения дополнительной функциональности.
Кстати, легко можно переписать эту реализацию для использования в качестве основы для новых классов. Но, с оглядкой на плюсы от разделения функциональности, надо ли?