General good practice
Playing the devil's advocate, I can use a good practice argument in either case to explain why it's a bad idea. Essentially:
- You're violating the interface segregation principle, the I in SOLID.
- Needing to upcast your objects is polymorphism abuse.
That being said, the second argument is the weaker one here (which I will elaborate on further in this answer). There are valid reasons to consider this abuse though, e.g.:
public void LetThisDuckSwim(Animal a)
{
if(a is Duck d)
d.Swim();
}
This is an exaggerated example, but it gets the point across that the method parameter should be of type Duck
, not Animal
.
While not exactly the same issue, you start seeing an overlap with the classic bad example for the Liskov Substitution Principle:
void MakeDuckSwim(IDuck duck)
{
if (duck is ElectricDuck educk)
educk.TurnOn();
duck.Swim();
}
Other than a minor difference in using ElectricDuck
vs IElectricDuck
, you basically have the same scenario as what you want to achieve, which is used as a prime example of bad practice.
Your scenario
Since you're talking about interfaces where you intentionally refuse to implement part of the interface in certain implementing classes, in order to both avoid LSP and/or ISP violations, the interfaces should be separated.
But, as you are already aware, this does in fact lead you to needing the following upcasting logic (or similar):
void Update()
{
foreach (var module in modules)
if (module is IUpdateable updateable)
updateable.Update();
}
Is there a better way to create a collection of objects, some of which has additional methods, and some of them don't?
You're somewhat asking the wrong question. The core question is whether you should be using that centralized "collection of objects" at all. Consider not putting everything together in one big list:
// 1
private List<IModule> modules = new List<IModule>();
// 2
private List<IUpdateableModule> updateableModules = new List<IUpdateableModule>();
private List<IDestroyableModule> destroyableModules = new List<IDestroyableModule>();
private List<IThrowableModule> throwableModules = new List<IThrowableModule>();
There's a reasonable argument for both cases here. Your question doesn't really shine enough of a light on whether the second approach would be equally viable for you.
There are justifications for the first approach. Unity (the game engine) actually uses components this way. It feels slightly dirty to have to upcast your components before you can use them, but admittedly it makes component handling significantly easier.
However, Unity does provide some query logic to fetch components of the right type, which is something you might want to do. I think this is a reasonable compromise between the two possibilities. LINQ actually already provides this possibility via OfType
private List<IModule> modules = new List<IModule>();
private List<IUpdateableModule> updateableModules => modules.OfType<IUpdateableModule>();
Whether you define these separate properties or use OfType
directly is arguable, though I would say that defining the properties would minimize the impact of changing the implementation of how modules are stored in the future.
I may end up with dozen of modules, and Update will fire 50 to 100 times per second, but I think I covered performance-wise with both approaches. As far as i can tell there are no garbage or expensive operations.
Performance is a relevant thing to consider here - though you may want to wait until an actual problem appears.
Assuming the lower end of the spectrum, iterating over 12 modules 50 times per second leads to 600 iterations in total. This is an O(m*n)
complexity, so this is sensitive to increases of either m
(module count) or n
(framerate).
Optimizing this by separating the lists is not as straightforward as you'd initially expect:
- If all modules implement all interfaces, you will lose performance (since you iterate over the module list once per available interface.
- If modules on average implement less than half of the available interfaces, you will gain performance by not needlessly iterating over them (when iterating over the interfaces that a given module doesn't implement).
- If modules on average implement the majority of interfaces, having them on separated lists will actually cause them to be iterated over several times (once per implemented interface), which would be detrimental to performance.
I would suggest leaving the optimizations for when there is an actual problem. It might be interesting to already log this information so you can keep an eye out for any changes to performance (e.g. as the framerate increases or more modules are developed).
Note: I suspect Unity already did the math here and decided to stick with the centralized list de/inspite of performance considerations; but I have not yet found explicit confirmation of my suspicion.
does this module, in this loop, at this iteration, is updatable? and throwable? Or just one? None?
modules.OfType<IUpdateable>().ForEach(m => m.Update());
will work I think. I suppose this is also provides evidence that filtering a collection on type is fairly common, since they've built it into LINQ.