Внедрение зависимостей «по требованию» для динамически создаваемых классов - PullRequest
0 голосов
/ 09 мая 2018

В моем приложении есть класс с именем EventTriggerActionService, который отвечает за создание пользовательских действий, которые находятся в разных сборках. Он использует имя класса и имя сборки для загрузки модуля через Reflection и выполняет его.

Моя основная проблема: как мне ввести необходимые зависимости в эти пользовательские действия? Все мои действия происходят от базового класса ActionBase.

Моим первоначальным решением было сначала предоставить все зависимости с помощью инжектора конструктора, просто добавив их в качестве параметров в (ActionBase)Activator.CreateInstance();, но это было проблематично, потому что я не хотел заставлять все мои производные действия выполнять в зависимости других действий.

Вот мое второе решение: я решил использовать события, чтобы поместить «Поставщики зависимостей» в EventTriggerActionService. При таком подходе все зависимости для всех пользовательских действий будут внедрены в EventTriggerActionService, и пользовательское действие запросит зависимость, вызвав событие.

Вот мой EventTriggerActionService:

public class EventTriggerActionService
{
    private IEmailerFactory _emailerFactory;
    public EventTriggerActionService(IEmailerFactory emailerFactory) 
    {
        _emailerFactory = emailerFactory;
    }

    public void Execute(EventTriggerAction eventTriggerAction, EventContext context)
    {   
        var assemblyName = eventTriggerAction.EventAction.EventActionHandlerAssembly;
        var className = eventTriggerAction.EventAction.EventActionHandlerClass;

        Assembly actionHandlerAssembly = Assembly.Load(assemblyName);
        Type actionHandlerType = actionHandlerAssembly.GetType(className);

        var action = (ActionBase)Activator.CreateInstance(); // Instantiate the action by its base class, ActionBase

        action.Arguments = data.Arguments; // This is a dictionary that contains the arguments of the action

        // Register all the dependency providers here
        GetEmailer emailerHandler = GetEmailer;
        actionHandlerType.GetEvent("GetEmailer").AddEventHandler(action, emailerHandler); // Register the event handler

        action.Execute(); // Execute the Action
    }

    // The custom action "Requests" a dependency by firing this event
    private void GetEmailer(ref IEmailer emailer)
    {
        emailer = _emailerFactory.Create();
    }
}

public delegate void GetEmailer(ref IEmailer emailer);

Это ActionBase:

public abstract class ActionBase
{
    public event GetEmailer GetEmailer;

    private IEmailer _emailer;

    protected IEmailer Emailer
    {
        get
        {
            if (_emailer == null)
            {
                GetEmailer(ref _emailer);
            }
            return _emailer;
        }
    }

    protected Dictionary<string,string> Arguments { get; set; }

    public void Execute()
    {
        // Perform some common logic here
        ExecuteAction(); // Execute the custom action
    }

    public abstract ResultBase ExecuteAction();
}

Это одно из моих пользовательских действий:

public class SimpleEmailSender : ActionBase
{
    public override ResultBase ExecuteAction()
    {
        Emailer.Send(Arguments["EmailBody"]);
    }
}

Имейте в виду, что EventTriggerActionService находится в изолированной библиотеке и будет использоваться различными потребительскими приложениями. Пользовательские приложения могут использовать контейнеры IoC или просто использовать DI для бедного.

У меня вопрос, есть ли более оптимальное решение моей проблемы? Я думаю, что мое решение определенно решает проблему принуждения зависимостей ко всем производным действиям.

Ответы [ 2 ]

0 голосов
/ 23 мая 2018

Поскольку EventTriggerActionService не знает, с каким типом он имеет дело, он должен позволять настраиваемым действиям решать, чего они хотят. Итак, я думаю, что шаблон сервисного локатора - хорошее решение для этого. Предположим, у вас есть сервисный локатор, определенный на стороне EventTriggerActionService, например:

public static class ServiceLocator
{
    private static Dictionary<string, Type> container = new Dictionary<string, Type>();

    public static void RegisterService(string name, Type implType)
    {
        container.Add(name, implType);
    }

    public static objcet GetService(string name)
    {
        return Actinator.CreateInstance(container[name]);
    }
}

Измените ActionBase и пользовательские действия, например:

public abstract class ActionBase
{
    private static MethodInfo getServiceMI;

    static ActionBase()
    {
        // Find out which assembly implement ServiceLocator.
        var asm = AppDomain.CurrentDomain.GetAssemblies().First(x => x.GetTypes().Any(y => y.Name == "ServiceLocator"));
        // Get MethodInfo of ServiceLocator.GetService.
        getServiceMI = asm.GetType("ServiceLocator").GetMethod("GetService", BindingFlags.Public | BindingFlags.Static);
    }

