该书“优雅的对象。 Java版»

图片 嗨,habrozhiteli! 本书认真修改了面向对象编程(OOP)的本质和原理,可以比喻为“ Lobachevsky OOP”。 拥有20年经验的开发人员Egor Bugaenko批判性地分析了OOP的教条,并提出以全新的方式研究这种范例。 因此,他认为静态方法,getter,setter和可变方法带有污名,并认为这是邪恶的。 对于新手程序员来说,这本书可能成为启迪或震惊,而对于经验丰富的程序员,这是必修课。

摘录“不要使用静态方法”


啊,静态方法...我最喜欢的主题之一。 我花了几年时间才意识到这个问题的重要性。 现在,我一直后悔花了很多时间来编写程序性软件而不是面向对象软件。 我是盲人,但现在我已经看到了。 静态方法在OOP中的问题甚至比没有NULL常量大。 原则上,静态方法不应该使用Java和其他面向对象的语言编写,但是,they,它们在那里。 我们不应该知道Java中的static关键字之类的东西,但是,a,是强制的。.我不知道是谁将它们带到Java的,但是它们纯属邪恶..静态方法,不是此功能的作者。 我希望如此。

让我们看看什么是静态方法以及为什么仍要创建它们。 假设我需要通过HTTP请求加载网页的功能。 我创建了这样一个“类”:

class WebPage { public static String read(String uri) { //  HTTP- //     UTF8- } } 

使用它非常方便:

 String html = WebPage.read("http://www.java.com"); 

read()方法属于我反对的方法类。 我建议改为使用一个对象(我还根据2.4节的建议更改了方法名称):

 class WebPage { private final String uri; public String content() { //  HTTP- //     UTF8- } } 

使用方法如下:

 String html = new WebPage("http://www.java.com") .content(); 

您可以说它们之间没有太大区别。 静态方法的工作速度更快,因为我们不需要每次下载网页时都创建一个新对象。 只需调用static方法,它将完成工作,您将获得结果并将继续工作..无需弄乱对象和垃圾收集器。 另外,我们可以将几个静态方法分组到一个实用程序类中,并命名为WebUtils。

这些方法将有助于加载网页,获取统计信息,确定响应时间等。其中将包含许多方法,并且使用它们既简单又直观。 另外,如何应用静态方法也是很直观的。 每个人都了解他们的工作方式。 只需编写WebPage.read(),然后-您猜对了! -页面将被读取。 我们给了计算机指令,然后它执行了..简单明了,对吧? 不!

在任何情况下,静态方法都可以清楚地表明一个糟糕的程序员根本不了解OOP。 在任何情况下都没有理由采用静态方法。 关心性能并不重要。 静态方法是对面向对象范例的嘲弄。 它们以Java,Ruby,C ++,PHP和其他语言存在。 不幸的是 我们不能扔掉它们,也不能重写所有包含静态方法的开源库,但是我们可以停止在代码中使用它们。

我们必须停止使用静态方法。

现在,让我们从几个不同的角度来看它们,并讨论它们的实际缺点。 我可以提前将它们概括给您:静态方法会降低软件的可维护性。 这不应该让您感到惊讶。 一切都归结为可维护性。

目标与计算机思维


最初,我将此部分称为“目标与过程思维”,但后来将其重命名。 “过程思维”的含义几乎相同,但是“像计算机一样思考”一词更能说明问题。.我们从汇编,C,COBOL,Basic,Pascal等早期编程语言继承了这种思维方式。 范例的基础是计算机为我们工作,我们告诉他该怎么做,并给他明确的指示,例如:

  CMP AX, BX JNAE greater MOV CX, BX RET greater: MOV CX, AX RET 

这是Intel 8086处理器的汇编程序“例程”,它查找并返回两个数字中较大的一个。 我们将它们分别放入寄存器AX和BX中,结果落入寄存器CX中。 这是完全相同的C代码:

