大量的时间,我想不出拥有对象而不是静态类的理由。对象有比我想象的更多的好处吗?[关闭]


9

我了解对象的概念,作为Java程序员,我觉得OO范式在实践中自然而然地出现在我身上。

但是最近我发现自己在想:

等等,使用对象比使用静态类(具有适当的封装和OO做法)实际具有哪些实际好处?

我可以想到使用对象的两个好处(既重要又强大):

  1. 多态性:允许您在运行时动态灵活地交换功能。还可以轻松为系统添加新功能“零件”和替代品。例如,如果有一个Car设计用于处理Engine对象的类,并且您想向Car可以使用的系统中添加一个新的Engine,则可以创建一个新的 Engine子类并将该类的Car对象简单地传递给该 对象,而不必改变一切Car。您可以在运行时决定这样做。

  2. 能够“传递功能”:您可以动态地在系统中传递对象。

但是与静态类相比,对象还有更多优势吗?

通常,当我向系统添加新的“部件”时,我会通过创建新类并从中实例化对象来实现。

但是最近,当我停下来想一想时,我意识到在很多我通常使用对象的地方,静态类将与对象相同。

例如,我正在为我的应用程序添加一个保存/加载文件机制。

对于一个对象,代码的调用行将如下所示: Thing thing = fileLoader.load(file);

对于静态类,它看起来像这样: Thing thing = FileLoader.load(file);

有什么不同?

通常,当普通的静态类的行为相同时,我只是想不出实例化对象的理由。但是在OO系统中,静态类很少见。所以我一定想念一些东西。

除了上面列出的两个对象以外,对象还有其他优点吗?请解释。

编辑:澄清。在交换功能或传递数据时,我确实发现对象非常有用。例如,我写了一个组成旋律的应用程序。MelodyGenerator有几个不同的子类创建旋律,并且这些类的对象是可互换的(策略模式)。

旋律也是对象,因为传递它们很有用。和弦和音阶也是如此。

但是,系统的“静态”部分又将如何传递呢?例如-一种“保存文件”机制。为什么要在对象而不是静态类中实现它?


那么,不是一个带有字段和方法的对象,就是没有对象,没有记录,只是传递给静态方法和从静态方法返回的大量标量值?编辑:您的新示例建议否则:对象,只是没有实例方法?否则是什么Thing

当您需要将其交换FileLoader为从套接字读取的内容时会发生什么?还是模拟测试?或者打开一个zip文件?
本杰明·霍奇森


1
@delnan好的,我想出了一个问题,答案将帮助我理解:您为什么将系统的“静态”部分实现为对象?就像问题中的示例一样:保存文件机制。在对象中实现它会得到什么?
Aviv Cohn 2014年

1
默认值应该是使用非静态对象。仅当您认为静态类确实可以更好地表达您的意图时,才使用静态类。System.Math.NET中的示例说明了作为静态类更有意义的事情:您无需交换或模拟它,并且从逻辑上讲,任何操作都不能成为实例的一部分。我真的不认为您的“节省”示例符合要求。
本杰明·霍奇森

Answers:


14

大声笑你听起来像我以前工作过的团队;)

Java(可能还有C#)当然支持该编程风格。我与第一个本能的人一起工作是:“我可以使它成为静态方法!” 但是随着时间的流逝,有些微妙的费用会赶上您。

1)Java是一种面向对象的语言。它使功能正常的人发疯,但实际上它保持得很好。OO背后的想法是将功能与状态捆绑在一起,使数据和功能的小单元通过隐藏状态并仅公开在该上下文中有意义的功能来保留其语义。

通过仅使用静态方法转到类,就破坏了等式的“状态”部分。但是国家仍然必须住在某个地方。因此,随着时间的推移,我看到具有所有静态方法的类开始具有越来越复杂的参数列表,因为状态从类移到了函数调用。

在使用所有静态方法创建一个类之后,请遍历并调查其中有多少个方法具有单个通用参数。暗示该参数应该是这些函数的包含类,否则该参数应该是实例的属性。

2)OO的规则已经很清楚了。一段时间后,您可以查看类设计,看看它是否符合SOLID之类的条件。经过大量的实践单元测试,您对使类“正确大小”和“连贯”的内容有了很好的认识。但是对于具有所有静态方法的类来说,没有好的规则,也没有真正的理由为什么您不应该将所有内容都捆绑在一起。该类已在您的编辑器中打开,那么到底呢?只需在此处添加新方法即可。一段时间后,您的应用程序变成了许多竞争的“上帝对象”,每个对象都试图统治世界。同样,将它们重构为较小的单元是非常主观的,并且很难判断您是否正确。

