Issue with design for combating lack of multiple inheritance

I have a ObjectPool class that implements IPool which has methods GetObject and ReturnObject.
The ObjectPool class is the main, barebone ObjectPool class.
By design, when I get an object from the pool, it removes that object from the NotInUse container and then gives me the object. I do not put it in a InUse container as a safety in case the object getting the object from the pool does not return it, otherwise since the InUse would be referencing the object it would not be garbage collected and cause a memory leak.

However, now I am in need of keeping track of what objects are currently being used, so now I think I can just extend the ObjectPool class in anyway I want to give me that functionality, whether its with pure inheritance, take in a IPool in the constructor, or just make a wrapper class that implements IPool. We will call it ObjectPoolKeep or something.

All is good there, except now I am thinking I also want a Pool that can handle shrinking when objects are not used for a long time to free up some memory. This is useful in case there happened to be a rare moment where there was a high demand for these pooled objects, but things cooled down now and we can free up some memory. We will call it ObjectPoolShrink or something.

The problem now is, I basically want all possible combinations of these classes. I want a ObjectPool that is just barebones, one that is barebone but also keeps references, one that is barebone but also can shrink, and one that is barebone but keeps references and can also shrink.

I can make a class ObjectPoolKeepAndShrink or something that can just be a big wrapper, but it feels kinda wrong. However I dont want to have a system like unitys gameobjects where I would need to call getcomponent and just check for null if its there or not. I basically want to be able to see everything through intellisense.
Whether its composition, association, aggregationā€¦ whatever, it seems I am just going to need to create that specific class that does what I want it to do.
While it may be annoying for large combinations, in this case I could be fine with it, but I am just wondering if this is good practice? Is this just how its done?

In the poolmanager i use, i usually destroy objects that havenā€™t been active for some time. Some objects have a natural life cycle they donā€™t need to shrink or removed.

The object pool can just be seen as an example of the design issue I am running into. I basically want to know what people would generally do in this case.

Right. I think there is not one size fit all pool-manager, i use different manager for different things. Also its very handy to use Linq with lambda experssion , to get a quick GetComponent null check (but be careful with GetComponent). or more info via inteliisense. I can show you an code example if you want.

Iā€™ve created a suggestion for an implementation here. The interesting parts regarding generation of objects and such is left out, but it shows how you could get around the polymorphism problem.

The idea is to have a very simple interface out (an ObjectPool class with two methods), and use factories to create the correct kind of pool.

Internally, the factories uses polymorphism on the ObjectPool class to handle resizing - they either create a default ObjectPool, or a ResizingObjectPool which automatically checks for resizing on adding and removing.

Having a count of the objects in use is handled by injecting objects that takes care of how adding or removing objects is done, and returning a Func that can be invoked by getting the count.

Thatā€™s maybe a bit unclear, hereā€™s an example of usage:

Usage

void UsageDemo() {
    //Use as normal pool system:
    ObjectPool<ParticleSystem> particlePool = ObjectPool<ParticleSystem>.CreateDefaultPool(resize: false);

    var p1 = particlePool.FetchFromPool();
    var p2 = particlePool.FetchFromPool();

    particlePool.ReturnToPool(p1);
    particlePool.ReturnToPool(p2);

    //Use as system with counter:

    Func<int> ParticlesInUseCount;
    ObjectPool<ParticleSystem> particlePool2 = ObjectPool<ParticleSystem>.CreateCountingPool(out ParticlesInUseCount, resize: false);

    var p3 = particlePool2.FetchFromPool();
    Debug.Log(ParticlesInUseCount()); // should print "1"
    var p4 = particlePool2.FetchFromPool();
    Debug.Log(ParticlesInUseCount()); // should print "2"

    particlePool2.ReturnToPool(p3);
    Debug.Log(ParticlesInUseCount()); // should print "1"
    particlePool2.ReturnToPool(p4);
    Debug.Log(ParticlesInUseCount()); // should print "0"

    //A pool that doesn't count, but resizes automatically:
    ObjectPool<ParticleSystem> particlePool3 = ObjectPool<ParticleSystem>.CreateDefaultPool(resize: true);

    //A pool that both counts and resizes automatically
    ObjectPool<ParticleSystem> particlePool4 = ObjectPool<ParticleSystem>.CreateCountingPool(out ParticlesInUseCount, resize: true);
}

