我如何改进此翻译器对象工厂以简化单元测试?

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.

  1. When you have classes that have no common abstract type (such as when using Generics in the interface declaration).
  2. 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.
  3. 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,尝试使用真实应用程序中的组合根解析每个实现.

如果您只是为单元测试构建另一个容器,则您没有任何实际证据证明实际应用程序可以正常工作。

总结:

  1. 对每个转换器进行单元测试
  2. 创建一个测试,确保所有转换器都在真实组合根中注册。

这并没有真正回答您更大的问题,但是您搜索了一种存储简单映射函数而不创建大量琐碎映射的方法 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.