【问题标题】:Creating a Delegating Factory with Autofac using a property使用属性使用 Autofac 创建委托工厂
【发布时间】:2015-07-03 18:03:11
【问题描述】:

我正在尝试创建一个工厂来帮助将基于接口 (IIncomingMessage) 的类转换为基于单个类的属性的其他类 (AMessage、BMessage) 的新实例,例如:

public interface IIncomingMessage
{
    public DeviceTypeEnum DeviceType {get;}
}

public class IncomingMessage : IIncomingMessage
{

    public DeviceTypeEnum DeviceType {get {return DeviceTypeEnum.TypeA;}}

    Public Byte[] RawData {get; set;}
}      

public interface IMessageTransformer<out T> where T:class
{
    T Transform(IIncomingMessage message);
}

public class AMessage 
{
    public int ChoppedUp1 {get; set;}
    public int ChoppedUp2 {get; set;}
    public int ChoppedUp3 {get; set;}       
}

public class BMessage
{
    public string SomeData {get; set;}
    public string SomeMoreData {get; set;}
}

public class AMessageTransformer : IMessageTransformer<AMessage>
{
    public AMessage Transform(IIncomingMessage message)
    {
        var result = new AMessage();

        result.ChoppedUp1 = message.RawData[1];
        result.ChoppedUp2 = message.RawData[2];
        ....
        return result;
    }
}

public class BMessageTransformation : IMessageTransformer<BMessage>
{
    public BMessage Transform(IIncomingMessage message)
    {
        throw new NotImplementedException();
    }
}

public class MessageTransformFactory<T> where T : class
{
    private readonly Func<DeviceTypeEnum, IMessageTransformer<T>> _create;

    public MessageTransformFactory(Func<DeviceTypeEnum, IMessageTransformer<T>> create)
    {
        _create = create;
    }

    public IMessageTransformer<T> GetTransformer(DeviceTypeEnum device)
    {
        return _create(device);
    } 
}

我正在使用 Autofac,我很确定应该有一种方法可以创建一个工厂,它可以根据 DeviceType 的属性值返回正确的转换器。使用并看到大量使用对象类型做出决定而不是值的示例。命名和键控服务看起来很有希望,但看起来我最终还是会在某个地方得到一个静态列表。

【问题讨论】:

  • 你是将你的工厂注入到依赖它的类中,还是在每次使用时自己实例化它?
  • 我试图坚持通过构造函数注入它,就像我通常的风格一样,但目前我满足于让它正常工作。
  • 我假设枚举是一个预先确定的值,并且一旦类被实例化就不会改变?
  • 不幸的是,由于与实例属性(枚举)进行比较,您必须保存实现IIncomingMessage 的每个类型的缓存,并且每个类型都已被实例化。如果您要使用属性,这会更简单一些,开销更少。我正在做一个例子来展示我过去是如何做到的
  • 这还不错,因为现在只有一个 IIncomingMessage 实现,它需要转换为许多不同的类型(枚举中的每个值一个)但是(目前大约 15,但会增长为新的添加设备)。根据设备类型,每个新消息类型都从传入的字节数组进行不同的转换。一个例子会很好,我希望这个上下文能让它更清楚。

标签: c# autofac factory-pattern


【解决方案1】:

好的,所以在与您交谈之后,您似乎很愿意对转换的实际发生方式进行一些调整。因此,我为您构建了一个解决方案,它使用属性和DeviceTypeEnum 来解析您的转换器。工厂松散地遵循抽象工厂模式。工厂负责确定需要创建的实际 Type 是什么;然后它将该类型的实例化委托给委托人。在这种情况下,Autofac。

概述

为了让它工作,我不得不添加一个额外的接口。 IMessageTransformer。然后我让IMessageTransformer&lt;T&gt; 继承它。基本接口只不过是一个标记接口,因此我可以使工厂本身非泛型,同时 Func 委托也非泛型。您现在可以将同一个工厂实例用于多个 IMessageTransformer&lt;T&gt; 请求。您也可以根据需要只实例化工厂,而不是注入它,因为它没有任何依赖关系。

为了显示工厂实际工作,我已经为它编写了一个单元测试。

    [TestMethod]
    public void Shared_factory_instance_resolves_multiple_transformers()
    {
        // Arrange
        var factory = new MessageTransformFactory();
        IMessageTransformer<AMessage> aTransformer = factory.CreateTransformer<AMessage>(DeviceTypeEnum.Foo);
        IMessageTransformer<BMessage> bTransformer = factory.CreateTransformer<BMessage>(DeviceTypeEnum.Bar);

        // Act
        AMessage aMessage = aTransformer.Transform(new IncomingFooMessage());
        BMessage bMessage = bTransformer.Transform(new IncomingBarMessage());

        // Assert
        Assert.IsNotNull(aMessage, "Transformer failed to convert the IncomingMessage");
        Assert.IsNotNull(bMessage, "Transformer failed to convert the IncomingMessage");
    }

请注意,由于引入了一个属性,通过将DeviceTypeEnum 一起删除,这可以被简化。但是我把它留在原地,因为我不确定你的总要求是什么。

实施

现在来看看实际的实现。我在原始问题中获取了您的示例代码,并用它来构建一个示例项目。包含实现的示例项目是available on GitHub 供您下拉。所有源代码如下所示; GitHub 存储库只是为了让您可以下载存储库并查看运行中的代码,确保它符合您的要求,然后再将其实施到您的项目中。

设备类型枚举

我做的第一件事是创建enum,它将用于将转换器与其IIncomingMessageMessage 类链接起来。

public enum DeviceTypeEnum
{
    Foo,
    Bar,
}

消息类

接下来我创建了两个你想将转换成的消息类。

public class AMessage
{
}

public class BMessage
{
}

IIncomingMessage

接下来是IIncomingMessage 接口。这个接口只包含一个属性,DeviceType。然后我创建了这个接口的两个实现,以便我可以测试实际的转换。

public interface IIncomingMessage
{
    DeviceTypeEnum DeviceType { get; }
}

public class IncomingFooMessage : IIncomingMessage
{
    public DeviceTypeEnum DeviceType { get { return DeviceTypeEnum.Foo; } }
}

public class IncomingBarMessage : IIncomingMessage
{
    public DeviceTypeEnum DeviceType { get { return DeviceTypeEnum.Bar; } }
}

IMessageTransformer

您在示例中提供的IMessageTransformer&lt;T&gt; 已被修改为从接口的非通用变体继承。

public interface IMessageTransformer
{
}

public interface IMessageTransformer<T> : IMessageTransformer where T : class
{
    T Transform(IIncomingMessage message);
}

这允许您指定 Autofac 在解析期间使用的委托方法,而不是通用委托。这意味着您现在使用Func&lt;IMessageTransformer&gt; 而不是Func&lt;IMessageTransformer&lt;T&gt;&gt;。这使您可以重复使用相同的工厂实例,因为该实例未绑定到特定的泛型类型。

可转换属性

现在我们需要创建一个属性。该属性将用于告诉每个转换器,DeviceTypeEnum 他们需要支持什么。

[AttributeUsage(AttributeTargets.Class)]
public class TransformableAttribute : Attribute
{
    public TransformableAttribute(DeviceTypeEnum deviceType)
    {
        this.DeviceType = deviceType;
    }

    public DeviceTypeEnum DeviceType { get; private set; }
}

如果DeviceTypeEnum只是用来促进这种转换映射,那么你真的可以把枚举全部删除。您可以将DeviceType 属性更改为public Type TargetType {get; private set;}。然后,工厂(如下所示)将使用 TargetType 解析(简化了我创建的工厂)。由于您的示例显示正在使用此枚举,因此我将其作为要求保留。

请注意,删除枚举并使用TargetType 属性还可以让您建立更复杂的关系,并且不必在每次必须创建新映射时更新枚举。而不是这样做:

var factory = new MessageTransformFactory();
IMessageTransformer<AMessage> aTransformer = factory.CreateTransformer<AMessage>(DeviceTypeEnum.Foo);

它会让你把参数放在一起。不需要枚举,因为该属性会让工厂知道每个转换器的目标转换结果需要是什么。

var factory = new MessageTransformFactory();
IMessageTransformer<AMessage> aTransformer = factory.CreateTransformer<AMessage>();

消息转换器

我们有属性和接口,所以我们可以继续创建几个转换器,将IIncomingMessage 转换为AMessageBMessageTransformableAttribute 将告诉变压器支持哪种设备类型。变压器本身不依赖属性;我们将用于解析的工厂需要将设备类型链接到转换器的属性。

[Transformable(DeviceTypeEnum.Foo)] // or [Transformable(typeof(AMessage)] if you replace the enum with a Target Type
public class AMessageTransformer : IMessageTransformer<AMessage>
{
    public AMessage Transform(IIncomingMessage message)
    {
        if (!(message is IncomingFooMessage))
        {
            throw new InvalidCastException("Message was not an IncomingFooMessage");
        }

        return new AMessage();
    }
}