3)接口是Java最强大的功能之一。事实证明,类继承是有问题的,但是使用接口进行编程仍然是该语言最强大的技巧之一。(同上C#)全静态类无法将自身放入该模型。

4)它为您无法利用的重要OO技术敲门。因此,您可能只用一把锤子在工具箱中工作了数年,却没有意识到如果您也有螺丝刀,事情会变得多么容易。

4.5)它创建了最困难,最坚不可摧的编译时依赖关系。因此,例如,如果没有,FileSystem.saveFile()那就没有办法改变,除非在运行时伪装您的JVM。这意味着每个引用您的静态函数类的类都对该特定实现具有硬的编译时依赖性,这使得扩展几乎不可能,并且使测试变得极为复杂。您可以单独测试静态类,但是很难单独测试引用该类的类。

5)你会让你的同事发疯。与我合作的大多数专业人员都认真对待他们的代码,并至少注意某些级别的设计原则。抛开某种语言的核心意图,这将使他们脱颖而出,因为他们将不断重构代码。

当我使用某种语言时,我总是会尽量使用一种语言。因此,例如,当我使用Java时,我会使用良好的OO设计,因为那样的话,我真的会充分利用该语言。当我使用Python时,我将模块级函数与偶然的类混合在一起-我只能用Python编写类,但后来我认为我不会因为其擅长而使用该语言。

另一种策略是严重使用一种语言,他们抱怨这种语言引起的所有问题。但这几乎适用于任何技术。

Java的关键功能是管理可挂在一起的小型可测试单元的复杂性,因此它们易于理解。Java强调与实现无关的清晰接口定义-这是一个巨大的好处。这就是为什么它(以及其他类似的OO语言)仍然被广泛使用的原因。对于所有的冗长和仪式性,当我用一个大型Java应用程序完成时,我总是觉得这些想法在代码中比在项目中使用动态语言时更加清晰。

但是,这很困难。我见过人们遇到“全静态”的错误,很难说服他们。但是我看到他们克服困难时会感到宽慰。


您主要讲的是使事物小型化和模块化,以及将状态和功能保持在一起。没有什么可以阻止我对静态类执行此操作。他们可以有状态。您可以使用getter-setter封装它们。然后,您可以将系统分为封装功能和状态的小类。所有这些都适用于类和对象。这恰恰是我的两难选择:说我要在我的应用程序中添加新功能。显然,它将遵循一个单独的类别来服从SRP。但是,为什么我应该实例化该类呢?这是我不明白的。
Aviv Cohn 2014年

我的意思是:有时很明显为什么我想要对象。我将使用从编辑到问题的示例:我有一个构成旋律的应用程序。我可以插入MelodyGenerators产生不同的旋律,有不同的音阶。旋律是对象,因此我可以将它们放进堆栈,然后再回弹旧的,等等。在这种情况下,我很清楚为什么我应该使用对象。我不明白的是为什么我应该使用对象来表示系统的“静态”部分:例如保存文件机制。为什么这不应该只是一个静态类?
Aviv Cohn 2014年

具有静态方法的类必须将其状态外部化。有时,这是非常重要的分解技术。但是大多数时候您都想封装状态。一个具体的例子:几年前,一位同事用Javascript编写了一个“日期选择器”。那是一场噩梦。顾客不断抱怨它。因此,我将其重构为“日历”对象,突然间我实例化了多个对象,将它们并排放置,在几个月和几年之间跳转,等等。它是如此丰富,以至于我们实际上不得不关闭功能。实例化为您提供了规模。
罗布(Rob)

3
“ OO背后的想法是将功能与状态绑定在一起,使数据和功能的小单元通过隐藏状态并仅公开在该上下文中有意义的功能来保留其语义。” 这根本是错误的。OO并不意味着可变状态,并且抽象不是OO专有的。实现抽象有两种方法,对象只是其中一种(另一种是抽象数据类型)。
Doval 2014年

1
@Doval我不明白为什么每次对OO的讨论都必须变成对函数编程的讨论。
罗布2014年

6

您问:

但是与静态类相比,对象还有更多优势吗?

在提出这个问题之前,您已列出了多态性,并将作为使用对象的两个好处进行了介绍。我想说的是OO范式的特征。封装是OO范式的另一个功能。

但是,这些不是好处。好处是:

  1. 更好的抽象
  2. 更好的可维护性
  3. 更好的可测试性

你说:

但是最近,当我停下来想一想时,我意识到在很多我通常使用对象的地方,静态类将与对象相同。

我认为您在那里有一个正确的观点。从本质上讲,编程不过是数据的转换和基于数据的副作用的创建。有时转换数据需要辅助数据。其他时间则没有。

在处理转换的第一类时,辅助数据必须作为输入传递或存储在某处。对于此类转换,对象是比静态类更好的方法。对象可以存储辅助数据并在适当的时间使用它。