And hereā€™s the skeleton of the implementation:

ObjectPool

public class ObjectPool<ObjectType> {

    private readonly List<ObjectType> currentObjects = new List<ObjectType>();
    private readonly ObjectHandler handler;

    protected ObjectPool(ObjectHandler handler) {
        this.handler = handler;
        handler.parentPool = this;
        //initialize the objects somehow
    }

    //Two public methods are available in the API:
    public virtual ObjectType FetchFromPool() {
        return handler.FetchFromPool();
    }

    public virtual void ReturnToPool(ObjectType obj) {
        handler.ReturnToPool(obj);
    }

    /*
     * Factory methods! I've used two here, but you could have four different ones, or one with overloads, or whatever.
     */
    public static ObjectPool<ObjectType> CreateDefaultPool(bool resize) {
        if(resize)
            return new ResizingObjectPool(new DefaultObjectHandler());
        else
            return new ObjectPool<ObjectType>(new DefaultObjectHandler());
    }

    // What's returned here is a function that just gives a count of objects in use, but you could easily return a function that 
    // gives the list of objects
    public static ObjectPool<ObjectType> CreateCountingPool(out Func<int> countInUse, bool resize) {
        var counter = new CountingObjectHandler();
        countInUse = () => counter.CountInUse;

        if (resize)
            return new ResizingObjectPool(counter);
        else
            return new ObjectPool<ObjectType>(counter);
    }



    protected abstract class ObjectHandler {

        public ObjectPool<ObjectType> parentPool;

        public abstract void ReturnToPool(ObjectType obj);

        public abstract ObjectType FetchFromPool();
    }

    protected class DefaultObjectHandler : ObjectHandler {

        public override void ReturnToPool(ObjectType obj) {
            parentPool.currentObjects.Add(obj);
        }

        public override ObjectType FetchFromPool() {
            //Skipping code to create new objects as neccessary
            var obj = parentPool.currentObjects[0];
            parentPool.currentObjects.RemoveAt(0);
            return obj;
        }
    }

    protected class CountingObjectHandler : DefaultObjectHandler {

        private List<ObjectType> objectsInUse = new List<ObjectType>();

        public override ObjectType FetchFromPool() {
            var fetched = base.FetchFromPool();
            objectsInUse.Add(fetched);
            return fetched;
        }

        public override void ReturnToPool(ObjectType obj) {
            objectsInUse.Remove(obj);
            base.ReturnToPool(obj);
        }

        public int CountInUse {
            get { return objectsInUse.Count; }
        }
    }

    protected class ResizingObjectPool : ObjectPool<ObjectType> {

        public ResizingObjectPool(ObjectHandler handler) : base(handler) {}

        public override ObjectType FetchFromPool() {
            ResizeIfShouldResize();
            return base.FetchFromPool();
        }

        public override void ReturnToPool(ObjectType obj) {
            ResizeIfShouldResize();
            base.ReturnToPool(obj);
        }

        private void ResizeIfShouldResize() {
            //figure out if resize should happen
        }
    }
}

You could handle both the resizing problem and the counting problem through injecting objects instead of using polymorphism for one of them. I did it this way to show that thereā€™s alternatives.

The nice thing about this solution is that you hide all of the implementation details - the API of your ObjectPool becomes very thin. The problem is that the function that returns you information about the current state of the ObjectPool isnā€™t on the ObjectPool itself, but rather a different object thatā€™s returned as you create the pool. I think the pros outweighs the cons.

