人们误解了OOP

19-01-09 banq
              

OOP死了吗?函数编程是未来吗?有些文章似乎暗示了这一点。我倾向于不同意。来!我们讨论一下!

每隔几个月我就会看到一篇博客文章,宣称它已成为过去,我们都应该转向函数式编程。

为什么这些文章的作者对OOP有如此多的意见?为什么FP似乎是如此明显的选择呢?

如何教授OOP

当在学校教授OOP时,它通常被教导为被这四个原则所阻碍:封装, 继承,抽象和多态。这通常也是讨论OOP消亡的文章倾向于攻击的项目列表。

然而,与FP一样,OOP是一种工具。这是一份工作的工具。因此,它可以被使用和滥用。例如,如果您创建了一个错误的抽象,那么您就是滥用该工具。

例如,Square正方形类应该永远不会扩展 Rectangle矩形类,在编程意义上,它们不是继承关系;然而在数学意义上,它们当然是相关的。矩形可以具有两个独立的边长,但是正方形具有严格的要求,即边都是相等的。(banq:这里引申出根据数学常识还是人类常识进行建模的问题)

继承

让我们再谈谈继承。继承通常被描述为OOP的A和O. 通常会看到教科书示例,其中构建了继承类的美妙层次结构来解决问题。但是,在实践中,您很少使用继承。相反,经常使用组合(banq注:聚合)。

我们来看一个例子。假设我们有一个非常简单的类,一个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类的作者可以使用不变性 来解决问题并确保不违反封装。

无经验的程序员经常违反封装的另一种方式是引入不需要的状态。通常没有经验的程序员使用私有类变量将数据从一个函数传递到同一个类中的另一个函数,而不是使用数据传输对象DTO将复杂结构传递给不同的函数。这会带来不必要的复杂性并经常导致错误。

通常,尽可能避免在类中存储状态(存储可变数据)是个好主意。如果我们这样做,它应该被很好地封装并确保它不会泄漏。

抽象化

抽象再次被误解了。您绝不应该将代码填充为抽象类并进行深层次结构化。

如果你没有任何充分理由这样做,那你只是在寻找麻烦。抽象是作为抽象类还是接口完成并不重要,这会带来额外的复杂性。这种复杂性必须是合理的。

在外行人看来,如果您实际上要花时间记录实现类所期望的行为,那么您应该只创建一个接口 。不要只写下需要实现的函数列表,记下它们应该如何表现。

多态性

我们列表中的最后一项是多态。这意味着一个类可以实现许多行为。糟糕的教科书例子是一个正方形Square可以是一个矩形Rectangle还是一个平行四边形Parallelogram。好吧,正如上面所讨论的那样,由于他们的行为不同,这在OOP中绝不是真的。

在谈论多态时,应该考虑行为而不是代码。一个很好的例子就是Soldier电脑游戏中的 类class。它可以实现Movable行为(如:它可以移动)和 Enemy行为(如:射击你)。相反,GunEmplacement可以只实现Enemy行为。

所以,仅仅因为你可以写Square implements Rectangle, Parallelogram,这不是真的。您的抽象需要在业务意义上实际工作。你需要考虑行为而不是代码。

为什么FP不是银弹

现在我们已经完成了这四个原则,这个函数式编程是什么,为什么它不能解决我们所有的代码问题呢?

在许多FP信徒的眼中,Class是可憎的,代码应该被表示为函数。根据语言,数据可以使用原始类型或结构化数据集(数组,映射等)在它们之间传递。

此外,大多数函数不应该有副作用。换句话说,它们不应该在后台的某些其他位置修改某些数据,而只是处理输入参数以产生输出。

这种方法将数据与函数分开,乍一看与OOP方法有根本的不同。它的吸引力在于它使事情变得简单。你想做点什么,写一个函数,结束故事。

当函数需要相互依赖时,问题就出现了。当函数A调用函数B和函数B 调用其他六个函数时,在链的末尾有一个可以突然中断的 left-pad 函数,你有问题啦。

如果您想要代码变得可维护,则最好遵守清洁代码原则。这包括使用 依赖性反转,这使得FP使用起来也复杂得多。

OOP还是FP?

OOP和FP是工具。你使用什么编程范例并不重要。大多数文章中提出的问题都与组织代码有关。

对我来说,应用程序的宏观结构更重要:模块是什么?他们如何相互沟通?什么是常见的数据结构?这些记录如何?有哪些重要的业务对象?

这些都是与使用的编程范例无关的问题,编程范例甚至都没有解决。一个优秀的程序员将学习范式作为知道工具的问题,并将使用适合于给定任务的范例。