Используйте ExpandoObject, чтобы создать "поддельную" реализацию интерфейса - динамическое добавление методов

Головоломка для вас!

Я разрабатываю модульную систему таким образом, чтобы модуль A мог нуждаться в модуле B, а модуль B мог также нуждаться в модуле A. Но если модуль B отключен, он просто не будет выполнять этот код и ничего не делать / возвращать ноль.

Немного больше в перспективе:

Скажем InvoiceBusinessLogic находится внутри модуля "Ядро". У нас также есть модуль "Ecommerce", который имеет OrderBusinessLogic, InvoiceBusinessLogic может выглядеть так:

public class InvoiceBusinessLogic : IInvoiceBusinessLogic
{
    private readonly IOrderBusinessLogic _orderBusinessLogic;

    public InvoiceBusinessLogic(IOrderBusinessLogic orderBusinessLogic)
    {
        _orderBusinessLogic = orderBusinessLogic;
    }

    public void UpdateInvoicePaymentStatus(InvoiceModel invoice)
    {
        _orderBusinessLogic.UpdateOrderStatus(invoice.OrderId);
    }
}

Итак, что я хочу, это: когда модуль "Электронная торговля" включен, он будет действительно что-то делать на OrderBusinessLogic, Когда нет, он просто ничего не сделает. В этом примере он ничего не возвращает, поэтому он может просто ничего не делать, в других случаях, когда что-то будет возвращено, он вернет ноль.

Заметки:

  • Как вы, вероятно, можете сказать, я использую Dependency Injection, это приложение ASP.NET Core, поэтому IServiceCollection заботится об определении реализаций.
  • Просто не определяя реализацию для IOrderBusinessLogic будет вызывать проблемы во время выполнения, по логике.
  • Из многих исследований я не хочу делать вызовы контейнера в моем домене / логике приложения. Не вызывайте DI-контейнер, он позвонит вам
  • Такого рода взаимодействия между модулями сведены к минимуму, желательно внутри контроллера, но иногда вы не можете обойти его (а также в контроллере, тогда мне понадобится способ ввести их и использовать их или нет).

Итак, есть 3 варианта, которые я выяснил до сих пор:

  1. Я никогда не звонил из модуля "Ядро" в модуль "Электронная коммерция", теоретически это звучит лучше, но на практике это сложнее для сложных сценариев. Не вариант
  2. Я мог бы создать много поддельных реализаций, в зависимости от конфигурации решить, какую из них реализовать. Но это, конечно, привело бы к двойному коду, и мне постоянно приходилось бы обновлять фальшивый класс при появлении нового метода. Так что не идеально.
  3. Я могу создать ложную реализацию, используя отражение и ExpandoObject и просто ничего не делать или возвращать ноль при вызове конкретного метода.

И последний вариант - то, что я сейчас делаю:

private static void SetupEcommerceLogic(IServiceCollection services, bool enabled)
{
    if (enabled)
    {
        services.AddTransient<IOrderBusinessLogic, OrderBusinessLogic>();
        return;
    }
    dynamic expendo = new ExpandoObject();
    IOrderBusinessLogic fakeBusinessLogic = Impromptu.ActLike(expendo);
    services.AddTransient<IOrderBusinessLogic>(x => fakeBusinessLogic);
}

Используя Impromptu Interface, я могу успешно создать фальшивую реализацию. Но теперь мне нужно решить, что динамический объект также содержит все методы (в основном свойства не нужны), но эти из них легко добавить. Так что в настоящее время я могу запустить код и подняться до того момента, когда он вызовет OrderBusinessLogic затем он, логически, сгенерирует исключение, что метод не существует.

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

dynamic expendo = new ExpandoObject();
var dictionary = (IDictionary<string, object>)expendo;
var methods = typeof(IOrderBusinessLogic).GetMethods(BindingFlags.Public);
foreach (MethodInfo method in methods)
{
    var parameters = method.GetParameters();
    //insert magic here
}

Примечание: сейчас напрямую звоню typeof(IOrderBusinessLogic) , но позже я бы перебрал все интерфейсы в определенной сборке.

Экспромт имеет следующий пример: expando.Meth1 = Return<bool>.Arguments<int>(it => it > 5);