[Transformable(DeviceTypeEnum.Bar)]
public class BMessageTransformer : IMessageTransformer<BMessage>
{
    public BMessage Transform(IIncomingMessage message)
    {
        if (!(message is IncomingBarMessage))
        {
            throw new InvalidCastException("Message was not an IncomingBarMessage");
        }

        return new BMessage();
    }
}

MessageTransformFactory

现在是肉和土豆。工厂有一些事情要负责。它必须首先扫描一组程序集,才能找到它可以创建的所有转换器Types。它还需要接受一个委托工厂,它将在需要时用于实例化它已缓存的转换器Types。

MessageTransformFactory 松散地遵循抽象工厂模式。它将转换对象的实际创建委托给其他东西,在本例中是 Autofac。

public class MessageTransformFactory
{
    /// <summary>
    /// The assemblies to cache. Defaults to including the assembly this factory exists in.
    /// if there are additional assemblies that hold transformers, they can be added via the 
    /// MessageTransformFactory.ScanAssembly(Assembly) method.
    /// </summary>
    private static List<Assembly> assembliesToCache
        = new List<Assembly> { typeof(MessageTransformFactory).GetTypeInfo().Assembly };

    /// <summary>
    /// The factory method used to instance a transformer
    /// </summary>
    private static Func<Type, IMessageTransformer> factoryMethod;

    /// <summary>
    /// The DeviceType to Transformer mapping cache
    /// </summary>
    private static Dictionary<DeviceTypeEnum, Type> deviceTransformerMapCache
        = new Dictionary<DeviceTypeEnum, Type>();

    /// <summary>
    /// Initializes the <see cref="CommandFormatterFactory"/> class.
    /// This will build the initial device to transformer mapping when the
    /// Factory is first used.
    /// </summary>
    static MessageTransformFactory()
    {
        BuildCache();
    }

    /// <summary>
    /// Sets the transformer factory used to instance transformers.
    /// </summary>
    /// <param name="factory">The factory delegate used to instance new IMessageTransformer objects.</param>
    public static void SetTransformerFactory(Func<Type, IMessageTransformer> factory)
    {
        if (factory == null)
        {
            throw new ArgumentNullException("factory", "Factory delegate can not be null.");
        }

        MessageTransformFactory.factoryMethod = factory;
    }

    /// <summary>
    /// Scans a given assembly for IMessageTransformer implementations.
    /// </summary>
    /// <param name="assemblyName">Name of the assembly to scan.</param>
    public static void ScanAssembly(AssemblyName assemblyName)
    {
        if (assemblyName == null)
        {
            throw new ArgumentNullException("assemblyName", "A valid assembly name must be provided.");
        }

        Assembly assembly = Assembly.Load(assemblyName);

        if (assembliesToCache.Any(a => a.FullName == assemblyName.FullName))
        {
            return;
        }

        assembliesToCache.Add(assembly);
        MapDeviceTypesFromAssembly(assembly);
    }

    /// <summary>
    /// Gets the available transformer types that have been registered to this factory.
    /// </summary>
    public static Type[] GetAvailableTransformerTypes()
    {
        return deviceTransformerMapCache.Values.ToArray();
    }

    /// <summary>
    /// Gets an IMessageTransformer implementation for the Device Type given.
    /// </summary>
    /// <param name="deviceType">The DeviceType that the factory must create an IMessageTransformer for.</param>
    public IMessageTransformer<T> CreateTransformer<T>(DeviceTypeEnum deviceType) where T : class
    {
        // If we have a factory method, then we use it.
        if (factoryMethod == null)
        {
            throw new NullReferenceException("The MessageTransformerFactory did not have its factory method set.");
        }

        // Cast the non-generic return value to the generic version for the caller.
        Type transformerType = MessageTransformFactory.deviceTransformerMapCache[deviceType];
        return factoryMethod(transformerType) as IMessageTransformer<T>;
    }

    /// <summary>
    /// Builds the cache of IMessageTransformer Types that can be used by this factory.
    /// </summary>
    private static void BuildCache()
    {
        foreach (var assembly in assembliesToCache)
        {
            MapDeviceTypesFromAssembly(assembly);
        }
    }

