首页 文章

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

提问于
浏览
39

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

我对这些事情的理解是:

  • Dependency Inversion Principle 暗示对象应该依赖于抽象,而不是结核 - 这是实现控制反转模式和依赖注入的基本原则 .

  • Inversion of Control 是依赖性倒置原则的模式实现,其中抽象依赖性替换具体的依赖性,允许在对象之外指定依赖性的具体化 .

  • Dependency Injection 是一种实现控制反转并提供依赖性解析的设计模式 . 将依赖项传递给依赖组件时会发生注入 . 实质上,依赖注入模式提供了一种将依赖抽象与具体实现耦合的机制 .

  • Encapsulation 是更高级别对象所需的数据和功能被隔离并且不可访问的过程,因此,程序员不知道对象是如何实现的 .

辩论与以下声明达成了共识:

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

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

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

Example 1:

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

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

Example 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会破坏封装 . 事实上,我会说:

封装和控制反转很愉快地相互重合,允许程序员只传递依赖的结构,同时通过封装隐藏整个实现 .

Questions:

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

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

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

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

7 回答

  • 40

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

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

    这就是Bertrand Meyer所说的 design by contract . 您可以在Object-Oriented Software Construction中阅读有关OOD这一开创性描述的更多信息 .

    对象通常被描述为 data with behaviour . 这意味着没有数据的对象实际上不是对象 . 因此,您必须以某种方式将数据导入对象 .

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

    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 现在是一个对象,而不是一个原始对象 . 然而,这是一个错误的区别,因为在面向对象的设计中,整数也是一个对象 . 它实际上是's only because of performance optimisations in various programming languages (Java, C#, etc.) that there'基元(字符串,整数,布尔等)和'real'对象之间的区别 . 从OOD的角度来看,它们都是一样的 . 字符串也有行为:你可以把它们变成全大写,反转它们等等 .

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

    bar 只是具有行为的数据,就像整数一样,但除此之外,违反了's no difference. So far, encapsulation isn' .

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

    封装破坏了吗?

    我们是否仍然可以表达关于 Foo 的前后条件,因为 Bar 有一个虚拟成员?

    如果 Bar 遵守Liskov Substitution Principle(LSP),则不会改变系统的正确性 . 只要满足 contract ,封装仍然完好无损 .

    因此,LSP(SOLID principles中的一个,其中Dependency Inversion Principle是另一个)不违反封装;它描述了 a principle for maintaining encapsulation in the presence of polymorphism .

    如果 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原理 . 封装(即按 Contract 设计)定义了基本规则 . SOLID描述了遵循这些规则的准则 .

  • 0

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

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

    一种设计,其中计算机程序的自定义编写部分从通用的可重用库(源)接收控制流

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

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

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

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

    这个问题的答案是:不,它没有 . 它没有破坏封装,原因有两个:

    • 由于依赖性倒置原则声明我们应该针对抽象进行编程,因此消费者将无法访问所使用的实现的内部,因此实现将不会破坏对客户端的封装 . 在编译时甚至可能不知道或无法访问实现(因为它存在于未引用的程序集中),并且在这种情况下实现可以不泄漏实现细节并破坏封装 .

    • 虽然实现接受它在整个构造函数中所需的依赖关系,但这些依赖关系通常会存储在私有字段中,并且任何人都无法访问(即使使用者直接依赖于具体类型),因此它不会破坏封装 .

    IoC应该谨慎,宗教或适当地使用吗?

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

    IoC与IoC容器有什么区别?

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

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

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

    在软件开发者的现实世界中,有用性胜过理论[来自Robert C. Martin的敏捷原则,模式和实践]

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

  • 1

    Summing up the question:

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

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

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

    My "pragmatic" answer

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

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

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

    那里的论点是......是的,但我只想聘请律师,我不关心他使用的书籍或服务 . 我会从interwebz中得到一些随机的内容而不关心他的实现细节......就像这样:

    sub main() {
        LegalService legalService = new LegalService();
    
        legalService.SueMyManagerForBeingMean();
    }
    
    public class LegalService {
        public void SueMyManagerForBeingMean(){
            // Implementation Details.
        }
    }
    

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

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

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

  • 1

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

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

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

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

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

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

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

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

    IoC是依赖倒置的机制,但容器是使用IoC的容器 .

  • 32

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

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

    您还可以在我的博客Here中详细了解依赖性倒置原则 .

  • 0

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

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

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

    只需2美分 .

  • -1

    我找到了一个ioc和依赖注入打破封装的情况 . 让我们假设我们有一个ListUtil类 . 在该类中有一个名为remove duplicatelicates的方法 . 此方法接受List . 有一个带有排序方法的ISortAlgorith接口 . 有一个名为QuickSort的类实现了这个接口 . 当我们编写alogorithm以删除重复项时,我们必须对列表进行排序 . 现在,如果RemoveDuplicates允许接口ISortAlgorithm作为参数(IOC /依赖注入)以允许其他人的可扩展性选择另一种删除重复的算法,那么我们暴露了删除ListUtil类的重复特性的复杂性 . 从而违反了哎呀的基石 .

相关问题