Also note that this is an idea I came up with on the fly - I havenā€™t actually tried to implement this, and I donā€™t know if it would work very well. It looks nice at first glance, though.

Its an interesting idea, and I thank you for taking the time to write it =), however it seems kinda messy to work with.
If I create a ShrinkableObjectPool of which I not only want to possibly decide if it wants to shrink on its own when you grab an object from it, but to also have a public method Shrink(), since your factory just returns everything as a plain ObjectPool, how would I call Shrink(). I dont want to do casts and stuff, and if we have the handler have a reference to the shrink class so that I can call the shrink method, then lets say I want to also have the ObjectPoolKeepInUse that has a list of the objects in use, as well as a public method that allows me to destroy all that is InUse or something.
It seems like it would just get messy and wouldnt really save any work. I would need to create a bunch of handlers for different combinations, which, wouldnt I be better of just making multiple classes with proper interfaces and be done with it?

Which leads to my main question which is, is making multiple classes for multiple combinations a legit ok thing to do? I can do composition/inheritance within those classes to get the function I need, but the user of the class is oblivious to it.

if you just need to shrink the pool, just remove the objects with the index that was first added (oldest), no need for a wall of codes, or fancy design patterns . Using Lambda expression is the best practice for that since it doesnā€™t generate gc. Then you can just construct your own constructor with Shrink(value) and it will know if the object is active or not (with anonymous expression).

Well the question is mainly a general question, but I am using object pools as an example since this is where I am having the issue.

The shrink cant just shrink whenever. the point of shrinking is when I return an object to the pool, a timestamp is made. When I do a shrink check, it checks what timestamps are old enough to allow me to remove however many objects. Its irrelevant what objects I remove since every object in the pool is the same (but I should remove the last item from the list or whatever for better performance).

Im not sure as to what you mean by lambdas not causing garbage. I thought they did (might depend on whats in the lambda), unless if you meant just making it once and then storing it in a Func or something, in which case yea that should be fine garbage wise.
However, how would the lambda be able to set a timestamp for when I return objects, and how would the lambda help for allowing me to call a shrink method on the objectpool to do the check if should shrink?

I understand that, i just give a general example for a solution with the use of Linq. If you want to shrink at the same time you add an object (good idea btw) , then you donā€™t need time stamp, just index number from your list. Lets say you shrink
size i 5, and your method will query the 5 last last objects in the list, and check if they are active and remove them, if not go to next(). I know this is code crafting, but i can show you an working code example if you want. Maybe some one else even have a better idea.

Lambda is just a way of using Linq in this case (object pooling) , Its acutely the GetComponent that will generate allocation. Thats why i think its better to just use the List array index and then check for null.

foreach (var li in last5Objectinlist)
var  SingleQuery = PoolManager.GOCollection.FirstOrDefault(i => i.index == li.index); 
        if (SingleQuery != null)
        {
// remove it
                 }

Hope you understood my idea.

You are basing shrink on an objects active state, however I am basing shrink on how long an object was not touched. All objects in my notInUse list will of course not be active.
Lets say I am doing a object pool for rockets, and it happens to be that this round everyone is using rockets. Alright, pool gives enough rockets to handle this round, but now the next round starts and only one person chose rockets. Now I got so many unneeded rockets, so my shrink method would handle this fine. It checked and saw the last time this rocket was used was 5 minutes ago and the amount of rockets in the pool is above the desired count, so it starts to remove some.
The problem without using timestamps is, how would I ever know that now is a good time to remove this rocket? There might be a high demand for rockets this round, but based on your index approach, I would just end up destroying rockets that were very much needed. Your shrink method would defeat the purpose of the pool.
Also linq is very bad for garbage in my experience.

If you donā€™t know that the ObjectPool you have a reference to implements the Shrink method, safely cast the object to the IShrinkableObjectPool type, and call the method on it. Or, alternatively, if you know that youā€™re creating a shrinkable object pool, then cast the return value of the factory method when you call it, and store the casted reference.

