Taking your code, and my consideration of how I would do it. This isnāt necessarily the right choice, but rather how Iād go about it, when considering the freedom you describe (or imply).
-
you seem to imply that we should be able to implement IPool freely, otherwise the IPool is uselessā¦ might as well just have āObjectPoolā. So with that assumption, we should implement in a manner that allows us freedom to construct a pool as we deem it.
-
You want to be able to mix and match functionality, doing so through inheritance would make for an every growing number of classes. 1 bonus functionality creates 1 more class. 2 bonus functionalities has A, AB, B, or 3 classes. 3 bonus functionalities is A, AB, ABC, BC, AC, C, or 6 classes. Weāre growing at a rate of !N where N is the number of bonus functions. This is cumbersome! Composite pattern would make this a lot easier.
I know, I know, you donāt like the idea of the composite pattern (similar to unityās GetComponent)ā¦ but it really honestly allows for the freedom you want. Even better than multiple inheritance would (honestly, multiple inheritance just opens up a bag of nightmares).
-
if weāre going to composite, we can encapsulate the compositing pattern as well, so that we donāt have to implement the compositing for every type of IPool that exists.
-
A shrinking pool expects the pool to have a remove method. We need some sort of guarantee of this. In your implementation this is because youāre inheriting from a kind of Pool that has a Remove method. But if we composite this āIPoolā doesnāt. Iām going to make the assumption that trackers should work with all pools, so Iām going to put a remove function on the IPool contract. Iām also going to rename it to Purgeā¦ just personal taste, IMO it conveys the intent more.
-
Lets also add a factory for easy building the different pools.
-
I changed the IPoolable slightly, so that way the āobjectPoolā property canāt be changed willy nilly. If the IPoolable should be able to be transferred from pool to pool, thereās better ways to do it, then having the setter for it just sitting out in the public like that.
public interface IPool<T>
{
T GetPooledObject();
void ReturnToPool(T item);
void Purge(int amount);
}
public interface IPoolable<T>
{
IPool<T> objectPool { get; }
void OnGetPooledObject();
void OnReturnToPool();
}
public interface IPoolTracker<T> where T : class, IPoolable<T>
{
void OnGetPooledObject(T item);
void OnReturnToPool(T item);
}
public class SimpleObjectPool<T> : IPool<T> where T : class, IPoolable<T>
{
#region Fields
private Stack<T> _pool;
private Func<IPool<T>, T> _factory;
private int _maxCount;
private int _currentCount;
#endregion
#region CONSTRUCTOR
public SimpleObjectPool(Func<IPool<T>, T> factory, int maxCount, int initialCreatedCount = 0)
{
_pool = new Stack<T>();
_factory = factory;
_maxCount = maxCount;
for (int i = 0; i < Math.Min(initialCreatedCount, maxCount); i++)
{
_pool.Push(factory(this));
}
_currentCount = _pool.Count;
}
#endregion
#region IPool Interface
public virtual T GetPooledObject()
{
T item = null;
if (_pool.Count > 0)
{
item = _pool.Pop();
if (item == null) item = _factory(this);
}
else if (_pool.Count < _maxCount && _currentCount < _maxCount)
{
item = _factory(this);
_currentCount++;
}
if (item != null)
{
item.OnGetPooledObject();
}
return item;
}
public virtual void ReturnToPool(T item)
{
if (item != null)
{
if ((_pool.Count < _maxCount) && !_pool.Contains(item))
{
item.OnReturnToPool();
_pool.Push(item);
}
}
}
public void Purge(int amount)
{
while (amount-- > 0 && _pool.Count > 0)
{
_pool.Pop();
}
}
#endregion
}
/// <summary>
/// Here to just show possible other pool types.
/// </summary>
/// <typeparam name="T"></typeparam>
public class ArbitraryPool<T> : IPool<T> where T : class, IPoolable<T>
{
public T GetPooledObject()
{
throw new NotImplementedException();
}
public void Purge(int amount)
{
throw new NotImplementedException();
}
public void ReturnToPool(T item)
{
throw new NotImplementedException();
}
}
public class CompositeObjectPool<T> : IPool<T> where T : class, IPoolable<T>
{
#region Fields
private IPool<T> _pool;
private HashSet<IPoolTracker<T>> _trackers = new HashSet<IPoolTracker<T>>();
#endregion
#region CONSTRUCTOR
public CompositeObjectPool(IPool<T> pool)
{
_pool = pool;
}
#endregion
#region Methods
public void AddTracker(IPoolTracker<T> tracker)
{
_trackers.Add(tracker);
}
public void RemoveTracker(IPoolTracker<T> tracker)
{
_trackers.Remove(tracker);
}
public TValue GetTracker<TValue>() where TValue : IPoolTracker<T>
{
if (_trackers.Count == 0) return default(TValue);
var e = _trackers.GetEnumerator();
while(e.MoveNext())
{
if (e.Current is TValue) return (TValue)e.Current;
}
return default(TValue);
}
#endregion
#region IPool Interface
public T GetPooledObject()
{
var item = _pool.GetPooledObject();
if(_trackers.Count > 0)
{
var e = _trackers.GetEnumerator();
while (e.MoveNext())
{
e.Current.OnGetPooledObject(item);
}
}
return item;
}
public void ReturnToPool(T item)
{
_pool.ReturnToPool(item);
if (_trackers.Count > 0)
{
var e = _trackers.GetEnumerator();
while (e.MoveNext())
{
e.Current.OnReturnToPool(item);
}
}
}
public void Purge(int amount)
{
_pool.Purge(amount);
}
#endregion
}
public class KeepPoolTracker<T> : IPoolTracker<T> where T : class, IPoolable<T>
{
public HashSet<T> _inUse = new HashSet<T>();
void IPoolTracker<T>.OnGetPooledObject(T item)
{
if (item != null) _inUse.Add(item);
}
void IPoolTracker<T>.OnReturnToPool(T item)
{
if(item != null) _inUse.Remove(item);
}
}
public class ShrinkPoolTracker<T> : IPoolTracker<T> where T : class, IPoolable<T>
{
#region Fields
private Queue<int> _timeStamps = new Queue<int>();
private IPool<T> _pool;
private int _shrinkTargetCount;
private int _shrinkTime;
private bool _checkShrinkOnGetObject;
#endregion
#region CONSTRUCTOR
public ShrinkPoolTracker(IPool<T> pool, int shrinkTargetCount, int shrinkTime, bool checkShrinkOnGetObject)
{
_pool = pool;
_shrinkTargetCount = shrinkTargetCount;
_shrinkTime = shrinkTime;
_checkShrinkOnGetObject = checkShrinkOnGetObject;
}
#endregion
#region Methods
public void Shrink()
{
for (int i = 0; i < _timeStamps.Count; i++)
{
if (_timeStamps.Peek() > DateTime.Now.Second) break;
_timeStamps.Dequeue();
_pool.Purge(1);
}
}
#endregion
#region IPoolTracker Interface
void IPoolTracker<T>.OnGetPooledObject(T item)
{
if (_checkShrinkOnGetObject) this.Shrink();
}
void IPoolTracker<T>.OnReturnToPool(T item)
{
if (item != null)
{
_timeStamps.Enqueue(DateTime.Now.Second + _shrinkTime);
}
}
#endregion
}
public static class PoolFactory
{
public static IPool<T> CreateSimpleObjectPool<T>(Func<IPool<T>, T> factory, int maxCount, int initialCreatedCount = 0) where T : class, IPoolable<T>
{
return new SimpleObjectPool<T>(factory, maxCount, initialCreatedCount);
}
public static IPool<T> CreateArbitraryPool<T>() where T : class, IPoolable<T>
{
return new ArbitraryPool<T>();
}
public static CompositeObjectPool<T> MakeComposite<T>(this IPool<T> pool) where T : class, IPoolable<T>
{
if (pool is CompositeObjectPool<T>)
{
return pool as CompositeObjectPool<T>;
}
else
{
return new CompositeObjectPool<T>(pool);
}
}
public static IPool<T> MakeKeepPool<T>(this IPool<T> pool) where T : class, IPoolable<T>
{
KeepPoolTracker<T> tracker;
return MakeKeepPool<T>(pool, out tracker);
}
public static IPool<T> MakeKeepPool<T>(this IPool<T> pool, out KeepPoolTracker<T> tracker) where T : class, IPoolable<T>
{
var comp = pool.MakeComposite();
tracker = new KeepPoolTracker<T>();
comp.AddTracker(tracker);
return comp;
}
public static IPool<T> MakeShrinkPool<T>(this IPool<T> pool, int shrinkTargetCount, int shrinkTime, bool checkShrinkOnGetObject) where T : class, IPoolable<T>
{
ShrinkPoolTracker<T> tracker;
return MakeShrinkPool<T>(pool, shrinkTargetCount, shrinkTime, checkShrinkOnGetObject, out tracker);
}
public static IPool<T> MakeShrinkPool<T>(this IPool<T> pool, int shrinkTargetCount, int shrinkTime, bool checkShrinkOnGetObject, out ShrinkPoolTracker<T> tracker) where T : class, IPoolable<T>
{
var comp = pool.MakeComposite();
tracker = new ShrinkPoolTracker<T>(comp, shrinkTargetCount, shrinkTime, checkShrinkOnGetObject);
comp.AddTracker(tracker);
return comp;
}
public static TValue GetTracker<T, TValue>(this IPool<T> pool) where T : class, IPoolable<T> where TValue : IPoolTracker<T>
{
if(pool is CompositeObjectPool<T>)
{
return (pool as CompositeObjectPool<T>).GetTracker<TValue>();
}
return default(TValue);
}
}
Now to create one. Note in the factory I gave a way to get the specific tracker on creation so we donāt have to repeatedly find the tracker with āGetTrackerā. Though the option is there.
ShrinkPoolTracker<SomeType> shrink;
IPool<SomeType> pool = PoolFactory.CreateSimpleObjectPool(factory, 1000, 10)
.MakeKeepPool()
.MakeShrinkPool(100, 10, true, out shrink);
//do stuff
shrink.Shrink(); //demonstrating we can call shrink
//do stuff
pool.GetTracker<ShrinkPoolTracker<SomeType>>().Shrink(); //demonstrating long hand method
Note, this is all just an idea.
Honestly, I find this to be a bit overkill IMO.
A lot of this code is so trivial that implementing on demand is probably easier, as well as more maintainable.
Iād call this all code smell/over engineering.
And not from just the implementation standpoint, but from the use scenario. Having a permutatable set of pool functionalities like thisā¦ just weird.
How many pools of varying functionality is one going to have!?
But for theoretical sakeā¦ it was fun. I have used similar patterns where the overkill was worth it. Like my tween engine.
Not accomplishing the same thing, but using a lot of the same design principles.