【问题标题】:Dependency Inversion: How to best manage versioning of your Abstractions依赖倒置:如何最好地管理抽象的版本控制
【发布时间】:2018-01-09 22:15:08
【问题描述】:

在高代码重用环境中应用依赖反转时如何在 .Net 中对抽象进行版本化

我有兴趣转向在 .Net 中使用依赖倒置,但遇到了一些令我困惑的事情。 我不认为它与 DIP 的特定方法或提供者有关,而是更多可能其他人已经解决的基本问题。我要解决的问题最好按照下面的场景逐步说明。

假设/限制

预先提出的一个相当大的假设或限制是,我的开发团队坚持将我们部署的程序集保持在一个且只有一个程序集版本的规则,特别是版本“1.0.0.0”。 到目前为止,为了简单起见,我们不支持将我们开发的任何给定程序集的多个程序集版本部署在服务器上。这可能是限制性的,并且可能有很多很好的理由来摆脱它,但无论如何,这是我们目前使用的规则。因此,考虑到这种做法,请继续下面的操作。

场景

  • 您有一个 IDoStuff 接口包含在一个抽象程序集中 Stuff.Abstractions.dll 有 2 种方法。
  • 您编译组件 A.dll 一个类显式地实现了 IDoStuff 和 2 种方法。
  • 您将 A.dll 移至生产使用,程序集版本 1.0.0.0,程序集文件 版本 1.0.0.0。
  • 您将 Interface.dll 移至 prod,程序集版本 1.0.0.0,程序集文件版本 1.0.0.0。
  • 一切正常。时光荏苒。
  • 您将另一个方法(例如“DoMoreStuff”)添加到 IDoStuff 接口,以便不同的组件 B 可以调用它。 (牢记接口隔离 OO 原则,假设 DoMoreStuff 方法在这个相对较小的 IDoStuff 接口中是有意义的。)
  • 您现在在 Stuff.Abstractions.dll 中拥有带有 3 个方法的 IDoStuff,并且您已经构建了组件 B 以使用新的第 3 个方法。
  • 您将 Stuff.Abstractions.dll 移至生产使用(升级它),程序集版本 1.0.0.0,程序集文件版本 1.0.0.1。 (请注意,文件版本会增加,但程序集版本和强名称保持不变)
  • 您将 B.dll 移至生产使用,程序集版本 1.0.0.0,程序集文件版本 1.0.0.17。
  • 您没有对 A.dll 做任何事情。您认为目前不需要进行任何更改。

现在您调用的代码会尝试在之前运行的同一生产服务器上执行 A.dll。在运行时,依赖反转框架将 IDoStuff 接口解析为 A.dll 中的一个类并尝试创建它。 问题是 A.dll 中的类实现了现在 extinct 2-method IDoStuff 接口。正如人们所预料的那样,你会得到一个像这样的异常:

程序集“程序集 A.dll 的强名称”中类型“A.dll 中的 IDoStuff 类”中的方法“DoMoreStuff”没有实现。

当我必须向现有接口添加方法时,我可以想到两种方法来处理这种情况:

1) 更新每个使用 Stuff.Abstractions.dll 的功能提供程序集,以实现新的“DoMoreStuff”方法。 这似乎是用艰难的方式做事,但以蛮力的方式会很痛苦。

2) 弯曲上述假设/限制并开始允许存在多个程序集版本(至少对于抽象定义程序集)。 这会有点不同,并且会在我们的服务器上产生更多的程序集,但它应该允许以下最终状态:

A.dll 依赖于 stuff.abstractions.dll,Assembly 版本 1.0.0.0,Assembly File 版本 1.0.0.22(AFV 与识别构建无关) B.dll 依赖于 stuff.abstractions.dll,Assembly Version 1.0.0.1,Assembly File Version 1.0.0.23(AFV 与识别构建无关) 两者都能愉快地在同一台服务器上执行。

如果两个版本的 stuff.abstractions.dll 都安装在服务器上,那么一切都应该很好。 A.dll 也不需要更改。每当它接下来需要修改时,您可以选择实现存根并升级接口,或者什么也不做。如果它只需要它们,也许最好将其保留为它首先可以访问的 2 种方法。 作为附带的好处,我们知道任何引用 stuff.abstractions.dll,版本 1.0.0.0 的东西只能访问 2 个接口方法,而 1.0.0.1 的用户可以访问 3 个方法。

是否有更好的方法或可接受的部署模式来控制版本抽象?

如果您尝试在 .Net 中实现依赖倒置方案,是否有更好的方法来处理版本控制抽象? 如果你有一个单一的应用程序,它看起来很简单,因为它都包含在内——只需更新界面用户和实现者。 我试图解决的特定场景是一个高代码重用环境,其中有很多组件依赖于很多组件。依赖倒置确实有助于分解事情并使单元测试感觉不像系统测试(由于紧密耦合的层)。

【问题讨论】:

  • 在大多数情况下,您只需将抽象(接口)放在一个项目中,然后在另一个项目中实现这些接口(可能还有更多项目用于不同的实现)。您将使用 IOC 容器来管理绑定。部署代码时,它将一次性部署,因此组件版本相同,根据环境,您将拥有不同的绑定配置文件。因此,如果您在 prod 中,请将 ClassA 绑定到 IInterfaceA,但如果您在 dev 中,请将 MockA 绑定到 IInterfaceA 等等。
  • 虽然不是 100% 准确,但出于您的目的,DI 与程序集版本控制完全无关。将 DI 视为跨不同层(在您的情况下为程序集)的桥接容器。只要解决方案能够编译,无论版本如何,它都可以工作。
  • 你真正的问题是,你永远不应该,永远不要简单地替换 prod 中的 dll 而不通过 CI/构建管道。无论您要部署什么应用程序,都应该作为一个整体构建。这将解决您的问题,因为更新依赖项B 的家伙也必须更新依赖项A

标签: c# .net .net-assembly abstraction dependency-inversion


【解决方案1】:

问题的一部分可能是您直接依赖于设计时考虑到更广泛目的的界面。你可以通过让你的类依赖于为它们创建的抽象来缓解这个问题。

如果您根据需要定义接口来表示类的依赖关系,而不是依赖于外部接口,那么您永远不必担心实现不需要的接口成员。

假设我正在编写一个涉及订单发货的课程,并且我意识到我需要验证地址。我可能有一个执行此类验证的库或服务。但我不一定只想将该接口直接注入到我的类中,因为现在我的类具有向外的依赖项。如果该接口增长,我可能会违反接口隔离原则,因为我依赖于我不使用的接口。

相反,我可能会停下来写一个界面:

public interface IAddressValidator
{
    ValidationResult ValidateAddress(Address address);
}

我将该接口注入到我的类中并继续编写我的类,将编写实现推迟到以后。

然后是实现该类的时候了,那时我可以引入我的其他服务,该服务的设计意图更广泛,而不仅仅是为这个类提供服务,并使其适应我的界面。

public class MyOtherServiceAddressValidator : IAddressValidator
{
    private readonly IOtherServiceInterface _otherService;

    public MyOtherServiceAddressValidator(IOtherServiceInterface otherService)
    {
         _otherService = otherService;
    }

    public ValidationResult ValidateAddress(Address address)
    {
        // adapt my address to whatever input the other service
        // requires, and adapt the response to whatever I want
        // to return.
    }
}

IAddressValidator 存在是因为我将它定义为我的类需要做的事情,所以我不必担心必须实现我不需要的接口成员。永远不会有。

【讨论】:

  • 我认为您对该问题的评估可能是正确的。将抽象作为一个单独的实体(作为共享的公共程序集)与将使用它们的程序集一起容纳​​的方法是有问题的。当一个方法被添加到这个方案下的接口时,迟早会提示所有其他人至少将其存根,即使他们不使用新方法。这违反了接口隔离原则。
  • 那么,我正在考虑一种不同的方法来处理程序集中用于 DIP 的抽象。也许给定程序集的依赖关系的抽象应该由该程序集拥有,而不是由其他程序集共享。如果多个程序集在给定程序集中包含的类上使用各种方法,则它们应该拥有并维护自己的抽象/接口。正如你所说,他们不会有外部理由改变。
  • 这样,本地适配器类将包装外部依赖,在适配器类中发生依赖反转。
【解决方案2】:

始终可以选择对接口进行版本控制;例如,如果有

public interface IDoStuff
{
    void GoFirst();

    void GoSecond();
}

可能会有

public interface IDoStuffV2 : IDoStuff
{
    void GoThird();
}

然后 ComponentA 可以引用 IDoStuff 并且 ComponentB 可以针对 IDoStuffV2 编写。有些人不赞成接口继承,但我没有看到任何其他方法可以轻松地对接口进行版本控制。

【讨论】:

    猜你喜欢
    • 2022-11-20
    • 1970-01-01
    • 1970-01-01
    • 2019-12-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-02-27
    相关资源
    最近更新 更多