JavaRush /Blog Java /Random-ES /Guía de estilo de programación general
pandaFromMinsk
Nivel 39
Минск

Guía de estilo de programación general

Publicado en el grupo Random-ES
Este artículo es parte del curso académico "Java avanzado". Este curso está diseñado para ayudarle a aprender cómo utilizar eficazmente las funciones de Java. El material cubre temas "avanzados" como creación de objetos, competencia, serialización, reflexión, etc. El curso le enseñará cómo dominar eficazmente las técnicas de Java. Detalles aquí .
Contenido
1. Introducción 2. Alcance de la variable 3. Campos de clase y variables locales 4. Argumentos de método y variables locales 5. Boxing y Unboxing 6. Interfaces 7. Cadenas 8. Convenciones de nomenclatura 9. Bibliotecas estándar 10. Inmutabilidad 11. Pruebas 12. Siguiente. .. 13. Descargar el código fuente
1. Introducción
En esta parte del tutorial continuaremos nuestra discusión de los principios generales de un buen estilo de programación y diseño responsivo en Java. Ya hemos visto algunos de estos principios en capítulos anteriores de la guía, pero se darán muchos consejos prácticos con el objetivo de mejorar las habilidades de un desarrollador de Java.
2. Alcance variable
En la tercera parte ("Cómo diseñar clases e interfaces") discutimos cómo se pueden aplicar la visibilidad y la accesibilidad a los miembros de clases e interfaces, dadas las restricciones de alcance. Sin embargo, todavía no hemos analizado las variables locales que se utilizan en las implementaciones de métodos. En el lenguaje Java, cada variable local, una vez declarada, tiene un alcance. Esta variable se vuelve visible desde el lugar donde se declara hasta el punto donde se completa la ejecución del método (o bloque de código). Generalmente la única regla a seguir es declarar una variable local lo más cerca posible del lugar donde se utilizará. Permítanme ver un ejemplo típico: for( final Locale locale: Locale.getAvailableLocales() ) { // блок códigoа } try( final InputStream in = new FileInputStream( "file.txt" ) ) { // блока códigoа } en ambos fragmentos de código, el alcance de las variables se limita a los bloques de ejecución donde se declaran estas variables. Cuando se completa el bloque, el alcance finaliza y la variable se vuelve invisible. Esto parece más claro, pero con el lanzamiento de Java 8 y la introducción de lambdas, muchos de los modismos más conocidos del lenguaje que utilizan variables locales se están volviendo obsoletos. Permítanme darles un ejemplo del ejemplo anterior usando lambdas en lugar de un bucle: Arrays.stream( Locale.getAvailableLocales() ).forEach( ( locale ) -> { // блок códigoа } ); se puede ver que la variable local se ha convertido en un argumento para la función, que a su vez se pasa como argumento al método forEach .
3. Campos de clase y variables locales.
Cada método en Java pertenece a una clase específica (o, en el caso de Java8, a una interfaz donde el método se declara como método predeterminado). Entre variables locales que son campos de una clase o métodos utilizados en la implementación, existe como tal la posibilidad de conflicto de nombres. El compilador de Java sabe cómo seleccionar la variable correcta entre las disponibles, aunque más de un desarrollador pretenda utilizar esa variable. Los IDE de Java modernos hacen un gran trabajo al informar al desarrollador cuándo están a punto de ocurrir dichos conflictos, a través de advertencias del compilador y resaltado de variables. Pero es mejor pensar en esas cosas mientras se escribe código. Sugiero mirar un ejemplo: public class LocalVariableAndClassMember { private long value; public long calculateValue( final long initial ) { long value = initial; value *= 10; value += value; return value; } } El ejemplo parece bastante fácil, pero es una trampa. El método calcularValue introduce un valor de variable local y, operando sobre él, oculta el campo de clase con el mismo nombre. La línea value += value; debería ser la suma del valor del campo de clase y la variable local, pero en cambio se está haciendo algo más. Una implementación adecuada se vería así (usando la palabra clave this): public class LocalVariableAndClassMember { private long value; public long calculateValue( final long initial ) { long value = initial; value *= 10; value += this.value; return value; } } Si bien este ejemplo es ingenuo en algunos aspectos, demuestra un punto importante: en algunos casos, depurar y corregir puede llevar horas.
4. Argumentos del método y variables locales.
Otro error en el que suelen caer los desarrolladores de Java sin experiencia es el uso de argumentos de métodos como variables locales. Java le permite reasignar valores a argumentos no constantes (sin embargo, esto no tiene ningún efecto sobre el valor original): public String sanitize( String str ) { if( !str.isEmpty() ) { str = str.trim(); } str = str.toLowerCase(); return str; } el fragmento de código anterior no es elegante, pero hace un buen trabajo al descubrir el problema: a str se le asigna un valor diferente valor (y se utiliza básicamente como variable local). En todos los casos (sin excepción), puedes y debes prescindir de este ejemplo (por ejemplo, declarando los argumentos como constantes). Por ejemplo: public String sanitize( final String str ) { String sanitized = str; if( !str.isEmpty() ) { sanitized = str.trim(); } sanitized = sanitized.toLowerCase(); return sanitized; } siguiendo esta sencilla regla, es más fácil rastrear el código dado y encontrar el origen del problema, incluso al introducir variables locales.
5. Embalaje y desembalaje
Boxing y unboxing es el nombre de una técnica utilizada en Java para convertir tipos primitivos ( int, long, double, etc. ) en envoltorios de tipos correspondientes ( Integer, Long, Double , etc.). En la Parte 4 del tutorial Cómo y cuándo usar genéricos, ya viste esto en acción cuando hablé sobre envolver tipos primitivos como parámetros de tipo de genéricos. Aunque el compilador de Java hace todo lo posible para ocultar dichas conversiones mediante la realización de autoboxing, a veces esto es menos de lo esperado y produce resultados inesperados. Veamos un ejemplo: public static void calculate( final long value ) { // блок códigoа } final Long value = null; calculate( value ); el fragmento de código anterior se compila bien. Sin embargo, generará una NullPointerException en la línea // блок donde se convierte entre Long y long . El consejo para tal caso es que es aconsejable utilizar tipos primitivos (sin embargo, ya sabemos que esto no siempre es posible).
6. Interfaces
En la Parte 3 del tutorial, "Cómo diseñar clases e interfaces", analizamos las interfaces y la programación por contrato, enfatizando que se deben preferir las interfaces a las clases concretas siempre que sea posible. El propósito de esta sección es alentarlo a considerar primero las interfaces demostrándolo con ejemplos de la vida real. Las interfaces no están vinculadas a una implementación específica (excepto los métodos predeterminados). Son sólo contratos y, por ejemplo, proporcionan mucha libertad y flexibilidad en la forma en que se pueden ejecutar los contratos. Esta flexibilidad se vuelve más importante cuando la implementación involucra sistemas o servicios externos. Veamos un ejemplo de una interfaz simple y su posible implementación: public interface TimezoneService { TimeZone getTimeZone( final double lat, final double lon ) throws IOException; } public class TimezoneServiceImpl implements TimezoneService { @Override public TimeZone getTimeZone(final double lat, final double lon) throws IOException { final URL url = new URL( String.format( "http://api.geonames.org/timezone?lat=%.2f&lng=%.2f&username=demo", lat, lon ) ); final HttpURLConnection connection = ( HttpURLConnection )url.openConnection(); connection.setRequestMethod( "GET" ); connection.setConnectTimeout( 1000 ); connection.setReadTimeout( 1000 ); connection.connect(); int status = connection.getResponseCode(); if (status == 200) { // Do something here } return TimeZone.getDefault(); } } El fragmento de código anterior muestra un patrón de interfaz típico y su implementación. Esta implementación utiliza un servicio HTTP externo ( http://api.geonames.org/ ) para recuperar la zona horaria de una ubicación específica. Sin embargo, porque el contrato depende de la interfaz, es muy fácil introducir otra implementación de la interfaz, utilizando, por ejemplo, una base de datos o incluso un archivo plano normal. Con ellos, las interfaces son muy útiles para diseñar código comprobable. Por ejemplo, no siempre es práctico llamar a servicios externos en cada prueba, por lo que tiene sentido implementar una implementación alternativa más simple (como un código auxiliar): public class TimezoneServiceTestImpl implements TimezoneService { @Override public TimeZone getTimeZone(final double lat, final double lon) throws IOException { return TimeZone.getDefault(); } } esta implementación se puede usar en cualquier lugar donde se requiera la interfaz TimezoneService , aislando el script de prueba de la dependencia de componentes externos. Muchos ejemplos excelentes de uso eficaz de dichas interfaces están encapsulados en la biblioteca estándar de Java. Colecciones, listas, conjuntos: estas interfaces tienen múltiples implementaciones que se pueden intercambiar sin problemas cuando se aprovechan los contratos. Por ejemplo: public static< T > void print( final Collection< T > collection ) { for( final T element: collection ) { System.out.println( element ); } } print( new HashSet< Object >( /* ... */ ) ); print( new ArrayList< Integer >( /* ... */ ) ); print( new TreeSet< String >( /* ... */ ) );
7. Cuerdas
Las cadenas son uno de los tipos más utilizados tanto en Java como en otros lenguajes de programación. El lenguaje Java simplifica muchas manipulaciones rutinarias de cadenas al admitir operaciones de concatenación y comparación desde el primer momento. Además, la biblioteca estándar contiene muchas clases que hacen que las operaciones con cadenas sean eficientes. Esto es exactamente lo que vamos a discutir en esta sección. En Java, las cadenas son objetos inmutables representados en codificación UTF-16. Cada vez que concatenas cadenas (o realizas cualquier operación que modifique la cadena original), se crea una nueva instancia de la clase String . Debido a esto, la operación de concatenación puede volverse muy ineficiente, provocando que se creen muchas instancias intermedias de la clase String (creando basura, en general). Pero la biblioteca estándar de Java contiene dos clases muy útiles cuyo propósito es hacer que la manipulación de cadenas sea conveniente. Estos son StringBuilder y StringBuffer (la única diferencia entre ellos es que StringBuffer es seguro para subprocesos, mientras que StringBuilder es todo lo contrario). Veamos un par de ejemplos del uso de una de estas clases: final StringBuilder sb = new StringBuilder(); for( int i = 1; i <= 10; ++i ) { sb.append( " " ); sb.append( i ); } sb.deleteCharAt( 0 ); sb.insert( 0, "[" ); sb.replace( sb.length() - 3, sb.length(), "]" ); Si bien usar StringBuilder/StringBuffer es la forma recomendada de manipular cadenas, puede parecer excesivo en el escenario más simple de concatenar dos o tres cadenas, de modo que el operador de suma normal ( ("+"), por ejemplo: String userId = "user:" + new Random().nextInt( 100 ); a menudo, la mejor alternativa para simplificar la concatenación es utilizar el formato de cadena, así como la biblioteca estándar de Java para ayudar a proporcionar un método auxiliar estático String.format . Esto admite un amplio conjunto de especificadores de formato, incluidos números, símbolos, fecha/hora, etc. (Consulte la documentación de referencia para obtener detalles completos). El String.format( "%04d", 1 ); -> 0001 String.format( "%.2f", 12.324234d ); -> 12.32 String.format( "%tR", new Date() ); -> 21:11 String.format( "%tF", new Date() ); -> 2014-11-11 String.format( "%d%%", 12 ); -> 12% método String.format proporciona un enfoque limpio y liviano para generar cadenas a partir de varios tipos de datos. Vale la pena señalar que los IDE de Java modernos pueden analizar la especificación de formato a partir de los argumentos pasados ​​al método String.format y advertir a los desarrolladores si se detecta alguna discrepancia.
8. Convenciones de nomenclatura
Java es un lenguaje que no obliga a los desarrolladores a seguir estrictamente ninguna convención de nomenclatura, pero la comunidad ha desarrollado un conjunto de reglas simples que hacen que el código Java parezca consistente tanto en la biblioteca estándar como en cualquier otro proyecto Java:
  • Los nombres de los paquetes están en minúsculas: org.junit, com.fasterxml.jackson, javax.json.
  • los nombres de clases, enumeraciones, interfaces y anotaciones se escriben con letra mayúscula: StringBuilder, Runnable, @Override
  • Los nombres de los campos o métodos (excepto static final ) se especifican en notación camel: isEmpty, format, addAll.
  • Los nombres de las constantes de enumeración o campos finales estáticos están en mayúsculas, separados por guiones bajos ("_"): LOG, MIN_RADIX, INSTANCE.
  • Las variables locales o los argumentos de los métodos se escriben en notación camel: str, newLength, minimalCapacity.
  • Los nombres de tipos de parámetros para genéricos están representados por una sola letra en mayúscula: T, U, E.
Si sigue estas convenciones simples, el código que escriba se verá conciso e indistinguible en estilo de otra biblioteca o marco, y se sentirá como si hubiera sido desarrollado por la misma persona (una de esas raras ocasiones en las que las convenciones realmente funcionan).
9. Bibliotecas estándar
No importa en qué tipo de proyecto Java esté trabajando, las bibliotecas estándar de Java son sus mejores amigas. Sí, es difícil no estar de acuerdo en que tienen algunas asperezas y decisiones de diseño extrañas; sin embargo, el 99% de las veces es código de alta calidad escrito por expertos. Vale la pena explorarlo. Cada versión de Java trae muchas características nuevas a las bibliotecas existentes (con algunos posibles problemas con características antiguas) y también agrega muchas bibliotecas nuevas. Java 5 trajo una nueva biblioteca de concurrencia como parte del paquete java.util.concurrent . Java 6 proporcionó soporte (menos conocido) para secuencias de comandos ( el paquete javax.script ) y una API del compilador de Java (como parte del paquete javax.tools ). Java 7 trajo muchas mejoras a java.util.concurrent , introduciendo una nueva biblioteca de E/S en el paquete java.nio.file y soporte para lenguajes dinámicos en java.lang.invoke . Y finalmente, Java 8 agregó la tan esperada fecha/hora en el paquete java.time . Java como plataforma está evolucionando y es muy importante que avance junto con los cambios anteriores. Siempre que considere incluir una biblioteca o marco de terceros en su proyecto, asegúrese de que la funcionalidad requerida no esté ya contenida en las bibliotecas estándar de Java (por supuesto, existen muchas implementaciones de algoritmos especializadas y de alto rendimiento que están por delante de las actuales). algoritmos en las bibliotecas estándar, pero en la mayoría de los casos realmente no son necesarios).
10. Inmutabilidad
La inmutabilidad a lo largo de la guía y en esta parte queda como un recordatorio: tómalo en serio. Si una clase que diseña o un método que implementa puede proporcionar una garantía de inmutabilidad, se puede utilizar en la mayoría de los casos en todas partes sin temor a ser modificado al mismo tiempo. Esto facilitará su vida como desarrollador (y, con suerte, la vida de los miembros de su equipo).
11. Pruebas
La práctica del desarrollo basado en pruebas (TDD) es extremadamente popular en la comunidad Java y eleva el listón de la calidad del código. Con todos los beneficios que ofrece TDD, es triste ver que la biblioteca estándar de Java actual no incluye ningún marco de prueba ni herramientas de soporte. Sin embargo, las pruebas se han convertido en una parte necesaria del desarrollo moderno de Java y en esta sección veremos algunas técnicas básicas que utilizan el marco JUnit . En JUnit, esencialmente, cada prueba es un conjunto de declaraciones sobre el estado o comportamiento esperado de un objeto. El secreto para escribir pruebas excelentes es mantenerlas simples y breves, probando una cosa a la vez. Como ejercicio, escribamos un conjunto de pruebas para verificar que String.format es una función de la sección de cadena que devuelve el resultado deseado. package com.javacodegeeks.advanced.generic; import static org.junit.Assert.assertThat; import static org.hamcrest.CoreMatchers.equalTo; import org.junit.Test; public class StringFormatTestCase { @Test public void testNumberFormattingWithLeadingZeros() { final String formatted = String.format( "%04d", 1 ); assertThat( formatted, equalTo( "0001" ) ); } @Test public void testDoubleFormattingWithTwoDecimalPoints() { final String formatted = String.format( "%.2f", 12.324234d ); assertThat( formatted, equalTo( "12.32" ) ); } } Ambas pruebas parecen muy legibles y su ejecución es por instancias. Hoy en día, el proyecto Java promedio contiene cientos de casos de prueba, lo que brinda al desarrollador comentarios rápidos durante el proceso de desarrollo sobre regresiones o características.
12. Siguiente
Esta parte de la guía completa una serie de discusiones relacionadas con la práctica de la programación en Java y manuales para este lenguaje de programación. La próxima vez volveremos a las características del lenguaje, explorando el mundo de Java en cuanto a excepciones, sus tipos, cómo y cuándo usarlas.
13. Descargar el código fuente
Esta fue una lección sobre principios generales de desarrollo del curso Java Avanzado. El código fuente de la lección se puede descargar aquí .
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION