简体   繁体   中英

Service locator and dependency injection

I think it's universally agreed that following is bad

public class Foo
{
    private IService _service;
    public Foo()
    {
        _service = IocContainer.Resolve<IService>();
    }
}

and that following is preferred (Dependency Injection)

public class Foo
{
    private IService _service;
    public Foo(IService service)
    {
    }
}

However now it's up to the consumer to provide the service. The consumer could of course require the IService in the constructor as well, but it seems to be annoying when the hierarchy becomes deeper. At some point someone needs to request the IService from the IoC container - but when...?

A former colleague at my workplace had written a UnitOfWork class for the UoW/Repository pattern like this (using Microsoft ServiceLocator):

public static UnitOfWork
{
    public static IUnitOfWork Current
    {
        get { return ServiceLocator.Current.GetInstance<IUnitOfWork>(); }
    }

    public static void Commit()
    {
        Current.Commit();
    }

    public static void Dispose()
    {
        Current.Dispose();
    }

    public static IRepository<T> GetRepository<T>() where T : class
    {
        return ServiceLocator.Current.GetInstance<IRepository>();
    }
}

and hooked up the IoC using Ninject so a request for IRepository would find the current UoW or create a new if required (if current is disposed). The usage becomes

public class MyController
{    
    public void RunTasks()
    {
        var rep = UnitOfWork.GetRepository<Tasks>();
        var newTasks = from t in rep.GetAll()
                       where t.IsCompleted == false
                       select t;

        foreach (var task in newTasks)
        {
            // Do something
        }

        UnitOfWork.Commit();
    }
}

It does however still suffer from static IoC (service locator) class, but would there be a smarter solution? In this case there is no need to know about the internal dependencies (the static class has no logic), and for testing purposes an alternate IoC configuration can set up everything with mocks - and it's easy to use.

EDIT:

I will try and clarify my confusion with a different example. Suppose I have a standard winforms application with a MainWindow class. When the user click a button, I need to load some data from a database, and pass it to a class which will process the data:

public class MainWindow : Form
{
    public MainWindow()
    {
    }

    private void OnUserCalculateClick(object sender, EventArgs args)
    {
        // Get UoW to connect to DB
        // Get instance of processor
    }
}

How would I get the instance of the processor and the unit of work? Can it be injected into a forms class?

I guess my question boils down to: If I am in a class which has been constructed without Ioc it could be a winform, a ria service class etc. - is it ok to refer to service locator/IoC controller to resolve instances of dependencies, or is there a preferred way of handling these cases? Or am I just doing something wrong...?

About the first part of the question:

The consumer could of course require the IService in the constructor as well, but it seems to be annoying when the hierarchy becomes deeper.

No, the consumer does not require an IService , it requires an IFoo . It does not know that the IFoo it will get has a dependency on IService , only your DI configuration knows that. So, don't worry, you will not end up with this dependency hierarchy hell you describe .

At some point someone needs to request the IService from the IoC container - but when...?

That will happen only in your composition root . So if it's an MVC application, you have somehow configured the MVC framework to use your DI configuration when it needs to instantiate controllers, so internally the framework decides (from routing) that it needs a MyController , and it does something like resolver.Get<MyController>() . So service location is used only up there, not in your controllers or anywhere else.

About the MyController part of the question:

Can't really get the connection with the previous part, but still you can use constructor injection. No static classes (that are not injected, so cannot be swapped or mocked out for testing purposes), no service location.

[As a side note, you could even avoid the extra code about unit of work (probably the ORM you use has one and you already using it at the implementation of your IRepositories ). Maybe your repositories can have a SaveChanges method, which will call the unitOfWork's SaveChanges - but that's a matter of preference, and rather irrelevant with the previous discussion].

The way I solved this is to have a UnitOfWorkFactory which has a Create method that creates your UnitOfWork .

public interface IUnitOfWorkFactory
{
    IUnitOfWork Create();
}

public interface IUnitOfWork : IDisposable
{
    T GetRepository<T>();
    void Commit();
}

public class MyController
{    
    private readonly IUnitOfWorkFactory _unitOfWorkFactory;

    public MyController(IUnitOfWorkFactory unitOfWorkFactory)
    {
        _unitOfWorkFactory = unitOfWorkFactory;
    }

    public void RunTasks()
    {
        using (var unitOfWork = _unitOfWorkFactory.Create())
        {
            var rep = UnitOfWork.GetRepository<Tasks>();
            var newTasks = from t in rep.GetAll()
                           where t.IsCompleted == false
                           select t;

            foreach (var task in newTasks)
            {
                // Do something
            }               

            unitOfWork.Commit();
        }
    }
}  

The advantage to having a factory is that it lets the user (the controller) control the creation and destruction of the unit-of-work.

This also makes unit-testing easier as you don't need to test using your IoC. I am also not a fan of having a global context available (like UnitOfWork.Current ) because it's difficult to determine when the UoW is going to be disposed or committed.

If another class needs an instance of a UoW to add additional work to an existing context, you can pass in a specific instance.

Using your first example, the container will construct both IFoo and IService . Here is some real code to illustrate:

        container.RegisterType<ISubmittingService, GnipSubmittingService>(
            new DisposingTransientLifetimeManager(),
            new InjectionConstructor(
                typeof (IGnipHistoricConnection),
                typeof (IUserDataInterface),
                new EstimateVerboseLoggingService.TitleBuilder(),
                new EstimateVerboseLoggingService.FixedEndDateBuilder(),
                typeof (ISendEmailService),
                addresses,
                typeof (ILog)
                )
            );

        container.RegisterType<IEstimateService, EstimateVerboseLoggingService>(
            new DisposingTransientLifetimeManager(),
            new InjectionConstructor(
                typeof(IEstimateDataInterface),
                typeof(ISubmittingService),
                typeof(ILog)
                )
            );

...

    public EstimateVerboseLoggingService(
        IEstimateDataInterface estimateData,
        ISubmittingService submittingService,
        ILog log)
    {
        _estimateData = estimateData;
        _submittingService = submittingService;
        _log = log;
    }

...

    public GnipSubmittingService(
        IGnipHistoricConnection gnip,
        IUserDataInterface userDb,
        IBuilder<string, int> titleBuilder,
        IBuilder<DateTime, DateTime> endDateBuilder,
        ISendEmailService emailService,
        IEnumerable<MailAddress> errorEmailRecipients,
        ILog log)
    {
        _gnip = gnip;
        _userDb = userDb;
        _titleBuilder = titleBuilder;
        _endDateBuilder = endDateBuilder;
        _emailService = emailService;
        _errorEmailRecipients = errorEmailRecipients;
        _log = log;
    }

In this code, EstimateVerboseLoggingService consumes an ISubmitingService . Both implementations are specified in the container.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM