依赖倒置原则 (SOLID) 与封装(OOP 的支柱)

Dependency Inversion Principle (SOLID) vs Encapsulation (Pillars of OOP)

我最近在争论依赖倒置原则控制倒置依赖注入。关于这个话题,我们正在讨论这些原则是否违反了 OOP 的支柱之一,即 封装

我对这些事情的理解是:

辩论到了一个关键点,下面的陈述:

IoC isn't OOP because it breaks Encapsulation

就我个人而言,我认为所有 OOP 开发人员都应该虔诚地遵守依赖倒置原则和控制倒置模式 - 我遵循以下引述:

If there is (potentially) more than one way to skin a cat, then do not behave like there is only one.

示例 1:

class Program {
    void Main() {
        SkinCatWithKnife skinner = new SkinCatWithKnife ();
        skinner.SkinTheCat();
    }
}

这里我们看一个封装的例子。程序员只需要调用 Main() 猫就会被剥皮,但是如果他想给猫剥皮,比如用一副锋利的牙齿怎么办?

示例 2:

class Program {
    // Encapsulation
    ICatSkinner skinner;

    public Program(ICatSkinner skinner) {
        // Inversion of control
        this.skinner = skinner;
    }

    void Main() {
        this.skinner.SkinTheCat();
    }
}

... new Program(new SkinCatWithTeeth());
    // Dependency Injection

这里我们观察依赖倒置原则和控制倒置,因为提供了抽象 (ICatSkinner) 以允许程序员传入具体的依赖项。 终于,给猫剥皮的方法不止一种了!

这里的吵架是;这会破坏封装吗?从技术上讲,人们可能会争辩说 .SkinTheCat(); 仍然封装在 Main() 方法调用中,因此程序员不知道该方法的行为,所以我认为这不会破坏封装。

再深入一点,我认为 IoC containers 打破了 OOP,因为它们使用了反射,但我不相信 IoC 打破了 OOP,也不相信 IoC 打破了封装.事实上,我什至会说:

Encapsulation and Inversion of Control coincide with each other happily, allowing programmers to pass in only the concretions of a dependency, whilst hiding away the overall implementation via encapsulation.

问题:

Is IoC a direct implementation of the Dependency Inversion Principle?

这两者在某种程度上是相关的,他们谈论抽象,但仅此而已。控制反转是:

a design in which custom-written portions of a computer program receive the flow of control from a generic, reusable library (source)

控制反转允许我们将自定义代码挂钩到可重用库的管道中。换句话说,反转控制是关于框架的。不应用控制反转的可重用库仅仅是一个库。框架是一个可重复使用的库, 应用控制反转。

请注意,如果我们自己编写框架,我们作为开发人员只能应用控制反转;作为应用程序开发人员,您不能应用控制反转。我们可以(也应该)应用依赖倒置原则和依赖注入模式。

Does IoC always break encapsulation, and therefore OOP?

由于 IoC 只是挂钩到框架的管道中,因此这里没有任何泄漏。所以真正的问题是:依赖注入是否会破坏封装。

这个问题的答案是:不,不是。由于两个原因,它不会破坏封装:

  • 由于依赖倒置原则规定我们应该针对抽象进行编程,因此消费者将无法访问所用实现的内部结构,因此该实现不会破坏对客户端的封装。该实现甚至可能在编译时未知或不可访问(因为它存在于未引用的程序集中),并且在这种情况下该实现不会泄漏实现细节和破坏封装。
  • 尽管实现在其构造函数中接受它所需的依赖项,但这些依赖项通常存储在私有字段中,任何人都无法访问(即使消费者直接依赖于具体类型),因此它将不破坏封装。

Should IoC be used sparingly, religiously or appropriately?

同样,问题是"Should DIP and DI be used sparingly"。在我看来,答案是:不,您实际上应该在整个应用程序中使用它。显然,你永远不应该虔诚地应用事物。您应该应用 SOLID 原则,而 DIP 是这些原则的重要组成部分。它们将使您的应用程序更加灵活和更易于维护,并且在大多数情况下应用 SOLID 原则是非常合适的。

What is the difference between IoC and an IoC container?

依赖注入是一种可以在有或没有 IoC 容器的情况下应用的模式。如果您的应用程序正确应用了 SOLID 原则,IoC 容器只是一种工具,可以帮助您以更方便的方式构建对象图。如果您的应用程序不应用 SOLID 原则,您将很难使用 IoC 容器。您将很难应用依赖注入。或者让我更广泛地说,您将很难维护您的应用程序。但是 没办法 IoC 容器是必需的工具。我正在为 .NET 开发和维护 IoC 容器,但我并不总是为 所有 我的应用程序使用容器。对于大型 BLOBA(无聊的业务应用程序),我经常使用容器,但对于较小的应用程序(或 windows 服务),我并不总是使用容器。但是我几乎总是使用依赖注入作为模式,因为这是坚持DIP的最有效方法。

