SOLID open/closed 原则如何适应依赖注入和依赖倒置

How does the SOLID open/closed principle fit in with Dependency Injection and dependency inversion

我开始应用 SOLID 原则,但发现它们有些矛盾。我的问题如下:

我对依赖倒置原则的理解是classes应该依赖抽象。实际上,这意味着 classes 应该从接口派生。目前一切正常。

接下来我对open/closed原则的理解是,在某个cutoff点之后,不应该改变class的内容,而是应该extend和override。到目前为止,这对我来说很有意义。

鉴于以上情况,我最终会得到这样的结果:

public interface IAbstraction
{
    string method1(int example);
}

public Class Abstraction : IAbstraction
{
   public virtual string method1(int example)
   {
       return example.toString();
   }
}

然后在时间 T,method1 现在需要将“ExtraInfo”添加到其返回值中。我不会改变当前的实现,而是创建一个新的 class 来扩展 Abstraction 并使其执行我需要的操作,如下所示。

public Class AbstractionV2 : Abstraction 
{
   public override string method1(int example)
   {
       return example.toString() + " ExtraInfo";
   }
}

而且我可以看出这样做的原因是只有我要调用这个更新方法的代码会调用它,其余代码会调用旧方法。

对我来说一切都有意义 - 我认为我的理解是正确的??

但是,我也使用依赖注入(简单注入器),所以我的实现从来没有通过具体的class,而是通过我的DI配置,如下:

container.Register<IAbstraction, Abstraction>();

这里的问题是,在此设置下,我可以将我的 DI 配置更新为:

container.Register<IAbstraction, AbstractionV2>();

这样的话所有的实例都会调用新的方法,也就是说我没能做到不改变原来的方法

我创建了一个新接口 IAbstractionV2 并在那里实现了更新的功能 - 这意味着接口声明的重复。

我看不出有任何解决办法——这让我想知道依赖注入和 SOLID 是否兼容?或者我在这里遗漏了什么?

模块一旦被其他模块引用,就禁止修改。关闭的是 public API 界面。可以通过多态替换更改行为(在新 class 中实现接口并注入它)。您的 IoC 容器可以注入这个新的实现。这种多态替换的能力是 'Open to extension' 部分。所以,DIP 和 Open/Closed 可以很好地协同工作。

参见Wikipedia:"During the 1990s, the open/closed principle became popularly redefined to refer to the use of abstracted interfaces..."

TL;DR

  • 当我们说代码是 "available for extension" 时,这并不意味着我们从它继承或向现有接口添加新方法。继承只是 "extend" 行为的一种方式。
  • 当我们应用依赖倒置原则时,我们不直接依赖于其他具体的 classes,因此如果我们需要它们做一些不同的事情,我们不需要更改这些实现。依赖于抽象的 classes 是可扩展的,因为替换抽象的实现可以从现有的 classes 中获得新的行为,而无需修改它们。

(我有点倾向于删除其余部分,因为它用更多的词表达了同样的事情。)


检查这句话可能有助于阐明问题:

and then at time T, method1 now needs to add " ExtraInfo" onto its returned value.

这听起来像是在吹毛求疵,但一种方法从来不需要到return任何东西。方法不像人有话要说,需要说。 "need" 取决于方法的调用者。调用者需要什么方法returns。

如果调用者传递int example并接收example.ToString(),但现在它需要接收example.ToString() + " ExtraInfo",那么改变的是调用者的需要,而不是需要被调用的方法。

如果来电者的需求发生了变化,是否意味着所有来电者的需求都发生了变化?如果您更改方法 returns 以满足一个调用者的需要,其他调用者可能会受到不利影响。这就是为什么您可以创建一些新的东西来满足某个特定调用者的需要,同时保持现有方法或 class 不变。从这个意义上说,现有代码是 "closed",同时它的行为可以扩展。

此外,扩展现有代码并不一定意味着修改 class、向接口添加方法或继承。这只是意味着它合并了现有代码,同时提供了一些额外的东西。

让我们回到您开始的 class。

public Class Abstraction : IAbstraction
{
     public virtual string method1(int example)
     {
         return example.toString();
     }
}

现在您需要一个 class,它包含此 class 的功能,但做一些不同的事情。它可能看起来像这样。 (在这个例子中它看起来有点矫枉过正,但在现实世界的例子中它不会。)

public class SomethingDifferent : IAbstraction
{
     private readonly IAbstraction _inner;

     public SomethingDifferent(IAbstraction inner)
     {
         _inner = inner;
     }

     public string method1(int example)
     {
         return _inner.method1 + " ExtraInfo";
     }
}

在这种情况下,新的 class 恰好实现了相同的接口,所以现在您有相同接口的两个实现。但它不需要。可能是这样的:

public class SomethingDifferent
{
     private readonly IAbstraction _inner;

     public SomethingDifferent(IAbstraction inner)
     {
         _inner = inner;
     }

     public string DoMyOwnThing(int example)
     {
         return _inner.method1 + " ExtraInfo";
     }
}

您还可以通过继承 "extend" 原始 class 的行为:

public Class AbstractionTwo : Abstraction
{
     public overrride string method1(int example)
     {
         return base.method1(example) + " ExtraInfo";
     }
}

所有这些示例都在不修改现有代码的情况下扩展了它。在实践中,有时将现有的属性和方法添加到新的 classes 中可能是有益的,但即便如此,我们仍希望避免修改已经完成其工作的部分。如果我们正在编写具有单一职责的简单 classes,那么我们就不太可能发现自己将厨房水槽扔进了现有的 class.


这与依赖倒置原则或依赖抽象有什么关系?没什么直接的,但是应用依赖倒置原则可以帮助我们应用Open/Closed原则。

在可行的情况下,我们的 classes 所依赖的抽象应该设计为供那些 classes 使用。我们不只是采用其他人创建的任何界面并将其粘贴到我们的中央 classes 中。我们正在设计满足我们需求的界面,然后调整其他 class 来满足这些需求。

例如,假设 AbstractionIAbstraction 在您的 class 库中,我恰好需要以某种方式格式化数字的东西,而您的 class 看起来喜欢它做我需要的。我不只是将 IAbstraction 注入我的 class。我要写一个界面来做我想做的事:

public interface IFormatsNumbersTheWayIWant
{
    string FormatNumber(int number);
}

然后我将编写一个使用您的 class 的接口的实现,例如:

public class YourAbstractionNumberFormatter : IFormatsNumbersTheWayIWant
{
    public string FormatNumber(int number)
    {
        return new Abstraction().method1 + " my string";
    }
}

(或者它可能取决于 IAbstraction 使用构造函数注入,无论如何。)

如果我没有应用依赖倒置原则,而是直接依赖于 Abstraction,那么我将不得不想办法改变你的 class 来做什么 我需要。但是因为我依赖于为满足我的需要而创建的抽象,所以我会自动考虑如何合并 class 的行为,而不是更改它。一旦我这样做了,我显然不希望你的 class 的行为意外改变。

我也可以依赖您的接口 - IAbstraction - 并创建我自己的实现。但是创建我自己的也有助于我遵守接口隔离原则。我依赖的接口是为我创建的,所以它不会有任何我不需要的东西。你的可能还有其他我不需要的东西,或者你可以稍后添加更多。

实际上,我们有时只是要使用提供给我们的抽象概念,例如 IDataReader。但希望那是稍后我们编写具体实施细节时。当谈到应用程序的主要行为时(如果你正在做 DDD,"domain"),最好定义我们的 classes 将依赖的接口,然后在 class 之外进行调整es 给他们。

最后,依赖于抽象的 classes 也更具可扩展性,因为我们可以替换它们的依赖关系——实际上改变(扩展)它们的行为而不需要对 classes 本身做任何改变。我们可以扩展它们而不是修改它们。

正在解决您提到的确切问题:

您有 class 依赖于 IAbstraction 并且您已经在容器中注册了一个实现:

container.Register<IAbstraction, Abstraction>();

但是你担心如果改成这样:

container.Register<IAbstraction, AbstractionV2>();

然后每个 class 依赖于 IAbstraction 将得到 AbstractionV2.

您不需要二选一。大多数 DI 容器提供的方法可以让您为同一接口注册多个实现,然后指定哪些 classes 获得哪些实现。在只有一个 class 需要 IAbstraction 的新实现的情况下,您可以将现有实现设为默认实现,然后只需指定一个特定的 class 获得不同的实现。

我找不到使用 SimpleInjector 执行此操作的简单方法。这是一个使用温莎的例子:

var container = new WindsorContainer();
container.Register(
    Component.For<ISaysHello, SaysHelloInSpanish>().IsDefault(),
    Component.For<ISaysHello, SaysHelloInEnglish>().Named("English"),
    Component.For<ISaysSomething, SaysSomething>()
        .DependsOn(Dependency.OnComponent(typeof(ISaysHello),"English")));

除了SaysSomething之外,每个依赖于ISaysHello的class都会得到SaysHelloInSpanish。那一个 class 得到 SaysHelloInEnglish

更新:

Simple Injector 等效项如下:

var container = new Container();

container.Register<ISaysSomething, SaysSomething>();

container.RegisterConditional<ISayHello, SaysHelloInEnglish>(
    c => c.Consumer.ImplementationType == typeof(SaysSomething));

container.RegisterConditional<ISayHello, SaysHelloInSpanish>(
    c => c.Consumer.ImplementationType != typeof(SaysSomething))