 int max(int a, int b) { if (a > b) { return a; } return b; } 

“这怎么了?” -你问。 没什么..这个代码一切都很好-它可以正常工作,这就是所有计算机的工作方式。 他们希望我们给他们指示,他们将一个接一个地跟随。。多年来,我们以这种方式编写程序。 这种方法的优点是我们保持靠近处理器的方向,以指导其进一步移动。 我们掌控一切,计算机将按照我们的指示进行操作。 我们告诉计算机如何找到两个较大的数字。 我们做出决定,他跟随他们。 从脚本的开始到结束,执行流程始终是一致的。

这种线性思维方式称为“像计算机一样思考”。 计算机有时会开始执行指令,有时会完成指令。 用C编写代码时,我们不得不这样思考。 以分号分隔的运算符从上到下。 此样式是从汇编程序继承的。
尽管比汇编程序更高级的语言具有过程,子例程和其他抽象机制,但它们并不能消除一致的思维方式,程序仍从上到下运行。 编写小型程序时,这种方法没有什么问题,但是在更大范围内,很难像这样想。

看一下用功能编程语言Lisp编写的相同代码:

 (defun max (ab) (if (> ab) ab)) 

您能说出这段代码的执行开始和结束的地方吗? 不行 我们不知道处理器将如何获得结果,或者if函数将如何工作。 我们离处理器很远。 我们认为它是功能,而不是计算机。 当我们需要新事物时,我们对其进行定义:

 (def x (max 5 9)) 

我们定义而不是给处理器指令。 通过这一行,我们将x绑定到(最大5 9)。 我们不要求计算机计算两个数字中的较大者。 我们简单地说x是两个数字中的较大者。 我们不控制如何以及何时计算它。 请注意,这很重要:x是较大的数字。 关系“是”(“待”,“待”)是功能,逻辑和面向对象的编程范例与过程范例之间的区别。

凭着计算机的思维定势,我们掌控着指令流。 使用面向对象的思维方式,我们只需确定谁是谁,并让他们在需要时进行交互即可。 这是在OOP中计算两个数字中较大的一个的样子:

 class Max implements Number { private final Number a; private final Number b; public Max(Number left, Number right) { this.a = left; this.b = right; } } 

因此,我将使用它:

 Number x = new Max(5, 9); 

看,我不是在计算两个数字中的较大者。 我确定x是两个数字中的较大者。 我并不真正担心Max类对象中的内容以及它如何实现Number接口。 我不给处理器有关此计算的说明。 我只是实例化对象。 这与Lisp中的def非常相似。从这个意义上说,OOP与函数编程非常相似。

相反,OOP中的静态方法与C或汇编器中的子例程相同。 它们与OOP不相关,因此迫使我们以面向对象的语法编写过程代码。 这是Java代码:

 int x = Math.max(5, 9); 

这是完全错误的,不应在实际的面向对象设计中使用。

声明式与命令式


命令式编程“用改变程序状态的运算符来描述计算”。另一方面,声明式编程“在不描述其执行流程的情况下表达了计算逻辑”(我引用维基百科)。 实际上,我们在前几页中都谈到了这一点。 命令式编程与计算机的操作类似-顺序执行指令。 声明式编程更接近自然思维方式,在这种思维方式中我们拥有实体和实体之间的关系。 显然,声明性编程是一种更强大的方法,但是命令性方法对于过程程序员更易于理解。 为什么声明式方法更强大? 不要切换,几页之后我们就到了重点。

这与静态方法有什么关系? 不管它是静态方法还是对象,我们都必须在某个地方写(a> b),对吗? 是的,完全正确。 静态方法和对象都只是if语句的包装,该语句执行将a与b比较的任务。 区别在于其他类,对象和方法如何使用此功能。 这是一个很大的区别。 考虑一个例子。
说我有一个间隔,该间隔限制为两个整数,一个应该落入其中的整数..我必须确保它是。 如果max()方法是静态的,这就是我要做的事情:

 public static int between(int l, int r, int x) { return Math.min(Math.max(l, x), r); } 

我们需要在()之间创建另一个静态方法,该方法使用两个可用的静态方法Math.min()和Math.max()。 只有一种方法可以执行此操作-命令式方法,因为该值会立即计算出来。 拨打电话时,我立即得到结果:

