Clases selladas. Semántica vs rendimiento

Probablemente no yo solo después de leer la documentación sobre las clases selladas, pensé: “Está bien. Tal vez sea útil algún día. Más tarde, cuando en mi trabajo conocí un par de tareas donde logré usar esta herramienta con éxito, pensé: “No está mal. A menudo debes pensar en la aplicación ". Y finalmente, encontré una descripción de la clase de tareas en el libro Effective Java (Joshua Bloch, 3rd) (sí, en el libro sobre Java).

Veamos una aplicación y la evaluamos en términos de semántica y rendimiento.


Creo que todos los que trabajaron con la interfaz de usuario una vez cumplieron la implementación de la interacción de la interfaz de usuario con el servicio a través de ciertos estados , donde algún marcador de tipo era uno de los atributos. La mecánica de procesar el siguiente estado en tales implementaciones, generalmente depende directamente del marcador especificado. Por ejemplo, tal implementación de la clase State :

class State( val type: Type, val data: String?, val error: Throwable? ) { enum class Type { LOADING, ERROR, EMPTY, DATA } } 

Enumeramos las desventajas de dicha implementación (pruébelo usted mismo)
Observaciones del capítulo 23 del libro "Prefiera las jerarquías de clases a las clases etiquetadas". Propongo conocerla.

  1. Consumo de memoria para atributos que se inicializan solo para ciertos tipos. El factor puede ser significativo en grandes volúmenes. La situación se exacerba si se crean objetos predeterminados para llenar los atributos.
  2. Carga semántica excesiva. El usuario de la clase necesita monitorear qué tipo, qué atributos están disponibles.
  3. Soporte complicado en clases de lógica de negocios. Suponga una implementación en la que un objeto puede realizar algunas operaciones en sus datos. Dicha clase se verá como una cosechadora , y agregar un nuevo tipo u operación puede ser difícil.


Procesar un nuevo estado podría verse así:

 fun handleState(state: State) { when(state.type) { State.Type.LOADING -> onLoading() State.Type.ERROR -> state.error?.run(::onError) ?: throw AssertionError("Unexpected error state: $state") State.Type.EMPTY -> onEmpty() State.Type.DATA -> state.data?.run(::onData) ?: throw AssertionError("Unexpected data state: $state") } } fun onLoading() {} fun onError(error: Throwable) {} fun onEmpty() {} fun onData(data: String) {} 

Tenga en cuenta que para estados como ERROR y DATA, el compilador no puede determinar la seguridad del uso de atributos, por lo que el usuario debe escribir código redundante. Los cambios en la semántica solo se pueden detectar en tiempo de ejecución.

Clase sellada


imagen

Con una refactorización simple, podemos dividir nuestro estado en un grupo de clases:

 sealed class State //    stateless  -     singleton object Loading : State() data class Error(val error: Throwable) : State() //  ,     ,  stateless  -  singleton object Empty : State() data class Data(val data: String) : State() 

En el lado del usuario, obtenemos un procesamiento de estado donde la accesibilidad de los atributos se determinará a nivel de idioma, y ​​el mal uso causará errores en la etapa de compilación:

 fun handleState(state: State) { when(state) { Loading -> onLoading() is Error -> onError(state.error) Empty -> onEmpty() is Data -> onData(state.data) } } 

Dado que solo hay atributos significativos en las copias, podemos hablar sobre ahorrar memoria y, lo que es más importante, mejorar la semántica. Los usuarios de clases selladas no necesitan implementar manualmente las reglas para trabajar con atributos dependiendo del marcador de tipo ; la disponibilidad de los atributos está garantizada por la separación en tipos.

¿Todo esto es gratis?


Spoiler
No, no es gratis.

Los desarrolladores de Java que probaron Kotlin deben haber mirado el código descompilado para ver cómo son los términos de Kotlin Java. Una expresión con when se verá así:

 public static final void handleState(@NotNull State state) { Intrinsics.checkParameterIsNotNull(state, "state"); if (Intrinsics.areEqual(state, Loading.INSTANCE)) { onLoading(); } else if (state instanceof Error) { onError(((Error)state).getError()); } else if (Intrinsics.areEqual(state, Empty.INSTANCE)) { onEmpty(); } else if (state instanceof Data) { onData(((Data)state).getData()); } } 

Las ramas con una gran cantidad de ejemplos pueden ser alarmantes debido a los estereotipos sobre el "signo de código incorrecto" y el "impacto en el rendimiento", pero no tenemos idea. Es necesario comparar de alguna manera la velocidad de ejecución, por ejemplo, usando jmh .

Basado en el artículo "Medición correcta de la velocidad del código Java" , se preparó una prueba de procesamiento de cuatro estados (CARGA, ERROR, VACÍO, DATOS), estos son sus resultados:

 Benchmark Mode Cnt Score Error Units CompareSealedVsTagged.sealed thrpt 500 940739,966 ± 5350,341 ops/s CompareSealedVsTagged.tagged thrpt 500 1281274,381 ± 10675,956 ops/s 

Se puede ver que la implementación sellada funciona ≈25% más lento (se suponía que el retraso no excedería el 10-15%).

Si en cuatro tipos tenemos un cuarto de retraso, con tipos crecientes (el número de instancias de controles), el retraso solo debería crecer. Para verificar, aumentaremos el número de tipos a 16 (supongamos que hemos logrado obtener una jerarquía tan amplia):

 Benchmark Mode Cnt Score Error Units CompareSealedVsTagged.sealed thrpt 500 149493,062 ± 622,313 ops/s CompareSealedVsTagged.tagged thrpt 500 235024,737 ± 3372,754 ops/s 

Junto con una disminución en la productividad, el retraso de las ventas selladas aumentó a ≈35%, no ocurrió ningún milagro.

Conclusión


En este artículo, no descubrimos América, y las implementaciones selladas, por ejemplo, de sucursales realmente funcionan más lentamente que las comparaciones de enlaces.


Sin embargo, es necesario expresar un par de pensamientos:

  • en la mayoría de los casos, queremos trabajar con una buena semántica de código, acelerar el desarrollo debido a sugerencias adicionales del IDE y las comprobaciones del compilador; en tales casos, puede usar clases selladas
  • Si no puede sacrificar el rendimiento en una tarea, debe descuidar la implementación sellada y reemplazarla, por ejemplo, con una implementación de etiqueta. Tal vez deberías abandonar completamente el kotlin en favor de los idiomas de nivel inferior

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


All Articles