Но, конечно, я хочу, чтобы это было динамически, так как мне динамически вставить тип возвращаемого значения и параметры.

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

  • Я посмотрел на этот вопрос, на самом деле я не планирую оставлять.dll вне, потому что, скорее всего, я не смогу иметь какую-либо форму IOrderBusinessLogic можно использовать внутри InvoiceBusinessLogic,
  • Я посмотрел на этот вопрос, но я не совсем понял, как TypeBuilder может быть использован в моем сценарии
  • Я также рассмотрел Mocking интерфейсы, но в большинстве случаев вам нужно будет определить "реализацию mocking" для каждого метода, который вы хотите изменить, исправьте меня, если я ошибаюсь.

2 ответа

Даже жесткий третий подход (с ExpandoObjectвыглядит как Святой Грааль, я призываю вас не идти по этому пути по следующим причинам:

  • Что гарантирует вам, что эта причудливая логика будет безошибочной сейчас и в будущем? (думаю: через 1 год вы добавляете недвижимость в IOrderBusinessLogic)
  • Каковы последствия, если нет? Возможно, неожиданное сообщение появится у пользователя или вызовет какое-то странное "априори не связанное" поведение

Я бы определенно отказался от второго варианта (поддельная реализация, также называемая Null-Object), даже если да, это потребует написания некоторого стандартного кода, но это даст вам гарантию во время компиляции, что ничего неожиданного не произойдет в rutime!

Так что мой совет будет сделать что-то вроде этого:

private static void SetupEcommerceLogic(IServiceCollection services, bool enabled)
{
    if (enabled)
    {
        services.AddTransient<IOrderBusinessLogic, OrderBusinessLogic>();
    }
    else
    {
        services.AddTransient<IOrderBusinessLogic, EmptyOrderBusinessLogic>();
    }
}

Поскольку нет другого ответа на искомое решение, я придумал следующее расширение:

using ImpromptuInterface.Build;
public static TInterface IsModuleEnabled<TInterface>(this TInterface obj) where TInterface : class
{
    if (obj is ActLikeProxy)
    {
        return default(TInterface);//returns null
    }
    return obj;
}

И затем используйте это как:

public void UpdateInvoicePaymentStatus(InvoiceModel invoice)
{
    _orderBusinessLogic.IsModuleEnabled()?.UpdateOrderStatus(invoice.OrderId);
   //just example stuff
   int? orderId = _orderBusinessLogic.IsModuleEnabled()?.GetOrderIdForInvoiceId(invoice.InvoiceId);
}

И на самом деле преимущество в том, что (в коде) ясно, что тип возвращаемого значения может быть нулевым или метод не будет вызываться при отключении модуля. Единственное, что должно быть тщательно задокументировано или выполнено иным образом, это должно быть ясно, какие классы не принадлежат текущему модулю. Единственное, о чем я мог думать сейчас, это не включать using автоматически, но используйте полное пространство имен или добавьте резюме к включенному _orderBusinessLogicпоэтому, когда кто-то его использует, становится ясно, что он принадлежит другому модулю, и следует выполнить проверку на нуль.

Для тех, кто заинтересован, вот код для правильного добавления всех поддельных реализаций:

private static void SetupEcommerceLogic(IServiceCollection services, bool enabled)
{
    if (enabled) 
    {
        services.AddTransient<IOrderBusinessLogic, OrderBusinessLogic>();
        return;
    }
    //just pick one interface in the correct assembly.
    var types = Assembly.GetAssembly(typeof(IOrderBusinessLogic)).GetExportedTypes();
    AddFakeImplementations(services, types);
}

using ImpromptuInterface;
private static void AddFakeImplementations(IServiceCollection services, Type[] types)
{
    //filtering on public interfaces and my folder structure / naming convention
    types = types.Where(x =>
        x.IsInterface && x.IsPublic &&
        (x.Namespace.Contains("BusinessLogic") || x.Namespace.Contains("Repositories"))).ToArray();
    foreach (Type type in types)
    {
        dynamic expendo = new ExpandoObject();
        var fakeImplementation = Impromptu.DynamicActLike(expendo, type);
        services.AddTransient(type, x => fakeImplementation);

    }
}
Другие вопросы по тегам