Factory Method Using Is/As Operator
I have factory that looks something like the following snippet. Foo is a wrapper class for Bar and in most cases (but not all), there is a 1:1 mapping. As a rule, Bar cannot know anything about Foo, yet Foo takes an instance of Bar. Is there a better/cleaner approach to doing this?
public Foo Make( Bar obj )
{
if( obj is Bar1 )
return new Foo1( obj as Bar1 );
if( obj is Bar2 )
return new Foo2( obj as Bar2 );
if( obj is Bar3开发者_JS百科 )
return new Foo3( obj as Bar3 );
if( obj is Bar4 )
return new Foo3( obj as Bar4 ); // same wrapper as Bar3
throw new ArgumentException();
}
At first glance, this question might look like a duplicate (maybe it is), but I haven't seen one exactly like it. Here is one that is close, but not quite:
Factory based on Typeof or is a
If these are reference types, then calling as
after is
is an unnecessary expense. The usual idiom is to cast with as
and check for null.
Taking a step back from micro-optimization, it looks like you could use some of the techniques in the article you linked to. In specific, you could create a dictionary keyed on the type, with the value being a delegate that constructs an instance. The delegate would take a (child of) Bar and return a (child of) Foo. Ideally, each child of Foo would register itself to the dictionary, which could be static within Foo itself.
Here's some sample code:
// Foo creator delegate.
public delegate Foo CreateFoo(Bar bar);
// Lookup of creators, for each type of Bar.
public static Dictionary<Type, CreateFoo> Factory = new Dictionary<Type, CreateFoo>();
// Registration.
Factory.Add(typeof(Bar1), (b => new Foo1(b)));
// Factory method.
static Foo Create(Bar bar)
{
CreateFoo cf;
if (!Factory.TryGetValue(bar.GetType(), out cf))
return null;
return cf(bar);
}
I'm not sure what you actually want to achieve. I would probably try to make it more generic.
You could use attributes on Foo, which Bar it supports, then you create a list in a initializing phase. We are doing quite a lot of stuff like this, it make adding and connecting new classes very easy.
private Dictionary<Type, Type> fooOfBar = new Dictionary<Type, Type>();
public initialize()
{
// you could scan all types in the assembly of a certain base class
// (fooType) and read the attribute
fooOfBar.Add(attribute.BarType, fooType);
}
public Foo Make( Bar obj )
{
return (Foo)Activator.CreateInstance(fooOfBar(obj.GetType()), obj);
}
In your question the mapping from one set of classes into another set of classes looks quite simple. However, often you want to call specific constructors and/or set properties on the output classes based on the input classes. Sometimes you can use a library like AutoMapper.
However, in other cases you need to create specific factory methods for each conversion. In your case it would be factory methods to create Foo1
from Bar1
, Foo2
from a Bar2
etc.:
Foo1 CreateFoo1(Bar1 bar1) { ... }
Foo2 CreateFoo2(Bar2 bar2) { ... }
You can store all these factory methods as delegates in a dictionary and then use the input type to select the factory to create the output type.
var inputType = input.GetType();
var factory = factories[inputType];
var output = factory(input);
By using reflection you can build this dictionary and to avoid the extra cost of using reflection while calling the factory you can use expressions to compile small lambdas the will do the required casting and calling.
This functionality can be exposed through a base class that assumes that the input types and output types are in parallel class hierarchies. E.g., in your case all the Foo#
classes might have Foo
as a base class and all the Bar#
classes might have Bar
as a base close. However, if that is not the case then all classes have object
as a base class so this approach will still work.
Your derived factory class will look something like this:
public class FooFactory : TypeBasedFactory<Bar, Foo>
{
private Foo1 CreateFoo1(Bar1 bar1)
{
return new Foo1(bar1.Id, bar1.Name, ...);
}
private Foo2 CreateFoo2(Bar2 bar2)
{
return new Foo2(bar2.Description, ...);
}
}
Notice how the factory methods are private. They are not intended to be called directly. Instead TypeBasedFactory
declares a CreateFrom
method that will invoke the correct factory:
var fooFactory = new TypeBasedFactory<Bar, Foo>();
var foo = fooFactory.CreateFrom(bar);
Here is the code for TypeBasedFactory
:
public abstract class TypeBasedFactory<TInput, TOutput>
where TInput : class where TOutput : class
{
private readonly Dictionary<Type, Func<TInput, TOutput>> factories;
protected TypeBasedFactory()
{
factories = CreateFactories();
}
private Dictionary<Type, Func<TInput, TOutput>> CreateFactories()
{
return GetType()
.GetMethods(
BindingFlags.Public
| BindingFlags.NonPublic
| BindingFlags.Instance)
.Where(methodInfo =>
!methodInfo.IsAbstract
&& methodInfo.GetParameters().Length == 1
&& typeof(TOutput).IsAssignableFrom(methodInfo.ReturnType))
.Select(methodInfo => new
{
MethodInfo = methodInfo,
methodInfo.GetParameters().First().ParameterType
})
.Where(factory =>
typeof(TInput).IsAssignableFrom(factory.ParameterType)
&& !factory.ParameterType.IsAbstract)
.ToDictionary(
factory => factory.ParameterType,
factory => CreateFactory(factory.MethodInfo, factory.ParameterType));
}
private Func<TInput, TOutput> CreateFactory(MethodInfo methodInfo, Type parameterType)
{
// Create this Func<TInput, TOutput>: (TInput input) => Method((Parameter) input)
var inputExpression = Expression.Parameter(typeof(TInput), "input");
var castExpression = Expression.Convert(inputExpression, parameterType);
var callExpression = Expression.Call(Expression.Constant(this), methodInfo, castExpression);
var lambdaExpression = Expression.Lambda<Func<TInput, TOutput>>(callExpression, inputExpression);
return lambdaExpression.Compile();
}
public TOutput CreateFrom(TInput input)
{
if (input == null)
throw new ArgumentNullException(nameof(input));
var inputType = input.GetType();
Func<TInput, TOutput> factory;
if (!factories.TryGetValue(inputType, out factory))
throw new InvalidOperationException($"No factory method defined for {inputType.FullName}.");
return factory(input);
}
}
The CreateFactories
method uses reflection to look for both public and private methods that are able to create a TOuput
(possibly a derived class) from a TInput
(a non-abstract derived class).
The CreateFactory
method creates a Func<TInput, TOutput>
that does the required downcasting before invoking the factory method. Once the lambda has been compiled there is no reflection overhead in calling it.
Constructing a class derived from TypeBasedFactory
will use reflection to build the dictionary of factories so you should avoid creating more than one instance (i.e., the factory should be a singleton).
精彩评论