^^ the sentence after was ā€œI dont want to do castsā€¦ā€.
Also, doing all these custom handlers for different combinations and then doing casts ect ect, is it actually helping anything? What do I gain?

Perhaps Iā€™m trivialising the question. But I would approach this with a single class that exposes a few bools. If you wanted the behaviour to be permanent then make them private and set them via the constructor.

If you really needed it you could expose several interfaces so the outside world could treat it as multiple classes.

Anything else is needlessly complicating your code base for little extra value. On modern systems there is no real penalty for having a few extra lines of code that might not be used.

Its not the extra lines of bool checks, its having a public InUse list that would return nothing or null since I set makeInUseList to false, or a public shrink method that does nothing since I set doShrink to false. This seems like bad code design.
The idea of having the object pool class implement multiple combination interfaces, and then when creating the object I choose the interface I want to use is an interesting idea though. It could hide the unused stuff and what not, but you would need to know that you would need to choose the interface, which is bad in my opinion.

You canā€™t have it both ways. At some point the consumer/creator of the object pool is going to need to make a choice about how the pool is designed to work.

Most consumers will only ever be interested in getting an object out of the pool. That should be your default interface that pretty much everything gets.

A few select manager classes will be interested in shrinking or growing pools, accessing in use objects ect. These classes are also likely to create the pools in the first place. So they already know which interfaces they require from the pools.

Doing it this way is still more complicated then I normally run with. How big a game are you building?

Not doing casts is a requirement that I donā€™t quite understand, but it is doable, if it has to happen. Use a generic factory method, where the factory knows how to create the appropriate type.

ShrinkableObjectPool<ObjectType> pool = ObjectPoolFactory<ShrinkableObjectPool, ObjectType>.Create(); Now you know that your pool implements whatever methods the Shrinkable one does.

The gist of the thread is when to use composition over inheritence, and how to organize them. I had a discussion about this with a coworker just today. The general conclusion that we came to was the same as conventional wisdom: Favor composition over inheritence. Once we have more than one or two responsibilities, a class needs to be separated. When making that separation, we can get more extensibility by looking at a composite design first, rather than jumping right to subclassing.

There are caveats with most of the implementations that we have to address the shortcomings of pure composition, though. They generally boil down to the same paradigm of Step 1) Test whether the object has the capability to do X, Step 2) If so, then do X. Whether this looks like a ā€œGetComponent() != nullā€, or ā€œobject.IThingReference != nullā€ or ā€œobject is IThingā€ is mostly semantics.

Not a big game at all, this is mainly for just learning about how others would go about this ^^.

I made this question before I even tackled this issue, and I was going to make a quick example of what I wanted to do, but realized that what ever the hell I had in my head that I thought I could just combine the two classes, was more complicated than I thought. I thought I could do something with generics and what not, but couldnt figure it out.

So now I am curious as to how your factory method would work kru. Baste example seems to work with inheritance behind the scenes with the handlers, but that still ends up with the same problem I am at.

I hate to just dump a shit ton of code, but Ill do it to give more to work with.

Click for code

public interface IPool<T>
{
    T GetPooledObject();
    void ReturnToPool(T item);
}

public interface IPoolable<T>
{
    IPool<T> objectPool {get; set;}
    void OnGetPooledObject();
    void OnReturnToPool();
}


public class ObjectPool<T> : IPool<T> where T : class, IPoolable<T>
{
    HashSet<T> notInUsePool;
    Func<T> factory;
    int maxCount;

    int currentCount;

    public ObjectPool(int initialCreatedCount, int maxCount, Func<T> factory)
    {
        notInUsePool = new HashSet<T>();
        this.maxCount = maxCount;
        this.factory = factory;

        for(int i = 0; i < initialCreatedCount; i++)
        {
            notInUsePool.Add(factory());
        }

        currentCount = notInUsePool.Count;
    }

