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

时间:2015-06-29 17:10:46

标签: oop design-patterns inversion-of-control encapsulation solid-principles

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

我对这些事情的理解是:

  • 依赖性倒置原则意味着对象应该依赖于抽象,而不是结果 - 这是实现控制反转模式和依赖注入的基本原则。
  • 控制反转是依赖性反转原则的模式实现,其中抽象依赖性取代了具体的依赖关系,允许在对象之外指定依赖关系的具体结构。
  • 依赖注入是一种实现控制反转并提供依赖性解析的设计模式。将依赖项传递给依赖组件时会发生注入。从本质上讲,依赖注入模式提供了一种将依赖抽象与具体实现耦合的机制。
  • 封装是一个过程,通过这个过程,更高级别对象所需的数据和功能被隔离开来并且无法访问,因此,程序员不知道如何实现对象。

辩论与以下陈述达成了共识:

  

IoC不是OOP,因为它破坏了封装

就我个人而言,我认为所有OOP开发人员都应该虔诚地遵守依赖性倒置原则和控制反转模式 - 我的生活方式如下:

  

如果有(可能)不止一种方法给猫皮肤,那么不要   表现得像只有一个。

示例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 容器会破坏OOP,因为它们使用反射,但我不相信IoC会破坏OOP,我也不相信IoC打破了封装。事实上,我甚至可以说:

  

封装和控制反转彼此一致   幸运的是,允许程序员只传入a的具体结果   依赖,同时通过隐藏整体实现   封装

问题:

  • IoC是直接实现依赖倒置原则吗?
  • IoC是否总是打破封装,因此是OOP?
  • IoC应该谨慎,宗教还是适当地使用?
  • IoC与IoC容器有什么区别?

7 个答案:

答案 0 :(得分:42)

  

IoC是否总是破坏封装,因此是OOP?

不,这些是与等级相关的问题。封装是OOP中最容易被误解的概念之一,但我认为这种关系最好通过抽象数据类型(ADT)来描述。本质上,ADT是数据和相关行为的一般描述。这种描述是抽象的;它省略了实现细节。相反,它用 pre - 后置条件来描述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从整数更改为另一个具体类会怎样?

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#等)中的性能优化,原语(字符串,整数,bool等)和' real&#之间存在实际差异。 39;对象。从OOD的角度来看,他们都是一样的。字符串也有行为:你可以把它们变成全大写,反转它们等等。

如果Bar是一个只有非虚拟成员的密封/最终具体类,是否违反了封装?

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

如果我们允许Bar拥有一个虚拟成员,会发生什么?

封装破坏了吗?

鉴于Foo只有一个虚拟成员,我们是否还可以表达关于Bar的前后条件?

如果Bar遵守Liskov Substitution Principle(LSP),则不会有所作为。 LSP明确指出改变行为不能改变系统的正确性。只要满足合同,封装就完好无损。

因此,LSP(SOLID principles中的一个Dependency Inversion Principle是另一个)并不违反封装;它描述了 在存在多态的情况下维护封装的原则

如果Bar是抽象基类,结论会改变吗?界面?

不,它并不是:那些只是不同程度的多态性。因此,我们可以将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描述了遵循这些规则的准则。

答案 1 :(得分:34)

  

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

这两者是相关的,他们谈论抽象,但这是关于抽象的。控制反转是:

  

