El libro "Objetos elegantes. Edición Java »

imagen Hola habrozhiteli! Este libro revisa seriamente la esencia y los principios de la programación orientada a objetos (OOP) y puede llamarse metafóricamente la "OOP Lobachevsky". Egor Bugaenko , un desarrollador con 20 años de experiencia, analiza críticamente los dogmas de OOP y ofrece mirar este paradigma de una manera completamente nueva. Entonces, él estigmatiza los métodos estáticos, captadores, establecedores, métodos mutables, creyendo que esto es malo. Para un programador novato, este volumen puede convertirse en iluminación o conmoción, y para un programador experimentado es una lectura obligatoria.

Extracto "No utilizar métodos estáticos"


Ah, métodos estáticos ... Uno de mis temas favoritos. Me tomó varios años darme cuenta de lo importante que es este problema. Ahora lamento todo el tiempo que pasé escribiendo software de procedimiento en lugar de software orientado a objetos. Estaba ciego, pero ahora lo he visto. Los métodos estáticos son tan grandes, si no un problema mayor en OOP que tener una constante NULL. Los métodos estáticos, en principio, no deberían haber estado en Java, y en otros lenguajes orientados a objetos, pero, por desgracia, están allí. No deberíamos saber cosas como la palabra clave estática en Java, pero, por desgracia, son forzadas ... No sé quién las trajo a Java, pero son pura maldad ... Métodos estáticos, no los autores de esta característica. Eso espero.

Veamos qué son los métodos estáticos y por qué aún los creamos. Digamos que necesito la funcionalidad de cargar una página web a través de solicitudes HTTP. Creo tal "clase":

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

Es muy conveniente usarlo:

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

El método read () pertenece a la clase de métodos a los que me opongo. Sugiero usar un objeto en su lugar (también cambié el nombre del método de acuerdo con las recomendaciones de la sección 2.4):

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

Aquí se explica cómo usarlo:

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

Puedes decir que no hay mucha diferencia entre ellos. Los métodos estáticos funcionan aún más rápido porque no necesitamos crear un nuevo objeto cada vez que necesitamos descargar una página web. Simplemente llame al método estático, hará el trabajo, obtendrá el resultado y continuará funcionando. No hay necesidad de meterse con los objetos y el recolector de basura. Además, podemos agrupar varios métodos estáticos en una clase de utilidad y nombrarla, por ejemplo, WebUtils.

Estos métodos ayudarán a cargar páginas web, obtener información estadística, determinar el tiempo de respuesta, etc. Habrá muchos métodos en ellos, y usarlos es simple e intuitivo. Además, cómo aplicar métodos estáticos también es intuitivo. Todos entienden cómo funcionan. Simplemente escriba WebPage.read () y, ¡lo adivinó! - la página será leída. Le dimos instrucciones a la computadora y la ejecuta. Simple y claro, ¿verdad? Y no!

Los métodos estáticos en cualquier contexto son un indicador inequívoco de un mal programador que no tiene idea de la POO. No hay justificación para aplicar métodos estáticos en ninguna situación. Cuidar el rendimiento no cuenta. Los métodos estáticos son una burla de un paradigma orientado a objetos. Existen en Java, Ruby, C ++, PHP y otros lenguajes. Desafortunadamente No podemos descartarlos, no podemos reescribir todas las bibliotecas de código abierto llenas de métodos estáticos, pero podemos dejar de usarlas en nuestro código.

Debemos dejar de usar métodos estáticos.

Ahora echemos un vistazo desde varias posiciones diferentes y discutamos sus defectos prácticos. Puedo generalizarlos de antemano: los métodos estáticos degradan el mantenimiento del software. Esto no debería sorprenderte. Todo se reduce a la mantenibilidad.

Objetivo versus pensamiento informático


Inicialmente, llamé a esta subsección Objetivo versus pensamiento procesal, pero luego la renombré. "Pensamiento procesal" significa casi lo mismo, pero la frase "pensar como una computadora" describe mejor el problema. Heredamos esta forma de pensar de los primeros lenguajes de programación como Assembly, C, COBOL, Basic, Pascal y muchos otros. La base del paradigma es que la computadora funciona para nosotros y le decimos qué hacer, dándole instrucciones explícitas, por ejemplo:

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

Esta es una "rutina" de ensamblador para el procesador Intel 8086. Encuentra y devuelve el mayor de los dos números. Los colocamos en los registros AX y BX, respectivamente, y el resultado cae en el registro CX. Aquí está exactamente el mismo código C:

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

"¿Qué tiene de malo eso?" - usted pregunta Nada ... Todo está bien con este código, funciona como debería, así es como funcionan todas las computadoras. Esperan que les demos instrucciones de que seguirán uno tras otro. Durante muchos años escribimos programas de esta manera. La ventaja de este enfoque es que nos mantenemos cerca del procesador, dirigiendo su movimiento adicional. Estamos al timón y la computadora sigue nuestras instrucciones. Le decimos a la computadora cómo encontrar el mayor de los dos números. Tomamos decisiones, él las sigue. El flujo de ejecución siempre es consistente, desde el comienzo del script hasta su final.

Este tipo lineal de pensamiento se llama "pensar como una computadora". La computadora en algún momento comienza a ejecutar instrucciones y en algún punto termina de hacerlo. Al escribir código en C, nos vemos obligados a pensar de esta manera. Los operadores separados por punto y coma van de arriba a abajo. Este estilo se hereda del ensamblador.
Aunque los lenguajes de un nivel más alto que el ensamblador tienen procedimientos, subrutinas y otros mecanismos de abstracción, no eliminan una forma consistente de pensar. El programa todavía se ejecuta de arriba a abajo. No hay nada de malo en este enfoque cuando se escriben programas pequeños, pero a mayor escala es difícil pensar así.

Eche un vistazo al mismo código escrito en el lenguaje de programación funcional Lisp:

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

¿Puedes decir dónde comienza y termina la ejecución de este código? No No sabemos cómo obtendrá el procesador el resultado o cómo funcionará la función if. Estamos muy distantes del procesador. Pensamos como una función, no como una computadora. Cuando necesitamos algo nuevo, lo definimos:

 (def x (max 5 9)) 

Definimos, no damos instrucciones al procesador. Con esta línea unimos x a (max 5 9). No le pedimos a la computadora que calcule el mayor de los dos números. Simplemente decimos que x es el mayor de los dos números. No controlamos cómo y cuándo se calculará. Tenga en cuenta que esto es importante: x es el mayor de los números. La relación "es" ("ser", "ser") es la diferencia entre un paradigma de programación funcional, lógico y orientado a objetos de un paradigma de procedimiento.

Con una mentalidad informática, estamos a la cabeza y controlamos el flujo de instrucciones. Con una forma de pensar orientada a objetos, simplemente determinamos quién es quién y los dejamos interactuar cuando lo necesitan. Así es como debería ser el cálculo del mayor de dos números en 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; } } 

Y así lo usaré:

 Number x = new Max(5, 9); 

Mira, no estoy calculando el mayor de los dos números. Determino que x es el mayor de los dos números. Realmente no me importa lo que hay dentro del objeto de la clase Max y cómo implementa exactamente la interfaz Number. No doy instrucciones al procesador con respecto a este cálculo. Acabo de instanciar el objeto. Esto es muy similar a def en Lisp .. En este sentido, OOP es muy similar a la programación funcional.

En contraste, los métodos estáticos en OOP son los mismos que las subrutinas en C o ensamblador. No están relacionados con OOP y nos obligan a escribir código de procedimiento en sintaxis orientada a objetos. Aquí está el código Java:

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

Esto es completamente incorrecto y no debe usarse en un diseño orientado a objetos real.

Estilo declarativo versus imperativo


La programación imperativa "describe los cálculos en términos de operadores que cambian el estado de un programa". La programación declarativa, por otro lado, "expresa la lógica del cálculo sin describir el flujo de su ejecución" (cito Wikipedia). De hecho, hablamos de esto en el transcurso de varias páginas anteriores. La programación imperativa es similar a lo que hacen las computadoras: ejecutar instrucciones secuencialmente. La programación declarativa está más cerca de la forma natural de pensar en la que tenemos entidades y relaciones entre ellos. Obviamente, la programación declarativa es un enfoque más poderoso, pero un enfoque imperativo es más comprensible para los programadores de procedimientos. ¿Por qué el enfoque declarativo es más poderoso? No cambie, y después de algunas páginas llegamos al punto.

