为什么静态方法被认为不是良好的面向对象实践?

53

我正在阅读《Programming Scala》。在第四章开头,作者评论说Java支持静态方法,这是“不太纯粹的面向对象概念”。为什么会这样?


11
我不确定“不太纯净”一词是否应立即与不良行为相关联。 - mellowsoon
不是坏的做法,只是根据作者的观点不是好的做法。 - Mike
1
作者没有给出任何理由吗? - Roger Pate
没有。这很令人困惑。但现在它有意义了。 - Mike
问题的核心:静态的东西无法实现多态。 - Dávid Horváth
7个回答

75

面向对象编程涵盖三个方面:

  • 消息传递
  • 本地保留和保护以及隐藏状态过程
  • 所有事物的极度后期绑定

在这三个方面中,最重要的是消息传递

“消息传递”的概念意味着,在面向对象编程中,计算通过自包含对象网络发送消息进行。发送消息是通信/计算的唯一方式。

静态方法违反了至少消息传递和后期绑定。

静态方法并不是与任何对象相关联的方法。根据通常定义,它们实际上并不是方法,而只是过程。 Java静态方法Foo.bar和BASIC子例程FOO_BAR几乎没有任何区别。

至于后期绑定:更现代的称呼是动态分派。静态方法也违反了这一原则,事实上,这甚至可以从它们的名称中看出来:“静态”方法。

静态方法打破了面向对象编程的一些非常好的属性。例如,面向对象系统自动具备对象作为能力的功能安全性。静态方法(或者实际上是任何静态方法和静态状态)破坏了这种属性。

您还可以在自己的进程中并行执行每个对象,因为它们只通过消息进行通信,从而提供了一些简单的并发性。 (基本上就像Actors一样,这应该不会太惊讶,因为Carl Hewitt基于Smalltalk-71创建了Actor模型,而Alan Kay则部分基于PLANNER创建了Smalltalk-71,而PLANNER又是由Carl Hewitt创建的。演员和对象之间的密切关系远非偶然,事实上,它们本质上是相同的。)再次强调,静态方法(包括静态方法和尤其是静态状态)破坏了这种良好的属性。


非常感谢您的深入回复。 - BillMan
1
什么是“能力安全”? - Guilherme de Lazari
关于并发代码,静态方法不会改变对象的状态,因此我倾向于认为它们比普通方法更安全,适合并发使用。 - Guilherme de Lazari

43
不要将“非非纯OO概念”与“不良实践”混淆。 “纯OO”并不是您应该尝试达到的万灵药。 静态方法之所以没有将实例变量作为参数是因为他们很有用,这并不意味着它们不应该使用。 有些事情就是不适合对象,它们不应该被迫塑造成那种形式,只是为了“纯净”。
有些人认为事物应该是“纯粹的”,因此任何“不纯”的东西都是不良实践。 实际上,不良实践就是做一些让人困惑、难以维护、难以使用等的事情。创建静态方法并将其作为实例变量的是不良实践,因为任何需要一个实例的方法可能都应该是实例方法。另一方面,像实用和工厂函数之类的东西通常不需要一个实例,所以它们应该是静态的。
如果您想知道它们为什么不是“纯OO”,那是因为它们不是实例方法。 “纯” OO语言会使所有内容都是对象,并且所有函数都是实例方法。当然,这并不总是有用的。例如,请考虑Math.atan2方法。它需要两个数字并且不需要任何状态。您甚至可以将其作为方法的对象进行制作吗?在“纯” OO语言中, Math 本身可能是一个对象(可能是单例),而 atan2 将是实例方法,但由于函数实际上没有使用 Math 对象中的任何状态,因此它也不是“纯OO”概念。

请记住,我从未说过这是一种不好的做法,所以我的问题并没有得到真正的回答。 ;) - Mike
4
没错。另一方面,我见过太多的代码,其中所有东西都是单例和静态的。这种代码通常更难修改、扩展和重新塑造以完成原来意图之外的任务。话虽如此,许多人对此过于教条化。 - Eddie
2
实际上,atan2函数用于测量一个单点到正x轴的弧度角。换句话说,它一开始不应该接受两个数字参数,而应该接受一个单点参数。用面向对象的术语来看,它可以被建模为一个点对象上的方法。 - Jörg W Mittag
1
Jorg: stdevaverage函数怎么处理?它们应该是Array的方法吗?还是它们应该是自己的类(Stats)的实例方法,该类保存所有数字,以便您必须将您的Array转换为Stats才能对它们求平均值?或者它们应该是静态方法,你只需要传递一个数字数组给它们? - Gabe
2
我完全同意。很多实用函数本质上并不具备面向对象的特性。你可以将它们塞进面向对象的语言中,但这只会增加低效率。 - Loren Pechtel

36

目前为止还没有提到静态方法不是很面向对象的一个原因是接口和抽象类只定义非静态方法。因此,静态方法不太适合继承。

请注意,静态方法没有访问“super”的权限,这意味着静态方法无法实际上被重写。实际上,它们根本无法被重写,只能被隐藏。尝试一下:

public class Test {
    public static int returnValue() {
        return 0;
    }

    public static void main(String[] arg) {
        System.out.println(Test.returnValue());
        System.out.println(Test2.returnValue());
        Test x = new Test2();
        System.out.println(x.returnValue());
    }
}


public class Test2 extends Test {
    public static int returnValue() {
        return 1;
    }
}
当你运行这段代码时,你得不到你所期望的结果。 Test.returnValue() 返回你期望的内容,而 Test2.returnValue()隐藏了超类中同名方法(它并没有覆盖它),并返回你期望的内容。
可能有人天真地期望以"非静态"的方式调用静态方法会使用多态性,但事实并非如此。无论变量声明为哪个类,都将用于查找方法。这是不好的做法,因为有些人可能期望代码执行与实际执行的不同。
这并不意味着"不要使用静态方法!",它意味着您应该仅在确实希望Class对象拥有该方法而不仅仅是作为创建单例的一种懒惰方式时才使用静态方法。

2
这个答案可能是我迄今所见最有意义的。打破三位一体的概念肯定是违背常规的。 - Mike
我并不是在质疑这个问题,但你所展示的例子只适用于Java,对吗?如果我没记错的话,Java是唯一一个允许通过类实例调用静态函数的面向对象语言(至少在我遇到的语言中是这样)。 - Jeff Mercado
@Jeff:VB.NET也允许这种荒谬的行为。 - Adam Robinson
1
@Jeff:C++和Python都允许这样做。虽然它有时会令人困惑(像任何功能一样),但并不像你想象的那么毫无意义。 - Roger Pate

11

静态方法会导致紧密耦合,这违反了良好的面向对象设计原则。调用代码和静态方法内部的代码之间的紧密耦合无法通过依赖反转来避免,因为静态方法本质上不支持面向对象的设计技术,如继承和多态。

此外,由于这些紧密耦合的依赖关系,静态方法往往难以进行测试,这通常会导致代码依赖于第三方基础设施(例如数据库),并且这使得在不实际更改代码的情况下更改行为变得非常困难。


简而言之,静态方法是面向对象编程中用于过程式编程的工具。 - ikirachen

11

由于以下原因,静态方法不被认为是良好的面向对象编程实践:

1) 防止重用:

静态方法无法重写,并且不能在接口中使用。

2) 对象生命周期很长:

静态方法会在内存中保留很长时间,垃圾回收需要很长时间。开发人员无法控制静态变量的销毁或创建。过度使用静态变量可能导致内存溢出。

3) 此外,还有一些其他要点:

它不尊重封装,因为对象不再完全受其状态的控制。 它不遵循控制反转、松耦合、依赖注入等概念。


2

静态方法并不是纯粹的面向对象概念,因为它们可以在没有与之关联的对象的情况下被调用。您可以使用类本身。像这样调用它们:Classname.method(...);


如果我正在使用像Ruby这样支持类作为对象和静态调用的语言,那该怎么办?实际上,一个对象将与静态调用相关联。 - Mike
我不确定作者在说这些话时与Java有多少关联,但事实是即使在Java中也有被创建和管理的对象来表示类本身,所以这个问题在某种程度上是可以讨论的。然而,你有一个静态main方法和静态方法可以在没有任何对象的情况下调用的简单想法,使它有点违背了面向对象编程范式。 - Luis Miguel Serrano

0

面向对象的概念是关于从对象中控制/访问数据,但静态方法不需要使用对象调用,它们属于类而不是对象。

--干杯


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接