在我的一个项目中,我有许多基于ITranslator接口的类,如下所示:
interface ITranslator<TSource, TDest>
{
TDest Translate(TSource toTranslate);
}
这些类将数据对象转换为新表单。为了获得翻译器的实例,我有一个带有方法ITranslator<TSource, TDest> GetTranslator<TSource, TDest>()
的ITranslatorFactory。我想不出任何方法来存储基于各种泛型的函数集合(这里只有共同的祖先是Object
),因此GetTranslator
方法目前只使用Unity来解析{ {1}}与请求的翻译人员匹配。
这种实现感觉非常尴尬。我已经读过服务定位器is an anti-pattern,无论它是否存在,这个实现使单元测试变得更加困难,因为我必须提供一个配置的Unity容器来测试任何依赖于翻译器的代码。
不幸的是,我无法想出一个更有效的策略来获得合适的翻译。有没有人对如何将此设置重构为更优雅的解决方案有任何建议?
答案 0 :(得分:2)
您是否同意该服务定位器是反模式的,将应用程序与DI容器分离是不可忽视的实际好处。有一些边缘情况,将容器注入应用程序的一部分是有意义的,但所有其他选项应该在走这条路线之前耗尽。
正如StuartLC指出的那样,你似乎正在重新发明轮子。有many 3rd party implementations已经在类型之间进行转换。我个人认为这些替代方案是首选,并评估哪种方案具有最佳DI支持以及是否符合您的其他要求。
<强>更新强>
当我第一次发布这个答案时,我没有考虑使用.NET Generics在转换器的接口声明中使用策略模式所遇到的困难,直到我尝试实现它。由于策略模式仍然是一个可能的选择,我将留下这个答案。然而,我提出的最终产品并不像我最初希望的那样优雅 - 也就是译者本身的实现有点尴尬。
与所有模式一样,战略模式并非适合各种情况的银弹。特别是有3个案例不合适。
- 如果您的类没有通用的抽象类型(例如在接口声明中使用Generics时)。
- 当接口的实现数量太多而内存成为问题时,因为它们都是同时加载的。
- 当您必须让DI容器控制对象生命周期时,例如当您处理昂贵的一次性依赖项时。
醇>也许有办法解决这个解决方案的通用方面,我希望其他人看到我在实施中出错的地方并提供更好的解决方案。
但是,如果你完全从用法和可测试性的观点来看(可测试性和使用的尴尬是OP的关键问题),那不是糟糕的解决方案。
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;
}
}
接下来,是实现策略模式的实际策略类。
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);
}
}
请注意,如果您将上述每个代码块(从最后一个代码块开始)复制到控制台应用程序中,它将按原样运行。
有关其他一些实施示例,请查看this answer和this answer。
通过使用策略模式,您可以将应用程序与DI容器分离,然后可以与DI容器分开进行单元测试。
目前还不清楚你想要翻译的对象是否具有依赖性。如果是这样的话,使用你已经提出的工厂比战略模式更合适,只要你把它作为组合根的一部分。这也意味着工厂应该被视为一个不可测试的类,它应该包含完成任务所需的逻辑。
答案 1 :(得分:2)
这并没有真正回答你的大问题,但你搜索一种存储简单映射函数的方法而不创建大量的普通映射类*导致这个嵌套映射由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
。通过对每个映射进行适当的单元测试覆盖,可以避免导致回归问题的任何concerns about fragility of the automagic mapping。
*
C# can't instantiate anonymous classes在.NET中实现您的ITranslator
接口(与Java不同),因此每个ITranslator
映射都需要是一个命名类。
答案 2 :(得分:1)
我担心你不能。
通过单元测试一段代码,您需要知道您的输入和预期输出。如果它如此通用以至于你没有指定它是什么,想象一下编译器/单元测试代码如何知道预期的内容?
答案 3 :(得分:0)
首先,Service locator is not an anti pattern。如果我们将模式标记为反模式只是因为它们不适用于某些用例,我们就会留下反模式。
关于Unity,你采取了错误的方法。您没有单元测试接口。您应该对实现该接口的每个类进行单元测试。
如果要确保在容器中正确注册所有实现,则应创建一个测试类,尝试使用实际应用程序中的组合根来解析每个实现。
如果您只是为单元测试构建另一个容器,那么您没有任何真实证据证明实际应用程序可以正常工作。
要点: