【问题标题】:Use ExpandoObject to create a 'fake' implementation of an interface - adding methods dynamically使用 ExpandoObject 创建接口的“假”实现 - 动态添加方法
【发布时间】:2019-04-05 08:27:35
【问题描述】:

给你的脑筋急转弯!

我正在开发一个模块化系统,这样模块 A 可能需要模块 B,模块 B 也可能需要模块 A。但是如果模块 B 被禁用,它将根本不执行该代码并且什么也不做/返回 null .

稍微深入一点:

假设InvoiceBusinessLogic 在模块“核心”中。我们还有一个“电子商务”模块,它有一个OrderBusinessLogicInvoiceBusinessLogic 可能看起来像这样:

public class InvoiceBusinessLogic : IInvoiceBusinessLogic
{
    private readonly IOrderBusinessLogic _orderBusinessLogic;

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

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

所以我想要的是:启用“电子商务”模块后,它实际上会在OrderBusinessLogic 处执行某些操作。如果没有,它根本不会做任何事情。在这个例子中,它什么也不返回,所以它可以简单地什么也不做,在其他将返回一些东西的例子中,它会返回 null。

注意事项:

  • 您可能知道,我使用的是依赖注入,它是一个 ASP.NET Core 应用程序,因此 IServiceCollection 负责定义实现。
  • 从逻辑上讲,不定义 IOrderBusinessLogic 的实现会导致运行时问题。
  • 根据已完成的大量研究,我不想在我的域/应用程序逻辑中调用容器。 Don't call the DI Container, it'll call you
  • 模块之间的这种交互保持在最低限度,最好在控制器内完成,但有时你无法绕过它(而且在控制器中,我需要一种方法来注入它们并使用它们)。

到目前为止,我想到了 3 个选项:

  1. 我从不从模块“Core”调用模块“Ecommerce”,理论上这听起来是最好的方法,但实际上对于高级场景它更复杂。不是一种选择
  2. 我可以创建很多假实现,具体取决于配置决定要实现哪一个。但这当然会导致双重代码,当引入新方法时,我必须不断更新假类。所以并不完美。
  3. 我可以使用反射和ExpandoObject 构建一个假实现,并且在调用特定方法时什么也不做或返回 null。

最后一个选项就是我现在所追求的:

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&lt;bool&gt;.Arguments&lt;int&gt;(it =&gt; it &gt; 5);

但我当然希望这是动态的,那么如何动态插入返回类型和参数。

我确实理解接口的行为类似于契约,并且应该遵循契约,我也理解这是一种反模式,但在达到这一点之前已经针对结果系统进行了广泛的研究和谈判我们想要,我们认为这是最好的选择,只是缺少一点:)。

  • 我查看了this question,我真的不打算将.dll 排除在外,因为我很可能无法在InvoiceBusinessLogic 中使用任何形式的IOrderBusinessLogic
  • 我看过this question,但我并不真正了解如何在我的场景中使用 TypeBuilder
  • 我还研究了模拟接口,但大多数情况下,您需要为要更改的每个方法定义“模拟实现”,如果我错了,请纠正我。

【问题讨论】:

  • 再想一想。我可以说这是选项 4:在业务逻辑层之上的层中管理模块存在,即如果电子商务模块未启用,则不要调用 UpdateInvoicePaymentStatus。它是适合您的解决方案吗?
  • 实际上,如果适用的话,这个选项甚至可能是比任何其他选项更好的选项(=不将此功能添加到现有的大型遗留代码库中)。尽可能高(在抽象层中)处理这种关注,这意味着至少在业务逻辑之上,对我来说很有意义。

标签: c# dependency-injection modularity impromptu-interface


【解决方案1】:

即使第三种方法(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>();
    }
}