对于第二类转换,静态类和对象一样好,即使不是更好。数学函数是此类的经典示例。大多数标准C库函数也属于此类。

您问:

对于一个对象,代码的调用行将如下所示: Thing thing = fileLoader.load(file);

对于静态类,它看起来像这样: Thing thing = FileLoader.load(file);

有什么不同?

如果FileLoader没有,永远,需要存储任何数据,我会用第二种方法去。如果存在需要辅助数据来执行操作的可能性,第一种方法是更安全的选择。

您问:

除了上面列出的两个对象以外,对象还有其他优点吗?请解释。

我列出了使用OO范例的好处(优势)。我希望他们能自我解释。如果没有,我会很乐意阐述。

您问:

但是,系统的“静态”部分又将如何传递呢?例如-一种“保存文件”机制。为什么要在对象而不是静态类中实现它?

这是一个静态类根本不会做的例子。有多种方法可以将应用程序数据保存到文件中:

  1. 将其保存为CSV文件。
  2. 将其保存在XML文件中。
  3. 将其保存在json文件中。
  4. 将其保存为二进制文件,并直接转储数据。
  5. 将其直接保存到数据库中的某个表中。

提供此类选项的唯一方法是创建一个接口并创建实现该接口的对象。

结论

使用正确的方法解决当前的问题。最好不要对一种方法比另一种方法守信。


此外,如果要挽救了许多不同的对象类型(类)需要(如MelodyChordImprovisationsSoundSample等),那么这将是经济的简化抽象通过实施。
rwong

5

有时取决于语言和上下文。例如,PHP用于服务请求的脚本在脚本的整个生命周期中始终具有一个要服务的请求和一个要生成的响应,因此对请求采取行动并生成响应的静态方法可能是适当的。但是在使用编写的服务器中Node.js,可能同时存在许多不同的请求和响应。所以第一个问题是-您确定静态类确实对应于一个单例对象吗?

其次,即使您有单身人士,使用对象也可以通过Dependency_injectionFactory_method_pattern之类的技术来利用多态。这通常用于各种Inversion_of_control模式中,并且对于创建用于测试,记录等的模拟对象很有用。

您提到了上面的优点,所以这里没有一个:继承。许多语言都无法像重写实例方法一样覆盖静态方法。通常,继承和覆盖静态方法要困难得多。


谢谢回答。您认为:在创建系统的“静态”部分时,不会“传递”某些东西-例如,播放声音的系统部分或将数据保存到文件的系统部分:我应该在对象还是静态类中实现它?
Aviv Cohn 2014年

3

除了Rob Y的帖子


只要load(File file)可以将您的功能与使用的所有其他功能清楚地分开,就可以使用静态方法/类。您将状态外部化(这不是一件坏事),并且您也不会获得冗余,因为您可以例如部分应用或管理您的功能,从而不必重复自己。(实际上与使用factory模式相同或相似)

但是,一旦其中两个功能开始普遍使用,您就希望能够以某种方式对它们进行分组。想象一下,您不仅具有load功能,而且具有hasValidSyntax功能。你会怎么做?

     if (FileLoader.hasValidSyntax(myfile))
          Thing thing = FileLoader.load(myfile);
     else
          println "oh noes!"

看到两个参考myfile这里?您开始重复自己,因为每次呼叫都必须传递您的外部状态。Rob Y描述了如何内部化状态(此处为file),以便您可以像这样进行操作:

     FileLoader myfileLoader = new FileLoader(myfile)
     if (myfileLoader.hasValidSyntax())
          Thing thing = myfileLoader.load();
     else
          println "oh noes!"

不得不两次通过同一个对象是一个肤浅的症状。它可能指示的实际问题是某些工作是多余执行的-如果hasValidSyntax()并且load()不允许重用状态(部分已解析文件的结果),则文件可能必须打开两次,解析两次并关闭两次。
rwong 2014年

2

使用静态类时的一个主要问题是它们迫使您隐藏依赖项,并迫使您依赖实现。在以下构造函数签名中,您的依赖性是什么:

public Person(String name)

好吧,从签名来看,一个人只需要一个名字,对吗?好吧,如果实现不是:

public Person(String name) {
    ResultSet rs = DBConnection.getPersonFilePathByName(name);
    File f = FileLoader.load(rs.getPath());
    PersonData.setDataFile(f);
    this.name = name;
    this.age = PersonData.getAge();
}

因此,一个人不只是被实例化。实际上,我们从数据库中提取数据,这为我们提供了文件的路径,然后必须对其进行解析,然后必须整理出我们想要的真实数据。这个例子显然是最重要的,但事实证明了这一点。但是,等等,还有更多!我编写以下测试:

public void testPersonConstructor() {
    Person p = new Person("Milhouse van Houten");
    assertEqual(10, p.age);
}