 int y = Math.between(5, 9, 13); //  9 

在()之间调用后,我得到了数字9。 拨打电话后,我的处理器将立即开始进行此计算。 这是当务之急。 然后,声明式方法是什么样的?

在这里,看看:

 class Between implements Number { private final Number num; Between(Number left, Number right, Number x) { this.num = new Min(new Max(left, x), right); } @Override public int intValue() { return this.num.intValue(); } } 

这就是我将如何使用它:

 Number y = new Between(5, 9, 13); //   ! 

感觉有所不同? 她非常重要。 这种风格是声明性的,因为我没有告诉处理器需要立即进行计算。 我只是确定了它的含义,然后由用户决定何时(以及是否有必要)使用intValue()方法来计算变量y。 也许永远也不会计算出来,我的处理器永远也不会知道数字是9 ..我所做的只是声明y是什么。 刚刚宣布。 我还没有给处理器做任何工作。 如定义中所示,表达的逻辑未描述过程。

我已经听到:“好,我了解你。 有两种方法-声明式和过程式,但是为什么第一种优于第二种?” 前面我提到过,显然声明式方法更强大,但没有解释原因。 现在我们已经通过示例研究了这两种方法,我们将讨论声明式方法的优点。

首先,它更快。 乍一看,它似乎较慢。 但是,如果您仔细看一看,就会发现实际上它更快,因为性能的优化完全掌握在我们手中。 确实,至少在编写本书时可用的大多数编程语言中,创建Between类的实例比调用static between()方法要花费更长的时间。我真的希望我们在不久的将来拥有一种语言在其中实例化对象将与调用方法一样快。 但是我们还没有来找他。 这就是为什么当执行路径简单明了时声明式方法较慢的原因。

如果我们在谈论对静态方法的简单调用,那么它肯定比创建对象的实例并调用其方法要快。 但是,如果我们有许多静态方法,那么在解决问题时将依次调用它们,而不仅仅是处理我们真正需要的结果。 怎么样:

 public void doIt() { int x = Math.between(5, 9, 13); if (/*  ? */) { System.out.println("x=" + x); } } 

在此示例中,无论是否需要它的值,我们都将计算x。在两种情况下,处理器都会找到值9.使用声明性方法的下一个方法是否能像上一个方法一样快地工作?

 public void doIt() { Integer x = new Between(5, 9, 13); if (/*  ? */) { System.out.println("x=" + x); } } 

我认为声明性代码会更快。 最好进行优化。 它并没有告诉处理器该怎么做,相反,它允许处理器确定真正需要何时何地需要结果-计算是按需执行的。

底线是声明式方法更快,因为它是最佳方法。 与面向对象编程中的命令式相比,这是支持声明式方法的第一个论点。 命令式样式在OOP中绝对不占地方,并且第一个原因是性能优化..您不应该说控制代码的优化越多,遵循的就越多。 我们自己做,而不是将计算过程的优化留给编译器,虚拟机或处理器。

第二个论点是多态性。 简而言之,多态性是打破代码块之间的依赖关系的能力。 假设我想更改确定数字是否落入特定间隔的算法。 它本身很原始,但是我想更改它。 我不想使用Max和Min类。 我希望他使用if-then-else语句进行比较。.这是声明式执行的方法:

 class Between implements Number { private final Number num; Between(int left, int right, int x) { this(new Min(new Max(left, x), right)); } Between(Number number) { this.num = number; } } 

这与上一个示例中的Between类相同,但是具有一个附加的构造函数。 现在,我可以将其与另一种算法一起使用:

 Integer x = new Between( new IntegerWithMyOwnAlgorithm(5, 9, 13) ); 

这可能不是最好的示例,因为Between类非常原始,但是我希望您理解我的意思。 Between类很容易与Max和Min类分开,因为它们是类。 在面向对象的编程中,对象是完全公民,但静态方法不是。 我们可以将对象作为参数传递给构造函数,但是我们不能对static方法执行相同的操作。 在OOP中,对象与对象关联,与对象通信并与它们交换数据。 为了将一个对象与其他对象完全分离,我们必须确保该对象在其任何方法(请参见第3.6节)以及主构造函数中均不使用new运算符。

让我重复一遍:要完全将一个对象与其他对象分离,只需确保new运算符的任何方法(包括主构造函数)都未使用new运算符。

您可以使用命令性代码片段进行相同的解耦和重构吗?