    public T GetPooledObject()
    {
        T item = null;

        if(notInUsePool.Count > 0)
        {
            item = notInUsePool.First(); //extension method, basically returns a random item
            notInUsePool.Remove(item);
            if(item == null) item = factory();
        }
        else if(notInUsePool.Count < maxCount && currentCount < maxCount)
        {
            item = factory();
            currentCount++;
        }

        if(item != null)
        {
            item.objectPool = this;
            item.OnGetPooledObject();
        }

        return item;
    }
    public void ReturnToPool(T item)
    {
        if(item != null)
        {
            if((notInUsePool.Count < maxCount) && !notInUsePool.Contains(item))
            {
                item.OnReturnToPool();
                notInUsePool.Add(item);
            }
        }
    }

    public void RemoveFromPool(int amount)
    {
        for(int i = 0; i < amount; i++)
        {
            notInUsePool.Remove(notInUsePool.First());
            currentCount--;
        }
    }
}


public class ObjectPoolKeep<T> : ObjectPool<T>, IPool<T> where T : class, IPoolable<T>
{
    public List<T> inUse;

    public ObjectPoolKeep(int initialCreatedCount, int maxCount, Func<T> factory) : base(initialCreatedCount, maxCount, factory){}

    public T GetPooledObject()
    {
        T item = base.GetPooledObject();
        if(item != null) inUse.Add(item);
        return item;
    }

    public void ReturnToPool(T item)
    {
        if(item != null)
        {
            inUse.RemoveFirstBySwap(item); //extension method, swaps item with last in list and then removes last for performance reasons
            base.ReturnToPool(item);
        }
    }
}




public class ObjectPoolShrink<T> : ObjectPool<T>, IPool<T> where T : class, IPoolable<T>
{
    Queue<int> timeStamp;
    int shrinkTargetCount;
    int shrinkTime;
    bool checkShrinkOnGetObject;

    public ObjectPoolShrink(int initialCreatedCount, int maxCount, int shrinkTargetCount, int shrinkTime, bool checkShrinkOnGetObject, Func<T> factory) : base(initialCreatedCount, maxCount, factory)
    {
        this.shrinkTargetCount = shrinkTargetCount;
        this.shrinkTime = shrinkTime;
        this.checkShrinkOnGetObject = checkShrinkOnGetObject;
    }

    public T GetPooledObject()
    {
        if(checkShrinkOnGetObject) Shrink();
        return base.GetPooledObject();
    }

    public void ReturnToPool(T item)
    {
        if(item != null)
        {
            timeStamp.Enqueue(DateTime.Now.Second + shrinkTime);
            base.ReturnToPool(item);
        }
    }

    public void Shrink()
    {
        for(int i = 0; i < timeStamp.Count; i++)
        {
            if(timeStamp.Peek() > DateTime.Now.Second) break;
             
            timeStamp.Dequeue();
            base.RemoveFromPool(1);
        }
    }
}



public class ObjectPoolKeepAndShrink //??????
{
    //???????
}

I quickly just made the shrink and keep class, so there might be bugs or something. I also just inherited from ObjectPool, but we could also inherit from IPool and have an internal ObjectPool or whatever.

However the issue is getting both the functionality of the Keep and Shrink without rewriting code.
At the end of the day I could just rewrite a tiny bit of code or do what boredmormon suggested (maybe even make things protected in the main class, and have other classes that inherit make a public wrapper of the protected), but I am here to learn different approaches =).
So how would a Generic factory handle this?

I zagged, and went with a decorator approach.
Click for code

public interface IPool<T> where T : class, IPoolable<T>
{
    T GetPooledObject();
    void ReturnToPool(T item);
    void RemoveFromPool(int amount);
}

public interface IShrinkingPool<T> : IPool<T> where T : class, IPoolable<T>
{
    void Shrink();
}

public interface IPoolable<T> where T : class, IPoolable<T>
{
    IPool<T> objectPool { get; set; }
    void OnGetPooledObject();
    void OnReturnToPool();
}


