How can I resolve a decorator using a particular instance of a component or another decorator object in .net core

▼魔方 西西 提交于 2021-02-10 07:01:36

问题


After learning about the Decorator Pattern with typical Coffee example where Decorator Pattern saves us from the class explosion problem, I wrote some code to use and see it myself. Lets take a look at the UML first...

and here is the code:

Component

ICofee defination:

public interface ICoffee
{
    string Name { get; }
    decimal Cost { get; } 
}

LatteCoffee definition:

public class LatteCoffee : ICoffee
{
    public string Name { get; } = "Latte";
    public decimal Cost => 2.00m;
}

Decorators

IAddOnDecorator defination:

public interface IAddOnDecorator : ICoffee
{
    ICoffee BaseCoffee { set; }
}

CaramelDecorator definition:

public class CaramelDecorator : IAddOnDecorator
{
    public ICoffee BaseCoffee { private get; set; }    
    public string Name { get; } = "Caramel";
    public decimal Cost => BaseCoffee.Cost + 0.5m;
}

AlmondSyrupDecorator definition:

public class AlmondSyrupDecorator : IAddOnDecorator
{
    public ICoffee BaseCoffee { private get; set; }
    public string Name { get; } = "AlmondSyrup";
    public decimal Cost => BaseCoffee.Cost + 0.3m;
}

You can see that the decorators are not taking ICoffee injected in the constructor instead, there is a setter property ICoffee BaseCoffee.

I would like to use the constructor injection into the decorator (IAddOnDecorator)for the component (ICoffee) which is the recommended way, however, I am then unable to pass in the concrete object in the unit test method.

The Usage

[TestFixture]
public class CoffeeTests
{
    private IServiceProvider provider;
    private IServiceCollection services;
    private IDictionary<string, ICoffee> coffeeMapper;
    private IDictionary<string, IAddOnDecorator> addonMapper;

    [SetUp]
    public void Setup()
    {
        services = new ServiceCollection();
        services.AddTransient<ICoffee, LatteCoffee>();
        services.AddTransient<IAddOnDecorator, CaramelDecorator>();
        services.AddTransient<IAddOnDecorator, AlmondSyrupDecorator>();
        provider = services.BuildServiceProvider();
    }

    [Test]
    public void LatteWithCaramelAndAlmodSyrupShouldReturnTheTotalPriceOfCoffeeAndItsAddOns()
    {
        string coffee = "Latte";
        IEnumerable<string> addOns = new List<string> { "Caramel", "AlmondSyrup" };

        IEnumerable<ICoffee> allCoffees = provider.GetServices<ICoffee>();

        coffeeMapper = allCoffees.ToDictionary(c => c.Name, c => c);

        ICoffee selectedCoffee = coffeeMapper[coffee];
        IEnumerable<IAddOnDecorator> resolvedDecorators = provider.GetServices<IAddOnDecorator>();

        IList<IAddOnDecorator> selectedDecorators = new List<IAddOnDecorator>();
        addonMapper = resolvedDecorators .ToDictionary(a => a.Name, a => a);

        IAddOnDecorator firstAddon = addonMapper[addOns.First()];
        firstAddon.BaseCoffee = selectedCoffee;
        selectedDecorators.Add(firstAddon);

        foreach (string nextAddon in addOns.Where(a => a != firstAddon.Name))
        {
            IAddOnDecorator decorator = addonMapper[nextAddon];
            decorator.BaseCoffee = selectedDecorators.Last();
            selectedDecorators.Add(decorator);
        }

        // Act.
        decimal totalCost = selectedDecorators.Last().Cost;

        // Assert.

        Assert.That(2.80m, Is.EqualTo(totalCost));

    }
}

My Question:

How can I resolve IAddOnDecorator using a particular instance of ICoffee object passing into the constructor of Decorator class in .net core? I do not want to use ICoffee BaseCoffee { private get; set; } property.


回答1:


Unfortunately, the default IoC Container in .Net core doesnt support decoration so I had to turn my attention to other available options. Since I have already used Structure Map and that I like its "Convention over Configuration" strategy I decided to try it. Following code achieves that I was looking for... Its not perfect but I allows me to instantiate decorator by inject an instance of another decorator or component.

Note: I have added another decorator SaltedCaramelDecorator just to keep it more interesting...

// Arrange.
Container container = new Container();
container.Configure(config =>
{
    // register coffees / components
    config.For<ICoffee>().Use<LatteCoffee>().Named("Latte");
    config.For<ICoffee>().Use<CappuccinoCoffee>().Named("Cappuccino");

    // register addOns / decorators
    config.For<IAddOnDecorator>().Use<CaramelDecorator>().Named("Caramel");
    config.For<IAddOnDecorator>().Use<AlmondSyrupDecorator>().Named("Almond");
    config.For<IAddOnDecorator>().Use<SaltedCaramelDecorator>().Named("SaltedCaramel");
});

const string coffeeName = "Latte";
IEnumerable<string> coffeeDecoratorNames = new List<string> { "SaltedCaramel", "Almond", "Caramel" };

// Act.

ICoffee theCoffee = container.GetInstance<ICoffee>(coffeeName);
if (coffeeDecoratorNames.Any())
{
    // set the baseCofee as argument to the next decorator / addon.
    ExplicitArguments baseCoffee = new ExplicitArguments();
    baseCoffee.Set<ICoffee>(theCoffee);

    foreach (string nextDeco in coffeeDecoratorNames)
    {
        ExplicitArguments addOn = new ExplicitArguments();
        addOn.Set<ICoffee>(theCoffee);
        theCoffee = container.GetInstance<IAddOnDecorator>(addOn, nextDeco);
    }
}



// Assert.

Assert.That(3.20m, Is.EqualTo(theCoffee.Cost));

Thanks for @Steven for helping comments. I hope someone else would find this post helpful.




回答2:


Since you are using a separate TService for the decorator (compared to the regular implementation), you should be able to do this fairly easily with the built-in container.

It's easy because ICoffee still resolves to the regular implementation, which you need as a dependency.

services.AddTransient<ICoffee, LatteCoffee>();

// With manual construction
services.AddTransient<IAddOnDecorator, CaramelDecorator>(serviceProvider =>
    new CaramelDecorator(serviceProvider.GetRequiredService<ICoffee>));

// With automatic construction, if there are other constructor params that you want auto-injected
services.AddTransient<IAddOnDecorator, CaramelDecorator>(sp =>
    ActivatorUtilities.CreateInstance<CaramelDecorator>(sp, sp.GetRequiredService<ICoffee>));

Does this help?



来源:https://stackoverflow.com/questions/50704145/how-can-i-resolve-a-decorator-using-a-particular-instance-of-a-component-or-anot

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!