 int y = Math.between(5, 9, 13); 

不,你不能。 静态之间()方法使用两种静态方法,即min()和max(),在完全重写之前,您将无法做任何事情。 以及如何重写它? 将第四个参数传递给新的静态方法?

它看起来有多丑? 我觉得很好

这是我支持声明式编程风格的第二个论点-它减少了对象的内聚力并使它非常优雅。.更不用说内聚力越少意味着可维护性越强的事实。

主张声明式方法优于命令式的第三个论点-声明式方法说明了结果,而命令式说明了获得结果的唯一方法。 第二种方法远没有第一种方法直观。 我必须首先“执行”脑海中的代码,以了解预期的结果。 这是一种必要的方法:

 Collection<Integer> evens = new LinkedList<>(); for (int number : numbers) { if (number % 2 == 0) { evens.add(number); } } 

要了解这段代码的作用,我必须仔细阅读一下,可视化此循环。.实际上,我必须执行处理器的作用-遍历整个数字数组并将偶数放入新列表中。 这是相同的算法,以声明性风格编写:

 Collection<Integer> evens = new Filtered( numbers, new Predicate<Integer>() { @Override public boolean suitable(Integer number) { return number % 2 == 0; } } ); 

这一段代码比上一段代码更接近英语。 它的内容如下:“ evens是一个过滤的集合,仅包含那些偶数元素。” 我不知道Filtered类如何创建集合-它使用for语句还是其他。 在阅读此代码时,我只需要知道该集合已被过滤即可。 隐藏实现细节,并表示行为。

我意识到,对于本书的某些读者来说,理解第一个片段会更容易..它更短一些,并且与您每天在处理的代码中看到的非常相似。 我向您保证,这是一个习惯问题。 这是一种欺骗性的感觉。 从对象及其行为而不是算法及其执行开始思考,您将获得真正的认知。 声明式样式直接与对象及其行为有关,而命令式则与算法及其执行直接有关。

如果您发现此代码很丑陋,请尝试使用Groovy,例如:

 def evens = new Filtered( numbers, { Integer number -> number % 2 == 0 } ); 

第四个参数是代码完整性。 再看一下前两个片段。 请注意,在第二个片段中,我们将evens声明为一个运算符-evens = Filtered(...)。 这意味着负责计算此集合的所有代码行都彼此相邻,并且不能错误地分开。 相反,在第一个片段中没有明显的“胶合”线。 您可以轻松地错误地更改其顺序,并且算法会中断。

在这样简单的代码中,这是一个小问题,因为算法很明显。 但是,如果命令性代码的片段较大(例如50行),则可能很难理解哪些代码行彼此相关..我们在不可变对象的讨论中较早地讨论了时间级联的问题。.声明式编程风格也有助于消除这种级联,因此可维护性得到改善。

可能仍然有争论,但是从我的角度来看,我引用了与OOP相关的最重要的观点。 我希望我能够说服您声明式的样式是您所需要的。 你们中有些人可能会说:“是的,我理解您的意思。 在适当的时候,我将结合声明式和命令式方法。 当我需要快速地做一些简单的事情时(例如计算两个数字中的较大者),我将使用有意义的对象和静态方法。“ ..”不,你错了!“ -我会回答你的。 您不得将它们组合在一起。.切勿使用命令式样式。 这不是教条..这有一个非常务实的解释。

命令式不能与纯粹的声明式技术结合使用。 , — .

, — max() min(). , . , , .. — Between, between(). ? , , , , . . , Between. , - , .

- : , — . .

« ! — . — ?» … , . - , - ( ). , , — . , .. , . , , — , , , . , Apache Commons FileUtils.readLines(), . :