【讨论】:

  • 感谢您的回答 Spotted,但我不同意,从现在起一年后,或稍后,使用为该特定客户端启用的模块的自动化测试将很快告诉您某些东西是否正常工作。并且不要忘记开发过程:如果我要在您的示例中添加一个属性,那么我可能还需要该属性,此时我可能会很快意识到我也需要“伪造”它。
  • @CularBytes 我明白你的意思,这是有道理的。但是,我仍然坚持认为使用 Null-Object 会更安全,因为反馈速度最快(您甚至无法编译)。沿着第三条路走下去会假设许多先决条件:如果代码被自动化测试正确覆盖,如果这些测试在您部署到生产之前运行,如果 通知您失败,如果您(或任何其他人)敢在添加一个小功能后在本地运行该软件以验证它是否确实有效(即使反馈仍然会更长比在编译时)等。
  • @CularBytes 有很多反对这个解决方案的论据,我不会冒险实施它如果我是决策者。你已经被警告过了。 :)
  • 我也明白你的意思 Spotted,这很有意义,但我只是不想创建很多空类,每次我必须添加一个方法时都会让我发疯.尽管它们是“空的”,但您仍然必须实现它,否则它将抛出 NotImplementedException,这意味着当 int 是返回类型时,您必须返回 0 或 -1。但是0好不好?还是-1不好?不必如此。出于兴趣,我仍在寻找所要求的解决方案,但现在我会回答我的答案(我可以返回 nullalbe int,可能想要改进扩展)。
  • 如果我切换回选项 1(以及您的答案),我会保证您会向您报告,我很好奇 1 年后它会是什么样子。反正转回来也没那么难。想一想:如果我切换回来,我仍然会寻找一种通用的方法来检查使用了什么实现,也许我的扩展毕竟不是那么糟糕......
【解决方案2】:

只要我正在寻找的解决方案没有其他答案,我就想出了以下扩展:

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

实际上它的优点是(在代码中)很清楚返回类型可以为 null 或者在禁用模块时不会调用该方法。唯一应该仔细记录或以其他方式强制执行的事情,就是必须清楚哪些类不属于当前模块。我现在唯一能想到的就是不自动包含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);

    }
}

【讨论】:

  • (仍在试图说服你:-))。所以现在你(或任何其他开发人员)必须记住在使用某些类型之前调用IsModuleEnabled() + 强制进行后续空检查(使用int? orderId 时)。太糟糕了,您的业务逻辑(软件中最重要的部分)会因此而变得混乱。
  • 我很佩服你的决心:)。业务逻辑或其上方的层(如您在问题下方的评论中所述)在决定我是否会调用某个方法时变得“混乱”。正如我在 [1.] 中提到的:在更复杂的逻辑中,根据需要调用的(另一个模块的)某些东西的返回类型来决定并不总是那么容易。您可以,但是做出这些决定的logic 超出了您可能想要重用的业务逻辑(当然)。我认为这里和那里的空检查不会受到伤害,无论如何都很有可能完成。事实上,我将不得不
  • ...记得打电话给IsModuleEnabled(),但就像我已经说过的,无论如何我都必须在某个地方进行检查。而且,这将保持在最低限度,我会尽可能长时间地将“工作单元”保持在一起。
  • 我又想到了一件事情(希望您不要生气,但我希望以正确和友善的方式挑战您的设计)。使用您的方法,我希望 OrderBusinessLogic 易于实例化,原因如下:单元测试(我确定您有一堆用于您的业务逻辑,是吗?)。
  • 为了对UpdateInvoicePaymentStatus 进行单元测试,您不能通过“即兴”实现,因为它会弄乱您的业务逻辑(它不会与 Null 对象实现一起使用)。因此,如果实例化OrderBusinessLogic 很容易,那很好。如果不是,您如何考虑维护您的测试?
猜你喜欢
  • 2017-04-26
  • 2021-11-10
  • 2020-04-30
  • 2021-01-11
  • 2017-08-21
  • 2017-05-15
  • 2017-08-05
  • 2020-09-24
  • 2011-06-23
相关资源
最近更新 更多