这应该通过,对吗?我的意思是,我们封装了所有其他内容,对吗?好吧,实际上它炸毁了一个例外。为什么?哦,是的,我们不知道的隐藏依赖项具有全局状态。在DBConnection需要初始化和连接。在FileLoader与初始化需要FileFormat的物品(如XMLFileFormatCSVFileFormat)。从来没有听说过那些?好吧,这就是重点。您的代码(和您的编译器)无法告诉您您需要这些东西,因为静态调用会隐藏这些依赖项。我说考试了吗 我的意思是,这个新的初级开发人员刚刚在您的最新版本中发布了类似的内容。毕竟,编译=有效,对吧?

此外,假设您使用的系统没有运行MySQL实例。或者,假设您使用的是Windows系统,但您的DBConnection课程只发送到Linux机器(带有Linux路径)上的MySQL服务器。或者,假设您使用的系统DBConnection不为您返回读/写的路径。这意味着在任何一种情况下尝试运行或测试该系统都将失败,这不是由于代码错误,而是由于设计错误限制了代码的灵活性并将您与实现联系在一起。

现在,让我们说,我们要记录一个特定Person实例对数据库的每次调用,该调用要经过一定的麻烦路径。我们可以登录DBConnection,但是这会记录所有内容,造成很多混乱,并且很难区分我们要跟踪的特定代码路径。但是,如果我们对DBConnection实例使用依赖项注入,则可以简单地在装饰器中实现接口(或扩展类,因为我们对一个对象都有两个选择)。对于静态类,我们不能注入依赖项,我们不能实现接口,不能将其包装在装饰器中,也不能扩展类。我们只能在代码深处的某个地方直接调用它。因此我们被迫 对实现具有隐藏的依赖性。

这总是不好吗?不一定,但是最好倒转您的观点并说:“是否有充分的理由不应该将其作为实例?” 而不是“是否有充分的理由应该将其作为实例?” 如果您真的可以说您的代码Math.abs()在实现和使用方式上都将像一样坚定不移(无状态),那么您可以考虑将其静态化。但是,在静态类上拥有实例可以为您提供一个灵活性世界,但事后并非总是那么容易获得。它还可以使您更加清楚代码依赖性的本质。


隐藏的依赖关系是非常好的一点。我几乎忘了我的成语“应该从代码的使用方式,而不是从代码的实现方式来判断代码的质量”。
欣快感2014年

但是隐藏的依赖关系可以存在于非静态类中吗?所以我不明白为什么这将是静态类而不是非静态类的缺点。
valenterry 2014年

@valenterry是的,我们也可以隐藏非静态依赖项,但是关键是隐藏非静态依赖项是一种选择。我可以new在构造函数中放置一堆对象(通常不建议这样做),但是我可以注入它们。对于静态类,无法注入它们(无论如何在Java中都不容易,对于允许这样做的语言,这将是其他讨论),因此没有选择。这就是为什么我在答案中强调被迫隐藏大量依赖项的想法。
cbojar 2014年

但是,您可以在每次调用该方法时通过提供它们作为参数注入它们。因此,您不会被迫隐藏它们,而是被迫将其明确显示,以便每个人都看到“啊哈,该方法取决于这些参数”,而不是“啊哈,该方法取决于这些(部分)参数和一些隐藏的内部状态,我不知道”知道”。
valenterry 2014年

就我所知,在Java中,如果不经历整个反射过程就无法提供静态类作为参数。(我尝试了所有可以想到的方法。如果您有办法,我想看看。)如果您选择这样做,则实际上是在破坏内置类型系统(参数是Class,那么我们确保它是正确的类),或者您正在使用鸭子输入(我们确保它响应正确的方法)。如果要选择后者,则应重新评估您的语言选择。如果你想要做的是前者,情况做就好了,并且内置的。
cbojar

1

只是我的两分钱。

对于您的问题:

但是,系统的“静态”部分又将如何传递呢?例如-一种“保存文件”机制。为什么要在对象而不是静态类中实现它?

您可以问自己,播放声音的系统部分或将数据保存到文件的系统部分的机制是否会发生变化。如果答案是肯定的,则表明您应该使用abstract class/ 对其进行抽象interface。您可能会问,我怎么知道未来的事情?绝对不能。因此,如果事物是​​无状态的,则可以使用“静态类”,例如java.lang.Math,否则,请使用面向对象的方法。


有一个不言而喻的建议:三击罢工,您重构,这表明可以坚持下去,直到真正需要进行更改为止。我所说的“改变”是:需要保存一种以上的对象;或需要保存为一种以上的文件格式(存储类型);或已保存数据的复杂性急剧增加。当然,如果可以肯定的是这种改变很快就会发生,那么可以预先采用一种更加灵活的设计。
rwong
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.