【问题标题】:How Can I Improve this Translator Object Factory to simplify unit testing?如何改进这个转换器对象工厂以简化单元测试?
【发布时间】:2015-03-17 19:16:53
【问题描述】:

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

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

这些类将数据对象转换为新形式。为了获得翻译器的实例,我有一个带有方法ITranslator&lt;TSource, TDest&gt; GetTranslator&lt;TSource, TDest&gt;() 的ITranslatorFactory。我想不出任何方法来存储基于各种泛型的函数集合(这里唯一的共同祖先是Object),所以GetTranslator方法目前只是使用Unity来解析匹配的ITranslator&lt;TSource, TDest&gt;请求的翻译。

这个实现感觉很尴尬。我读过服务定位器is an anti-pattern,不管它是否存在,这个实现都使单元测试变得更加困难,因为我必须提供一个配置的 Unity 容器来测试任何依赖于翻译器的代码。

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

【问题讨论】:

  • 这可能属于程序员,但我会注意到我认为这没有问题。您的基于 Unity 的工厂可能相对微不足道,并且与 ITranslator 或其依赖项的任何实现分离。只需将工厂隔离到一个单独的引导命名空间并完成它。任何避免这种情况的方法都将被过度设计。这种方式有效,简单而优雅,没有真正的实际缺点。不要仅仅因为这种方法被任意冠以“反模式”的标签而过度设计。
  • 不幸的是,Mark Seeman 将 Service Locator 标记为反模式。他的文章继续描述了服务定位器的一些缺点,但这并不能使它本身成为一种反模式。它只是一种反模式,适用于那些不利因素不能接受利益权衡的情况。
  • @AntP - 我认为与 DI 容器紧密耦合,因此无法轻松进行单元测试是一个实际的缺点。在 99% 的情况下,您可以使用不需要大量设计工作的现有模式来设计应用程序,使其在没有服务定位器的情况下工作。
  • @NightOwl888 - 拥有一组实现通用接口的处理程序,每个处理程序指定不同的类型参数,并让工厂提供这些实现以响应指定的运行时类型通常非常有用,因为在所述工厂的实施中利用 IoC 容器。大多数 IoC 容器将允许您注入它们的注册上下文,因此您不需要将任何接口耦合到它们,并且如果您真的迫切需要对仅将调用委托给第三方的工厂进行单元测试,则上下文本身仍然可以被模拟-party 解析器。
  • 不。服务定位器仍然是一种反模式。但是,服务定位器是在您的应用程序中使用的一种模式。如果您从组合根中访问容器,这不是服务定位器反模式的实现。请阅读this。长话短说,只要您在 Composition Root 中定义您的 Translator Factory 实现,就可以了,并且没有应用服务定位器模式。

标签: c# unit-testing dependency-injection unity-container factory-pattern


【解决方案1】:

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

选项 1

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

选项 2

更新

当我第一次发布这个答案时,我没有考虑到在翻译器的接口声明中使用 .NET 泛型和策略模式所涉及的困难,直到我尝试实现它。由于策略模式仍然是一种可能的选择,因此我将保留此答案。然而,我想出的最终产品并不像我最初希望的那样优雅——即翻译器本身的实现有点尴尬。

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

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

也许有一种方法可以修复此解决方案的通用方面,我希望其他人能看到我在实施中出错的地方并提供更好的解决方案。

但是,如果你完全从 usagetestability 的角度来看它(可测试性和使用的尴尬是 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 answerthis answer 以了解其他一些实施示例。

通过使用策略模式,您可以将应用程序与 DI 容器分离,然后可以将其与 DI 容器分开进行单元测试。

选项 3

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

【讨论】:

    【解决方案2】:

    这并不能真正回答您更大的问题,但是您寻找一种存储简单映射函数而不创建大量琐碎映射类*的方法导致这个嵌套映射由 Source 键入,然后是 Destination 类型(我大量借用 @ 987654321@):

    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 映射都需要是一个命名类。

    【讨论】:

    • 我走神了,忘记回复了,但是 +1 了一些非常狡猾的代码。我见过另一位同事做这样的事情,但我在阅读代码时无法真正理解发生了什么。您的示例帮助我解决了这个问题,谢谢!
    • 现在我们将函数作为一等变量公民来接受,一段时间后这将成为一种非常自然的模式。但是使用 AutoMapper - 我的解决方案不会支持图表等:)
    • 我也刚开始在这个项目上使用 AutoMapper,而且效果非常好!我们实际上在大部分翻译中都使用了它,但是 ITranslator 结构已经到位。当 95% 的翻译器只返回 Mapper.Map(mySource) 时,使用这种抽象似乎真的很尴尬!我对函数式编程非常感兴趣,我已经开始尝试在业余时间学习 F#。我有很多东西要学,但我已经知道这将是一次启发性的经历!
    【解决方案3】:

    恐怕你不能。

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

    【讨论】:

      【解决方案4】:

      首先,Service locator is not an anti pattern。如果我们仅仅因为它们不适用于某些用例而将模式标记为反模式,那么我们只会留下反模式。

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

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

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

      总结:

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

      【讨论】:

      • 我知道这是一个古老的论点,但我仍然不同意。用于业务线应用程序甚至可重用库的服务定位器IS an anti-pattern 是有问题的,因为there arebetter alternatives。但对于其余的,我同意你的回答:-)
      • 但是,请注意,从工厂内部调用容器并不是服务定位器反模式的暗示,因为这个工厂应该是您的组合根的一部分,正如 Mark 解释的 @987654325 @ 清楚。
      • 我发现 .Net 世界中 ServiceLocators 的一个用例是用于 WebForms 或 ASMX Web 服务等传统技术堆栈,其中无法使用通过 IoC 委派根对象实例化的钩子盒子。但这仅适用于顶级 - 对于其他所有内容,都有 DI。
      • @Steven The Service Locator IS an anti-pattern for Line Of Business applications。我从未见过有人说This is a anti-pattern, but just for XXX。它要么是一种模式(总是有特定的用例),要么是一种反模式(即没有用例)。你不能两者兼得。
      • 反模式的定义非常明确:An anti-pattern (or antipattern) is a common response to a recurring problem that is usually ineffective and risks being highly counterproductive
      猜你喜欢
      • 2011-06-23
      • 1970-01-01
      • 2011-10-18
      • 1970-01-01
      • 1970-01-01
      • 2016-05-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多