    /// <summary>
    /// Creates a DeviceType to IMessageTransformer Type mapping.
    /// </summary>
    /// <param name="assembly"></param>
    private static void MapDeviceTypesFromAssembly(Assembly assembly)
    {
        var transformableTypes = assembly.DefinedTypes
            .Where(type => type
            .ImplementedInterfaces
            .Any(inter => inter == typeof(IMessageTransformer)) && !type.IsAbstract);

        foreach (TypeInfo transformer in transformableTypes)
        {
            var commandCode = transformer.GetCustomAttribute<TransformableAttribute>();
            deviceTransformerMapCache.Add(commandCode.DeviceType, transformer.AsType());
        }
    }
}

Autofac 设置

有了这个实现,映射的责任现在落在了工厂身上。结果,我们的 Autofac 注册变得非常简单。

var builder = new ContainerBuilder();

// Register all of the available transformers.
builder
    .RegisterTypes(MessageTransformFactory.GetAvailableTransformerTypes())
    .AsImplementedInterfaces()
    .AsSelf();

// Build the IoC container
this.container = builder.Build();

// Define our factory method for resolving the transformer based on device type.
MessageTransformFactory.SetTransformerFactory((type) =>
{
    if (!type.IsAssignableTo<IMessageTransformer>())
    {
        throw new InvalidOperationException("The type provided to the message transform factory resolver can not be cast to IMessageTransformer");
    }

    return container.Resolve(type) as IMessageTransformer;
});

一旦我们完成了 Autofac 注册,然后我们设置 MessageTransformFactory 的委托工厂方法来解析工厂使用的类型。

这也适用于测试,因为您现在可以自定义工厂在单元测试期间使用的实例化过程。您可以通过调用MessageTransformFactory.SetTransformFactory()MessageTransformFactory.SetTransformFactory()

来创建模拟转换器并构建一个返回模拟的简单委托

最后,您提到您更喜欢注入工厂以保持您已经使用的约定。虽然这个工厂没有任何依赖关系(因此可以在不需要任何 IoC 的情况下进行实例化),但您可以在接口后面抽象它拥有的 1 个实例方法,CreateTransformer&lt;T&gt; 方法。然后你用那个接口注册工厂到 Autofac 并注入它。这样一来,什么都没有工厂的具体实现。这也可以防止其他人访问工厂必须具有的静态方法,以促进与 Autofac 的映射和耦合。

【讨论】:

  • 这太棒了,一个问题是看我是否正确理解了模式,在 MessageTransformer 中你可以执行以下 if (!(message is IncomingFooMessage)) 但我只有一个 IIncomingMessage (I我想我可能把你弄糊涂了,因为现在只有一个具体的类,即 IncomingMessage) 所以我可以根据这些检查修改这些检查,它不会破坏设计。
  • 你可以让 ITransformer 类更通用一点。可能将其设置为ITransformer.Transform&lt;IncomingFooMessage&gt;(msg) 而不是传入接口。您可以约束泛型参数where T : IIncomingMessage, IncomingFooMessage。如果您希望更新 github 存储库以反映这一点,请告诉我
【解决方案2】:

从您提供的示例中,它看起来像:

public IMessageTransformer<T> GetTransformer(DeviceTypeEnum device)
{
    return _create(device);
}

可以改为:

// (Injected via Ctor.)
Func<IMessageTransformer<T>> _create;

public IMessageTransformer<T> GetTransformer(DeviceTypeEnum device)
{
    return _create();
}

然后您可以使用 RegisterAssemblyTypes().AsClosedTypesOf(typeof(IMessageTransformer&lt;&gt;)) 注册您的转换器。

也就是说,从DeviceType 到消息类型的映射在哪里发挥作用还不是很清楚...

我猜想映射是有充分理由的,但该示例并没有真正显示它适合的位置。映射是否可以在 Autofac 外部完成,在容器上调用 Resolve() 的常规 ol' 方法中?

【讨论】:

  • 我明白你的意思,我错过了从设备类型到消息转换器的映射!我最初计划在 MessageTransformer 上有一个 DeviceTypeEnum 属性,希望我能以某种方式在分辨率中使用它
  • 对于一些添加的上下文,IIncomingMessage 被转换为 AMessage 或 BMessage 我正在计划使用某种中介实现的消息,用于添加解密、验证、最终存储 - 使用类似这样的东西:github。 com/jbogard/MediatR
猜你喜欢
  • 2016-12-12
  • 1970-01-01
  • 1970-01-01
  • 2015-06-20
  • 2017-05-14
  • 1970-01-01
  • 1970-01-01
  • 2011-08-18
  • 1970-01-01
相关资源
最近更新 更多