I often find myself adding some sort factory in almost every project I’ve ever worked on.
Let’s say we have a pizza price checker factory that returns a validator to check if the price is valid based on a particular type of pizza. It would look something like below
public enum PizzaType
{
Supreme,
Pepperoni
}
public interface IPizzaPriceValidator
{
bool IsValid(decimal price);
}
public class SupremePizzaPriceValidator : IPizzaPriceValidator
{
public bool IsValid(Account account)
{
// Logics go here
return true;
}
}
public class PepperoniPizzaPriceValidator : IPizzaPriceValidator
{
public bool IsValid(Account account)
{
// Logics go here
return true;
}
}
public interface IPizzaPriceValidatorFactory
{
IPizzaPriceValidator Get(PizzaType pizzaType);
}
public class PizzaPriceValidatorFactory : IPizzaPriceValidatorFactory
{
public IPizzaPriceValidator Get(PizzaType pizzaType)
{
switch(pizzaType)
{
case PizzaType.Supreme:
return new SupremePizzaPriceValidator();
case PizzaType.Pepperoni:
return new PepperoniPizzaPriceValidator();
default:
throw new ArgumentException("Invalid pizza type.");
}
}
}
However, if you have other similar factories, you’ll have to have a separate one for each type.
Generic key based factory
Below is the generic key based factory that would eliminate the need to write a separate factory for each type of validator.
public interface IGenericFactory<out T, in TKey>
{
T Get(TKey key);
}
public interface IGenericFactory<out T> : IGenericFactory<T, string>
{
}
The first interface allows generic key whereas the second supports single string key per item.
public class GenericKeyBasedFactory<T, TKey> : IGenericFactory<T, TKey>
{
private readonly Dictionary<TKey, T> _items;
public GenericKeyBasedFactory(
IEnumerable<T> items,
Func<T, TKey> keySelector)
: this(items, t => new[] { keySelector(t) }, EqualityComparer<TKey>.Default)
{
}
public GenericKeyBasedFactory(
IEnumerable<T> items,
Func<T, TKey> keySelector,
IEqualityComparer<TKey> keyComparer)
: this(items, t => new[] { keySelector(t) }, keyComparer)
{
}
public GenericKeyBasedFactory(
IEnumerable<T> items,
Func<T, IEnumerable<TKey>> keySelector)
: this(items, keySelector, EqualityComparer<TKey>.Default)
{
}
public GenericKeyBasedFactory(
IEnumerable<T> items,
Func<T, IEnumerable<TKey>> keySelector,
IEqualityComparer<TKey> keyComparer)
{
_items = new Dictionary<TKey, T>(keyComparer);
foreach (var item in items)
{
var itemKeys = keySelector(item);
if (itemKeys == null || !itemKeys.Any())
continue;
foreach (var key in itemKeys)
{
if (_items.ContainsKey(key))
throw new Exception($"{typeof(T).Name} already exists for key '{key}'");
_items[key] = item;
}
}
}
public virtual bool Exists(TKey key)
{
return _items.ContainsKey(key);
}
public virtual T Get(TKey key)
{
if (_items.TryGetValue(key, out var item))
{
return item;
}
else
{
throw new Exception($"No {typeof(T).Name} found for Key: '{key}'");
}
}
}
public class GenericKeyBasedFactory<T> : GenericKeyBasedFactory<T, string>, IGenericFactory<T>
{
/// <summary>
/// Supports single string key per item
/// </summary>
/// <param name="items"></param>
/// <param name="keySelector"></param>
/// <param name="ignoreCase"></param>
public GenericKeyBasedFactory(IEnumerable<T> items, Func<T, string> keySelector, bool ignoreCase = true)
: base(items, t => new[] { keySelector(t) }, ignoreCase ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal)
{
}
/// <summary>
/// Supports multiple string keys per item
/// (E.G If you had a single class which could be identified by multiple keys)
/// </summary>
/// <param name="items"></param>
/// <param name="keySelector"></param>
/// <param name="ignoreCase"></param>
public GenericKeyBasedFactory(IEnumerable<T> items, Func<T, IEnumerable<string>> keySelector, bool ignoreCase = true)
: base(items, keySelector, ignoreCase ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal)
{
}
}
Usage
Using the pizza price validator sample from above, the code now looks like the following
public interface IPizzaPriceValidator
{
PizzaType PizzaType { get; }
bool IsValid(decimal price);
}
public class SupremePizzaPriceValidator : IPizzaPriceValidator
{
public PizzaType PizzaType => PizzaType.Supreme;
public bool IsValid(Account account)
{
// Logics go here
return true;
}
}
public class PepperoniPizzaPriceValidator : IPizzaPriceValidator
{
public PizzaType PizzaType => PizzaType.Pepperoni;
public bool IsValid(Account account)
{
// Logics go here
return true;
}
}
Then in your StartUp.cs file, where you register your services, you need to register your factory as followed
services.AddSingleton<IPizzaPriceValidator, SupremePizzaPriceValidator>();
services.AddSingleton<IPizzaPriceValidator, PepperoniPizzaPriceValidator>();
services.AddSingleton<IGenericFactory<IPizzaPriceValidator, PizzaType>>(provider =>
new GenericKeyBasedFactory<IPizzaPriceValidator, PizzaType>(provider.GetServices<IPizzaPriceValidator>(), decoder => decoder.PizzaType));
After registering your generic factory, just inject the factory IGenericFactory<IPizzaPriceValidator, PizzaType> and call the Get method passing in the pizza type.
Note that I use AddSingleton for the validators here since they don’t have any dependencies. You should use AddScoped where appropriate.