繁体   English   中英

如何改善此Translator Object Factory来简化单元测试?

[英]How Can I Improve this Translator Object Factory to simplify unit testing?

在我的一个项目中,基于ITranslator接口,我有许多类,如下所示:

interface ITranslator<TSource, TDest>
{
    TDest Translate(TSource toTranslate);
}

这些类将数据对象转换为新形式。 为了获得翻译器的实例,我有一个ITranslatorFactory,其方法为ITranslator<TSource, TDest> GetTranslator<TSource, TDest>() 我想不出任何方法来存储基于广泛泛型的函数集合(这里唯一的共同祖先是Object ),因此GetTranslator方法当前仅使用Unity来解析与以下内容匹配的ITranslator<TSource, TDest>要求翻译。

这种实现感觉很尴尬。 我已经了解到Service Locator 是一种反模式 ,无论是否实现,该实现都会使单元测试变得更加困难,因为我必须提供一个已配置的Unity容器来测试任何依赖于转换器的代码。

不幸的是,我想不出一种更有效的策略来获得合适的翻译者。 有人对我如何将该设置重构为更优雅的解决方案有任何建议吗?

无论您是否同意该服务定位符都是反模式的,将应用程序与DI容器分离具有实际的好处,这是不容忽视的。 在某些情况下,将容器注入应用程序的一部分是有意义的,但是在执行该路线之前,应先穷尽所有其他选项。

选项1

正如StuartLC指出的那样,您似乎在这里重新发明了轮子。 许多第三方实现已经在类型之间进行转换。 我个人认为这些替代方案是首选,并评估哪种方案具有最佳的DI支持以及它是否满足您的其他要求。

选项2

更新

当我第一次发布此答案时,直到尝试实现它时,我才考虑到在具有策略模式的转换器的接口声明中使用.NET Generics所涉及的困难。 由于策略模式仍然是一种可能的选择,因此我保留了此答案。 但是,我想到的最终产品并不像我最初希望的那样优雅-也就是说,翻译器本身的实现有些尴尬。

像所有模式一样,“战略”模式并不是适用于所有情况的灵丹妙药。 特别是有3种情况不合适。

  1. 当您具有没有通用抽象类型的类时(例如,在接口声明中使用泛型时)。
  2. 当接口的实现数量太多而导致内存成为问题时,因为它们都是同时加载的。
  3. 当您必须让DI容器控制对象的生存期时,例如在处理昂贵的一次性依赖项时。

也许有一种方法可以解决此解决方案的一般性问题,我希望其他人可以看到我在实现方面出错了,并提供了更好的解决方案。

但是,如果您从使用可测试性的角度来看它(OP的关键问题是使用的可测试性和笨拙性),那么解决方案并不是那么糟糕。

策略模式可用于解决此问题,而无需注入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;
    }
}

接下来,是实现Strategy模式的实际Strategy类。

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

尚不清楚您要转换的对象之间是否具有依赖关系。 如果是这样, 只要您将其视为组合根的一部分 ,使用您已经想出的工厂比Strategy模式更合适。 这也意味着工厂应被视为不可测试的类,并且它应包含完成任务所需的尽可能少的逻辑。

这并不能真正回答您的大问题,但是您在寻找一种存储简单映射函数而不创建大量映射类*的方法时,会导致此嵌套映射以Source为源,然后以Destination类型为键(我在这里Darin的答案中大量借用 ) :

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选择一个更成熟的映射器,例如AutoMapper 通过对每个映射进行适当的单元测试覆盖,可以避免对自动映射的脆弱性造成的回归问题。

* C#无法实例化在.NET中实现ITranslator接口的匿名类 (与Java不同),因此每个ITranslator映射都需要是一个命名类。

恐怕你不能。

通过对一段代码进行单元测试,您需要知道您的输入和预期的输出。 如果它是如此通用,以至于您没有指定它是什么,那么想象一下编译器/单元测试代码如何知道它的期望值?

首先, 服务定位器不是反模式 如果我们将模式标记为反模式只是因为它们在某些用例中不起作用,我们将只剩下反模式。

关于Unity,您采用了错误的方法。 您不对测试接口进行单元化。 您应该对实现该接口的每个类进行单元测试。

如果要确保所有实现都在容器中正确注册,则应创建一个测试类,尝试使用实际应用程序中组合根来解析每个实现。

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

摘要:

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM