我如何改进此翻译器对象工厂以简化单元测试?
How Can I Improve this Translator Object Factory to simplify unit testing?
在我的一个项目中,我有一些类基于ITranslator接口如下:
interface ITranslator<TSource, TDest>
{
TDest Translate(TSource toTranslate);
}
这些 类 将数据对象转换为新形式。为了获得翻译器的实例,我有一个带有方法 ITranslator<TSource, TDest> GetTranslator<TSource, TDest>()
的 ITranslatorFactory。我想不出任何方法来存储基于广泛泛型的函数集合(这里唯一的共同祖先是 Object
),所以 GetTranslator
方法目前只是使用 Unity 来解析 ITranslator<TSource, TDest>
与请求的译员匹配。
这个实现感觉很别扭。我读过服务定位器 is an anti-pattern,不管它是否存在,这个实现都使单元测试变得更加困难,因为我必须提供一个配置的 Unity 容器来测试任何依赖于翻译器的代码。
不幸的是,我想不出更有效的策略来获得合适的翻译器。有人对我如何将此设置重构为更优雅的解决方案有任何建议吗?
恐怕你做不到。
通过对一段代码进行单元测试,您需要知道您的输入和预期的输出。如果它是如此通用以至于您没有指定它是什么,想象一下 compiler/unit 测试代码如何知道它是什么?
无论您是否同意服务定位器是反模式,将应用程序与 DI 容器解耦都有不容忽视的实际好处。在某些边缘情况下,将容器注入应用程序的一部分是有意义的,但在走这条路之前应该用尽所有其他选项。
选项 1
正如 StuartLC 所指出的,您似乎是在重新发明轮子。有 many 3rd party implementations 已经在类型之间进行转换。我个人会认为这些替代方案是首选,并评估哪个选项具有最好的 DI 支持以及它是否满足您的其他要求。
选项 2
UPDATE
When I first posted this answer, I didn't take into account the difficulties involved with using .NET Generics in the interface declaration of the translator with the Strategy pattern until I tried to implement it. Since Strategy pattern is still a possible option, I am leaving this answer in place. However, the end product I came up with isn't as elegant as I had first hoped - namely the implementations of the translators themselves are a bit awkward.
Like all patterns, the Strategy pattern is not a silver bullet that works for every situation. There are 3 cases in particular where it is not a good fit.
- When you have classes that have no common abstract type (such as when using Generics in the interface declaration).
- When the number of implementations of the interface are so many that memory becomes an issue, since they are all loaded at the same time.
- When you must give the DI container control of the object lifetime, such as when you are dealing with expensive disposable dependencies.
Maybe there is a way to fix the generic aspect of this solution and I hope someone else sees where I went wrong with the implementation and provides a better one.
However, if you look at it entirely from a usage and testability standpoint (testability and awkwardness of use being the key problems of the OP), it is not that bad of a solution.
Strategy Pattern 可以在不注入 DI 容器的情况下解决这个问题。这需要一些重新管道来处理您制作的通用类型,以及一种映射转换器以与所涉及的类型一起使用的方法。
public interface ITranslator
{
Type SourceType { get; }
Type DestinationType { get; }
TDest Translate<TSource, TDest>(TSource toTranslate);
}
public static class ITranslatorExtensions
{
public static bool AppliesTo(this ITranslator translator, Type sourceType, Type destinationType)
{
return (translator.SourceType.Equals(sourceType) && translator.DestinationType.Equals(destinationType));
}
}
我们有几个对象要在它们之间进行转换。
class Model
{
public string Property1 { get; set; }
public int Property2 { get; set; }
}
class ViewModel
{
public string Property1 { get; set; }
public string Property2 { get; set; }
}
然后,我们有了翻译器实现。
public class ModelToViewModelTranslator : ITranslator
{
public Type SourceType
{
get { return typeof(Model); }
}
public Type DestinationType
{
get { return typeof(ViewModel); }
}
public TDest Translate<TSource, TDest>(TSource toTranslate)
{
Model source = toTranslate as Model;
ViewModel destination = null;
if (source != null)
{
destination = new ViewModel()
{
Property1 = source.Property1,
Property2 = source.Property2.ToString()
};
}
return (TDest)(object)destination;
}
}
public class ViewModelToModelTranslator : ITranslator
{
public Type SourceType
{
get { return typeof(ViewModel); }
}
public Type DestinationType
{
get { return typeof(Model); }
}
public TDest Translate<TSource, TDest>(TSource toTranslate)
{
ViewModel source = toTranslate as ViewModel;
Model destination = null;
if (source != null)
{
destination = new Model()
{
Property1 = source.Property1,
Property2 = int.Parse(source.Property2)
};
}
return (TDest)(object)destination;
}
}
接下来是实现策略模式的实际策略 class。
public interface ITranslatorStrategy
{
TDest Translate<TSource, TDest>(TSource toTranslate);
}
public class TranslatorStrategy : ITranslatorStrategy
{
private readonly ITranslator[] translators;
public TranslatorStrategy(ITranslator[] translators)
{
if (translators == null)
throw new ArgumentNullException("translators");
this.translators = translators;
}
private ITranslator GetTranslator(Type sourceType, Type destinationType)
{
var translator = this.translators.FirstOrDefault(x => x.AppliesTo(sourceType, destinationType));
if (translator == null)
{
throw new Exception(string.Format(
"There is no translator for the specified type combination. Source: {0}, Destination: {1}.",
sourceType.FullName, destinationType.FullName));
}
return translator;
}
public TDest Translate<TSource, TDest>(TSource toTranslate)
{
var translator = this.GetTranslator(typeof(TSource), typeof(TDest));
return translator.Translate<TSource, TDest>(toTranslate);
}
}
用法
using System;
using System.Linq;
using Microsoft.Practices.Unity;
class Program
{
static void Main(string[] args)
{
// Begin Composition Root
var container = new UnityContainer();
// IMPORTANT: For Unity to resolve arrays, you MUST name the instances.
container.RegisterType<ITranslator, ModelToViewModelTranslator>("ModelToViewModelTranslator");
container.RegisterType<ITranslator, ViewModelToModelTranslator>("ViewModelToModelTranslator");
container.RegisterType<ITranslatorStrategy, TranslatorStrategy>();
container.RegisterType<ISomeService, SomeService>();
// Instantiate a service
var service = container.Resolve<ISomeService>();
// End Composition Root
// Do something with the service
service.DoSomething();
}
}
public interface ISomeService
{
void DoSomething();
}
public class SomeService : ISomeService
{
private readonly ITranslatorStrategy translatorStrategy;
public SomeService(ITranslatorStrategy translatorStrategy)
{
if (translatorStrategy == null)
throw new ArgumentNullException("translatorStrategy");
this.translatorStrategy = translatorStrategy;
}
public void DoSomething()
{
// Create a Model
Model model = new Model() { Property1 = "Hello", Property2 = 123 };
// Translate to ViewModel
ViewModel viewModel = this.translatorStrategy.Translate<Model, ViewModel>(model);
// Translate back to Model
Model model2 = this.translatorStrategy.Translate<ViewModel, Model>(viewModel);
}
}
请注意,如果您将上述每个代码块(从最后一个开始)复制到控制台应用程序中,它将 运行 保持原样。
查看 and this answer 以获取一些其他实施示例。
通过使用策略模式,您可以将应用程序与 DI 容器分离,然后可以独立于 DI 容器对其进行单元测试。
选项 3
不清楚您要转换的对象是否具有依赖性。如果是这样,使用你已经想到的工厂比策略模式 更合适,只要你将它视为组合根 的一部分。这也意味着工厂应该被视为不可测试的 class,并且它应该包含尽可能少的逻辑来完成它的任务。
首先,Service locator is not an anti pattern。如果我们仅仅因为模式不适用于某些用例就将模式标记为反模式,我们只会留下反模式。
关于 Unity,您采用了错误的方法。您不对接口进行单元测试。您应该对实现该接口的每个 class 进行单元测试。
如果您想确保所有实现都在容器中正确注册,您应该创建一个测试class,尝试使用真实应用程序中的组合根解析每个实现.
如果您只是为单元测试构建另一个容器,则您没有任何实际证据证明实际应用程序可以正常工作。
总结:
- 对每个转换器进行单元测试
- 创建一个测试,确保所有转换器都在真实组合根中注册。
这并没有真正回答您更大的问题,但是您搜索了一种存储简单映射函数而不创建大量琐碎映射的方法 classes* 导致了这个由 Source 和 Destination 键控的嵌套映射类型(我大量借鉴了Darin's answer here):
public class TranslatorDictionary
{
private readonly IDictionary<Type, IDictionary<Type, Delegate>> _mappings
= new Dictionary<Type, IDictionary<Type, Delegate>>();
public TDest Map<TSource, TDest>(TSource source)
{
IDictionary<Type, Delegate> typeMaps;
Delegate theMapper;
if (_mappings.TryGetValue(source.GetType(), out typeMaps)
&& typeMaps.TryGetValue(typeof(TDest), out theMapper))
{
return (TDest)theMapper.DynamicInvoke(source);
}
throw new Exception(string.Format("No mapper registered from {0} to {1}",
typeof(TSource).FullName, typeof(TDest).FullName));
}
public void AddMap<TSource, TDest>(Func<TSource, TDest> newMap)
{
IDictionary<Type, Delegate> typeMaps;
if (!_mappings.TryGetValue(typeof(TSource), out typeMaps))
{
typeMaps = new Dictionary<Type, Delegate>();
_mappings.Add(typeof (TSource), typeMaps);
}
typeMaps[typeof(TDest)] = newMap;
}
}
这将允许注册映射 Funcs
// Bootstrapping
var translator = new TranslatorDictionary();
translator.AddMap<Foo, Bar>(
foo => new Bar{Name = foo.Name, SurrogateId = foo.ID});
translator.AddMap<Bar, Foo>(bar =>
new Foo { Name = bar.Name, ID = bar.SurrogateId, Date = DateTime.MinValue});
// Usage
var theBar = translator.Map<Foo, Bar>(new Foo{Name = "Foo1", ID = 1234, Date = DateTime.Now});
var theFoo = translator.Map<Bar, Foo>(new Bar { Name = "Bar1", SurrogateId = 9876});
显然,与其重新发明轮子,不如选择更成熟的映射器,例如 AutoMapper
. With appropriate unit test coverage of each mapping, any concerns about fragility of the automagic mapping,从而避免回归问题。
*
C# can't instantiate anonymous classes 在 .NET 中实现您的 ITranslator
接口(与 Java 不同),因此每个 ITranslator
映射都需要命名class.
在我的一个项目中,我有一些类基于ITranslator接口如下:
interface ITranslator<TSource, TDest>
{
TDest Translate(TSource toTranslate);
}
这些 类 将数据对象转换为新形式。为了获得翻译器的实例,我有一个带有方法 ITranslator<TSource, TDest> GetTranslator<TSource, TDest>()
的 ITranslatorFactory。我想不出任何方法来存储基于广泛泛型的函数集合(这里唯一的共同祖先是 Object
),所以 GetTranslator
方法目前只是使用 Unity 来解析 ITranslator<TSource, TDest>
与请求的译员匹配。
这个实现感觉很别扭。我读过服务定位器 is an anti-pattern,不管它是否存在,这个实现都使单元测试变得更加困难,因为我必须提供一个配置的 Unity 容器来测试任何依赖于翻译器的代码。
不幸的是,我想不出更有效的策略来获得合适的翻译器。有人对我如何将此设置重构为更优雅的解决方案有任何建议吗?
恐怕你做不到。
通过对一段代码进行单元测试,您需要知道您的输入和预期的输出。如果它是如此通用以至于您没有指定它是什么,想象一下 compiler/unit 测试代码如何知道它是什么?
无论您是否同意服务定位器是反模式,将应用程序与 DI 容器解耦都有不容忽视的实际好处。在某些边缘情况下,将容器注入应用程序的一部分是有意义的,但在走这条路之前应该用尽所有其他选项。
选项 1
正如 StuartLC 所指出的,您似乎是在重新发明轮子。有 many 3rd party implementations 已经在类型之间进行转换。我个人会认为这些替代方案是首选,并评估哪个选项具有最好的 DI 支持以及它是否满足您的其他要求。
选项 2
UPDATE
When I first posted this answer, I didn't take into account the difficulties involved with using .NET Generics in the interface declaration of the translator with the Strategy pattern until I tried to implement it. Since Strategy pattern is still a possible option, I am leaving this answer in place. However, the end product I came up with isn't as elegant as I had first hoped - namely the implementations of the translators themselves are a bit awkward.
Like all patterns, the Strategy pattern is not a silver bullet that works for every situation. There are 3 cases in particular where it is not a good fit.
- When you have classes that have no common abstract type (such as when using Generics in the interface declaration).
- When the number of implementations of the interface are so many that memory becomes an issue, since they are all loaded at the same time.
- When you must give the DI container control of the object lifetime, such as when you are dealing with expensive disposable dependencies.
Maybe there is a way to fix the generic aspect of this solution and I hope someone else sees where I went wrong with the implementation and provides a better one.
However, if you look at it entirely from a usage and testability standpoint (testability and awkwardness of use being the key problems of the OP), it is not that bad of a solution.
Strategy Pattern 可以在不注入 DI 容器的情况下解决这个问题。这需要一些重新管道来处理您制作的通用类型,以及一种映射转换器以与所涉及的类型一起使用的方法。
public interface ITranslator
{
Type SourceType { get; }
Type DestinationType { get; }
TDest Translate<TSource, TDest>(TSource toTranslate);
}
public static class ITranslatorExtensions
{
public static bool AppliesTo(this ITranslator translator, Type sourceType, Type destinationType)
{
return (translator.SourceType.Equals(sourceType) && translator.DestinationType.Equals(destinationType));
}
}
我们有几个对象要在它们之间进行转换。
class Model
{
public string Property1 { get; set; }
public int Property2 { get; set; }
}
class ViewModel
{
public string Property1 { get; set; }
public string Property2 { get; set; }
}
然后,我们有了翻译器实现。
public class ModelToViewModelTranslator : ITranslator
{
public Type SourceType
{
get { return typeof(Model); }
}
public Type DestinationType
{
get { return typeof(ViewModel); }
}
public TDest Translate<TSource, TDest>(TSource toTranslate)
{
Model source = toTranslate as Model;
ViewModel destination = null;
if (source != null)
{
destination = new ViewModel()
{
Property1 = source.Property1,
Property2 = source.Property2.ToString()
};
}
return (TDest)(object)destination;
}
}
public class ViewModelToModelTranslator : ITranslator
{
public Type SourceType
{
get { return typeof(ViewModel); }
}
public Type DestinationType
{
get { return typeof(Model); }
}
public TDest Translate<TSource, TDest>(TSource toTranslate)
{
ViewModel source = toTranslate as ViewModel;
Model destination = null;
if (source != null)
{
destination = new Model()
{
Property1 = source.Property1,
Property2 = int.Parse(source.Property2)
};
}
return (TDest)(object)destination;
}
}
接下来是实现策略模式的实际策略 class。
public interface ITranslatorStrategy
{
TDest Translate<TSource, TDest>(TSource toTranslate);
}
public class TranslatorStrategy : ITranslatorStrategy
{
private readonly ITranslator[] translators;
public TranslatorStrategy(ITranslator[] translators)
{
if (translators == null)
throw new ArgumentNullException("translators");
this.translators = translators;
}
private ITranslator GetTranslator(Type sourceType, Type destinationType)
{
var translator = this.translators.FirstOrDefault(x => x.AppliesTo(sourceType, destinationType));
if (translator == null)
{
throw new Exception(string.Format(
"There is no translator for the specified type combination. Source: {0}, Destination: {1}.",
sourceType.FullName, destinationType.FullName));
}
return translator;
}
public TDest Translate<TSource, TDest>(TSource toTranslate)
{
var translator = this.GetTranslator(typeof(TSource), typeof(TDest));
return translator.Translate<TSource, TDest>(toTranslate);
}
}
用法
using System;
using System.Linq;
using Microsoft.Practices.Unity;
class Program
{
static void Main(string[] args)
{
// Begin Composition Root
var container = new UnityContainer();
// IMPORTANT: For Unity to resolve arrays, you MUST name the instances.
container.RegisterType<ITranslator, ModelToViewModelTranslator>("ModelToViewModelTranslator");
container.RegisterType<ITranslator, ViewModelToModelTranslator>("ViewModelToModelTranslator");
container.RegisterType<ITranslatorStrategy, TranslatorStrategy>();
container.RegisterType<ISomeService, SomeService>();
// Instantiate a service
var service = container.Resolve<ISomeService>();
// End Composition Root
// Do something with the service
service.DoSomething();
}
}
public interface ISomeService
{
void DoSomething();
}
public class SomeService : ISomeService
{
private readonly ITranslatorStrategy translatorStrategy;
public SomeService(ITranslatorStrategy translatorStrategy)
{
if (translatorStrategy == null)
throw new ArgumentNullException("translatorStrategy");
this.translatorStrategy = translatorStrategy;
}
public void DoSomething()
{
// Create a Model
Model model = new Model() { Property1 = "Hello", Property2 = 123 };
// Translate to ViewModel
ViewModel viewModel = this.translatorStrategy.Translate<Model, ViewModel>(model);
// Translate back to Model
Model model2 = this.translatorStrategy.Translate<ViewModel, Model>(viewModel);
}
}
请注意,如果您将上述每个代码块(从最后一个开始)复制到控制台应用程序中,它将 运行 保持原样。
查看
通过使用策略模式,您可以将应用程序与 DI 容器分离,然后可以独立于 DI 容器对其进行单元测试。
选项 3
不清楚您要转换的对象是否具有依赖性。如果是这样,使用你已经想到的工厂比策略模式 更合适,只要你将它视为组合根 的一部分。这也意味着工厂应该被视为不可测试的 class,并且它应该包含尽可能少的逻辑来完成它的任务。
首先,Service locator is not an anti pattern。如果我们仅仅因为模式不适用于某些用例就将模式标记为反模式,我们只会留下反模式。
关于 Unity,您采用了错误的方法。您不对接口进行单元测试。您应该对实现该接口的每个 class 进行单元测试。
如果您想确保所有实现都在容器中正确注册,您应该创建一个测试class,尝试使用真实应用程序中的组合根解析每个实现.
如果您只是为单元测试构建另一个容器,则您没有任何实际证据证明实际应用程序可以正常工作。
总结:
- 对每个转换器进行单元测试
- 创建一个测试,确保所有转换器都在真实组合根中注册。
这并没有真正回答您更大的问题,但是您搜索了一种存储简单映射函数而不创建大量琐碎映射的方法 classes* 导致了这个由 Source 和 Destination 键控的嵌套映射类型(我大量借鉴了Darin's answer here):
public class TranslatorDictionary
{
private readonly IDictionary<Type, IDictionary<Type, Delegate>> _mappings
= new Dictionary<Type, IDictionary<Type, Delegate>>();
public TDest Map<TSource, TDest>(TSource source)
{
IDictionary<Type, Delegate> typeMaps;
Delegate theMapper;
if (_mappings.TryGetValue(source.GetType(), out typeMaps)
&& typeMaps.TryGetValue(typeof(TDest), out theMapper))
{
return (TDest)theMapper.DynamicInvoke(source);
}
throw new Exception(string.Format("No mapper registered from {0} to {1}",
typeof(TSource).FullName, typeof(TDest).FullName));
}
public void AddMap<TSource, TDest>(Func<TSource, TDest> newMap)
{
IDictionary<Type, Delegate> typeMaps;
if (!_mappings.TryGetValue(typeof(TSource), out typeMaps))
{
typeMaps = new Dictionary<Type, Delegate>();
_mappings.Add(typeof (TSource), typeMaps);
}
typeMaps[typeof(TDest)] = newMap;
}
}
这将允许注册映射 Funcs
// Bootstrapping
var translator = new TranslatorDictionary();
translator.AddMap<Foo, Bar>(
foo => new Bar{Name = foo.Name, SurrogateId = foo.ID});
translator.AddMap<Bar, Foo>(bar =>
new Foo { Name = bar.Name, ID = bar.SurrogateId, Date = DateTime.MinValue});
// Usage
var theBar = translator.Map<Foo, Bar>(new Foo{Name = "Foo1", ID = 1234, Date = DateTime.Now});
var theFoo = translator.Map<Bar, Foo>(new Bar { Name = "Bar1", SurrogateId = 9876});
显然,与其重新发明轮子,不如选择更成熟的映射器,例如 AutoMapper
. With appropriate unit test coverage of each mapping, any concerns about fragility of the automagic mapping,从而避免回归问题。
*
C# can't instantiate anonymous classes 在 .NET 中实现您的 ITranslator
接口(与 Java 不同),因此每个 ITranslator
映射都需要命名class.