¿Qué tiene que ver todo esto con los métodos estáticos? No importa si es un método u objeto estático, todavía tenemos que escribir if (a> b) en algún lugar, ¿verdad? Si exactamente. Tanto el método estático como el objeto son solo un contenedor en la instrucción if, que realiza la tarea de comparar a con b. La diferencia es cómo esta funcionalidad es utilizada por otras clases, objetos y métodos. Y esta es una diferencia significativa. Considéralo con un ejemplo.
Digamos que tengo un intervalo limitado a dos enteros, y un entero que debería caer en él. Tengo que asegurarme de que sea así. Esto es lo que tengo que hacer si el método max () es estático:

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

Necesitamos crear otro método estático, entre (), que use los dos métodos estáticos disponibles, Math.min () y Math.max (). Solo hay una forma de hacerlo: un enfoque imperativo, ya que el valor se calcula de inmediato. Cuando hago una llamada, obtengo inmediatamente el resultado:

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

Obtengo el número 9 justo después de llamar entre (). Cuando se realiza la llamada, mi procesador comenzará inmediatamente a trabajar en este cálculo. Este es un enfoque imperativo. ¿Y cómo se ve el enfoque declarativo?

Aquí, eche un vistazo:

 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(); } } 

Así es como lo usaré:

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

Siente la diferencia? Ella es extremadamente importante. Este estilo será declarativo, porque no le digo al procesador que los cálculos deben hacerse de inmediato. Simplemente determiné qué era y dejé que el usuario decidiera cuándo (y si era necesario) calcular la variable y utilizando el método intValue (). Tal vez nunca se calculará y mi procesador nunca sabrá cuál es el número 9. Todo lo que hice fue declarar qué es. Recién anunciado. Todavía no he dado ningún trabajo al procesador. Como se indica en la definición, expresó la lógica sin describir el proceso.

Ya escucho: “Ok, te entiendo. Hay dos enfoques: declarativo y de procedimiento, pero ¿por qué el primero es mejor que el segundo? Mencioné anteriormente que es obvio que el enfoque declarativo es más poderoso, pero no explicó por qué. Ahora que hemos examinado ambos enfoques con ejemplos, discutiremos las ventajas de un enfoque declarativo.

En primer lugar, es más rápido. A primera vista, puede parecer más lento. Pero si observa más de cerca, quedará claro que, de hecho, es más rápido, ya que la optimización del rendimiento está completamente en nuestras manos. De hecho, llevará más tiempo crear una instancia de la clase Between que llamar al método estático between (), al menos en la mayoría de los lenguajes de programación disponibles al momento de escribir este libro. Realmente espero que tengamos un lenguaje en el futuro cercano en el que crear instancias de un objeto será tan rápido como llamar a un método. Pero aún no hemos llegado a él. Es por eso que el enfoque declarativo es más lento ... cuando el camino de ejecución es simple y directo.

Si estamos hablando de una simple llamada a un método estático, entonces será más rápido que crear una instancia de un objeto y llamar a sus métodos. Pero si tenemos muchos métodos estáticos, se los llamará secuencialmente al resolver el problema, y ​​no solo para trabajar en los resultados que realmente necesitamos. ¿Qué tal esto?

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

En este ejemplo, calculamos x independientemente de si necesitamos su valor o no ... El procesador encontrará el valor 9 en ambos casos. ¿Funcionará el siguiente método con el enfoque declarativo tan rápido como el anterior?

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

Creo que el código declarativo será más rápido. Está mejor optimizado. Y no le dice al procesador qué hacer. Por el contrario, le permite decidir cuándo y dónde se necesita realmente el resultado: los cálculos se realizan a pedido.

