Creating object “Catalog” for DI agnostic library

2019-07-28 18:04发布

问题:

I am developing an internal library that is to be used by other developers in the company that I'm working for. I am applying SOLID patterns and following the best practices as described in Dependency Inject (DI) “friendly” library.

My end users would be developers of different applications. Some of them are complex legacy applications with no DI, and others are newer apps that have DI and TDD.

Now, I am trying to figure out how to call this DI friendly library from a legacy ASP.NET Webforms application that has no DI implemented in it, and obviously, I can't revise 250+ aspx pages to support constructor injection because it is out of scope of my project. (Yes, I have read Introducing an IoC Container to Legacy Code )

One idea that I had was creating a static global wrapper for Common Service Locator to automatically resolve dependencies throughout the app:

public static class GlobalResolver
{
    public static T Resolve<T>()
    {
        return ServiceLocator.Current.GetInstance<T>();
    }
}

The nice thing about this approach is that I can use any IoC library in my composition root (I currently use Unity). I would use this GlobalResolver like this:

protected void OnClick(object sender, EventArgs e)
{
    IMailMessage message = MessageFactory.Create("Jack.Daniels@jjj.com", "John.Doe@jjj.com", "subject", "Body", true, MailPriority.High);
    GlobalResolver.Resolve<IMailer>().SendMail(message);
}

I like this approach and I think it's clean, but novice developers in my company might get confused with this GlobalResolver.Resolve<IMailer> line, so I'm trying to see if there is alternative to this out there.

One thing that comes to my mind is something like this:

public static class CommonCatalog
{
    public static IMailer Mailer => ServiceLocator.Current.GetInstance<IMailer>();
    public static IMailMessageFactory MessageFactory => ServiceLocator.Current.GetInstance<IMailMessageFactory>();
    public static IFtpSecureClientFactory FTPClientFactory => ServiceLocator.Current.GetInstance<IFtpSecureClientFactory>();

    // And so on...
}

And simply use it like this: CommonCatalog.Mailer.SendMail(message);. Developers at my company are used to seeing static methods, and I think this approach might be desirable for them.

My questions are:

  1. Is this the best solution for my problem?
  2. Am I violating any of the best practices?
  3. Is there a design pattern that descibes the CommonCatalog class? Is it a "Facade" or "Proxy"?

TLDR: Developers at my company like to use Static methods, but static methods are incompatible with DI and SOLID practices. Is there any way to trick people into thinking that they are using static methods, but behind the scenes call DI code?

回答1:

If you want to avoid the Service Locator anti-pattern (which you should, because - it's an anti-pattern), then the first option with a GlobalResolver is out of the question, because it's definitely a Service Locator.

The catalog of services is closer to the Facades I recommend in my expanded article on DI-friendly libraries, although I usually prefer not having an aggregated catalog of objects. It always makes me uncomfortable when I don't know how to name objects, and a name like CommonCatalog seems too devoid of meaning.

Rather, I'd prefer making instance-based Facades with the Fluent Builder pattern as described in the article, since it tends to be more flexible when you, down the line, discover that you need to add various options and switches to the facade.

If you absolutely must, though, you can add a static method for each of the Facades. Something like this:

public static class Mailer
{
    public static IMailer Default
    {
        get { return new MailerBuilder().Create(); }
    }
}

If the instance conceivably has Singleton lifetime, you can, instead, use the Singleton design pattern, as the next example demonstrates.

You could implement a default MessageFactory the same way, but here instead using the Singleton design pattern:

public static class MailMessageFactory
{
    public static IMailMessageFactory Default { get } =
        new MailMessageFactoryBuilder().Create();
}

Notice that this doesn't use a Service Locator for the implementation, either.

To be clear, though, what's behind such Facades could easily be implemented according to the SOLID principles, but the calling code would still have a hard time doing that.