web-dev-qa-db-ja.com

InjectionConstructorのすべてのパラメーターを明示的に指定せずに、Unityでデコレーターパターンを使用するにはどうすればよいですか?

David Haydnからのこの役立つ記事(編集:詐欺リンクが削除された可能性があります この記事 )は、InjectionConstructorクラスを使用してデコレータを使用してチェーンを設定する方法を示していますUnityのパターン。ただし、デコレータチェーン内のアイテムのコンストラクターに他のパラメーターがある場合、InjectionConstructorはそれらの各パラメーターを明示的に宣言する必要があります(そうでない場合、Unityは適切なコンストラクターが見つからないと文句を言います)。つまり、Unity構成コードを更新せずに、デコレータチェーン内のアイテムに新しいコンストラクターパラメーターを単純に追加することはできません。

これが私が何を意味するかを説明するためのいくつかのサンプルコードです。 ProductRepositoryクラスは最初にCachingProductRepositoryでラップされ、次にLoggingProductRepostioryでラップされます。 CachingProductRepositoryとLoggingProductRepositoryはどちらも、コンストラクターでIProductRepositoryを取得することに加えて、コンテナーからの他のインターフェイスも必要とします。

    public class Product 
    {
        public int Id;
        public string Name;
    }

    public interface IDatabaseConnection { }

    public interface ICacheProvider 
    { 
        object GetFromCache(string key);
        void AddToCache(string key, object value);
    }

    public interface ILogger
    {
        void Log(string message, params object[] args);
    }


    public interface IProductRepository
    {
        Product GetById(int id);    
    }

    class ProductRepository : IProductRepository
    {
        public ProductRepository(IDatabaseConnection db)
        {
        }

        public Product GetById(int id)
        {
            return new Product() { Id = id, Name = "Foo " + id.ToString() };
        }
    }

    class CachingProductRepository : IProductRepository
    {
        IProductRepository repository;
        ICacheProvider cacheProvider;
        public CachingProductRepository(IProductRepository repository, ICacheProvider cp)
        {
            this.repository = repository;
            this.cacheProvider = cp;
        }

        public Product GetById(int id)
        {       
            string key = "Product " + id.ToString();
            Product p = (Product)cacheProvider.GetFromCache(key);
            if (p == null)
            {
                p = repository.GetById(id);
                cacheProvider.AddToCache(key, p);
            }
            return p;
        }
    }

    class LoggingProductRepository : IProductRepository
    {
        private IProductRepository repository;
        private ILogger logger;

        public LoggingProductRepository(IProductRepository repository, ILogger logger)
        {
            this.repository = repository;
            this.logger = logger;
        }

        public Product GetById(int id)
        {
            logger.Log("Requesting product {0}", id);
            return repository.GetById(id);
        }
    }

これが(合格した)ユニットテストです。次の必要性を排除したい余剰構成のビットについては、コメントを参照してください。

    [Test]
    public void ResolveWithDecorators()
    {
        UnityContainer c = new UnityContainer();            
        c.RegisterInstance<IDatabaseConnection>(new Mock<IDatabaseConnection>().Object);
        c.RegisterInstance<ILogger>(new Mock<ILogger>().Object);
        c.RegisterInstance<ICacheProvider>(new Mock<ICacheProvider>().Object);

        c.RegisterType<IProductRepository, ProductRepository>("ProductRepository");

        // don't want to have to update this line every time the CachingProductRepository constructor gets another parameter
        var dependOnProductRepository = new InjectionConstructor(new ResolvedParameter<IProductRepository>("ProductRepository"), new ResolvedParameter<ICacheProvider>());
        c.RegisterType<IProductRepository, CachingProductRepository>("CachingProductRepository", dependOnProductRepository);

        // don't want to have to update this line every time the LoggingProductRepository constructor changes
        var dependOnCachingProductRepository = new InjectionConstructor(new ResolvedParameter<IProductRepository>("CachingProductRepository"), new ResolvedParameter<ILogger>());
        c.RegisterType<IProductRepository, LoggingProductRepository>(dependOnCachingProductRepository);
        Assert.IsInstanceOf<LoggingProductRepository>(c.Resolve<IProductRepository>());
    }
36
Mark Heath

@ DarkSquirrel42からの提案のおかげで、別のアプローチはInjectionFactoryを使用することです。欠点は、新しいコンストラクターパラメーターがチェーン内の何かに追加されるたびにコードを更新する必要があることです。利点は、コードを理解するのがはるかに簡単で、コンテナへの登録が1つだけです。

Func<IUnityContainer,object> createChain = container =>
    new LoggingProductRepository(
        new CachingProductRepository(
            container.Resolve<ProductRepository>(), 
            container.Resolve<ICacheProvider>()), 
        container.Resolve<ILogger>());

c.RegisterType<IProductRepository>(new InjectionFactory(createChain));
Assert.IsInstanceOf<LoggingProductRepository>(c.Resolve<IProductRepository>());
28
Mark Heath

デコレータコンテナ拡張の実装については、 この記事 を参照してください。これにより、コンストラクターの署名が変更された場合に構成を変更する必要がないという点で、目的の場所にたどり着くことができます。

12

別の解決策は、Unityが装飾された型を解決できるように、コードベースに型パラメーターを追加することです。幸い、Unityは型パラメーターとその依存関係を独自に解決できるため、デコレーターチェーンを定義するときにコンストラクターパラメーターを気にする必要はありません。

登録は次のようになります。

_unityContainer.RegisterType<IService, Logged<Profiled<Service>>>();
_

これが基本的な実装例です。テンプレート化されたデコレータ_Logged<TService>_および_Profiled<TService>_に注意してください。私がこれまでに気づいたいくつかの欠点については、以下を見てください。

_public interface IService { void Do(); }

public class Service : IService { public void Do() { } }

public class Logged<TService> : IService where TService : IService
{
    private TService decoratee;
    private ILogger logger;

    public Logged(ILogger logger, TService decoratee) {
        this.decoratee = decoratee;
        this.logger = logger;
    }

    public void Do() {
        logger.Debug("Do()");
        decoratee.Do();
    }
}

public class Profiled<TService> : IService where TService : IService
{
    private TService decoratee;
    private IProfiler profiler;

    public Profiled(IProfiler profiler, TService decoratee) {
        this.decoratee = decoratee;
        this.profiler = profiler;
    }

    public void Do() {
        profiler.Start();
        decoratee.Do();
        profiler.Stop();
    }
}
_

欠点

  • uC.RegisterType<IService, Logged<IService>>();のような誤った登録は、アプリケーションをスタックオーバーフローさせる無限再帰を引き起こします。これは、プラグインアーキテクチャの脆弱性である可能性があります。
  • それはあなたのコードベースをある程度醜くします。 Unityをあきらめて、別のDIフレームワークに切り替えると、これらのテンプレートパラメーターは誰にとっても意味がなくなります。

私はこれのためにかなり大雑把な拡張メソッドをノックアップしました。これは実行したときに期待どおりに動作しました。

public static class UnityExtensions
{
    public static IUnityContainer Decorate<TInterface, TDecorator>(this IUnityContainer container, params InjectionMember[] injectionMembers)
        where TDecorator : class, TInterface
    {
        return Decorate<TInterface, TDecorator>(container, null, injectionMembers);
    }

    public static IUnityContainer Decorate<TInterface, TDecorator>(this IUnityContainer container, LifetimeManager lifetimeManager, params InjectionMember[] injectionMembers)
        where TDecorator : class, TInterface
    {
        string uniqueId = Guid.NewGuid().ToString();
        var existingRegistration = container.Registrations.LastOrDefault(r => r.RegisteredType == typeof(TInterface));
        if(existingRegistration == null)
        {
            throw new ArgumentException("No existing registration found for the type " + typeof(TInterface));
        }
        var existing = existingRegistration.MappedToType;

        //1. Create a wrapper. This is the actual resolution that will be used
        if (lifetimeManager != null)
        {
            container.RegisterType<TInterface, TDecorator>(uniqueId, lifetimeManager, injectionMembers);
        }
        else
        {
            container.RegisterType<TInterface, TDecorator>(uniqueId, injectionMembers);
        }

        //2. Unity comes here to resolve TInterface
        container.RegisterType<TInterface, TDecorator>(new InjectionFactory((c, t, sName) =>
        {
            //3. We get the decorated class instance TBase
            var baseObj = container.Resolve(existing);

            //4. We reference the wrapper TDecorator injecting TBase as TInterface to prevent stack overflow
            return c.Resolve<TDecorator>(uniqueId, new DependencyOverride<TInterface>(baseObj));
        }));

        return container;
    }
}

