有关OOP的常见误解

哈Ha!

今天,翻译出版物正等着您,在某种程度上反映了我们与OOP和FI新书相关的搜索。 请参加投票。



OOP范式已经死了吗? 可以说函数式编程是未来吗? 似乎有许多文章对此进行了介绍。 我倾向于不同意这种观点。 聊聊吧!

每隔几个月,我就会在某个博客上遇到一个帖子,在该帖子中,作者对面向对象编程提出了看似有充分根据的主张,此后,她宣布OOP成为过去的遗物,我们所有人都必须转向功能编程。

之前,我写道 OOP和FI并不矛盾。 而且,我能够非常成功地将它们组合在一起。

为什么这些文章的作者在OOP上有如此多的问题,为什么AF在他们看来如此明显呢?

如何教OOP


当我们学习OOP时,他们通常会强调它基于四个原则: 封装继承抽象多态 。 作者通常会在文章中批评这四项原则,以解释PLO的下降。

但是,像FI一样,OOP是一种工具。 解决问题。 它可以被消费,也可以被滥用。 例如,通过创建错误的抽象,您将滥用OOP。
因此, Square类永远不要继承Rectangle类。 从数学意义上讲,它们当然是相连的。 但是,从编程的角度来看,它们不是继承关系。 事实是,对正方形的要求比对矩形的要求严格。 在一个矩形中有两对相等的边,而一个正方形的所有边必须相等。

传承


让我们更详细地讨论继承。 您可能会想起带有继承类的美丽层次结构的教科书示例,所有这些结构都可以解决问题。 但是,实际上,继承不像组合那样经常使用。
考虑一个例子。 假设我们有一个非常简单的类,即Web应用程序中的控制器。 大多数现代框架都假定您将像这样使用它:

 class BlogController extends FrameworkAbstractController { } 

假定通过这种方式,您可以更轻松地进行诸如this.renderTemplate(...)类的调用,因为此类方法是从FrameworkAbstractController类继承的。

正如有关该主题的许多文章所指出的,这里出现了许多明显的问题。 基类中的任何内部函数实际上都变成了API。 她不能再改变了。 基本控制器的所有受保护变量现在都将或多或少与API相关。

没有什么值得困惑的。 而且,如果我们选择使用组合和依赖注入的方法,结果将是这样的:

 class BlogController { public BlogController ( TemplateRenderer templateRenderer ) { } } 

您会看到,您不再依赖于一些模糊的FrameworkAbstractController ,而是依赖于定义非常明确且狭窄的东西TemplateRenderer 。 实际上, BlogController不会继承任何其他控制器,因为它不会继承任何行为。

封装形式


OOP的第二个经常被批评的特征是封装。 在文学语言中,封装的含义表示如下:数据和功能一起传递,并且类的内部状态对外界隐藏。

这个机会再次允许使用和滥用。 在这种情况下,滥用的主要示例是泄漏状态。

相对而言,假设List<>类包含一个元素列表,并且可以更改此列表。 让我们创建一个类来处理订单篮,如下所示:

 class ShoppingCart { private List<ShoppingCartItem> items; public List<ShoppingCartItem> getItems() { return this.items; } } 

在这里,在大多数面向OOP的语言中,将发生以下情况:items变量将通过引用返回。 因此,我们进一步可以做到这一点:

 shoppingCart.getItems().clear(); 

因此,我们实际上将清除购物篮中的物品清单,而ShoppingCart甚至都不知道。 但是,如果仔细查看此示例,很明显问题不在于封装原理。 此处违反了该原理,因为内部状态从ShoppingCart类泄漏。

在此特定示例中, ShoppingCart类的作者可以使用不变性来规避此问题,并确保不违反封装原理。

没有经验的程序员经常以另一种方式违反封装原理:他们引入了不需要这种状态。 这种经验不足的程序员经常使用私有类变量将数据从一个函数传输到同一类中的另一个函数,而使用数据传输对象将复杂的结构传输到另一个函数会更合适。 由于此类错误,导致代码不必要地变得复杂,从而可能导致错误。

通常,最好完全省去状态-尽可能将可变数据存储在类中。 这样,您需要确保可靠的封装并确保任何地方都没有泄漏。

抽象化


同样,抽象在很多方面都被错误地理解。 在任何情况下,都不应使用抽象类填充代码并在其中建立深层次结构。

如果您没有充分的理由这样做,那么您只是在寻找麻烦。 抽象的完成方式无关紧要-作为抽象类还是作为接口; 在任何情况下,代码中都会出现额外的复杂性。 必须证明这种复杂性。
简而言之,仅当您愿意花费时间并记录实现类的预期行为时,才可以创建接口。 是的,你没看错我。 仅列出所需实现的功能还不够,还要描述它们的工作方式(理想情况下)。

多态性


最后,让我们谈谈多态性。 他建议一个班级可以实施许多行为。 一个不好的教科书例子是,写出多态的Square可以是RectangleParallelogram 。 正如我上面已经指出的那样,在OOP中这绝对是不可能的,因为这些实体的行为是不同的。

说到多态,应该牢记行为 ,而不是代码 。 一个很好的例子是计算机游戏中的Soldier类。 它可以实现Movable行为(位置:可以移动)和Enemy行为(位置:向您射击)。 相反, GunEmplacement类只能实现Enemy行为。

因此,如果您编写Square implements Rectangle, Parallelogram ,则该语句将不成立。 您的抽象应该根据业务逻辑工作。 您应该更多地考虑行为而不是代码。

为什么FP不是灵丹妙药


因此,当我们重复OOP的四个基本原理时,让我们考虑一下函数式编程的功能是什么,为什么不使用它来解决代码中的所有问题呢?

从FP的许多拥护者的角度来看,类是牺牲品 ,并且代码应以函数的形式呈现。 根据语言的不同,可以使用原始类型或以一个或另一个结构化数据集(数组,字典等)的形式在函数之间传递数据。

此外,大多数功能都不应有副作用。 换句话说,它们不应在后台的任何意外位置更改数据,而只能使用输入参数并产生输出。

这种方法将数据功能分开-乍一看,该FP与OOP根本不同。 FP强调以这种方式使代码保持简单。 您想要做一些事情,为此目的编写一个函数,仅此而已。

当某些功能必须依赖其他功能时,问题就开始了。 当函数A调用函数B,而函数B调用另外五个到六个函数时,最后发现一个零填充函数可能会中断-这是您不会羡慕的地方。

多数自以为是FP的支持者的程序员都喜欢FP的简单性,并且认为此类问题并不严重。 如果您的任务只是简单地传递代码,而再也不考虑它了,这是很诚实的。 如果要构建便于支持的代码库,则最好遵循纯代码的原则,尤其是应用依赖倒置 ,在这种情况下,FI在实践中也会变得更加复杂。

OOP还是FP?


OOP和FI是工具 。 最终,使用哪种编程范例都没有关系。 关于该主题的大多数文章中描述的问题都与代码组织有关。

我认为,应用程序的宏观结构更为重要。 它包含哪些模块? 他们如何彼此交换信息? 哪种数据结构最常见? 如何记录? 就业务逻辑而言,哪些对象最重要?

所有这些问题都与所使用的编程范例无关;在这种范例的层次上,它们甚至无法解决。 优秀的程序员研究范例以掌握其提供的工具,然后选择最适合解决任务的工具。

Source: https://habr.com/ru/post/zh-CN469135/


All Articles