一种设计,其中计算机程序的自定义写入部分   从通用的可重用库(source

接收控制流

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

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

  

IoC是否总是破坏封装,因此是OOP?

由于IoC只是挂钩到框架的管道,所以没有任何东西在这里泄漏。所以真正的问题是:依赖注入是否打破了封装。

这个问题的答案是:不,它没有。由于两个原因,它并没有打破封装:

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

IoC应该谨慎,虔诚还是恰当地使用?

同样,问题是&#34; DIP和DI应该谨慎使用&#34;。在我看来,答案是:不,你应该在整个应用程序中使用它。显然,你不应该虔诚地申请。您应该应用SOLID原则,DIP是这些原则的重要组成部分。它们将使您的应用程序更灵活,更易于维护,在大多数情况下,应用SOLID原则非常合适。

  

IoC与IoC容器有什么区别?

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

注意:由于IoC容器可以帮助我们应用依赖注入模式,所以#34; IoC容器&#34;这个库是一个可怕的名字。

但是,尽管我上面说过,但请注意:

  

在软件开发者的现实世界中,有用性胜过理论[来自Robert C. Martin Agile Principle, Patterns and Practices]

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

答案 2 :(得分:1)

总结问题:

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

然而,我们还能够让服务简单地定义抽象,并要求应用程序了解依赖抽象,创建具体实现并将其传递。

问题不在于,&#34;为什么我们这样做?&#34; (因为我们知道有很多原因列表)。但问题是,&#34;没有选项2打破封装?&#34;

我的实用主义&#34;答案

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

封装隐藏了服务或抽象的实现细节。依赖性不是实现细节。如果您将服务视为合同,并将其后续的子服务依赖关系视为子合同(等等链接在一起),那么您实际上最终只能得到一份带有附录的巨大合同。

想象一下,我是一名来电者,我想利用法律服务来起诉我的老板。我的应用程序必须知道关于这样做的服务。仅这一点就打破了一个理论,即了解实现目标所需的服务/合同是错误的。

那里的论点是......是的,但我只想聘请律师,我不关心他使用的书籍或服务。我会从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 }
    }
}

现在,我所知道的是,我有一份合同,其他合同可能还有其他合同。我对这些合同负有很好的责任,而不是他们的实施细节。虽然我非常乐意签署那些与我的要求相关的具体结构的合同。再说一遍,我并不关心这些结核是如何完成工作的,只要我知道我有一份具有约束力的合同,即我们以某种明确的方式交换信息。

答案 3 :(得分:1)

根据我的理解,我会尽力回答你的问题:

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

    我们不能将IoC标记为DIP的直接实现,因为DIP侧重于根据抽象而不是基于较低级别模块的具体结构来创建更高级别的模块。但IoC是依赖注入的实现。

  • IoC是否总是打破封装,因此OOP?

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

  • IoC应该谨慎,宗教还是适当地使用?

    IoC可以用作许多模式,例如Br​​idge Pattern,其中从抽象中分离Concretion可以改进代码。因此可以用来实现DIP。

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

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

答案 4 :(得分:0)

封装与面向对象编程世界中的依赖倒置原则并不矛盾。例如,在汽车设计中,您将拥有一个内置引擎&#39;它将被外部世界封装,而且还有轮子和轮子。可以轻松更换,并被视为汽车的外部组件。汽车具有旋转车轮轴的规格(接口),车轮部件实现与轴相互作用的部件。

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

此外,您可以在我的博客Here中详细了解依赖性倒置原则。

答案 5 :(得分:0)

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

IoC应该谨慎,宗教还是适当地使用? 我的经验使我相信依赖注入只应该用于一般的类,并且可能需要在将来进行更改。宗教上使用它将导致一些类在构造函数中需要15个接口,这可能非常耗时。这往往导致20%的发展和80%的家务。

有人提出了汽车的例子,以及汽车制造商如何改变轮胎。依赖注入允许人们在不关心具体实施细节的情况下更换轮胎。但是如果我们虔诚地接受依赖注入......那么我们需要开始建立与轮胎成分的接口......那么,轮胎的螺纹呢?那些轮胎的缝合怎么样?那些线程中的化学物质怎么样?那些化学物质中的原子怎么样?等等......好的!嘿!在某些时候,你必须说&#34;足够了&#34;!我们不要将每一件小事都变成一个界面......因为这会花费太多时间。可以将一些类自包含在类本身中并在其中实例化,这是可以的!开发速度更快,实例化课程更容易。

只需2美分。

答案 6 :(得分:-1)

I have found a case when ioc and dependency injection breaks encapsulation . Lets assume we have a ListUtil class . In that class there is a method called remove duplicates . This method accepts a List . There is an interface ISortAlgorith with a sort method . There is a class called QuickSort which implements this interface. When we write the alogorithm to remove duplicates we have to sort the list inside . Now if RemoveDuplicates allow an interface ISortAlgorithm as a parameter(IOC/Dependency Injection) to allow extensibility for others to choose an another algorithm for remove duplicate we are exposing the complexity of remove duplicate feature of the ListUtil class . Thus violating the foundation stone of Oops.