La conclusión es que el enfoque declarativo es más rápido porque es óptimo. Este es el primer argumento a favor de un enfoque declarativo en comparación con el imperativo en la programación orientada a objetos. El estilo imperativo definitivamente no tiene cabida en OOP, y la primera razón para esto es la optimización del rendimiento. No debe decir que cuanto más controla la optimización del código, más se sigue. En lugar de dejar la optimización del proceso de cálculo al compilador, la máquina virtual o el procesador, lo hacemos nosotros mismos.

El segundo argumento es el polimorfismo. En pocas palabras, el polimorfismo es la capacidad de romper dependencias entre bloques de código. Supongamos que quiero cambiar el algoritmo para determinar si un número cae en un cierto intervalo. Es bastante primitivo en sí mismo, pero quiero cambiarlo. No quiero usar las clases Max y Min. Y quiero que realice una comparación usando las declaraciones if-then-else. Aquí se explica cómo hacerlo declarativamente:

 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; } } 

Esta es la misma clase Entre que en el ejemplo anterior, pero con un constructor adicional. Ahora puedo usarlo con otro algoritmo:

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

Probablemente este no sea el mejor ejemplo, ya que la clase Between es muy primitiva, pero espero que entiendan lo que quiero decir. La clase Between es muy fácil de separar de las clases Max y Min, ya que son clases. En la programación orientada a objetos, un objeto es un ciudadano completo, pero el método estático no lo es. Podemos pasar el objeto como argumento al constructor, pero no podemos hacer lo mismo con el método estático. En OOP, los objetos están asociados con objetos, se comunican con objetos e intercambian datos con ellos. Para separar completamente un objeto de otros objetos, debemos asegurarnos de que no utilice el nuevo operador en ninguno de sus métodos (ver sección 3.6), así como en el constructor principal.

Permítanme repetir: para separar completamente un objeto de otros objetos, solo debe asegurarse de que el nuevo operador no se utilice en ninguno de sus métodos, incluido el constructor principal.

¿Se puede hacer el mismo desacoplamiento y refactorización con un fragmento de código imperativo?

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

No, no puedes. El método static between () utiliza dos métodos estáticos, min () y max (), y no puede hacer nada hasta que lo reescriba por completo. ¿Y cómo puedes reescribirlo? ¿Pasar el cuarto parámetro al nuevo método estático?

¿Qué tan feo se verá? Pienso muy

Aquí está mi segundo argumento a favor de un estilo de programación declarativo: reduce la cohesión de los objetos y lo hace muy elegante. Sin mencionar el hecho de que menos cohesión significa más mantenibilidad.

El tercer argumento a favor de la superioridad del enfoque declarativo sobre el imperativo: el enfoque declarativo habla de los resultados, mientras que el imperativo explica la única forma de obtenerlos. El segundo enfoque es mucho menos intuitivo que el primero. Primero debo "ejecutar" el código en mi cabeza para entender qué resultado esperar. Aquí hay un enfoque imperativo:

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

Para comprender lo que hace este código, tengo que revisarlo, visualizar este ciclo. De hecho, tengo que hacer lo que hace el procesador: revisar toda la matriz de números y poner los números pares en la nueva lista. Aquí está el mismo algoritmo, escrito en un estilo declarativo:

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

Este código está mucho más cerca del idioma inglés que el anterior. Dice lo siguiente: "evens es una colección filtrada que incluye solo aquellos elementos que son pares". No sé exactamente cómo la clase Filtered crea una colección: ¿utiliza una declaración for u otra cosa? Todo lo que necesito saber al leer este código es que la colección se ha filtrado. Los detalles de implementación están ocultos y el comportamiento se expresa.

Me doy cuenta de que para algunos lectores de este libro fue más fácil percibir el primer fragmento ... Es un poco más corto y muy similar a lo que ves a diario en el código con el que estás tratando. Te aseguro que esto es una cuestión de costumbre. Este es un sentimiento engañoso. Comience a pensar en términos de objetos y su comportamiento, en lugar de algoritmos y su ejecución, y obtendrá una verdadera percepción. El estilo declarativo se relaciona directamente con los objetos y su comportamiento, y el imperativo, con los algoritmos y su ejecución.

Si encuentra este código feo, pruebe Groovy, por ejemplo:

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

