Используйте ExpandoObject, чтобы создать «поддельную» реализацию интерфейса - динамическое добавление методов - PullRequest
1 голос
/ 05 апреля 2019

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

Я разрабатываю модульную систему таким образом, чтобы модуль 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 ]

2 голосов
/ 05 апреля 2019

Даже если третий подход (с 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>();
    }
}
0 голосов
/ 05 апреля 2019

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

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);

    }
}
...