public class ObjectPool<T> : IPool<T> where T : class, IPoolable<T>
{
    HashSet<T> notInUsePool;
    Func<T> factory;
    int maxCount;

    int currentCount;

    public ObjectPool(int initialCreatedCount, int maxCount, Func<T> factory)
    {
        notInUsePool = new HashSet<T>();
        this.maxCount = maxCount;
        this.factory = factory;

        for (int i = 0; i < initialCreatedCount; i++)
        {
            notInUsePool.Add(factory());
        }

        currentCount = notInUsePool.Count;
    }

    public T GetPooledObject()
    {
        T item = null;

        if (notInUsePool.Count > 0)
        {
            item = notInUsePool.First(); //extension method, basically returns a random item
            notInUsePool.Remove(item);
            if (item == null) item = factory();
        }
        else if (notInUsePool.Count < maxCount && currentCount < maxCount)
        {
            item = factory();
            currentCount++;
        }

        if (item != null)
        {
            item.objectPool = this;
            item.OnGetPooledObject();
        }

        return item;
    }
    public void ReturnToPool(T item)
    {
        if (item != null)
        {
            if ((notInUsePool.Count < maxCount) && !notInUsePool.Contains(item))
            {
                item.OnReturnToPool();
                notInUsePool.Add(item);
            }
        }
    }

    public void RemoveFromPool(int amount)
    {
        for (int i = 0; i < amount; i++)
        {
            notInUsePool.Remove(notInUsePool.First());
            currentCount--;
        }
    }
}


public class ObjectPoolDecorator<T> : IPool<T> where T : class, IPoolable<T>
{
    protected IPool<T> decoratedPool;

    public ObjectPoolDecorator(IPool<T> poolToDecorate)
    {
        decoratedPool = poolToDecorate;
    }

    public virtual T GetPooledObject()
    {
        return decoratedPool.GetPooledObject();
    }

    public virtual void ReturnToPool(T item)
    {
        decoratedPool.ReturnToPool(item);
    }

    public virtual void RemoveFromPool(int amount)
    {
        decoratedPool.RemoveFromPool(amount);
    }
}

public class ObjectPoolKeep<T> : ObjectPoolDecorator<T> where T : class, IPoolable<T>
{
    public List<T> inUse;

    public ObjectPoolKeep(IPool<T> poolToDecorate) : base(poolToDecorate) { }

    public override T GetPooledObject()
    {
        T item = base.GetPooledObject();
        if (item != null) inUse.Add(item);
        return item;
    }

    public override void ReturnToPool(T item)
    {
        if (item != null)
        {
            //inUse.RemoveFirstBySwap(item); //extension method, swaps item with last in list and then removes last for performance reasons
            base.ReturnToPool(item);
        }
    }
}

public class ObjectPoolShrink<T> : ObjectPoolDecorator<T>, IShrinkingPool<T> where T : class, IPoolable<T>
{
    Queue<int> timeStamp;
    int shrinkTargetCount;
    int shrinkTime;
    bool checkShrinkOnGetObject;

    public ObjectPoolShrink(IPool<T> poolToDecorate, int shrinkTargetCount, int shrinkTime, bool checkShrinkOnGetObject)
        : base(poolToDecorate)
    {
        this.shrinkTargetCount = shrinkTargetCount;
        this.shrinkTime = shrinkTime;
        this.checkShrinkOnGetObject = checkShrinkOnGetObject;
    }

    public override T GetPooledObject()
    {
        if (checkShrinkOnGetObject) Shrink();
        return base.GetPooledObject();
    }

    public override void ReturnToPool(T item)
    {
        if (item != null)
        {
            timeStamp.Enqueue(DateTime.Now.Second + shrinkTime);
            base.ReturnToPool(item);
        }
    }

    public void Shrink()
    {
        for (int i = 0; i < timeStamp.Count; i++)
        {
            if (timeStamp.Peek() > DateTime.Now.Second) break;

            timeStamp.Dequeue();
            base.RemoveFromPool(1);
        }
    }
}



