Home > other >  How to implement Inversion of Control using Pure DI on a Console Application? (Without using IoC con
How to implement Inversion of Control using Pure DI on a Console Application? (Without using IoC con

Time:10-14

I have an example application where I would like to use the concepts of Inversion of Control to inject dependencies into lower level classes. My example application is a gummy bear factory. The gummy bear factory is responsible for creating each individual Gummy Bear, Adding it to a "Mini-Travel Size" bag, then adding the correct number of mini-bags to the larger package.

The Gummy Bear factory is provided a configuration that could change each time the factory project is run. In my example, it is a static class but just imagine this was loaded from say a configuration text file or XML file instead of being hard coded. Therefore, I have no idea of knowing at compile time what the user is going request for the number of gummy bears in a bag and the number of mini travel sized bags in a package.

Therefore, I am confused about how to decouple the Gummy Factory's AssembleProducts() method from the concrete implementation of GummyPackage, GummyBag, and GummyBear classes.

I can't necessarily use constructor injection because I don't know exactly how many GummyBears will be required. How would one decouple this code using the notion of "Pure DI". Is there any way to do this without the use of an IoC container? In my real world application, I am unable to use any third party packages (i.e. - IoC containers such as AutoFac).

This is what the Configuration looks like (remember, this could have been from a file):

public enum GummyFlavor { Cherry, Lemon, Orange, Pinapple, Apple, Strawberry }
public static class GummyBearPackageConfiguration
{
    public static int NumberOfGummieBagsPerPackage { get; set; } // Number of travel size gummy "mini-bags" per package

    public static int NumberOfGummiesPerBags { get; set; } // Number of gummies in each travel size bag
}

Here is the code:

namespace GummyBearFactory
{
    class Program
    {
        static void Main(string[] args)
        {
            // Load the "Configuration" 
            GummyBearPackageConfiguration.NumberOfGummieBagsPerPackage = 24;
            GummyBearPackageConfiguration.NumberOfGummiesPerBags = 7;

            GummyFactory gummyFactory = new GummyFactory();
            IGummyPackage gummyPackage = gummyFactory.AssembleProducts();

            OpenPackage(gummyPackage);

            Console.ReadLine();
        }

        public static void OpenPackage(IGummyPackage package)
        {
            // Itterate trough the Gummies
            Console.WriteLine($"Opening Gummy Package: \n");
            foreach (GummyBag bag in package.GummyBags)
            {
                Console.WriteLine($"Opening Gummy Bag #{bag.BagNumber}");

                foreach (GummyBear bear in bag.GummyBears)
                {
                    Console.WriteLine($"Gummy Number {bear.GummyID} is {bear.flavor.ToString()} flavor");
                }
            }
        }
    }
}

Generic Factory Class:

namespace GummyBearFactory
{
    public interface IFactory<T>
    {
        T AssembleProducts();
    }
    public abstract class Factory<T> : IFactory<T>
    {
        public abstract T AssembleProducts();
    }
}

Gummy Factory:

namespace GummyBearFactory
{
    public class GummyFactory : Factory<IGummyPackage>
    {
        public override IGummyPackage AssembleProducts()
        {
            IGummyPackage gummyPackage = new GummyPackage();
            Random rnd = new Random();

            for (int i = 1; i <= GummyBearPackageConfiguration.NumberOfGummieBagsPerPackage; i  )
            {
                IGummyBag gummyBag = new GummyBag();
                gummyBag.BagNumber = i;

                for (int b = 1; b <= GummyBearPackageConfiguration.NumberOfGummiesPerBags; b  )
                {
                    IGummyBear gummyBear = new GummyBear();
                    gummyBear.GummyID = b;

                    int flvr = rnd.Next(0, 5);
                    gummyBear.flavor = (GummyFlavor)flvr; // Pick a random flavor 

                    gummyBag.GummyBears.Add(gummyBear); // Add Bear to the bag
                }

                gummyPackage.GummyBags.Add(gummyBag); // Add Bag to the Package
            }

            return gummyPackage;
        }
    }
}

Gummy Package:

namespace GummyBearFactory
{
    public interface IGummyPackage
    {
        List<IGummyBag> GummyBags { get; set; }
    }

    public class GummyPackage : IGummyPackage
    {
        public GummyPackage()
        {
            GummyBags = new List<IGummyBag>();
        }

        public List<IGummyBag> GummyBags { get; set; }
    }
}

Gummy Bag:

namespace GummyBearFactory
{
    public interface IGummyBag
    {
        List<IGummyBear> GummyBears { get; set; }

        int BagNumber { get; set; }
    }

    public class GummyBag : IGummyBag
    {
        public GummyBag()
        {
            GummyBears = new List<IGummyBear>();
        }
        public List<IGummyBear> GummyBears { get; set; }

        public int BagNumber { get; set; }
    }
}

Last but not least, the Gummy Bear:

namespace GummyBearFactory
{
    public interface IGummyBear
    {
        GummyFlavor flavor { get; set; }

        int GummyID { get; set; }
    }

    public class GummyBear : IGummyBear
    {
         public GummyFlavor flavor { get; set; }

        public int GummyID { get; set; }
    }
}

The AssembleProducts() class depends on the concrete implementations of a GummyPackage, GummyBag, and GummyBear. Say in the future, management wants to change over from using GummyBags to mini Gummy cardboard "Boxes" because of new environmental rules. In the above scenario, I would need to crack open the GummyFactory.AssembleProducts() logic and re-wire the class to use boxes instead of Bags.

Is there a way to move this dependency linking up a level in the object graph?

CodePudding user response:

If you really can't use constructor injection then you could simply use Setter Injection this way :

var configuration = new GummyBearPackageConfiguration
{
    NumberOfGummieBagsPerPackage = 24,
    NumberOfGummiesPerBags = 7
};

GummyFactory gummyFactory = new GummyFactory();
gummyFactory.GummyBearPackageConfiguration = configuration;
IGummyPackage gummyPackage = gummyFactory.AssembleProducts();

GummyFactory could contain the following setter :

public GummyBearPackageConfiguration GummyBearPackageConfiguration { private get; set; }

The AssembleProducts() class depends on the concrete implementations of a GummyPackage, GummyBag, and GummyBear. Say in the future, management wants to change over from using GummyBags to mini Gummy cardboard "Boxes" because of new environmental rules. In the above scenario, I would need to crack open the GummyFactory.AssembleProducts() logic and re-wire the class to use boxes instead of Bags.

I'm not sure that this is a problem in which dependency injection could help. You could create a new factory MiniGummyFactory for this case so that would not require to change the AssembleProducts() function.

CodePudding user response:

The package, bag and gummy bear behave more like data models and do not really need backing abstractions.

As you described in your example

Say in the future, management wants to change over from using GummyBags to mini Gummy cardboard "Boxes" because of new environmental rules.

Then design the model to reflect that possibility.

For example here are my refactored models for this example application

public class GummyPackage {
    public GummyPackage() {
        Containers = new List<GummyBearContainer>();
    }

    public List<GummyBearContainer> Containers { get; set; }
}


public class GummyBearContainer {
    public GummyBearContainer() {
        GummyBears = new List<GummyBear>();
    }
    public List<GummyBear> GummyBears { get; set; }
    public string ContainerType { get; set; }
    public int ContainerNumber { get; set; }
}

public class GummyBear {
    public GummyFlavor flavor { get; set; }
    public int GummyID { get; set; }
}

the static configuration can be abstracted to reflect the required configuration for the application.

public interface IGummyBearPackageConfiguration {
    int NumberOfGummieBagsPerPackage { get; set; } // Number of travel size gummy "mini-bags" per package
    int NumberOfGummiesPerContainer { get; set; } // Number of gummies in each travel size bag
    string GummiesContainerType { get; set; }
}

Where it is populated from is an implementation detail/concern that can be handled by the user/developer of the application.

The factory can now explicitly depend on what it needs to perform its function.

public interface IGummyFactory : IFactory<GummyPackage> {

}

public class GummyFactory : Factory<GummyPackage>, IGummyFactory {
    private readonly IGummyBearPackageConfiguration configuration;

    public GummyFactory(IGummyBearPackageConfiguration configuration) {
        this.configuration = configuration;
    }

    public override GummyPackage AssembleProducts() {
        GummyPackage gummyPackage = new GummyPackage();
        Random rnd = new Random();

        for (int i = 1; i <= configuration.NumberOfGummieBagsPerPackage; i  ) {
            GummyBearContainer container = new GummyBearContainer();
            container.ContainerNumber = i;
            container.ContainerType = configuration.GummiesContainerType;

            for (int b = 1; b <= configuration.NumberOfGummiesPerContainer; b  ) {
                GummyBear gummyBear = new GummyBear();
                gummyBear.GummyID = b;

                int flvr = rnd.Next(0, 5);
                gummyBear.flavor = (GummyFlavor)flvr; // Pick a random flavor 

                container.GummyBears.Add(gummyBear); // Add Bear to the bag
            }

            gummyPackage.Containers.Add(container); // Add Bag to the Package
        }

        return gummyPackage;
    }
}

The fact that the package, container and gummy bears are created with the factory is an implementation concern and not really tight coupling since that relates to coupling to concrete dependencies (services).

The following Program still follows Pure DI and does not use an IoC container. (It is however container friendly.)

class Program {

    class DefaultConfiguration : IGummyBearPackageConfiguration {
        public int NumberOfGummieBagsPerPackage { get; set; }
        public int NumberOfGummiesPerContainer { get; set; }
        public string GummiesContainerType { get; set; }
    }

    static void Main(string[] args) {
        // Load the "Configuration" 
        IGummyBearPackageConfiguration GummyBearPackageConfiguration = new DefaultConfiguration {
            NumberOfGummieBagsPerPackage = 24,
            NumberOfGummiesPerContainer = 7,
            GummiesContainerType = "Bag",
        };

        IGummyFactory gummyFactory = new GummyFactory(GummyBearPackageConfiguration);
        GummyPackage gummyPackage = gummyFactory.AssembleProducts();

        OpenPackage(gummyPackage);

        Console.ReadLine();
    }

    public static void OpenPackage(GummyPackage package) {
        // Itterate trough the Gummies
        Console.WriteLine($"Opening Gummy Package: \n");
        foreach (GummyBearContainer item in package.Containers) {
            Console.WriteLine($"Opening Gummy {item.ContainerType} #{item.ContainerNumber}");

            foreach (GummyBear bear in item.GummyBears) {
                Console.WriteLine($"Gummy Number {bear.GummyID} is {bear.flavor.ToString()} flavor");
            }
        }
    }
}

and changing to boxes is simply a matter of updating the configuration data source.

  • Related