El cuarto argumento es la integridad del código. Eche otro vistazo a los dos fragmentos anteriores. Tenga en cuenta que en el segundo fragmento declaramos pares como un operador: pares = filtrado (...). Esto significa que todas las líneas de código responsables de calcular esta colección están una al lado de la otra y no se pueden separar por error. Por el contrario, en el primer fragmento no hay un "pegado" obvio de las líneas. Puede cambiar fácilmente su orden por error y el algoritmo se romperá.

En un código tan simple, este es un pequeño problema, ya que el algoritmo es obvio. Pero si el fragmento del código imperativo es mayor, digamos 50 líneas, puede ser difícil entender qué líneas de código están relacionadas entre sí. Discutimos el problema de la concatenación temporal un poco antes, durante la discusión de los objetos inmutables. El estilo de programación declarativa también ayuda a eliminar esta concatenación. , por lo que se mejora la mantenibilidad.

Probablemente todavía haya argumentos, pero he citado el más importante, desde mi punto de vista, de aquellos relacionados con la POO. Espero haber podido convencerte de que el estilo declarativo es lo que necesitas. Algunos de ustedes pueden decir: “Sí, entiendo lo que quieres decir. Combinaré enfoques declarativos e imperativos cuando sea apropiado. Usaré objetos donde tenga sentido, y métodos estáticos cuando necesite hacer algo rápido rápidamente, como calcular el mayor de dos números. ".." ¡No, te equivocas! " - Te responderé. No debes combinarlos. Nunca uses un estilo imperativo. Esto no es un dogma. Tiene una explicación muy pragmática.

El estilo imperativo no se puede combinar con el declarativo puramente técnico. , — .

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

Los métodos estáticos recuerdan el cáncer de un software orientado a objetos: una vez que les permita establecerse en el código, no podremos deshacernos de ellos; su colonia solo crecerá. Simplemente omítalos en principio.

« ! — . — ?» … , . - , - ( ). , , — . , .. , . , , — , , , . , 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(); } } 

Ahora, para leer todas las líneas de un archivo de texto, nuestra aplicación tendrá que hacer lo siguiente:

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

Solo se llamará a un método estático dentro de la clase FileLines y con el tiempo podremos deshacernos de él. O nunca sucederá. Pero la conclusión es que en nuestro código, los métodos estáticos no se llamarán a ninguna parte, excepto a un lugar: la clase FileLines. Entonces aislamos a los muertos, lo que nos permite tratar con ellos gradualmente.

Clases de utilidad


- , , ( -).. , 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; } } 

, -, , , . , , , .

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

«»


El patrón Singleton es una técnica popular que dice ser un reemplazo para los métodos estáticos. De hecho, solo habrá un método estático en la clase, y el singleton casi se verá como un objeto real. Sin embargo, él no es:

 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; } } 

Así es como se usará el método max ():

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

Cual es la diferencia Parece que la segunda línea es más larga, pero hace lo mismo. ¿Por qué reinventar singleton si ya teníamos métodos estáticos y clases de utilidad? A menudo hago esta pregunta en entrevistas con programadores de Java. Lo primero que escucho generalmente en respuesta es: "Singleton le permite encapsular un estado". Por ejemplo:

 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; } } 

Esta clase de utilidad almacena el estado, y no hay diferencia entre ella y el singleton mencionado. Entonces, ¿cuál es el problema? ¿Y cuál es la respuesta correcta? La única respuesta verdadera es que un singleton es una dependencia que se puede romper, y una clase de utilidad es una conexión estrecha codificada que no se puede romper. En otras palabras, la ventaja de los singletones es que puede agregarles el método setInstance () junto con getInstance (). Esta respuesta es correcta, aunque la escucho con poca frecuencia. Supongamos que uso singleton de la siguiente manera:

 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, ( ) , . .


Parece que acuñé el término. Los decoradores enlazables son simplemente objetos envolventes sobre otros objetos. Son decoradores, un patrón bien conocido de diseño orientado a objetos, pero se vuelven compostables cuando los combinamos en estructuras multicapa, por ejemplo:

 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 — , , .

»Se puede encontrar más información sobre el libro en el sitio web del editor

20% — Java

Source: https://habr.com/ru/post/es418157/


All Articles