【问题标题】:How does the SOLID open/closed principle fit in with Dependency Injection and dependency inversionSOLID 开/关原则如何与依赖注入和依赖倒置相适应
【发布时间】:2018-02-06 17:03:44
【问题描述】:

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

我对依赖倒置原则的理解是类应该依赖于抽象。在实践中,这意味着类应该从接口派生。到目前为止一切都很好。

接下来我对开闭原则的理解是,在某个截止点之后,你不应该改变一个类的内容,而应该扩展和覆盖。到目前为止,这对我来说很有意义。

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

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

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

然后在时间 T,method1 现在需要在其返回值上添加“ExtraInfo”。我不会更改当前的实现,而是创建一个扩展 Abstraction 的新类,并让它做我需要的事情,如下所示。

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

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

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

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

container.Register<IAbstraction, Abstraction>();

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

container.Register<IAbstraction, AbstractionV2>();

在这种情况下,所有实例现在都将调用新方法,这意味着我未能实现不更改原始方法。

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

我看不到任何解决方法 - 这让我想知道依赖注入和 SOLID 是否兼容?还是我在这里遗漏了什么?

【问题讨论】:

  • "意思是我没能做到不改变原来的方法。"我不确定是不是这样。原始方法作为未修改的Abstraction 类坐在那里。
  • 也就是说,如果您的问题只是想更改用于有限数量消费者的实现,您可以为您的AbstractionV2 类添加一个IAbstractionV2 : IAbstraction 接口来实现,然后注入@987654330 @通过那个。
  • 您是否曾经在全球范围内使用过IAbstraction 的一种实现?看起来没有任何东西直接依赖于Abstraction.method1,所以您可以简单地删除它?
  • 请注意,OCP 并不禁止更改现有代码,但其想法是防止彻底更改。所以一旦你看到一个变化导致许多类发生变化,那就表明违反了 OCP。

标签: c# solid-principles


【解决方案1】:

TL;DR

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

(我倾向于删除其余部分,因为它用更多的词说了同样的话。)


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

然后在时间 T,method1 现在需要在其返回值上添加“ExtraInfo”。

这听起来像是在扯皮,但一个方法永远不需要返回任何东西。方法不像有话要说、需要说的人。 “需要”取决于方法的调用者。调用者需要方法返回的内容。

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

如果调用者的需求发生了变化,是否意味着所有调用者的需求都发生了变化?如果您更改方法返回的内容以满足一个调用者的需求,其他调用者可能会受到不利影响。这就是为什么您可以创建新的东西来满足特定调用者的需要,同时保持现有方法或类不变。从这个意义上说,现有代码是“封闭的”,同时它的行为是开放的。

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

让我们回到你开始的课程。

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

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

public class SomethingDifferent : IAbstraction
{
     private readonly IAbstraction _inner;

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

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

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

public class SomethingDifferent
{
     private readonly IAbstraction _inner;

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

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

您还可以通过继承“扩展”原始类的行为:

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

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


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

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

例如,假设 AbstractionIAbstraction 在您的类库中,我碰巧需要以某种方式格式化数字的东西,而您的类看起来符合我的需要。我不只是将IAbstraction 注入我的课堂。我要写一个做我想做的接口:

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

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

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

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

如果我没有应用依赖倒置原则,而是直接依赖于Abstraction,那么我必须弄清楚如何改变你的班级来做什么 我需要。但是因为我依赖于我为满足我的需求而创建的抽象,所以我自动地在考虑如何合并你的类的行为,而不是改变它。一旦我这样做了,我显然不希望你班级的行为发生意外变化。

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

实际上,我们有时只是使用给我们的抽象,例如IDataReader。但希望那是稍后我们编写具体实现细节的时候。当涉及到应用程序的主要行为(如果您正在使用 DDD,“域”)时,最好定义我们的类将依赖的接口,然后使外部类适应它们。

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

【讨论】:

    【解决方案2】:

    解决您提到的确切问题:

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

    container.Register<IAbstraction, Abstraction>();
    

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

    container.Register<IAbstraction, AbstractionV2>();
    

    那么依赖于IAbstraction 的每个类都会得到AbstractionV2

    您不需要选择其中之一。大多数 DI 容器提供了可以为同一个接口注册多个实现的方法,然后指定哪些类获得哪些实现。在您的场景中,只有一个类需要 IAbstraction 的新实现,您可以将现有实现设为默认值,然后指定一个特定类获得不同的实现。

    我找不到使用 SimpleInjector 的简单方法。这是一个使用 Windsor 的示例:

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

    依赖于ISaysHello 的每个类都将获得SaysHelloInSpanishSaysSomething 除外。那一类得到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))
    

    【讨论】:

    • 如果可以的话,我想更新这个答案以添加一个如何使用 Simple Injector 执行此操作的示例。您可能会惊讶这实际上是多么容易(提示:使用RegisterConditional 完成)。
    • @Steven - 当然,去吧。我开始四处寻找它,但从未走得太远。
    【解决方案3】:

    模块一旦被其他模块引用,就无法修改。封闭的是公共 API,即接口。行为可以通过多态替换来改变(在新类中实现接口并注入它)。你的 IoC 容器可以注入这个新的实现。这种多态替换的能力是“开放扩展”部分。因此,DIP 和 Open/Closed 可以很好地协同工作。

    参见Wikipedia:“在 1990 年代,开放/封闭原则被重新定义为使用抽象接口……”

    【讨论】:

      猜你喜欢
      • 2022-11-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-09-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-07-08
      相关资源
      最近更新 更多