public class ObjectPoolKeepAndShrink<T> : ObjectPoolDecorator<T>, IShrinkingPool<T> where T : class, IPoolable<T>
{
    private ObjectPoolShrink<T> shrinkingDecorator; 

    public ObjectPoolKeepAndShrink(IPool<T> poolToDecorate, int shrinkTargetCount, int shrinkTime, bool checkShrinkOnGetObject)
        : base(poolToDecorate)
    {
        var keepDecorator = new ObjectPoolKeep<T>(decoratedPool);
        shrinkingDecorator = new ObjectPoolShrink<T>(keepDecorator, shrinkTargetCount, shrinkTime, checkShrinkOnGetObject);
    }

    public void Shrink()
    {
        shrinkingDecorator.Shrink();
    }
}

class PoolUseExample
{
    void Example<T>() where T : class, IPoolable<T>
    {
        var basicObjPool = GetBasicObjectPool<T>();
        basicObjPool.GetPooledObject();

        var keepingObjPool = new ObjectPoolKeep<T>(GetBasicObjectPool<T>());
        keepingObjPool.GetPooledObject();
        foreach (var thing in keepingObjPool.inUse);
        {
            // blah
        }

        var shrinkingObjPool = new ObjectPoolShrink<T>(GetBasicObjectPool<T>(), 0, 0, false);
        shrinkingObjPool.GetPooledObject();
        shrinkingObjPool.Shrink();

        var keepAndShrkingObjPool = new ObjectPoolKeepAndShrink<T>(GetBasicObjectPool<T>(), 0, 0, false);
        shrinkingObjPool.GetPooledObject();
        keepAndShrkingObjPool.Shrink();
    }

    private static ObjectPool<T> GetBasicObjectPool<T>() where T : class, IPoolable<T>
    {
        return new ObjectPool<T>(0, 0, () => { return null; });
    }
}

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).

  1. 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.

  2. 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).

  1. 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.

  2. 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.

  3. Lets also add a factory for easy building the different pools.

  4. 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.

When I stated above

I was actually meaning I tried something similar to what you (kar) posted above, and I thought that it would work except I forgot about somethingā€¦

keepAndShrkingObjPool.inUse //Cannot do this

KeepAndShrinkingObjPool is not really Keep and Shrink. Its Shrink with some internal stuff of Keep, but I cant even access the reason for having the keep in the first placeā€¦ the inUse list. (unless if I am missing something?)

A neat idea utilizing the OnGet and OnReturn, and I agree that its way overkill XD.
The simple solution would be to just reimplement the shrink and keep for the combination, though that only works for this case since they are simple. I could have KeeAndShrink that inherits Shrink since that has the most stuff, and then just redo the inUse list stuff. Doing that triggers all kinds of code smell triggers though, but I would much prefer it for its simplicity. However, what if both keep and shrink classes were so very complicated? I guess I would really need to just do the composition way you described, but that also only works mainly because both shrink and keep dont affect the actual pool logic, but just do stuff ontop of it. If Shrink or Keep actually overridden the main pools GetPooledObject, then the composition wouldnt work. I guess at that point I would just need to abstract out as much code as I can, and just reuse those smaller classes within the new bigger classes.
Actually, I also have a GameObjectPool that uses a ObjectPool internally, which means I would need to rewrite a lot more stuff. Maybe utilizing the OnGet and OnReturn isnt so overkill in this case after all. I think I would need to use the idea of putting the code in OnGet and OnReturn, composite the class combinations I need, and hide the composition inside a wrapper class. It can work for this situation since all these combinations seem to just do stuff when OnGet or OnReturn is called, and not affect the actual pool logic.
The reason why it is so important for me to have Keep be separated from everything unless if I absolutely need it, is due to the whole object pool thing. Using Keep means I am at risk of memory leaks, so if I can avoid it then that would be desired.