注意:由于 IoC 容器帮助我们应用依赖注入模式,因此 "IoC container" 是此类库的糟糕名称。

但是不管我上面说了什么,请注意:

in the real world of the software developer, usefulness trumps theory [from Robert C. Martin's Agile Principle, Patterns and Practices]

换句话说,即使 DI 会破坏封装,也没关系,因为这些技术和模式已被证明是非常有价值的,因为它产生了非常灵活和可维护的系统。实践胜过理论。

Does IoC always break encapsulation, and therefore OOP?

不,这些是层级相关的问题。封装是 OOP 中最容易被误解的概念之一,但我认为这种关系最好通过抽象数据类型 (ADT) 来描述。本质上,ADT 是对数据和相关行为的一般描述。这种描述是抽象的;它省略了实现细节。相反,它根据 pre-post-conditions.

描述 ADT

这就是 Bertrand Meyer 所说的按合同设计。您可以在 Object-Oriented Software Construction.

中阅读更多关于 OOD 的开创性描述

对象通常被描述为具有行为的数据。这意味着 没有 数据的对象并不是真正的对象。因此,您必须以某种方式将数据获取到对象中。

例如,您可以通过对象的构造函数将数据传递到对象中:

public class Foo
{
    private readonly int bar;

    public Foo(int bar)
    {
        this.bar = bar;
    }

    // Other members may use this.bar in various ways.
}

另一种选择是使用 setter 函数或 属性。我希望我们能达成共识,到目前为止,没有违反封装。

如果我们将 bar 从一个整数更改为另一个具体的 class 会怎样?

public class Foo
{
    private readonly Bar bar;

    public Foo(Bar bar)
    {
        this.bar = bar;
    }

    // Other members may use this.bar in various ways.
}

与之前相比唯一的区别是 bar 现在是一个对象,而不是原始类型。然而,这是错误的区分,因为在面向对象的设计中,一个整数也是一个对象。只是由于各种编程语言(Java、C# 等)的性能优化,原语(字符串、整数、布尔值等)和 'real' 对象之间存在实际差异。从 OOD 的角度来看,它们都是相似的。字符串也有行为:你可以把它们变成全大写,反转它们等等。

如果 Bar 是一个 sealed/final,只有非虚拟成员的具体 class,是否违反了封装?

bar只是有行为的数据,就像一个整数,但除此之外,没有区别。到目前为止,没有违反封装。

如果我们允许 Bar 拥有一个虚拟成员会怎样?

封装是否被破坏了?

考虑到 Bar 有一个虚拟成员,我们还能表达关于 Foo 的前置条件和 post 条件吗?

如果 Bar 遵守 Liskov Substitution Principle (LSP),它不会有什么不同。 LSP 明确声明改变行为不得改变系统的正确性。只要那个契约被履行,封装仍然完好。

因此,LSP(SOLID principles, of which the Dependency Inversion Principle 中的一个是另一个)不违反封装;它描述了在存在多态性维护封装的原则。

如果Bar是一个抽象基class,结论会改变吗?接口?

不,它不是:那些只是不同程度的多态性。因此我们可以将 Bar 重命名为 IBar(以表明它是一个接口)并将其作为其数据传递给 Foo

public class Foo
{
    private readonly IBar bar;

    public Foo(IBar bar)
    {
        this.bar = bar;
    }

    // Other members may use this.bar in various ways.
}

bar只是另一个多态对象,只要LSP成立,封装就成立。

TL; DR

SOLID 也被称为 OOD 原则 是有原因的。封装(即按合同设计)定义了基本规则。 SOLID 描述了遵循这些规则的准则。

问题总结:

我们有能力让服务实例化它自己的依赖项。

然而,我们也有能力让服务简单地定义抽象,并要求应用程序了解依赖的抽象、创建具体实现并将它们传入。

问题不在于,"Why we do it?"(因为我们知道有很多原因)。但问题是,"Doesn't option 2 break encapsulation?"

我的"pragmatic"回答

我认为 Mark 是任何此类答案的最佳选择,正如他所说:不,封装不是人们认为的那样。

封装隐藏了服务或抽象的实现细节。依赖关系不是实现细节。如果您将服务视为合同,将其后续的子服务依赖项视为子合同(依此类推),那么您实际上只是得到了一份带有附录的巨大合同。

假设我是来电者,我想使用法律服务来起诉我的老板。我的应用程序必须知道 这样做的服务。仅这一点就打破了知道实现我的目标所需的 services/contracts 是错误的理论。

那里的争论是...是的,但我只想聘请律师,我不关心他使用什么书籍或服务。我会从 interwebz 上随机获取一些东西,而不关心他的实现细节......像这样:

sub main() {
    LegalService legalService = new LegalService();

    legalService.SueMyManagerForBeingMean();
}

public class LegalService {
    public void SueMyManagerForBeingMean(){
        // Implementation Details.
    }
}

但事实证明,完成工作还需要其他服务,例如了解工作场所法律。而且事实证明......我对律师以我的名义签署的合同以及他为窃取我的钱所做的其他事情非常感兴趣。例如... 为什么这个互联网律师在韩国?这对我有什么帮助!?!?这不是实现细节,而是我乐于管理的需求依赖链的一部分。

sub main() {
    IWorkLawService understandWorkplaceLaw = new CaliforniaWorkplaceLawService();
    //IWorkLawService understandWorkplaceLaw = new NewYorkWorkplaceLawService();
    LegalService legalService = new LegalService(understandWorkplaceLaw);

    legalService.SueMyManagerForBeingMean();
}

public interface ILegalContract {
    void SueMyManagerForBeingMean();
}

public class LegalService : ILegalContract {
    private readonly IWorkLawService _workLawService;

    public LegalService(IWorkLawService workLawService) {
        this._workLawService = workLawService;
    }

    public void SueMyManagerForBeingMean() {
        //Implementation Detail
        _workLawService.DoSomething; // { implementation detail in there too }
    }
}

现在,我所知道的是我有一份合同,其中有其他合同,可能还有其他合同。我对这些合同负有很好的责任,而不是它们的实施细节。尽管我非常乐意签署那些与我的要求相关的具体合同。再说一次,我不关心这些实体是如何工作的,只要我知道我有一份有约束力的合同,上面写着我们以某种明确的方式交换信息。

我会尽量回答你的问题,据我了解:

  • IoC是依赖倒置原则的直接实现吗?

    我们不能将 IoC 标记为 DIP 的直接实现,因为 DIP 侧重于根据抽象而不是低级模块的具体化来制作更高级别的模块。相反,IoC 是依赖注入的一种实现。

  • IoC 是否总是破坏封装,从而破坏 OOP?

    我不认为IoC的机制会违反封装。但是会使系统变得紧耦合。

  • IoC 应该谨慎使用、虔诚使用还是适当使用?

    IoC 可以在许多模式中使用,例如 Bridge Pattern,其中将 Concretion 与 Abstraction 分开可以改进代码。因此可以用来实现DIP。

  • IoC 和 IoC 容器有什么区别?

    IoC 是一种依赖倒置机制,但容器是那些使用 IoC 的机制。

封装与面向对象编程世界中的依赖倒置原则并不矛盾。例如在汽车设计中,您将有一个 'internal engine' 可以从外界封装,还有 'wheels' 可以很容易地更换,并被视为汽车的外部组件。汽车有规范(接口)来旋转车轮的轴,车轮组件实现与轴交互的部分。

这里,内部引擎代表封装过程,而车轮组件代表汽车设计中的依赖倒置原则(DIP)。使用 DIP,基本上我们可以避免构建一个整体对象,而是让我们的对象可组合。你能想象你制造了一辆汽车,你无法更换车轮,因为它们是内置在汽车中的。

您还可以在我的博客Here.

中更详细地阅读有关依赖倒置原则的更多信息

我只回答一个问题,因为很多人已经回答了其他所有问题。请记住,没有正确或错误的答案,只有用户偏好。

IoC 应该谨慎使用、虔诚使用还是适当使用? 我的经验让我相信依赖注入应该只用于 class 一般的并且将来可能需要改变的东西。虔诚地使用它会导致一些 classes 在构造函数中需要 15 个接口,这会非常耗时。这往往会导致 20% 的开发和 80% 的内务管理。

有人举了一个汽车的例子,汽车的制造商会如何更换轮胎。依赖注入允许人们在不关心具体实现细节的情况下更换轮胎。但是如果我们虔诚地接受依赖注入……那么我们也需要开始构建与轮胎成分的接口……那么,轮胎的线程呢?那些轮胎的缝线怎么样?这些线中的化学物质怎么样?那些化学物质中的原子呢?等等...好吧!嘿!在某些时候,您将不得不说 "enough is enough"!我们不要把每件小事都变成一个界面……因为那样会太耗时。在 class 本身中包含并实例化一些 classes 是可以的!它的开发速度更快,实例化 class 也容易得多。

只是我的 2 美分。

我发现了 ioc 和依赖注入破坏封装的情况。假设我们有一个 ListUtil class 。在那个 class 中有一个叫做 remove duplicates 的方法。此方法接受一个 List 。有一个带有排序方法的接口 ISortAlgorith。有一个名为 QuickSort 的 class 实现了这个接口。当我们编写删除重复项的算法时,我们必须对内部列表进行排序。现在,如果 RemoveDuplicates 允许接口 ISortAlgorithm 作为参数(IOC/Dependency 注入)以允许其他人选择另一种算法来删除重复项的可扩展性,我们将暴露 ListUtil class 删除重复项功能的复杂性。从而违反了哎呀的基石。