关于设计模式:依赖性倒置原则(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:

1
2
3
4
5
6
class Program {
    void Main() {
        SkinCatWithKnife skinner = new SkinCatWithKnife ();
        skinner.SkinTheCat();
    }
}

这里我们看到一个封装的例子。程序员只需要打电话给Main(),猫就会被剥皮,但是如果他想用一组锋利的牙齿给猫剥皮呢?

例2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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会破坏封装。事实上,我会这么说:

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.

问题:

  • IOC是依赖倒置原则的直接实现吗?
  • IOC是否总是破坏封装,从而破坏OOP?
  • 国际奥委会应该谨慎、虔诚或适当地使用吗?
  • 国际奥委会和国际奥委会集装箱有什么区别?


Does IoC always break encapsulation, and therefore OOP?

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

这就是BertrandMeyer所说的契约式设计。您可以阅读更多关于面向对象软件构造中对ood的开创性描述。

对象通常被描述为具有行为的数据。这意味着没有数据的对象实际上不是一个对象。因此,您必须以某种方式将数据输入到对象中。

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

1
2
3
4
5
6
7
8
9
10
11
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从整数更改为另一个具体类,会发生什么?

1
2
3
4
5
6
7
8
9
10
11
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等)中的性能优化,在原语(字符串、整数、布尔等)和"真实"对象之间存在实际差异。从伍德的角度来看,他们都很相似。字符串也有一些行为:您可以将它们转换为所有大写、反转等等。

如果bar是只包含非虚拟成员的密封/最终的具体类,是否违反了封装?

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

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

封装被破坏了吗?

考虑到bar只有一个虚拟成员,我们还能表达Foo的前后条件吗?

如果bar坚持Liskov替换原则(LSP),就不会有什么区别。LSP明确指出,改变行为不能改变系统的正确性。只要满足该契约,封装仍然是完整的。

因此,LSP(其中一个实体原则,依赖倒置原则是另一个)不会违反封装;它描述了在存在多态性的情况下保持封装的原则。

如果bar是抽象基类,结论会改变吗?接口?

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

1
2
3
4
5
6
7
8
9
10
11
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存在,封装就存在。

DR

有一个原因,固体也被称为原则的木材。封装(即合同设计)定义了基本规则。Solid描述了遵循这些规则的准则。


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?

同样,问题是"应该减少使用DI"。在我看来,答案是:不,你应该在整个应用程序中使用它。显然,你不应该把事情用在宗教上。你应该应用坚实的原则,而这种倾向是这些原则的关键部分。它们将使您的应用程序更加灵活和易于维护,在大多数情况下,应用可靠的原则是非常合适的。

What is the difference between IoC and an IoC container?

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

注意:由于IOC容器帮助我们应用依赖注入模式,"IOC容器"对于此类库来说是一个糟糕的名称。

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

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

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


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

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

    我们不能将IOC标记为DIP的直接实现,因为DIP关注的是根据抽象而不是低层模块的具体化来生成更高级别的模块。但实际上,IOC是依赖注入的一种实现。

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

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

  • 国际奥委会应该谨慎、虔诚或适当地使用吗?

    IOC可以在许多模式中使用,如桥接模式,分离抽象的具体化可以改进代码。因此可用于实现倾斜。

  • 国际奥委会和国际奥委会集装箱有什么区别?

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


总结问题:

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

然而,我们还能够让服务简单地定义抽象,并要求应用程序了解相关的抽象,创建具体的实现,然后将它们传入。

问题不是,"我们为什么要这样做?"(因为我们知道有很多原因)。但问题是,"选项2是否会破坏封装?"

我的"务实"回答

我认为对于任何这样的答案,马克都是最好的选择,正如他所说:不,封装并不是人们认为的那样。

封装隐藏了服务或抽象的实现细节。依赖项不是实现细节。如果你把一个服务看作是一个契约,而它随后的子服务依赖性又被看作是子契约(等等),那么你最终只得到一个带有附录的巨大契约。

假设我是一个打电话的人,我想用法律服务起诉我的老板。我的应用程序必须知道这样做的服务。这就打破了这样一个理论:了解实现我的目标所需的服务/合同是错误的。

这里的争论是…是的,但我只想雇个律师,我不在乎他用什么书或服务。我会从interwebz上得到一些随机的涂鸦,而不关心他的实现细节…像这样:

1
2
3
4
5
6
7
8
9
10
11
sub main() {
    LegalService legalService = new LegalService();

    legalService.SueMyManagerForBeingMean();
}

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 }
    }
}

现在,我所知道的是,我有一个合同,有其他的合同,可能有其他的合同。我对那些合同非常负责,而不是它们的实施细节。虽然我非常乐意与具体的合同签署与我的要求是相关的。再说一次,我不关心这些具体化是如何完成他们的工作的,只要我知道我有一个有约束力的合同,说我们以某种定义的方式交换信息。


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

国际奥委会应该谨慎、虔诚或适当地使用吗?我的经验使我相信依赖注入应该只用于一般类,并且在将来可能需要更改的类。认真使用它会导致一些类在构造函数中需要15个接口,这会非常耗时。这往往导致20%的发展和80%的住房。

有人举了一个汽车的例子,以及汽车制造商想要如何更换轮胎。依赖注入允许在不考虑具体实现细节的情况下更改轮胎。但是如果我们虔诚地接受依赖注射…然后我们需要开始建立与轮胎成分的接口…那么,轮胎的螺纹呢?轮胎上的针脚怎么办?那些线里的化学物质呢?那些化学物质中的原子呢?等。。。可以!嘿!在某些时候,你必须说"足够了就足够了"!让我们不要把每一件小事都变成一个界面…因为那样太费时了。可以让一些类自包含在类中并在类本身中实例化!它的开发速度更快,而且实例化类要容易得多。

只有我的2美分。


封装并不违背面向对象编程世界中的依赖倒置原则。例如,在汽车设计中,您将拥有一个从外部世界封装的"内部发动机",以及一个易于更换的"车轮",并被视为汽车的外部部件。汽车有旋转车轮轴的规格(接口),车轮部件实现了与轴交互的部分。

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

此外,您还可以在我的博客中了解更多关于依赖倒置原则的详细信息。


我发现了一个IOC和依赖注入破坏封装的情况。假设我们有一个listutil类。在该类中有一个名为"移除重复项"的方法。此方法接受列表。有一个带有排序方法的接口isortalgorith。有一个名为QuickSort的类实现了这个接口。当我们编写删除重复项的算法时,必须对列表内部进行排序。现在,如果removeduplicates允许接口isortalgorithm作为参数(ioc/dependency injection),以允许其他人选择其他算法来删除重复项,那么我们将暴露listutil类的remove duplicate功能的复杂性。从而违反了OOPS的基石。