2

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...

enter image description here

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.

DavidG
  • 113,891
  • 12
  • 217
  • 223
Yawar Murtaza
  • 3,655
  • 5
  • 34
  • 40

2 Answers2

2

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.

Yawar Murtaza
  • 3,655
  • 5
  • 34
  • 40
0

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?

Timo
  • 7,992
  • 4
  • 49
  • 67