While trying to solve my Data Persistence issues, I discovered another small problem that would need to be worked out. Since the IPeristedStorage
objects are generic, it does not expose anyway for you to fetch the factory associated with objects you are trying to load. So invoking the following can be problematic.
this.Worlds = this.StorageSource.Load< IWorld>().ToList();
Since the method signature is:
public IEnumerable< T> Load< T>() where T : class
We can provide any Type in the Load method, but the Load method won't know what factory to use to fetch the Type. How does the storage source know what factory to use? I could possibly force it with a secondary generic parameter:
this.Worlds = this.StorageSource.Load< IWorld, WorldFactory>().ToList();
That tightly couples the engines restore code to that factory. What happens if someone wants to create their own factory? Out of luck chuck. We can't have that can we? So what do we do? Well, we create a factory object that can be used to fetch other factories of course! Lets start with what we want to get back, we know we want to get back a collection of IWorld objects right? The StorageSource doesn't know what T (IWorld) is ahead of time (only at run-time) so we have to take T and find a factory that supports it.
Our Factory method would look like this:
Type factoryType = EngineFactory.FindFactory< T>();
Creating the initial Factory
To start out, we need to force all of our Factorys to implement a new interface, IFactory
. This is what the interface looks like.
/// < summary>
/// Defines a contract for creating a Factory object.
/// < /summary>
/// < typeparam name="T">An interface that the factory is targeting.< /typeparam>
public interface IFactory< T>
{
/// < summary>
/// Gets a collection of Types matching < T>.
/// < /summary>
/// < param name="fromAssemblies">From assemblies.< /param>
/// < returns>Returns a collection of objects matching T< /returns>
List< T> GetObjects(Assembly[] fromAssemblies = null, string[] compatibleTypes = null);
/// < summary>
/// Gets a single Type matching < T>.
/// < /summary>
/// < typeparam name="UTypeToFetch">The type of the type to fetch.< /typeparam>
/// < param name="fromAssemblies">From assemblies.< /param>
/// < returns>Returns an instance of UTypeToFetch casted as T.< /returns>
T GetObject< UTypeToFetch>(Assembly[] fromAssemblies = null, string compatibleType = null);
}
Now that we have an interface defined we can start writing our factories. We have two methods that they must implement, one that fetches all Types that match < T>
and a method that fetches one Type that matches < T>
. It is important to note that there are no constraints set on this interface. < T> can be anything at this point. We will set the constraints on the actual Factory implementations instead.
For our first Factory, we will create the WorldFactory object. This factory will fetch all objects implementing IWorld and return them to the caller.
/// < summary>
/// Provides a means to fetch objects that implement the IWorld interface
/// < /summary>
public class WorldFactory < T> : IFactory< T> where T : class, IWorld
{
/// < summary>
/// Gets a collection objects implementing IWorld.
/// < /summary>
/// < returns>A collection of objects in memory implementing IWorld< /returns>
public List< T> GetObjects(Assembly[] fromAssemblies = null, string[] compatibleTypes = null)
{
var types = new List< Type>();
if (compatibleTypes == null)
{
compatibleTypes = new string[0];
}
// Loop through each assembly in our current app domain
// generating a collection of Types that implement IWorld
// If we are not provided with assemblies, we fetch all of them from the current domain.
foreach (Assembly assembly in fromAssemblies ?? AppDomain.CurrentDomain.GetAssemblies())
{
types.AddRange(assembly.GetTypes().Where(
type => (type.GetInterface(typeof(T).Name) != null || compatibleTypes.Contains(type.Name)) &&
!type.IsAbstract && // Do not add abstract classes
!type.IsInterface)); // Do not add interfaces. Concrete Types only.
}
// Convert our collection or Types into instances of IWorld
// then return the IWorld collection.
return new List< T>(
(from type in types
select Activator.CreateInstance(type) as T));
}
/// < summary>
/// Gets the World specified.
/// < /summary>
/// < typeparam name="T">The Type implementing IWorld that you want to find< /typeparam>
/// < returns>Returns the World specified.< /returns>
public T GetObject< UTypeToFetch>(Assembly[] fromAssemblies = null, string compatibleType = null)
{
// This isn't the most efficient.
// Considering that GetWorlds should really only be returning a few (1-5?) IWorld objects
// Filtering the list a second time (filtered once in GetWorlds) shouldn't hurt much.
// If users start loading dozens of IWorlds (bad practice!) in their scripts folder, then
// this needs to be revisited.
List< T> results = this.GetObjects(fromAssemblies);
T selectedType = null;
if (compatibleType != null)
{
selectedType = results.FirstOrDefault(World => World.GetType().Name == compatibleType);
}
else
{
selectedType = results.FirstOrDefault(World => World.GetType() == typeof(UTypeToFetch));
}
return selectedType;
}
}
The meat of this factory is in this chunk of code:
foreach (Assembly assembly in fromAssemblies ?? AppDomain.CurrentDomain.GetAssemblies())
{
types.AddRange(assembly.GetTypes().Where(
type => (type.GetInterface(typeof(T).Name) != null || compatibleTypes.Contains(type.Name)) &&
!type.IsAbstract && // Do not add abstract classes
!type.IsInterface)); // Do not add interfaces. Concrete Types only.
}
In this code, we loop through each assembly loaded in memory and check the Types they contain for any that are concrete classes implementing < T>. Since we have set a constraint at the class level, requiring < T> to be an IWorld based object, we are guaranteed that any object that comes back will be an instance of an IWorld based object.
We add the onjects that are found to a collection of objects and then return the list. At this point, we have a full collection of all instances objects that implement IWorld. The next method, GetObject< T>
allows us to specify just a single Type instead of every Type.
public T GetObject< UTypeToFetch>(Assembly[] fromAssemblies = null, string compatibleType = null)
{
// This isn't the most efficient.
// Considering that GetWorlds should really only be returning a few (1-5?) IWorld objects
// Filtering the list a second time (filtered once in GetWorlds) shouldn't hurt much.
// If users start loading dozens of IWorlds (bad practice!) in their scripts folder, then
// this needs to be revisited.
List< T> results = this.GetObjects(fromAssemblies);
T selectedType = null;
if (compatibleType != null)
{
selectedType = results.FirstOrDefault(World => World.GetType().Name == compatibleType);
}
else
{
selectedType = results.FirstOrDefault(World => World.GetType() == typeof(UTypeToFetch));
}
return selectedType;
}
With this method, we accept a secondary Type (such as DefaultWorld
) and we will scan all of the Types that implement IWorld
to find DefaultWorld (UTypeToFetch)
. We also provide an overload called compatibleType
that can be used to specify what Type we want using a string. This is handy for when we want to search for a Type via a user interface of some kind.
With that, our first IFactory implementation is completed!
Creating a Factory to Fetch our Factory
Now remember our initial problem? We have an IDataPersisted
object that has a method signature of Load< T>
with no constraints. How does the IDataPersisted object know what factory to fetch < T>
from? That's what we are going to solve next. We will create an EngineFactory
object that accepts < T>, which will be the interface
that the Factory
we are looking for must support.
The EngineFactory
object will only contain a single method, FindFactory< T>. If < T> in this case is IWorld
then we must search all Types in memory and find one that both implements IFactory
and has a Type constraint of IWorld
.
/// < summary>
/// A simple factory that is used to fetch other factories
/// < /summary>
public class EngineFactory
{
/// < summary>
/// Finds a Factory that can be used with T
/// < /summary>
/// < typeparam name="T">The interface that the Factory found must support.< /typeparam>
/// < returns>Returns a Factory that can fetch objects matching T< /returns>
public static Type FindFactory< T>() where T : class
{
var supportedTypes = new List< Type>();
// Loop through each assembly in memory, fetching the Types they contain.
foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
{
// We only want Types that satisfy the following criteria:
// 1: Must be Generic (as it must support being given < T> specified
// 2: Must implelemt IFactory, so we have guaranteed method signatures
Type[] types = assembly.GetTypes()
.Where(t => t.IsGenericType && t.GetInterfaces()
.FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IFactory< >)) != null)
.ToArray();
// Now that we have the Types meeting our criteria, we loop
// through them to determine which have Constraints matching
// the < T> supplied to us.
foreach (Type type in types)
{
foreach (var arg in type.GetGenericArguments())
{
Type constraint = arg.GetGenericParameterConstraints().FirstOrDefault();
// Does this constraint match T?
if (constraint == typeof(T))
{
// If so, then we have found a Factory object that
// can take < T> and return a concrete Type matching it.
return type;
}
}
continue;
}
}
return null;
}
}
The end result of the above method, will give us back a Factory that can take < T> and return a concrete object already instanced.
Putting it to use.
Now we are ready to go, we can implement a Generic load method using the following code.
public IEnumerable< T> Load< T>() where T : class
{
// Attempt to find a factory that supports < T>
Type factoryType = EngineFactory.FindFactory< T>();
// Create an instance of the Factory found, and provide it with < T> as its constraint.
IFactory< T> instance = Activator.CreateInstance(factoryType.MakeGenericType(typeof(T))) as IFactory< T>;
// Instance the objects associated with < T>
var result = instance.GetObjects();
// Return our collection of < T>.
return result as IEnumerable< T>;
}
Our load method is all wired up, factory fetcher is created and our WorldFactory implemented. Now we can call Load< T> and pass in any Type and it will return all of the objects we want.
this.Worlds = this.StorageSource.Load< IWorld>().ToList();
We can also call
this.Players = this.StorageSource.Load< IPlayer>().ToList();
The exact same object and method can be used, and we will get back a collection of IPlayer
objects (assuming a PlayerFactory
object implementing IFactory
exists).
Now you can create Factories to your hearts content, make sure and set their constraints properly and the engine will fetch and use them for you.