SOLID打开/关闭原理如何适应依赖注入和依赖性反转

时间:2018-02-06 17:03:45

标签: c# solid-principles

我开始应用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>();

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

OR

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

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

3 个答案:

答案 0 :(得分:3)

TL; DR

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

(我有一半倾向于删除其余部分,因为它用更多的单词说同样的内容。)

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

  

然后在时间T,method1现在需要添加&#34; ExtraInfo&#34;到它的返回值。

这可能听起来像是它的分裂头发,但是一种方法永远不会需要来返回任何东西。方法并不像那些有话要说并且需要说出来的人。 &#34;需要&#34;取决于方法的调用者。调用者需要方法返回的内容。

如果来电者正在通过int example并接收example.ToString(),但现在需要接收example.ToString() + " ExtraInfo",那么调用者需要更改,而不是需要被调用的方法。

如果呼叫者的需求发生了变化,那么是否所有呼叫者的需求都发生了变化?如果更改方法返回的内容以满足一个调用方的需要,则其他调用方可能会受到不利影响。这就是为什么你可以创建新的东西,满足一个特定调用者的需要,同时保持现有的方法或类不变。从这个意义上说,现有的代码已经关闭了#34;与此同时,它的行为可以延伸。

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

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

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

你也可以&#34;延伸&#34;通过继承原始类的行为:

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,&#34;域&#34;),最好定义我们的类将依赖的接口,然后调整外部类他们。

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

答案 1 :(得分:2)

一旦模块被其他模块引用,模块就会关闭以进行修改。关闭的是公共API,即界面。可以通过多态替换来改变行为(在新类中实现接口并注入它)。您的IoC容器可以注入此新实现。这种多态替代的能力是“开放式扩展”&#39;部分。所以,DIP和Open / Closed很好地协同工作。

参见Wikipedia:&#34;在20世纪90年代,开放/封闭原则被普遍重新定义,以指代抽象接口的使用......&#34;

答案 2 :(得分: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之外,依赖SaysHelloInSpanish的每个班级都会获得SaysSomething。那个班得到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))