    protected static object GetService(string name)
    {
        return getServiceMI.Invoke(null, new object[] { name });
    }

    // And your original staff.
}

public class ActionSample : ActionBase
{
    IDependency1 d1 = GetService("name1");
    ...
    IDependencyN dN = GetService("nameN");

    public override ResultBase ExecuteAction()
    {
        d1.DoSomething();
        ...
        dN.DoSomething();
    }
}

Наконец, в EventTriggerActionService зарегистрируйте все зависимости перед созданием и экземпляром настраиваемого действия.

public class EventTriggerActionService
{
    static EventTriggerActionService()
    {
        ServiceLocator.RegisterService("name1", typeof(dependencyImpl1));
        ...
        ServiceLocator.RegisterService("nameN", typeof(dependencyImplN));
    }

    public void Execute(EventTriggerAction eventTriggerAction, EventContext context)
    {   
        var assemblyName = eventTriggerAction.EventAction.EventActionHandlerAssembly;
        var className = eventTriggerAction.EventAction.EventActionHandlerClass;

        Assembly actionHandlerAssembly = Assembly.Load(assemblyName);  
        var action = (ActionBase)Activator.CreateInstance(); // Instantiate the action by its base class, ActionBase
        action.Arguments = data.Arguments; // This is a dictionary that contains the arguments of the action

        action.Execute(); // Execute the Action
    }
}

Все коды существуют только для демонстрации идеи. Вы можете настроить ServiceLocator или другие вещи в соответствии с вашими целями.

0 голосов
/ 22 мая 2018

Я думаю, что ваша основная проблема связана с вашим базовым классом ActionBase.

Метод Action в этом классе - это, по сути, оболочка, которая выполняет некоторую общую логику перед вызовом фактического действия. Общеизвестно, что для работы требуются некоторые внешние службы (здесь IEmailer), зависимость которых вы не хотите навязывать производным классам.


Возможно, более простым подходом к использованию событий было бы сделать открытое свойство IEmailer и установить его при создании действия в методе EventTriggerActionService.Execute.

Это избавит вас от необходимости передавать ссылку через конструктор, но при этом иметь возможность доступа к IEmailer instance. Вы бы получили что-то вроде:

public abstract class ActionBase
{
    protected IEmailer Emailer { get; set; }

    protected Dictionary<string,string> Arguments { get; set; }

    public void Execute()
    {
        // Do something with Emailer.
        ExecuteAction(); // Execute the custom action
    }

    public abstract ResultBase ExecuteAction();
}

public class EventTriggerActionService
{
    // omit for readability 
    public void Execute(EventTriggerAction eventTriggerAction, EventContext context)
    {   
        // omit for readability

        var action = (ActionBase)Activator.CreateInstance(); // Instantiate the action by its base class, ActionBase

        action.Arguments = data.Arguments; // This is a dictionary that contains the arguments of the action
        action.Emailer = _emailerFactory.Create();

        // omit for readability
    }
    // omit for readability
}

Однако, поскольку у вас есть контроль над тем, как выполняются действия, зачем вводить общую логику в базовый класс?

Вы можете легко извлечь общую логику и отделить ее от действия.
Например:

public interface IActionBase
{
    ResultBase ExecuteAction();
}

public class SimpleEmailSender : IActionBase
{
    private Dictionary<string, string> arguments;
    private IEmailer Emailer;

    public SimpleEmailSender(Dictionary<string, string> arguments, IEmailer emailer)
    {
        this.arguments = arguments;
        this.emailer = emailer;
    }

    public override ResultBase ExecuteAction()
    {
        this.emailer.Send(Arguments["EmailBody"]);
        return null;
    }
}   

public class EventTriggerActionService
{
    // omit for readability 
    public void Execute(EventTriggerAction eventTriggerAction, EventContext context)
    {   
        // omit for readability

        this.RunPreLogic();
        action.ExecuteAction();
        this.RunPostLogic();

        // omit for readability
    }

    public void RunPreLogic() {}        
    public void RunPostLogic() {}

    // omit for readability
}

Как видно из примера, логика удалена из базового класса. Освобождение любого производного класса от необходимости предоставлять эти услуги. Осталось только внедрить необходимые сервисы в конструктор созданного класса.

Контейнеры DI, которые я использовал в прошлом, всегда обеспечивали какой-либо способ внедрения сервисов. Если вы не можете (или не хотите) использовать контейнер, не должно быть слишком сложно извлечь информацию конструктора и решить ее самостоятельно.

Если вы используете ядро ​​ASP.NET, по умолчанию для внедрения зависимостей Microsoft предоставляется служебный класс ActivatorUtilities для этого ( msdn )


Если вам не нужна ваша общая логика в EventTriggerActionService, вы всегда можете извлечь ее в отдельный сервис или выполнить специальное действие, которое обрабатывает общую логику.

...