 class FileLines implements Iterable<String> { private final File file; public Iterator<String> iterator() { return Arrays.asList( FileUtils.readLines(this.file) ).iterator(); } } 

, , :

 Iterable<String> lines = new FileLines(f); 

FileLines, . . , , — FileLines. , .

-


- , , ( -).. , java.lang.Math — -. Java, Ruby , , . ? . 1.1 , — . - , :

 class Math { private Math() { //   } public static int max(int a, int b) { if (a < b) { return b; } return a; } } 

, -, , , . , , , .

- — - . - — — . , , . - — .. .

«»


«» — , , . , , . :

 class Math { private static Math INSTANCE = new Math(); private Math() {} public static Math getInstance() { return Math.INSTANCE; } public int max(int a, int b) { if (a < b) { return b; } return a; } } 

. Math, INSTANCE.. , getInstance(). , . INSTANCE — getInstance().

«» , . , . , . , , , , -, . - Math, , :

 class Math { private Math() {} public static int max(int a, int b) { if (a < b) { return b; } return a; } } 

max():

 Math.max(5, 9); // - Math.getInstance().max(5, 9); //  

? , , . , -? Java-. , : « ». 例如:

 class User { private static User INSTANCE = new User(); private String name; private User() {} public static User getInstance() { return User.INSTANCE; } public String getName() { return this.name; } public String setName(String txt) { this.name = txt; } } 

, . «, ». -, , - . .. , -: « ».. . .. -, , :

 class User { private static String name; private User() {} public static String getName() { return User.name; } public static String setName(String txt) { User.name = txt; } } 

- , . , ? ? , — , , - — , . , , setInstance() getInstance(). , . , :

 Math.getInstance().max(5, 9); 

Math. , Math — , . , Math , . , . , , , -, . , , Math.max() -. ? :

 Math math = new FakeMath(); Math.setInstance(math); 

«» , . : - , . - — . - — — .

, ? -, , . 怎么了 , — , , .. . . , :

 #include <stdio> int line = 0; void echo(char* text) { printf("[%d] %s\n", ++line, text); } 

echo(), line. line *.-.. . Java , . Java, Ruby --, . 怎么了 . . . . , . , , GOTO.

, , - Java, «».. - , . .
. .

« ? — . — , , ?» , , , . - . ? !

, .

, , .. . . , . , : , , . . , , , . , — , 2.1.

. .


: , ()? , , , .. , ? Lisp, Clojure Haskell Java C++?

, :

 class Max implements Number { private final int a; private final int b; public Max(int left, int right) { this.a = left; this.b = right; } @Override public int intValue() { return this.a > this.b ? this.a : this.b; } } 

:

 Number x = new Max(5, 9); 

Lisp , :

 (defn max (ab) (if (> ab) ab)) 

, ? Lisp .

, , — . - , - -, . , - Java, , Java , -. — , . .

, - . -, Java, ( ) , . .


, . — - . — - , — , , :

 names = new Sorted( new Unique( new Capitalized( new Replaced( new FileNames( new Directory( "/var/users/*.xml" ) ), "([^.]+)\\.xml", "$1" ) ) ) ); 

, , -. , 3.2. , names, , , . , , , . .

? , , , .

, . Directory, FileNames, Replaced, Capitalized, Unique Sorted — , . . .

, ( ). , Unique — Iterable, . FileNames — , .
- . , .. - app.run(), . if, for, switch while. , .

if Java , . Java , If? :

 float rate; if (client.age() > 65){ rate = 2.5; } else { rate = 3.0; } 

- :

 float rate = new If( client.age() > 65, 2.5, 3.0 ); 

?

 float rate = new If( new Greater(client.age(), 65), 2.5, 3.0 ); 

, :

 float rate = new If( new GreaterThan( new AgeOf(client), 65 ), 2.5, 3.0 ); 

- . — , rate.

, , . if, for, switch while. If, For, Switch While. ?

, . . . , .. , .
, - — .

? , : . , . . . , — .

: static — , , .

»这本书的更多信息可以在出版商的网站上找到

20% — Java

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


All Articles