そしてあなたのセットアップでは:

container.RegisterType<IProductRepository, ProductRepository>();

// Wrap ProductRepository with CachingProductRepository,
// injecting ProductRepository into CachingProductRepository for
// IProductRepository
container.Decorate<IProductRepository, CachingProductRepository>();

// Wrap CachingProductRepository with LoggingProductRepository,
// injecting CachingProductRepository into LoggingProductRepository for
// IProductRepository
container.Decorate<IProductRepository, LoggingProductRepository>();
4
garryp

最も簡潔答えうまくいく別の stackoverflow post マークシーマンによって言及されています。 conciseであり、名前付き登録を使用する必要はなく、Unity拡張機能の使用を提案する必要もありません。 Log4NetLoggerとDecoratorLoggerと呼ばれるデコレータ実装の2つの実装を持つILoggerと呼ばれるインターフェイスについて考えてみます。次のように、ILoggerインターフェイスに対してDecoratorLoggerを登録できます。

container.RegisterType<ILogger, DecoratorLogger>(
    new InjectionConstructor(
        new ResolvedParameter<Log4NetLogger>()));
3
user3613932

これについての回答を待っている間に、かなりハッキーな回避策を思いつきました。 IUnityContainerに拡張メソッドを作成しました。これにより、リフレクションを使用してデコレータチェーンを登録し、InjectionConstructorパラメータを作成できます。

static class DecoratorUnityExtensions
{
    public static void RegisterDecoratorChain<T>(this IUnityContainer container, Type[] decoratorChain)
    {
        Type parent = null;
        string parentName = null;
        foreach (Type t in decoratorChain)
        {
            string namedInstance = Guid.NewGuid().ToString();
            if (parent == null)
            {
                // top level, just do an ordinary register type                    
                container.RegisterType(typeof(T), t, namedInstance);
            }
            else
            {
                // could be cleverer here. Just take first constructor
                var constructor = t.GetConstructors()[0];
                var resolvedParameters = new List<ResolvedParameter>();
                foreach (var constructorParam in constructor.GetParameters())
                {
                    if (constructorParam.ParameterType == typeof(T))
                    {
                        resolvedParameters.Add(new ResolvedParameter<T>(parentName));
                    }
                    else
                    {
                        resolvedParameters.Add(new ResolvedParameter(constructorParam.ParameterType));
                    }
                }
                if (t == decoratorChain.Last())
                {
                    // not a named instance
                    container.RegisterType(typeof(T), t, new InjectionConstructor(resolvedParameters.ToArray()));
                }
                else
                {
                    container.RegisterType(typeof(T), t, namedInstance, new InjectionConstructor(resolvedParameters.ToArray()));
                }
            }
            parent = t;
            parentName = namedInstance;
        }
    }
}

これにより、はるかに読みやすい構文でコンテナーを構成できます。

[Test]
public void ResolveWithDecorators2()
{
    UnityContainer c = new UnityContainer();
    c.RegisterInstance<IDatabaseConnection>(new Mock<IDatabaseConnection>().Object);
    c.RegisterInstance<ILogger>(new Mock<ILogger>().Object);
    c.RegisterInstance<ICacheProvider>(new Mock<ICacheProvider>().Object);

    c.RegisterDecoratorChain<IProductRepository>(new Type[] { typeof(ProductRepository), typeof(CachingProductRepository), typeof(LoggingProductRepository) });

    Assert.IsInstanceOf<LoggingProductRepository>(c.Resolve<IProductRepository>());

}

Unityでこれにもっとエレガントな解決策があるかどうか知りたいです

0
Mark Heath